Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion crates/ark/src/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use libr::Rf_ScalarLogical;
use libr::SEXP;

use crate::help::message::HelpEvent;
use crate::help::message::ShowHelpUrlKind;
use crate::help::message::ShowHelpUrlParams;
use crate::interface::RMain;
use crate::ui::events::send_open_with_system_event;
Expand All @@ -30,7 +31,10 @@ fn is_help_url(url: &str) -> bool {

fn handle_help_url(url: String) -> anyhow::Result<()> {
RMain::with(|main| {
let event = HelpEvent::ShowHelpUrl(ShowHelpUrlParams { url });
let event = HelpEvent::ShowHelpUrl(ShowHelpUrlParams {
url,
kind: ShowHelpUrlKind::HelpProxy,
});
main.send_help_event(event)
})
}
Expand Down
7 changes: 7 additions & 0 deletions crates/ark/src/help/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ pub enum HelpEvent {
ShowHelpUrl(ShowHelpUrlParams),
}

#[derive(Debug)]
pub enum ShowHelpUrlKind {
HelpProxy,
External,
}

#[derive(Debug)]
pub struct ShowHelpUrlParams {
/// Url to attempt to show.
pub url: String,
pub kind: ShowHelpUrlKind,
}

impl std::fmt::Display for HelpEvent {
Expand Down
95 changes: 82 additions & 13 deletions crates/ark/src/help/r_help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@ use crossbeam::channel::Sender;
use crossbeam::select;
use harp::exec::RFunction;
use harp::exec::RFunctionExt;
use harp::RObject;
use libr::R_GlobalEnv;
use libr::R_NilValue;
use libr::SEXP;
use log::info;
use log::trace;
use log::warn;
use stdext::spawn;

use crate::help::message::HelpEvent;
use crate::help::message::ShowHelpUrlKind;
use crate::help::message::ShowHelpUrlParams;
use crate::interface::RMain;
use crate::methods::ArkGenerics;
use crate::r_task;

/**
Expand Down Expand Up @@ -182,27 +189,37 @@ impl RHelp {
/// coming through here has already been verified to look like a help URL with
/// `is_help_url()`, so if we get an unexpected prefix, that's an error.
fn handle_show_help_url(&self, params: ShowHelpUrlParams) -> anyhow::Result<()> {
let url = params.url;
let url = params.url.clone();

if !Self::is_help_url(url.as_str(), self.r_port) {
let prefix = Self::help_url_prefix(self.r_port);
return Err(anyhow!(
"Help URL '{url}' doesn't have expected prefix '{prefix}'."
));
}
let url = match params.kind {
ShowHelpUrlKind::HelpProxy => {
if !Self::is_help_url(url.as_str(), self.r_port) {
let prefix = Self::help_url_prefix(self.r_port);
return Err(anyhow!(
"Help URL '{url}' doesn't have expected prefix '{prefix}'."
));
}

// Re-direct the help event to our help proxy server.
let r_prefix = Self::help_url_prefix(self.r_port);
let proxy_prefix = Self::help_url_prefix(self.proxy_port);
// Re-direct the help event to our help proxy server.
let r_prefix = Self::help_url_prefix(self.r_port);
let proxy_prefix = Self::help_url_prefix(self.proxy_port);

let proxy_url = url.replace(r_prefix.as_str(), proxy_prefix.as_str());
url.replace(r_prefix.as_str(), proxy_prefix.as_str())
},
ShowHelpUrlKind::External => {
// The URL is not a help URL; just use it as-is.
url
},
};

log::trace!(
"Sending frontend event `ShowHelp` with R url '{url}' and proxy url '{proxy_url}'"
"Sending frontend event `ShowHelp` with R url '{}' and proxy url '{}'",
params.url,
url
);

let msg = HelpFrontendEvent::ShowHelp(ShowHelpParams {
content: proxy_url,
content: url,
kind: ShowHelpKind::Url,
focus: true,
});
Expand All @@ -216,6 +233,10 @@ impl RHelp {
#[tracing::instrument(level = "trace", skip(self))]
fn show_help_topic(&self, topic: String) -> anyhow::Result<bool> {
let found = r_task(|| unsafe {
if let Ok(Some(result)) = Self::r_help_handler(topic.clone()) {
return Ok(result);
}

RFunction::from(".ps.help.showHelpTopic")
.add(topic)
.call()?
Expand All @@ -224,6 +245,42 @@ impl RHelp {
Ok(found)
}

// Must be called in a `r_task` context.
fn r_help_handler(_topic: String) -> anyhow::Result<Option<bool>> {
unsafe {
let env = (|| {
#[cfg(not(test))]
if RMain::is_initialized() {
if let Some(debug_env) = &RMain::get().debug_env() {
// Mem-Safety: Object protected by `RMain` for the duration of the `r_task()`
return debug_env.sexp;
}
}

R_GlobalEnv
})();

let obj = harp::parse_eval0(_topic.as_str(), env)?;
let handler: Option<RObject> =
ArkGenerics::HelpGetHandler.try_dispatch(obj.sexp, vec![])?;

if let Some(handler) = handler {
let mut fun = RFunction::new_inlined(handler);
match fun.call_in(env) {
Err(err) => {
log::error!("Error calling help handler: {:?}", err);
return Err(anyhow!("Error calling help handler: {:?}", err));
},
Ok(result) => {
return Ok(Some(result.try_into()?));
},
}
}
}

Ok(None)
}

pub fn r_start_or_reconnect_to_help_server() -> harp::Result<u16> {
// Start the R help server.
// If it is already started, it just returns the preexisting port number.
Expand All @@ -232,3 +289,15 @@ impl RHelp {
.and_then(|x| x.try_into())
}
}

#[harp::register]
pub unsafe extern "C-unwind" fn ps_help_browse_external_url(
url: SEXP,
) -> Result<SEXP, anyhow::Error> {
RMain::get().send_help_event(HelpEvent::ShowHelpUrl(ShowHelpUrlParams {
url: RObject::view(url).to::<String>()?,
kind: ShowHelpUrlKind::External,
}))?;

Ok(R_NilValue)
}
8 changes: 8 additions & 0 deletions crates/ark/src/lsp/help_topic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ fn locate_help_node(tree: &Tree, point: Point) -> Option<Node<'_>> {
// Even if they are at `p<>kg::fun`, we assume they really want docs for `fun`.
let node = match node.parent() {
Some(parent) if matches!(parent.node_type(), NodeType::NamespaceOperator(_)) => parent,
Some(parent) if matches!(parent.node_type(), NodeType::ExtractOperator(_)) => parent,
Some(_) => node,
None => node,
};
Expand Down Expand Up @@ -138,5 +139,12 @@ mod tests {
let node = locate_help_node(&tree, point).unwrap();
let text = node.utf8_text(text.as_bytes()).unwrap();
assert_eq!(text, "dplyr:::across");

// R6 methods, or reticulate accessors
let (text, point) = point_from_cursor("tf$a@bs(x)");
let tree = parser.parse(text.as_str(), None).unwrap();
let node = locate_help_node(&tree, point).unwrap();
let text = node.utf8_text(text.as_bytes()).unwrap();
assert_eq!(text, "tf$abs");
}
}
3 changes: 3 additions & 0 deletions crates/ark/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub enum ArkGenerics {

#[strum(serialize = "ark_positron_variable_has_viewer")]
VariableHasViewer,

#[strum(serialize = "ark_positron_help_get_handler")]
HelpGetHandler,
}

impl ArkGenerics {
Expand Down
5 changes: 5 additions & 0 deletions crates/ark/src/modules/positron/help.R
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ getHtmlHelpContentsDevImpl <- function(x) {
.ps.Call("ps_browse_url", as.character(url))
}

#' @export
.ps.help.browse_external_url <- function(url) {
.ps.Call("ps_help_browse_external_url", as.character(url))
}

# @param rd_file Path to an `.Rd` file.
# @returns The result of converting that `.Rd` to HTML and concatenating to a
# string.
Expand Down
69 changes: 68 additions & 1 deletion crates/ark/src/modules/positron/methods.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,90 @@
#

ark_methods_table <- new.env(parent = emptyenv())

#' Customize display value for objects in Variables Pane
#'
#' @param x Object to get the display value for
#' @param ... Additional arguments (unused)
#' @param width Maximum expected width. This is just a suggestion, the UI
#' can still truncate the string to different widths.
#' @return A length 1 character vector containing the display value
ark_methods_table$ark_positron_variable_display_value <- new.env(
parent = emptyenv()
)

#' Customize display type for objects in Variables Pane
#'
#' @param x Object to get the display type for
#' @param ... Additional arguments (unused)
#' @param include_length Boolean indicating whether to include object length
#' @return A length 1 character vector describing the object type
ark_methods_table$ark_positron_variable_display_type <- new.env(
parent = emptyenv()
)

#' Check if object has inspectable children in Variables Pane
#'
#' @param x Object to check for children
#' @param ... Additional arguments (unused)
#' @return Logical value: TRUE if the object can be inspected, FALSE otherwise
ark_methods_table$ark_positron_variable_has_children <- new.env(
parent = emptyenv()
)

#' Specify variable kind for Variables Pane organization
#'
#' @param x Object to get the variable kind for
#' @param ... Additional arguments (unused)
#' @return Length 1 character vector specifying the kind of variable (e.g., "table", "other")
#' See the `pub enum VariableKind` for all accepted types.
ark_methods_table$ark_positron_variable_kind <- new.env(parent = emptyenv())

#' Get specific child element from object for Variables Pane inspection
#'
#' @param x Object to get child from
#' @param ... Additional arguments (unused)
#' @param index Integer > 1, representing the index position of the child
#' @param name Character string or NULL, the name of the child
#' @return The child object at the specified index/name
ark_methods_table$ark_positron_variable_get_child_at <- new.env(
parent = emptyenv()
)

#' Control viewer availability for objects in Variables Pane
#'
#' @param x Object to check for viewer support
#' @param ... Additional arguments (unused)
#' @return Logical value: TRUE if viewer should be enabled, FALSE to disable
ark_methods_table$ark_positron_variable_has_viewer <- new.env(
parent = emptyenv()
)

#' Get child objects for Variables Pane inspection
#'
#' @param x Object to get children from
#' @param ... Additional arguments (unused)
#' @return Named list of child objects to be displayed.
#' The above methods are called in the elements of this list to make the display
#' of child objects consistent.
ark_methods_table$ark_positron_variable_get_children <- new.env(
parent = emptyenv()
)
ark_methods_table$ark_positron_variable_has_viewer <- new.env(

#' Get the help handler for an R object
#'
#' @param obj An R object to obtain the help handler for.
#'
#' @returns Returns a help handler or `NULL` if
#' the object can't be handled.
#'
#' The returned help handler is a function with no arguments that is expected to
#' show the help documentation for the object as a side effect and return
#' `TRUE` if it was able to do so, or `FALSE` otherwise.
#'
#' It may use e.g `.ps.help.browse_external_url` to display a URL
#' in the help pane.
ark_methods_table$ark_positron_help_get_handler <- new.env(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be a good place to document the expected behaviour for these handlers?

E.g. TRUE/FALSE/NULL return values, expected side effect when TRUE (something that eventually causes a show_help notification to be emitted from Ark), when should a handler return FALSE instead of NULL.

This should be documented somewhere else if not here.

parent = emptyenv()
)
lockEnvironment(ark_methods_table, TRUE)
Expand Down
Loading