diff --git a/crates/ark/src/connections/r_connection.rs b/crates/ark/src/connections/r_connection.rs index bb30f50ec..a88d5ff58 100644 --- a/crates/ark/src/connections/r_connection.rs +++ b/crates/ark/src/connections/r_connection.rs @@ -358,3 +358,22 @@ pub unsafe extern "C-unwind" fn ps_connection_updated(id: SEXP) -> Result Result { + if !RMain::is_initialized() { + return Ok(R_NilValue); + } + + let main = RMain::get(); + let comm_id: String = RObject::view(id).to::()?; + + let event = ConnectionsFrontendEvent::Focus; + + main.get_comm_manager_tx().send(CommManagerEvent::Message( + comm_id, + CommMsg::Data(serde_json::to_value(event)?), + ))?; + + Ok(R_NilValue) +} diff --git a/crates/ark/src/methods.rs b/crates/ark/src/methods.rs index 8f9f22f8b..61b8dca9e 100644 --- a/crates/ark/src/methods.rs +++ b/crates/ark/src/methods.rs @@ -45,6 +45,9 @@ pub enum ArkGenerics { #[strum(serialize = "ark_positron_help_get_handler")] HelpGetHandler, + + #[strum(serialize = "ark_positron_variable_view")] + VariableView, } impl ArkGenerics { diff --git a/crates/ark/src/modules/positron/connection.R b/crates/ark/src/modules/positron/connection.R index 601481cdf..abea1beeb 100644 --- a/crates/ark/src/modules/positron/connection.R +++ b/crates/ark/src/modules/positron/connection.R @@ -21,6 +21,10 @@ .ps.Call("ps_connection_updated", id) } +connection_focus <- function(id) { + .ps.Call("ps_connection_focus", id) +} + #' @export .ps.connection_observer <- function() { connections <- new.env(parent = emptyenv()) @@ -45,6 +49,7 @@ for (id in ls(envir = connections)) { con <- get(id, envir = connections) if (identical(con$host, host) && identical(con$type, type)) { + connection_focus(id) return(invisible(id)) } } @@ -67,6 +72,7 @@ # until the end of the connection. objectTypes = connection_flatten_object_types(listObjectTypes()) ) + connection_focus(id) invisible(id) } @@ -170,6 +176,36 @@ connection_flatten_object_types <- function(object_tree) { return(TRUE) } +# Helper to reconstruct ODBC connection code from connection info +odbc_connection_code <- function(info) { + # Try to reconstruct a reasonable connection string + # Priority: DSN > connection string parameters + if (!is.null(info$sourcename) && nzchar(info$sourcename)) { + # DSN-based connection + sprintf('odbc::dbConnect(odbc::odbc(), dsn = "%s")', info$sourcename) + } else { + # Build connection string from available parameters + params <- character() + + if (!is.null(info$servername) && nzchar(info$servername)) { + params <- c(params, sprintf('server = "%s"', info$servername)) + } + if (!is.null(info$dbname) && nzchar(info$dbname)) { + params <- c(params, sprintf('database = "%s"', info$dbname)) + } + + if (length(params) > 0) { + sprintf( + "odbc::dbConnect(odbc::odbc(), %s)", + paste(params, collapse = ", ") + ) + } else { + # Fallback if we can't determine connection parameters + "# Connection opened from Variables Pane" + } + } +} + #' @export .ps.connection_icon <- function(id, ...) { con <- get(id, getOption("connectionObserver")$.connections) @@ -283,6 +319,107 @@ connection_flatten_object_types <- function(object_tree) { actions = NULL ) - if (is.null(id)) return("hello") + if (is.null(id)) { + return("hello") + } id } + +# ODBC Connection Support +# These methods enable viewing ODBC connections in the Connections Pane +# from the Variables Pane + +# Register ODBC connection methods when the odbc package is loaded +setHook( + packageEvent("odbc", "onLoad"), + function(...) { + # Mark ODBC connections as "connection" kind + .ark.register_method( + "ark_positron_variable_kind", + "OdbcConnection", + function(x) "connection" + ) + + # Enable viewer for valid ODBC connections + .ark.register_method( + "ark_positron_variable_has_viewer", + "OdbcConnection", + function(x) { + odbc::dbIsValid(x) + } + ) + + # View an ODBC connection in the Connections Pane + .ark.register_method( + "ark_positron_variable_view", + "OdbcConnection", + function(x) { + # Reconstruct the connection code from the connection info + info <- x@info + code <- odbc_connection_code(info) + + # Use odbc's built-in connection observer integration + odbc:::on_connection_opened(x, code = code) + invisible(TRUE) + } + ) + } +) + +# BigQuery Connection Support +# These methods enable viewing BigQuery connections in the Connections Pane +# from the Variables Pane + +# Register BigQuery connection methods when the bigrquery package is loaded +setHook( + packageEvent("bigrquery", "onLoad"), + function(...) { + # Mark BigQuery connections as "connection" kind + .ark.register_method( + "ark_positron_variable_kind", + "BigQueryConnection", + function(x) "connection" + ) + + # Enable viewer for valid BigQuery connections + .ark.register_method( + "ark_positron_variable_has_viewer", + "BigQueryConnection", + function(x) { + DBI::dbIsValid(x) + } + ) + + # View a BigQuery connection in the Connections Pane + .ark.register_method( + "ark_positron_variable_view", + "BigQueryConnection", + function(x) { + # Reconstruct the connection code + code <- bigrquery_connection_code(x) + + # Use bigrquery's built-in connection observer integration + bigrquery:::on_connection_opened(x, code = code) + invisible(TRUE) + } + ) + } +) + +# Helper to reconstruct BigQuery connection code +bigrquery_connection_code <- function(con) { + project <- con@project + dataset <- con@dataset + billing <- con@billing + + params <- c(sprintf('project = "%s"', project)) + + if (!is.null(dataset) && nzchar(dataset)) { + params <- c(params, sprintf('dataset = "%s"', dataset)) + } + if (!is.null(billing) && nzchar(billing) && billing != project) { + params <- c(params, sprintf('billing = "%s"', billing)) + } + + sprintf("DBI::dbConnect(bigrquery::bigquery(), %s)", paste(params, collapse = ", ")) +} diff --git a/crates/ark/src/modules/positron/methods.R b/crates/ark/src/modules/positron/methods.R index 5c9a4337d..3b81cc791 100644 --- a/crates/ark/src/modules/positron/methods.R +++ b/crates/ark/src/modules/positron/methods.R @@ -92,6 +92,16 @@ ark_methods_table$ark_positron_variable_get_children <- new.env( ark_methods_table$ark_positron_help_get_handler <- new.env( parent = emptyenv() ) + +#' Custom view action for objects in Variables Pane +#' +#' @param x Object to view +#' @param ... Additional arguments (unused) +#' @return Logical value: TRUE on success, FALSE otherwise +ark_methods_table$ark_positron_variable_view <- new.env( + parent = emptyenv() +) + lockEnvironment(ark_methods_table, TRUE) ark_methods_allowed_packages <- c("torch", "reticulate", "duckplyr") @@ -169,7 +179,14 @@ call_ark_method <- function(generic, object, ...) { return(NULL) } - for (cls in class(object)) { + # Get all classes to check, including S4 superclasses + classes <- class(object) + if (isS4(object)) { + # For S4 objects, get the full inheritance hierarchy + classes <- methods::extends(class(object)) + } + + for (cls in classes) { if (!is.null(method <- get0(cls, envir = methods_table))) { return(eval( as.call(list(method, object, ...)), diff --git a/crates/ark/src/variables/r_variables.rs b/crates/ark/src/variables/r_variables.rs index 7db8fd0c7..793ba040f 100644 --- a/crates/ark/src/variables/r_variables.rs +++ b/crates/ark/src/variables/r_variables.rs @@ -45,6 +45,7 @@ use crate::data_explorer::summary_stats::summary_stats; use crate::lsp::events::EVENTS; use crate::r_task; use crate::thread::RThreadSafe; +use crate::variables::variable::try_dispatch_view; use crate::variables::variable::PositronVariable; use crate::view::view; @@ -381,6 +382,11 @@ impl RVariables { let env = self.env.get().clone(); let obj = PositronVariable::resolve_data_object(env.clone(), &path)?; + // Try custom view method first (e.g., for connections) + if try_dispatch_view(obj.sexp).map_err(harp::Error::Anyhow)? { + return Ok(None); + } + if r_is_function(obj.sexp) { harp::as_result(view(&obj, &path, &env))?; return Ok(None); diff --git a/crates/ark/src/variables/variable.rs b/crates/ark/src/variables/variable.rs index 6aa8c9ceb..56edd0ce4 100644 --- a/crates/ark/src/variables/variable.rs +++ b/crates/ark/src/variables/variable.rs @@ -594,11 +594,7 @@ fn has_viewer(value: SEXP) -> bool { return true; } - if !(r_is_data_frame(value) || r_is_matrix(value)) { - return false; - } - - // We have a data.frame or matrix. Dispatch to the has_viewer method + // Dispatch to the has_viewer method for any object match ArkGenerics::VariableHasViewer.try_dispatch::(value, vec![]) { Err(err) => { log::error!( @@ -608,10 +604,30 @@ fn has_viewer(value: SEXP) -> bool { // The viewer method exists, but failed false }, - // A matching viewer method was not found - Ok(None) => true, // The viewer method was found, use its result Ok(Some(val)) => val, + // No method found, fall back to default logic for data frames/matrices + Ok(None) => r_is_data_frame(value) || r_is_matrix(value), + } +} + +/// Try to view an object using a custom view method. +/// This dispatches to the `ark_positron_variable_view` method. +/// Returns Ok(true) if a custom view was handled, Ok(false) if no method found, +/// or Err if the method failed. +pub fn try_dispatch_view(value: SEXP) -> anyhow::Result { + match ArkGenerics::VariableView.try_dispatch::(value, vec![]) { + Err(err) => { + return Err(anyhow!("Error in custom view: {err}")); + }, + Ok(None) => { + // No custom view method found + Ok(false) + }, + Ok(Some(false)) => { + return Err(anyhow!("Custom view method failed")); + }, + Ok(Some(true)) => Ok(true), } } @@ -1780,9 +1796,8 @@ mod tests { assert_eq!(variable.kind, VariableKind::Other); - // Even though the viewer method returns TRUE, the object is not a data.frame - // or matrix, so it doesn't have a viewer. - assert_eq!(variable.has_viewer, false); + // The has_viewer method returns TRUE, so the object has a viewer + assert_eq!(variable.has_viewer, true); // Now inspect `x` let path = vec![String::from("x")];