From 4bb6ca9cb0a60ea00f9dfab941defd553ac372a4 Mon Sep 17 00:00:00 2001 From: Daniel Falbel Date: Wed, 23 Oct 2024 09:20:23 -0300 Subject: [PATCH] Protocol for extending the variables pane (#560) Mechanism to extend Positron's variable pane --------- Co-authored-by: Tomasz Kalinowski Co-authored-by: Lionel Henry Co-authored-by: Davis Vaughan --- Cargo.lock | 26 ++++- crates/ark/Cargo.toml | 1 + crates/ark/src/lib.rs | 1 + crates/ark/src/methods.rs | 89 ++++++++++++++ crates/ark/src/modules/positron/methods.R | 61 ++++++++++ crates/ark/src/variables/variable.rs | 136 ++++++++++++++++++++++ doc/variables-pane-extending.md | 136 ++++++++++++++++++++++ doc/variables-pane.png | Bin 0 -> 8246 bytes 8 files changed, 447 insertions(+), 3 deletions(-) create mode 100644 crates/ark/src/methods.rs create mode 100644 crates/ark/src/modules/positron/methods.R create mode 100644 doc/variables-pane-extending.md create mode 100644 doc/variables-pane.png diff --git a/Cargo.lock b/Cargo.lock index 5c7b27089..2e2e4fd7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,7 +255,7 @@ dependencies = [ "sha2", "stdext", "strum 0.24.1", - "strum_macros", + "strum_macros 0.24.3", "tracing", "uuid", "zmq", @@ -320,6 +320,7 @@ dependencies = [ "stdext", "struct-field-names-as-array", "strum 0.26.2", + "strum_macros 0.26.4", "tempfile", "tokio", "tower-lsp", @@ -1166,6 +1167,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.2.6" @@ -2773,13 +2780,26 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.32", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2824,7 +2844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" dependencies = [ "cfg-expr", - "heck", + "heck 0.4.1", "pkg-config", "toml 0.7.3", "version-compare", diff --git a/crates/ark/Cargo.toml b/crates/ark/Cargo.toml index 8a75b7b09..8d966ab43 100644 --- a/crates/ark/Cargo.toml +++ b/crates/ark/Cargo.toml @@ -55,6 +55,7 @@ yaml-rust = "0.4.5" winsafe = { version = "0.0.19", features = ["kernel"] } struct-field-names-as-array = "0.3.0" strum = "0.26.2" +strum_macros = "0.26.2" futures = "0.3.30" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/crates/ark/src/lib.rs b/crates/ark/src/lib.rs index f63d8d6e0..4ec6d7811 100644 --- a/crates/ark/src/lib.rs +++ b/crates/ark/src/lib.rs @@ -21,6 +21,7 @@ pub mod json; pub mod logger; pub mod logger_hprof; pub mod lsp; +pub mod methods; pub mod modules; pub mod modules_utils; pub mod plots; diff --git a/crates/ark/src/methods.rs b/crates/ark/src/methods.rs new file mode 100644 index 000000000..02c3135d2 --- /dev/null +++ b/crates/ark/src/methods.rs @@ -0,0 +1,89 @@ +// +// methods.rs +// +// Copyright (C) 2024 by Posit Software, PBC +// +// + +use anyhow::anyhow; +use harp::call::RArgument; +use harp::exec::RFunction; +use harp::exec::RFunctionExt; +use harp::r_null; +use harp::utils::r_is_object; +use harp::RObject; +use libr::SEXP; +use strum_macros::Display; +use strum_macros::EnumIter; +use strum_macros::EnumString; +use strum_macros::IntoStaticStr; + +use crate::modules::ARK_ENVS; + +#[derive(Debug, PartialEq, EnumString, EnumIter, IntoStaticStr, Display, Eq, Hash, Clone)] +pub enum ArkGenerics { + #[strum(serialize = "ark_positron_variable_display_value")] + VariableDisplayValue, + + #[strum(serialize = "ark_positron_variable_display_type")] + VariableDisplayType, + + #[strum(serialize = "ark_positron_variable_has_children")] + VariableHasChildren, + + #[strum(serialize = "ark_positron_variable_kind")] + VariableKind, +} + +impl ArkGenerics { + // Dispatches the method on `x` + // Returns + // - `None` if no method was found, + // - `Err` if method was found and errored + // - `Err` if the method result could not be coerced to `T` + // - T, if method was found and was successfully executed + pub fn try_dispatch(&self, x: SEXP, args: Vec) -> anyhow::Result> + where + // Making this a generic allows us to handle the conversion to the expected output + // type within the dispatch, which is much more ergonomic. + T: TryFrom, + >::Error: std::fmt::Debug, + { + if !r_is_object(x) { + return Ok(None); + } + + let generic: &str = self.into(); + let mut call = RFunction::new("", "call_ark_method"); + + call.add(generic); + call.add(x); + + for RArgument { name, value } in args.into_iter() { + call.param(name.as_str(), value); + } + + let result = call.call_in(ARK_ENVS.positron_ns)?; + + // No method for that object + if result.sexp == r_null() { + return Ok(None); + } + + // Convert the result to the expected return type + match result.try_into() { + Ok(value) => Ok(Some(value)), + Err(err) => Err(anyhow!("Conversion failed: {err:?}")), + } + } + + pub fn register_method(&self, class: &str, method: RObject) -> anyhow::Result<()> { + let generic_name: &str = self.into(); + RFunction::new("", ".ark.register_method") + .add(RObject::try_from(generic_name)?) + .add(RObject::try_from(class)?) + .add(method) + .call_in(ARK_ENVS.positron_ns)?; + Ok(()) + } +} diff --git a/crates/ark/src/modules/positron/methods.R b/crates/ark/src/modules/positron/methods.R new file mode 100644 index 000000000..c0779b5a8 --- /dev/null +++ b/crates/ark/src/modules/positron/methods.R @@ -0,0 +1,61 @@ +# +# methods.R +# +# Copyright (C) 2024 Posit Software, PBC. All rights reserved. +# +# + +ark_methods_table <- new.env(parent = emptyenv()) +ark_methods_table$ark_positron_variable_display_value <- new.env(parent = emptyenv()) +ark_methods_table$ark_positron_variable_display_type <- new.env(parent = emptyenv()) +ark_methods_table$ark_positron_variable_has_children <- new.env(parent = emptyenv()) +ark_methods_table$ark_positron_variable_kind <- new.env(parent = emptyenv()) +lockEnvironment(ark_methods_table, TRUE) + +ark_methods_allowed_packages <- c("torch", "reticulate") + +#' Register the methods with the Positron runtime +#' +#' @param generic Generic function name as a character to register +#' @param class Class name as a character +#' @param method A method to be registered. Should be a call object. +#' @export +.ark.register_method <- function(generic, class, method) { + + # Check if the caller is an allowed package + if (!in_ark_tests()) { + calling_env <- .ps.env_name(topenv(parent.frame())) + if (!(calling_env %in% paste0("namespace:", ark_methods_allowed_packages))) { + stop("Only allowed packages can register methods. Called from ", calling_env) + } + } + + stopifnot( + is_string(generic), + generic %in% names(ark_methods_table), + typeof(class) == "character" + ) + for (cls in class) { + assign(cls, method, envir = ark_methods_table[[generic]]) + } + invisible() +} + +call_ark_method <- function(generic, object, ...) { + methods_table <- ark_methods_table[[generic]] + + if (is.null(methods_table)) { + return(NULL) + } + + for (cls in class(object)) { + if (!is.null(method <- get0(cls, envir = methods_table))) { + return(eval( + as.call(list(method, object, ...)), + envir = globalenv() + )) + } + } + + NULL +} diff --git a/crates/ark/src/variables/variable.rs b/crates/ark/src/variables/variable.rs index 8ee223934..facaefa04 100644 --- a/crates/ark/src/variables/variable.rs +++ b/crates/ark/src/variables/variable.rs @@ -12,6 +12,7 @@ use amalthea::comm::variables_comm::ClipboardFormatFormat; use amalthea::comm::variables_comm::Variable; use amalthea::comm::variables_comm::VariableKind; use anyhow::anyhow; +use harp::call::RArgument; use harp::environment::Binding; use harp::environment::BindingValue; use harp::environment::Environment; @@ -51,6 +52,8 @@ use libr::*; use stdext::local; use stdext::unwrap; +use crate::methods::ArkGenerics; + // Constants. const MAX_DISPLAY_VALUE_ENTRIES: usize = 1_000; const MAX_DISPLAY_VALUE_LENGTH: usize = 100; @@ -70,6 +73,11 @@ fn plural(text: &str, n: i32) -> String { impl WorkspaceVariableDisplayValue { pub fn from(value: SEXP) -> Self { + // Try to use the display method if there's one available\ + if let Some(display_value) = Self::try_from_method(value) { + return display_value; + } + match r_typeof(value) { NILSXP => Self::new(String::from("NULL"), false), VECSXP if r_inherits(value, "data.frame") => Self::from_data_frame(value), @@ -312,6 +320,34 @@ impl WorkspaceVariableDisplayValue { log::trace!("Error while formatting variable: {err:?}"); Self::new(String::from("??"), true) } + + fn from_untruncated_string(mut value: String) -> Self { + let Some((index, _)) = value.char_indices().nth(MAX_DISPLAY_VALUE_LENGTH) else { + return Self::new(value, false); + }; + + // If an index is found, truncate the string to that index + value.truncate(index); + Self::new(value, true) + } + + fn try_from_method(value: SEXP) -> Option { + let display_value = + ArkGenerics::VariableDisplayValue.try_dispatch::(value, vec![RArgument::new( + "width", + RObject::from(MAX_DISPLAY_VALUE_LENGTH as i32), + )]); + + let display_value = unwrap!(display_value, Err(err) => { + log::error!("Failed to apply '{}': {err:?}", ArkGenerics::VariableDisplayValue.to_string()); + return None; + }); + + match display_value { + None => None, + Some(value) => Some(Self::from_untruncated_string(value)), + } + } } pub struct WorkspaceVariableDisplayType { @@ -327,6 +363,15 @@ impl WorkspaceVariableDisplayType { /// - include_length: Whether to include the length of the object in the /// display type. pub fn from(value: SEXP, include_length: bool) -> Self { + match Self::try_from_method(value, include_length) { + Err(err) => log::error!( + "Error from '{}' method: {err}", + ArkGenerics::VariableDisplayType.to_string() + ), + Ok(None) => {}, + Ok(Some(display_type)) => return display_type, + } + if r_is_null(value) { return Self::simple(String::from("NULL")); } @@ -425,6 +470,15 @@ impl WorkspaceVariableDisplayType { } } + fn try_from_method(value: SEXP, include_length: bool) -> anyhow::Result> { + let args = vec![RArgument::new( + "include_length", + RObject::try_from(include_length)?, + )]; + let result: Option = ArkGenerics::VariableDisplayType.try_dispatch(value, args)?; + Ok(result.map(Self::simple)) + } + fn new(display_type: String, type_info: String) -> Self { Self { display_type, @@ -434,6 +488,15 @@ impl WorkspaceVariableDisplayType { } fn has_children(value: SEXP) -> bool { + match ArkGenerics::VariableHasChildren.try_dispatch(value, vec![]) { + Err(err) => log::error!( + "Error from '{}' method: {err}", + ArkGenerics::VariableHasChildren.to_string() + ), + Ok(None) => {}, + Ok(Some(answer)) => return answer, + } + if RObject::view(value).is_s4() { unsafe { let names = RFunction::new("methods", ".slotNames") @@ -625,6 +688,15 @@ impl PositronVariable { return VariableKind::Empty; } + match try_from_method_variable_kind(x) { + Err(err) => log::error!( + "Error from '{}' method: {err}", + ArkGenerics::VariableKind.to_string() + ), + Ok(None) => {}, + Ok(Some(kind)) => return kind, + } + let obj = RObject::view(x); if obj.is_s4() { @@ -1266,6 +1338,16 @@ impl PositronVariable { } } +fn try_from_method_variable_kind(value: SEXP) -> anyhow::Result> { + let kind: Option = ArkGenerics::VariableKind.try_dispatch(value, vec![])?; + match kind { + None => Ok(None), + // We want to parse a VariableKind from it's string representation. + // We do that by reading from a json which is just `"{kind}"`. + Some(kind) => Ok(serde_json::from_str(format!(r#""{kind}""#).as_str())?), + } +} + pub fn is_binding_fancy(binding: &Binding) -> bool { match &binding.value { BindingValue::Active { .. } => true, @@ -1281,3 +1363,57 @@ pub fn plain_binding_force_with_rollback(binding: &Binding) -> anyhow::Result Err(anyhow!("Unexpected binding type")), } } + +#[cfg(test)] +mod tests { + use harp; + + use super::*; + use crate::r_task; + + #[test] + fn test_variable_with_methods() { + r_task(|| { + // Register the display value method + harp::parse_eval_global( + r#" + .ark.register_method("ark_positron_variable_display_value", "foo", function(x, width) { + # We return a large string and make sure it gets truncated. + paste0(rep("a", length.out = 2*width), collapse="") + }) + + .ark.register_method("ark_positron_variable_display_type", "foo", function(x, include_length) { + paste0("foo (", length(x), ")") + }) + + .ark.register_method("ark_positron_variable_has_children", "foo", function(x) { + FALSE + }) + + .ark.register_method("ark_positron_variable_kind", "foo", function(x) { + "other" + }) + + "#, + ) + .unwrap(); + + // Create an object with that class in an env. + let obj = harp::parse_eval_base(r#"structure(list(1,2,3), class = "foo")"#).unwrap(); + + let variable = + PositronVariable::from(String::from("foo"), String::from("foo"), obj.sexp); + + assert_eq!( + variable.var.display_value, + String::from("a".repeat(MAX_DISPLAY_VALUE_LENGTH)) + ); + + assert_eq!(variable.var.display_type, String::from("foo (3)")); + + assert_eq!(variable.var.has_children, false); + + assert_eq!(variable.var.kind, VariableKind::Other); + }) + } +} diff --git a/doc/variables-pane-extending.md b/doc/variables-pane-extending.md new file mode 100644 index 000000000..5afce42ec --- /dev/null +++ b/doc/variables-pane-extending.md @@ -0,0 +1,136 @@ +# Extending the Variables Pane in Ark + +Ark allows package authors to customize how the variables pane displays specific R objects by defining custom methods, similar to S3 methods. + +![Variables pane annotated](variables-pane.png) + +## Defining Ark Methods for S3 Classes + +To implement an Ark Variables Pane method for an S3 class (`"foo"`) in an R package, define pseudo-S3 methods like this: + +```r +ark_variable_display_value.foo <- function(x, ..., width = NULL) { + toString(x, width) +} +``` + +These methods don't need to be exported in the `NAMESPACE` file. Ark automatically finds and registers them when the package is loaded. + +You can also register a method outside an R package using `.ps.register_ark_method()`, similar to `base::.S3method()`: + +```r +.ps.register_ark_method("ark_variable_display_value", "foo", + function(x, width) { toString(x, width) }) +``` + +## Available Methods + +Ark currently supports six methods with the following signatures: + +- `ark_variable_display_value(x, ..., width = getOption("width"))` +- `ark_variable_display_type(x, ..., include_length = TRUE)` +- `ark_variable_kind(x, ...)` +- `ark_variable_has_children(x, ...)` +- `ark_variable_get_children(x, ...)` +- `ark_variable_get_child_at(x, ..., index, name)` + +### Customizing Display Value + +The `ark_variable_display_value` method customizes how the display value of an object is shown. This is the text marked as "1. Display value" in the image above. + +Example: + +```r +#' @param x Object to get the display value for +#' @param width Maximum expected width. This is just a suggestion, the UI +#' can stil truncate the string to different widths. +ark_variable_display_value.foo <- function(x, ..., width = getOption("width")) { + "Hello world" # Should return a length 1 character vector. +} +``` + +### Customizing Display Type + +The `ark_variable_display_type` method customizes how the type of an object is shown. This is marked as "2. Display type" in the image. + +Example: + +```r +#' @param x Object to get the display type for +#' @param include_length Boolean indicating whether to include object length. +ark_variable_display_type.foo <- function(x, ..., include_length = TRUE) { + sprintf("foo(%d)", length(x)) +} +``` + +### Specifying Variable Kind + +The `ark_variable_kind` method defines the kind of the variable. This allows the UI to organize variables in the variables pane differently. Currently, only `"table"` is used, but other possible values are [listed here](https://github.com/posit-dev/ark/blob/50f335183c5a13eda561a48d2ce21441caa79937/crates/amalthea/src/comm/variables_comm.rs#L107-L160). + +Example: + +```r +#' @param x Object to get the variable kind for +ark_variable_kind.foo <- function(x, ...) { + "other" +} +``` + +## Inspecting Objects + +Package authors can also implement methods that allow users to inspect R objects, similar to how the `str()` function works in R. This enables displaying object structures in the variables pane. + +### Checking for Children + +To check if an object has children that can be inspected, implement the `ark_variable_has_children` method: + +```r +#' @param x Check if `x` has children +ark_variable_has_children.foo <- function(x, ...) { + TRUE # Return TRUE if the object can be inspected, FALSE otherwise. +} +``` + +### Getting Children and Child Elements + +To allow inspection, implement these methods: + +- `ark_variable_get_children`: Returns a named list of child objects to be displayed. +- `ark_variable_get_child_at`: Retrieves a specific element from the object. + +Example: + +```r +ark_variable_get_children.foo <- function(x, ...) { + # Return an R list of children. The order of children should be + # stable between repeated calls on the same object. For example: + list( + a = c(1, 2, 3), + b = "Hello world", + c = list(1, 2, 3) + ) +} + +#' @param index An integer > 1, representing the index position of the child in the +#' list returned by `ark_variable_get_children()`. +#' @param name The name of the child, corresponding to `names(ark_variable_get_children(x))[index]`. +#' This may be a string or `NULL`. If using the name, it is the method author's responsibility to ensure +#' the name is a valid, unique accessor. Additionally, if the original name from `ark_variable_get_children()` +#' was too long, `ark` may discard the name and supply `name = NULL` instead. +ark_variable_get_child_at.foo <- function(x, ..., name, index) { + # This could be implemented as: + # ark_variable_get_children(x)[[index]] + # However, we expose an API that allows access by either name or index + # without needing to rebuild the full list of children. + + if (name == "a") { + c(1, 2, 3) + } else if (name == "b") { + "Hello world" + } else if (name == "c") { + list(1, 2, 3) + } else { + stop("Unknown name: ", name) + } +} +``` diff --git a/doc/variables-pane.png b/doc/variables-pane.png new file mode 100644 index 0000000000000000000000000000000000000000..fceff4298b24f506f29312e9c14d18a89e5df3be GIT binary patch literal 8246 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