Skip to content

Commit 778da3e

Browse files
Add plot file/origin attribution (#1067)
* initial impl * add ui comm to dummy frontend * update formatting * code review feedback * code review feedpack part 2 * Update crates/ark/src/plots/graphics_device.rs Co-authored-by: Davis Vaughan <davis@rstudio.com> * test nested source contexts * simplify source context push * explicit drop context --------- Co-authored-by: Davis Vaughan <davis@rstudio.com>
1 parent 8c8274d commit 778da3e

File tree

16 files changed

+594
-55
lines changed

16 files changed

+594
-55
lines changed

crates/amalthea/src/comm/connections_comm.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @generated
22

33
/*---------------------------------------------------------------------------------------------
4-
* Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved.
4+
* Copyright (C) 2024-2026 Posit Software, PBC. All rights reserved.
55
*--------------------------------------------------------------------------------------------*/
66

77
//

crates/amalthea/src/comm/help_comm.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @generated
22

33
/*---------------------------------------------------------------------------------------------
4-
* Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved.
4+
* Copyright (C) 2024-2026 Posit Software, PBC. All rights reserved.
55
*--------------------------------------------------------------------------------------------*/
66

77
//

crates/amalthea/src/comm/plot_comm.rs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @generated
22

33
/*---------------------------------------------------------------------------------------------
4-
* Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved.
4+
* Copyright (C) 2024-2026 Posit Software, PBC. All rights reserved.
55
*--------------------------------------------------------------------------------------------*/
66

77
//
@@ -30,7 +30,7 @@ pub struct IntrinsicSize {
3030
/// The plot's metadata
3131
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
3232
pub struct PlotMetadata {
33-
/// A human-readable name for the plot
33+
/// A unique, human-readable name for the plot
3434
pub name: String,
3535

3636
/// The kind of plot e.g. 'Matplotlib', 'ggplot2', etc.
@@ -40,7 +40,10 @@ pub struct PlotMetadata {
4040
pub execution_id: String,
4141

4242
/// The code fragment that produced the plot
43-
pub code: String
43+
pub code: String,
44+
45+
/// The origin of the plot, if known
46+
pub origin: Option<PlotOrigin>
4447
}
4548

4649
/// A rendered plot
@@ -66,6 +69,34 @@ pub struct PlotSize {
6669
pub width: i64
6770
}
6871

72+
/// The origin (source) of a plot
73+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
74+
pub struct PlotOrigin {
75+
/// The URI of the document containing the code that produced the plot, if
76+
/// available
77+
pub uri: String,
78+
79+
/// The range within the document at uri that produced the plot, if
80+
/// available
81+
pub range: Option<PlotRange>
82+
}
83+
84+
/// The range of a plot within a document
85+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
86+
pub struct PlotRange {
87+
/// The line number on which the plot starts (0-indexed)
88+
pub start_line: i64,
89+
90+
/// The character number on which the plot starts (0-indexed)
91+
pub start_character: i64,
92+
93+
/// The line number on which the plot ends (0-indexed)
94+
pub end_line: i64,
95+
96+
/// The character number on which the plot ends (0-indexed)
97+
pub end_character: i64
98+
}
99+
69100
/// The settings used to render the plot
70101
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
71102
pub struct PlotRenderSettings {

crates/amalthea/src/comm/variables_comm.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @generated
22

33
/*---------------------------------------------------------------------------------------------
4-
* Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved.
4+
* Copyright (C) 2024-2026 Posit Software, PBC. All rights reserved.
55
*--------------------------------------------------------------------------------------------*/
66

77
//

crates/ark/src/console.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1561,10 +1561,16 @@ impl Console {
15611561
reply_tx,
15621562
});
15631563

1564-
// Push execution context to graphics device for plot attribution
1564+
// Push execution context to graphics device for plot attribution.
1565+
// Extract code_location from the execute request for source file origin.
1566+
let code_location = exec_req.code_location().log_err().flatten().map(|mut loc| {
1567+
loc.uri = ExtUrl::normalize(loc.uri);
1568+
loc
1569+
});
15651570
graphics_device::on_execute_request(
15661571
originator.header.msg_id.clone(),
15671572
exec_req.code.clone(),
1573+
code_location,
15681574
);
15691575

15701576
input

crates/ark/src/modules/positron/completions.R

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ customCompletionHandlers <- new.env(parent = emptyenv())
122122
"Sys.setenv",
123123
"...",
124124
function(position) {
125-
if (position != "name") return(NULL)
125+
if (position != "name") {
126+
return(NULL)
127+
}
126128

127129
.ps.completions.createCustomCompletions(
128130
values = names(Sys.getenv()),
@@ -180,8 +182,9 @@ customCompletionHandlers <- new.env(parent = emptyenv())
180182
)
181183

182184
method <- eval(call, envir = globalenv())
183-
if (is.function(method))
185+
if (is.function(method)) {
184186
return(.ps.completions.formalNamesDefault(method))
187+
}
185188
}
186189
}
187190

crates/ark/src/modules/positron/connection.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,5 +421,8 @@ bigrquery_connection_code <- function(con) {
421421
params <- c(params, sprintf('billing = "%s"', billing))
422422
}
423423

424-
sprintf("DBI::dbConnect(bigrquery::bigquery(), %s)", paste(params, collapse = ", "))
424+
sprintf(
425+
"DBI::dbConnect(bigrquery::bigquery(), %s)",
426+
paste(params, collapse = ", ")
427+
)
425428
}

crates/ark/src/modules/positron/graphics.R

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,4 +529,3 @@ render_path <- function(id, format) {
529529
file <- paste0("render-", id, ".", format)
530530
file.path(directory, file)
531531
}
532-

crates/ark/src/modules/positron/hooks_source.R

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,28 @@ make_ark_source <- function(original_source) {
7070
args$catch.aborts <- NULL
7171
}
7272

73+
# Try to resolve the file URI early so we can attribute plots to this
74+
# source file. This is best-effort; if it fails we proceed without attribution.
75+
uri <- tryCatch(path_to_file_uri(file), error = function(e) NULL)
76+
77+
# Push source context for plot attribution (if we have a URI).
78+
# The defer ensures we always pop, even if source() errors.
79+
# Use tryCatch so that source() still works if the native function
80+
# is not yet available (e.g. during development with mismatched builds).
81+
if (!is.null(uri)) {
82+
tryCatch(
83+
{
84+
.ps.Call("ps_graphics_push_source_context", uri)
85+
defer(tryCatch(
86+
.ps.Call("ps_graphics_pop_source_context"),
87+
error = function(e) NULL
88+
))
89+
TRUE
90+
},
91+
error = function(e) FALSE
92+
)
93+
}
94+
7395
# DRY: Promise for calling `original_source` with all arguments.
7496
# Evaluated lazily only when needed for fallback paths.
7597
eval(bquote(
@@ -97,7 +119,6 @@ make_ark_source <- function(original_source) {
97119
return(fall_back)
98120
}
99121

100-
uri <- path_to_file_uri(file)
101122
if (is.null(uri)) {
102123
return(fall_back)
103124
}

crates/ark/src/modules/positron/package.R

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,21 @@
7979

8080
get_package_addins <- function(pkg) {
8181
path <- system.file("rstudio", "addins.dcf", package = pkg)
82-
if (!nzchar(path)) return(list())
82+
if (!nzchar(path)) {
83+
return(list())
84+
}
8385

8486
dcf <- tryCatch(read.dcf(path), error = function(cnd) {
85-
log_error(sprintf("Failed to read addins.dcf for '%s': %s", pkg, conditionMessage(cnd)))
87+
log_error(sprintf(
88+
"Failed to read addins.dcf for '%s': %s",
89+
pkg,
90+
conditionMessage(cnd)
91+
))
8692
NULL
8793
})
88-
if (is.null(dcf)) return(list())
94+
if (is.null(dcf)) {
95+
return(list())
96+
}
8997

9098
cols <- colnames(dcf)
9199
out <- lapply(seq_len(nrow(dcf)), function(i) {
@@ -96,7 +104,9 @@ get_package_addins <- function(pkg) {
96104

97105
parse_addin_row <- function(dcf, row, cols, pkg) {
98106
binding <- dcf_field(dcf, row, "Binding", cols)
99-
if (!nzchar(binding)) return(NULL)
107+
if (!nzchar(binding)) {
108+
return(NULL)
109+
}
100110

101111
list(
102112
name = dcf_field(dcf, row, "Name", cols),
@@ -120,7 +130,9 @@ parse_addin_row <- function(dcf, row, cols, pkg) {
120130

121131
get_package_templates <- function(pkg) {
122132
path <- system.file("rmarkdown", "templates", package = pkg)
123-
if (!nzchar(path)) return(list())
133+
if (!nzchar(path)) {
134+
return(list())
135+
}
124136

125137
dirs <- list.dirs(path, recursive = FALSE, full.names = TRUE)
126138
out <- lapply(dirs, function(dir) parse_template_dir(dir, pkg))
@@ -129,13 +141,21 @@ get_package_templates <- function(pkg) {
129141

130142
parse_template_dir <- function(dir, pkg) {
131143
yaml_path <- file.path(dir, "template.yaml")
132-
if (!file.exists(yaml_path)) return(NULL)
144+
if (!file.exists(yaml_path)) {
145+
return(NULL)
146+
}
133147

134148
meta <- tryCatch(yaml::read_yaml(yaml_path), error = function(cnd) {
135-
log_error(sprintf("Failed to read template.yaml at '%s': %s", yaml_path, conditionMessage(cnd)))
149+
log_error(sprintf(
150+
"Failed to read template.yaml at '%s': %s",
151+
yaml_path,
152+
conditionMessage(cnd)
153+
))
136154
NULL
137155
})
138-
if (is.null(meta)) return(NULL)
156+
if (is.null(meta)) {
157+
return(NULL)
158+
}
139159

140160
list(
141161
name = meta$name %||% basename(dir),

0 commit comments

Comments
 (0)