Skip to content
60 changes: 60 additions & 0 deletions crates/amalthea/src/comm/help_comm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@
use serde::Deserialize;
use serde::Serialize;

/// A help topic offered as an autocomplete suggestion.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct HelpTopicSuggestion {
/// The topic label shown to the user.
pub label: String,

/// The exact topic value used to open help.
pub topic: String,

/// Optional context such as the package containing the topic.
pub detail: Option<String>
}

/// Possible values for Kind in ShowHelp
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, strum_macros::Display, strum_macros::EnumString)]
pub enum ShowHelpKind {
Expand All @@ -34,6 +47,27 @@ pub struct ShowHelpTopicParams {
pub topic: String,
}

/// Parameters for the SearchHelp method.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct SearchHelpParams {
/// The help query to search for
pub query: String,

/// Opaque identifier supplied by the frontend for this UI search. Echo it
/// in the resulting Show Help notification.
pub search_id: String,
}

/// Parameters for the GetHelpTopics method.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct GetHelpTopicsParams {
/// The text to match against help topic labels.
pub query: String,

/// Maximum number of suggestions to return, from 1 to 50.
pub limit: i64,
}

/// Parameters for the ShowHelp method.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ShowHelpParams {
Expand All @@ -45,6 +79,11 @@ pub struct ShowHelpParams {

/// Whether to focus the Help pane when the content is displayed.
pub focus: bool,

/// Identifier of the UI search that requested this navigation, if any.
/// Omit for console help and other help navigation. The frontend ignores
/// identifiers that are no longer current.
pub search_id: Option<String>,
}

/**
Expand All @@ -62,6 +101,20 @@ pub enum HelpBackendRequest {
#[serde(rename = "show_help_topic")]
ShowHelpTopic(ShowHelpTopicParams),

/// Search the active interpreter's help system.
///
/// Searches interpreter-wide help and displays the resulting page via a
/// Show Help notification.
#[serde(rename = "search_help")]
SearchHelp(SearchHelpParams),

/// Find help topics for autocomplete.
///
/// Returns at most limit matching help topic suggestions, filtered and
/// ranked by the backend. An empty query returns no suggestions.
#[serde(rename = "get_help_topics")]
GetHelpTopics(GetHelpTopicsParams),

}

/**
Expand All @@ -74,6 +127,13 @@ pub enum HelpBackendReply {
/// Help notification.
ShowHelpTopicReply(bool),

/// Whether the search results navigation was requested. This does not
/// confirm that the frontend displayed or finished loading the page.
SearchHelpReply(bool),

/// Help topic suggestions.
GetHelpTopicsReply(Vec<HelpTopicSuggestion>),

}

/**
Expand Down
97 changes: 96 additions & 1 deletion crates/ark/src/help/r_help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
//
//

use std::cell::RefCell;

use amalthea::comm::comm_channel::CommMsg;
use amalthea::comm::help_comm::HelpBackendReply;
use amalthea::comm::help_comm::HelpBackendRequest;
use amalthea::comm::help_comm::HelpFrontendEvent;
use amalthea::comm::help_comm::HelpTopicSuggestion;
use amalthea::comm::help_comm::ShowHelpKind;
use amalthea::comm::help_comm::ShowHelpParams;
use anyhow::anyhow;
Expand All @@ -31,6 +34,26 @@ use crate::methods::ArkGenerics;

pub const HELP_COMM_NAME: &str = "positron.help";

thread_local! {
// Browser callbacks run synchronously on the R thread while a search is printed.
// Keep this separate from RHelp, which is already borrowed during RPC dispatch.
static SEARCH_ID: RefCell<Option<String>> = const { RefCell::new(None) };
}

struct SearchContext(Option<String>);

impl SearchContext {
fn enter(id: String) -> Self {
Self(SEARCH_ID.replace(Some(id)))
}
}

impl Drop for SearchContext {
fn drop(&mut self) {
SEARCH_ID.set(self.0.take());
}
}

/// Ports for the R help server and our proxy, recorded on `Console` once both
/// are running.
#[derive(Clone, Copy)]
Expand Down Expand Up @@ -75,6 +98,36 @@ impl RHelp {
Err(err) => Err(err),
}
},
HelpBackendRequest::SearchHelp(search) => {
let _search_context = SearchContext::enter(search.search_id);
let shown = RFunction::from(".ps.help.searchHelp")
.add(search.query)
.call()?
.to::<bool>()?;
Ok(HelpBackendReply::SearchHelpReply(shown))
},
HelpBackendRequest::GetHelpTopics(params) => {
if !(1..=50).contains(&params.limit) {
return Err(anyhow!("Help suggestion limit must be between 1 and 50."));
}
let topics = RFunction::from(".ps.help.getHelpTopics")
.add(params.query)
.add(params.limit as i32)
.call()?
.to::<Vec<String>>()?;
let suggestions = topics
.into_iter()
.filter_map(|entry| {
let (package, topic) = entry.split_once('\u{1f}')?;
Some(HelpTopicSuggestion {
label: topic.to_string(),
topic: format!("{package}::{topic}"),
detail: Some(package.to_string()),
})
})
.collect();
Ok(HelpBackendReply::GetHelpTopicsReply(suggestions))
},
}
}

Expand Down Expand Up @@ -136,6 +189,7 @@ impl RHelp {
content: url,
kind: ShowHelpKind::Url,
focus: true,
search_id: SEARCH_ID.with(|id| id.borrow().clone()),
});
ctx.send_event(&msg);

Expand All @@ -144,6 +198,18 @@ impl RHelp {

#[tracing::instrument(level = "trace", skip(self))]
fn show_help_topic(&self, topic: String) -> anyhow::Result<bool> {
// Suggestions contain literal package-qualified aliases, including `$` and
// `@`. Prefer their documentation before trying a custom expression handler.
if topic.contains("::") &&
RFunction::from(".ps.help.showHelpTopic")
.add(topic.clone())
.param("qualified_only", true)
.call()?
.to::<bool>()?
{
return Ok(true);
}

let topic = HelpTopic::parse(topic);

let found = match topic {
Expand Down Expand Up @@ -257,7 +323,7 @@ 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
// after checking for a literal package-qualified help alias
Expression(String),
}

Expand All @@ -282,3 +348,32 @@ pub unsafe extern "C-unwind" fn ps_help_browse_external_url(

Ok(R_NilValue)
}

#[cfg(test)]
mod tests {
use super::SearchContext;
use super::SEARCH_ID;

#[test]
fn search_context_restores_after_error() {
fn fail() -> anyhow::Result<()> {
let _context = SearchContext::enter(String::from("inner"));
assert_eq!(
SEARCH_ID.with(|id| id.borrow().clone()),
Some(String::from("inner"))
);
Err(anyhow::anyhow!("search failed"))
}

assert_eq!(SEARCH_ID.with(|id| id.borrow().clone()), None);
{
let _context = SearchContext::enter(String::from("outer"));
assert!(fail().is_err());
assert_eq!(
SEARCH_ID.with(|id| id.borrow().clone()),
Some(String::from("outer"))
);
}
assert_eq!(SEARCH_ID.with(|id| id.borrow().clone()), None);
}
}
Loading
Loading