diff --git a/crates/ark/src/browser.rs b/crates/ark/src/browser.rs index 37cd7f521..93db1c4d0 100644 --- a/crates/ark/src/browser.rs +++ b/crates/ark/src/browser.rs @@ -12,6 +12,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; @@ -29,7 +30,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) }) } diff --git a/crates/ark/src/help/message.rs b/crates/ark/src/help/message.rs index fad7d8390..f75cbf14b 100644 --- a/crates/ark/src/help/message.rs +++ b/crates/ark/src/help/message.rs @@ -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 { diff --git a/crates/ark/src/help/r_help.rs b/crates/ark/src/help/r_help.rs index f4bb0ccd4..959578ceb 100644 --- a/crates/ark/src/help/r_help.rs +++ b/crates/ark/src/help/r_help.rs @@ -18,13 +18,18 @@ use crossbeam::channel::Sender; use crossbeam::select; use harp::exec::RFunction; use harp::exec::RFunctionExt; +use harp::RObject; +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::r_task; /** @@ -182,27 +187,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, }); @@ -232,3 +247,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 { + RMain::get().send_help_event(HelpEvent::ShowHelpUrl(ShowHelpUrlParams { + url: RObject::view(url).to::()?, + kind: ShowHelpUrlKind::External, + }))?; + + Ok(R_NilValue) +} diff --git a/crates/ark/src/lsp/help_topic.rs b/crates/ark/src/lsp/help_topic.rs index 8b75e8081..6319c8c8b 100644 --- a/crates/ark/src/lsp/help_topic.rs +++ b/crates/ark/src/lsp/help_topic.rs @@ -89,6 +89,7 @@ fn locate_help_node(tree: &Tree, point: Point) -> Option { // 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, }; @@ -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"); } } diff --git a/crates/ark/src/modules/positron/help.R b/crates/ark/src/modules/positron/help.R index 7ba6749d1..5dd5245af 100644 --- a/crates/ark/src/modules/positron/help.R +++ b/crates/ark/src/modules/positron/help.R @@ -44,6 +44,37 @@ help <- function(topic, package = NULL) { # found. #' @export .ps.help.showHelpTopic <- function(topic) { + help_handler <- tryCatch( + { + # Before we do anything to find the help page, we evaluate the topic expression + # to see if the object can be found in the current environment and if it has a + # custom help handler (eg. reticulate objects). + object <- eval( + parse(text = topic), + envir = new.env(parent = globalenv()) + ) + # call_ark_method() returns NULL if no method is found for the object + # ark_positron_help_get_handler() must return a function that's called for + # its side effects (potentially showing documentation) and returning `TRUE` + # if it could handle the request. We could also make it + # actually show help imediatelly, but that makes it hard to separate + # non-existant methods, from methods that return `NULL` and actual errors. + # This also allows methods to skip matching objects for which they don't want + # to support, by simply returning a `NULL` handler. + call_ark_method( + "ark_positron_help_get_handler", + object + ) + }, + error = function(e) { + NULL + } + ) + + if (!is.null(help_handler)) { + return(help_handler(topic)) + } + info <- split_topic(topic) topic <- info$topic package <- info$package @@ -230,6 +261,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. diff --git a/crates/ark/src/modules/positron/methods.R b/crates/ark/src/modules/positron/methods.R index 1961319d3..edc664039 100644 --- a/crates/ark/src/modules/positron/methods.R +++ b/crates/ark/src/modules/positron/methods.R @@ -25,6 +25,9 @@ ark_methods_table$ark_positron_variable_get_children <- new.env( ark_methods_table$ark_positron_variable_has_viewer <- new.env( parent = emptyenv() ) +ark_methods_table$ark_positron_help_get_handler <- new.env( + parent = emptyenv() +) lockEnvironment(ark_methods_table, TRUE) ark_methods_allowed_packages <- c("torch", "reticulate", "duckplyr")