diff --git a/crates/ark/src/browser.rs b/crates/ark/src/browser.rs index f9d6a630f..4de512063 100644 --- a/crates/ark/src/browser.rs +++ b/crates/ark/src/browser.rs @@ -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; @@ -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) }) } 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..49044140d 100644 --- a/crates/ark/src/help/r_help.rs +++ b/crates/ark/src/help/r_help.rs @@ -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; /** @@ -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, }); @@ -216,6 +233,10 @@ impl RHelp { #[tracing::instrument(level = "trace", skip(self))] fn show_help_topic(&self, topic: String) -> anyhow::Result { 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()? @@ -224,6 +245,42 @@ impl RHelp { Ok(found) } + // Must be called in a `r_task` context. + fn r_help_handler(_topic: String) -> anyhow::Result> { + 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 = + 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 { // Start the R help server. // If it is already started, it just returns the preexisting port number. @@ -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 { + 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 978a6f30d..2dd670ec0 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/methods.rs b/crates/ark/src/methods.rs index f4d859700..8f9f22f8b 100644 --- a/crates/ark/src/methods.rs +++ b/crates/ark/src/methods.rs @@ -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 { diff --git a/crates/ark/src/modules/positron/help.R b/crates/ark/src/modules/positron/help.R index 531c95422..c0fd46ff5 100644 --- a/crates/ark/src/modules/positron/help.R +++ b/crates/ark/src/modules/positron/help.R @@ -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. diff --git a/crates/ark/src/modules/positron/methods.R b/crates/ark/src/modules/positron/methods.R index 1961319d3..7c15eed38 100644 --- a/crates/ark/src/modules/positron/methods.R +++ b/crates/ark/src/modules/positron/methods.R @@ -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( parent = emptyenv() ) lockEnvironment(ark_methods_table, TRUE)