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
145 changes: 126 additions & 19 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 @@ -215,15 +232,75 @@ impl RHelp {

#[tracing::instrument(level = "trace", skip(self))]
fn show_help_topic(&self, topic: String) -> anyhow::Result<bool> {
let found = r_task(|| unsafe {
RFunction::from(".ps.help.showHelpTopic")
.add(topic)
.call()?
.to::<bool>()
})?;
let topic = HelpTopic::parse(topic);

let found = match topic {
HelpTopic::Simple(symbol) => r_task(|| unsafe {
// Try evaluating the help handler first and then fall back to
// the default help topic display function.

if let Ok(Some(result)) = Self::r_custom_help_handler(symbol.clone()) {
return Ok(result);
}

RFunction::from(".ps.help.showHelpTopic")
.add(symbol)
.call()?
.to::<bool>()
}),
HelpTopic::Expression(expression) => {
// For expressions, we have to use the help handler
// If that fails there's no fallback.
r_task(|| match Self::r_custom_help_handler(expression) {
Ok(Some(result)) => Ok(result),
// No method found
Ok(None) => Ok(false),
// Error during evaluation
Err(err) => Err(harp::Error::Anyhow(err)),
})
},
}?;

Ok(found)
}

// Must be called in a `r_task` context.
// Tries calling a custom help handler defined as an ark method.
fn r_custom_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 +309,33 @@ impl RHelp {
.and_then(|x| x.try_into())
}
}

enum HelpTopic {
// no obvious expression syntax — e.g. "abs", "base::abs"
Simple(String),
// contains expression syntax — e.g. "tensorflow::tf$abs", "model@coef"
// such that there will never exist a help topic with that name
Expression(String),
}

impl HelpTopic {
pub fn parse(topic: String) -> Self {
if topic.contains('$') || topic.contains('@') {
Self::Expression(topic)
} else {
Self::Simple(topic)
}
}
}

#[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)
}
54 changes: 26 additions & 28 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 All @@ -110,33 +111,30 @@ mod tests {
.set_language(&tree_sitter_r::LANGUAGE.into())
.expect("failed to create parser");

// On the RHS
let (text, point) = point_from_cursor("dplyr::ac@ross(x:y, sum)");
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, "dplyr::across");

// On the LHS (Returns function help for `across()`, not package help for `dplyr`,
// as we assume that is more useful for the user).
let (text, point) = point_from_cursor("dpl@yr::across(x:y, sum)");
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, "dplyr::across");

// In the operator
let (text, point) = point_from_cursor("dplyr:@:across(x:y, sum)");
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, "dplyr::across");

// Internal `:::`
let (text, point) = point_from_cursor("dplyr:::ac@ross(x:y, sum)");
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, "dplyr:::across");
// (text cursor, expected help topic)
let cases = vec![
// On the RHS
("dplyr::ac@ross(x:y, sum)", "dplyr::across"),
// On the LHS (Returns function help for `across()`, not package help for `dplyr`,
// as we assume that is more useful for the user).
("dpl@yr::across(x:y, sum)", "dplyr::across"),
// In the operator
("dplyr:@:across(x:y, sum)", "dplyr::across"),
// Internal `:::`
("dplyr:::ac@ross(x:y, sum)", "dplyr:::across"),
// R6 methods, or reticulate accessors
("tf$a@bs(x)", "tf$abs"),
("t@f$abs(x)", "tf$abs"),
// With the package namespace
("tensorflow::tf$ab@s(x)", "tensorflow::tf$abs"),
];

for (code, expected) in cases {
let (text, point) = point_from_cursor(code);
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, expected);
}
}
}
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
Loading