Skip to content

Allow extensions to contribute to F1 show help topic #785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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 @@ -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;

Expand All @@ -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)
})
}
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
53 changes: 40 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,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;

/**
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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<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");
}
}
36 changes: 36 additions & 0 deletions crates/ark/src/modules/positron/help.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions crates/ark/src/modules/positron/methods.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down