Skip to content

Commit de2e8b6

Browse files
authored
Set options lazily and conditionally (#1123)
1 parent e838797 commit de2e8b6

11 files changed

Lines changed: 245 additions & 65 deletions

File tree

crates/ark/src/console/console_repl.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,12 @@ impl Console {
518518
startup::source_user_r_profile();
519519
}
520520

521+
// Apply Positron's default options after profiles so that user-defined
522+
// options take precedence over our defaults
523+
if let Some(ref ns) = console.positron_ns {
524+
modules::initialize_options(ns.sexp).log_err();
525+
}
526+
521527
// Start the REPL. Does not return!
522528
crate::sys::console::run_r();
523529
}

crates/ark/src/fixtures/utils.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ pub fn r_test_init() {
1818
harp::fixtures::r_test_init();
1919
INIT.call_once(|| {
2020
// Initialize the positron module so tests can use them.
21-
modules::initialize().unwrap();
21+
let ns = modules::initialize().unwrap();
22+
modules::initialize_options(ns.sexp).unwrap();
2223
});
2324
}
2425

crates/ark/src/modules.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ pub fn initialize() -> anyhow::Result<RObject> {
162162
Ok(namespace)
163163
}
164164

165+
pub fn initialize_options(ns: libr::SEXP) -> anyhow::Result<()> {
166+
RFunction::from("initialize_options").call_in(ns)?;
167+
Ok(())
168+
}
169+
165170
#[cfg(debug_assertions)]
166171
mod debug {
167172
use std::collections::HashMap;

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,6 @@ connection_focus <- function(id) {
107107
)
108108
}
109109

110-
options("connectionObserver" = .ps.connection_observer())
111-
112110
connection_flatten_object_types <- function(object_tree) {
113111
# RStudio actually flattens the objectTree to make it easier to find metadata for an object type.
114112
# See link below for the original implementation

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@
88
# The Ark graphics device name
99
ARK_GRAPHICS_DEVICE_NAME <- ".ark.graphics.device"
1010

11-
# Declare the function name that `dev.new()` and `GECurrentDevice()`
12-
# go looking for to create a new graphics device when the current one
13-
# is `"null device"` and a new plot is requested
14-
options(device = ARK_GRAPHICS_DEVICE_NAME)
15-
1611
# Set up "before plot new" hooks. This is our cue for
1712
# saving up the state of a plot before it gets wiped out.
1813
setHook("before.plot.new", action = "replace", function(...) {

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
#
66
#
77

8-
options(help_type = "html")
9-
108
# A wrapper around `help()` that works for our specific use cases:
119
# - Picks up devtools `help()` if the shim is on the search path.
1210
# - Expects that `topic` and `package` don't require NSE and are just strings or `NULL`.

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

Lines changed: 107 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,110 @@
55
#
66
#
77

8-
# Avoid overwhelming the console
9-
options(max.print = 1000)
10-
11-
# Enable HTML help
12-
options(help_type = "html")
13-
14-
# Use internal editor
15-
options(editor = function(file, title, ..., name = NULL) {
16-
handler_editor(file = file, title = title, ..., name = name)
17-
})
18-
19-
# Use custom browser implementation
20-
options(browser = function(url) {
21-
.ps.Call("ps_browse_url", as.character(url))
22-
})
23-
24-
# Register our password handler as the generic `askpass` option.
25-
# Same as RStudio, see `?rstudioapi::askForPassword` for rationale.
26-
options(askpass = function(prompt) {
27-
.ps.ui.askForPassword(prompt)
28-
})
29-
30-
# Show Plumber apps in the viewer
31-
options(plumber.docs.callback = function(url) {
32-
.ps.ui.showUrl(url)
33-
})
34-
35-
# Show Shiny applications in the viewer
36-
options(shiny.launch.browser = function(url) {
37-
.ps.ui.showUrl(url)
38-
})
39-
40-
# Show Profvis output in the viewer
41-
options(profvis.print = function(x) {
42-
# Render the widget to a tag list to create standalone HTML output.
43-
# (htmltools is a Profvis dependency so it's guaranteed to be available)
44-
rendered <- htmltools::as.tags(x, standalone = TRUE)
45-
46-
# Render the HTML content to a temporary file
47-
tmp_file <- htmltools::html_print(rendered, viewer = NULL)
48-
49-
# Pass the file to the viewer
50-
.ps.Call("ps_html_viewer", tmp_file, "R Profile", -1L, "editor")
51-
})
8+
# Called from Rust after sourcing the user's Rprofile so that user-defined
9+
# options take precedence over our defaults.
10+
initialize_options <- function() {
11+
# These options have non-NULL defaults in R, so we can't detect user
12+
# overrides by checking for NULL. They are always set unless the user
13+
# has listed the option name in `positron.protected_options`.
14+
15+
# Use Positron editor
16+
set_override(
17+
"editor",
18+
function(file, title, ..., name = NULL) {
19+
handler_editor(file = file, title = title, ..., name = name)
20+
}
21+
)
22+
23+
# Use Positron viewer to browse URLs
24+
set_override(
25+
"browser",
26+
function(url) {
27+
.ps.Call("ps_browse_url", as.character(url))
28+
}
29+
)
30+
31+
# Register our password handler as the generic `askpass` option.
32+
# Same as RStudio, see `?rstudioapi::askForPassword` for rationale.
33+
set_override(
34+
"askpass",
35+
function(prompt) {
36+
.ps.ui.askForPassword(prompt)
37+
}
38+
)
39+
40+
set_override("connectionObserver", .ps.connection_observer())
41+
42+
# Declare the function name that `dev.new()` and `GECurrentDevice()`
43+
# go looking for to create a new graphics device when the current one
44+
# is `"null device"` and a new plot is requested
45+
set_override("device", ARK_GRAPHICS_DEVICE_NAME)
46+
47+
# Avoid overwhelming the console
48+
set_override("max.print", 1000)
49+
50+
# These options default to NULL in R, so a non-NULL value means the
51+
# user has set them. They are only set when the current value is NULL,
52+
# unless the user has also listed them in `positron.protected_options`,
53+
# which allows the user to preserve the default `NULL` value.
54+
55+
# Enable HTML help
56+
set_default("help_type", "html")
57+
58+
set_default("viewer", viewer_option_handler)
59+
60+
# Show Shiny applications in the viewer
61+
set_default(
62+
"shiny.launch.browser",
63+
function(url) {
64+
.ps.ui.showUrl(url)
65+
}
66+
)
67+
68+
# Show Plumber apps in the viewer
69+
set_default(
70+
"plumber.docs.callback",
71+
function(url) {
72+
.ps.ui.showUrl(url)
73+
}
74+
)
75+
76+
# Show Profvis output in the viewer
77+
set_default(
78+
"profvis.print",
79+
function(x) {
80+
# Render the widget to a tag list to create standalone HTML output.
81+
# (htmltools is a Profvis dependency so it's guaranteed to be available)
82+
rendered <- htmltools::as.tags(x, standalone = TRUE)
83+
84+
# Render the HTML content to a temporary file
85+
tmp_file <- htmltools::html_print(rendered, viewer = NULL)
86+
87+
# Pass the file to the viewer
88+
.ps.Call("ps_html_viewer", tmp_file, "R Profile", -1L, "editor")
89+
}
90+
)
91+
}
92+
93+
is_protected <- function(name) {
94+
name %in% getOption("positron.protected_options", default = character())
95+
}
96+
97+
# Set an option unconditionally, unless listed in `positron.protected_options`
98+
set_override <- function(name, value) {
99+
if (is_protected(name)) {
100+
return(invisible())
101+
}
102+
do.call(options, set_names(list(value), name))
103+
}
104+
105+
# Set an option only when currently `NULL`, unless listed in `positron.protected_options`
106+
set_default <- function(name, value) {
107+
if (is_protected(name)) {
108+
return(invisible())
109+
}
110+
if (!is.null(getOption(name))) {
111+
return(invisible())
112+
}
113+
do.call(options, set_names(list(value), name))
114+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ paste_line <- function(x) {
171171
}
172172

173173
set_names <- function(x, names = x) {
174-
names(x) <- x
174+
names(x) <- names
175175
x
176176
}
177177

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#
66
#
77

8-
options("viewer" = function(url, height = NULL, ...) {
8+
viewer_option_handler <- function(url, height = NULL, ...) {
99
# Validate the URL argument.
1010
if (!is_string(url)) {
1111
stop("`url` must be a string.")
@@ -37,4 +37,4 @@ options("viewer" = function(url, height = NULL, ...) {
3737
# If not, fall back to opening it in the system browser.
3838
utils::browseURL(normalizedPath, ...)
3939
}
40-
})
40+
}

crates/ark/tests/options.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use std::io::Write;
2+
3+
use ark_test::DummyArkFrontendRprofile;
4+
5+
// These tests verify that options set in `.Rprofile` interact correctly with
6+
// `initialize_options()` during startup. Each test needs its own process
7+
// because `DummyArkFrontendRprofile` can only be locked once, which is
8+
// handled by nextest.
9+
10+
#[test]
11+
fn test_override_option_replaces_user_value() {
12+
let mut file = tempfile::NamedTempFile::new().unwrap();
13+
writeln!(file, "options(max.print = 500)").unwrap();
14+
unsafe { std::env::set_var("R_PROFILE_USER", file.path()) };
15+
16+
let frontend = DummyArkFrontendRprofile::lock();
17+
18+
frontend.execute_request("getOption('max.print')", |result| {
19+
assert_eq!(result, "[1] 1000");
20+
});
21+
}
22+
23+
#[test]
24+
fn test_protected_override_option_keeps_user_value() {
25+
let mut file = tempfile::NamedTempFile::new().unwrap();
26+
writeln!(
27+
file,
28+
"options(max.print = 500, positron.protected_options = 'max.print')"
29+
)
30+
.unwrap();
31+
unsafe { std::env::set_var("R_PROFILE_USER", file.path()) };
32+
33+
let frontend = DummyArkFrontendRprofile::lock();
34+
35+
frontend.execute_request("getOption('max.print')", |result| {
36+
assert_eq!(result, "[1] 500");
37+
});
38+
}
39+
40+
#[test]
41+
fn test_default_option_keeps_user_value() {
42+
let mut file = tempfile::NamedTempFile::new().unwrap();
43+
writeln!(file, "options(help_type = 'text')").unwrap();
44+
unsafe { std::env::set_var("R_PROFILE_USER", file.path()) };
45+
46+
let frontend = DummyArkFrontendRprofile::lock();
47+
48+
frontend.execute_request("getOption('help_type')", |result| {
49+
assert_eq!(result, "[1] \"text\"");
50+
});
51+
}
52+
53+
#[test]
54+
fn test_default_option_sets_when_null() {
55+
let mut file = tempfile::NamedTempFile::new().unwrap();
56+
writeln!(file).unwrap();
57+
unsafe { std::env::set_var("R_PROFILE_USER", file.path()) };
58+
59+
let frontend = DummyArkFrontendRprofile::lock();
60+
61+
frontend.execute_request("getOption('help_type')", |result| {
62+
assert_eq!(result, "[1] \"html\"");
63+
});
64+
}
65+
66+
#[test]
67+
fn test_protected_default_option_stays_null() {
68+
let mut file = tempfile::NamedTempFile::new().unwrap();
69+
writeln!(file, "options(positron.protected_options = 'help_type')").unwrap();
70+
unsafe { std::env::set_var("R_PROFILE_USER", file.path()) };
71+
72+
let frontend = DummyArkFrontendRprofile::lock();
73+
74+
frontend.execute_request("is.null(getOption('help_type'))", |result| {
75+
assert_eq!(result, "[1] TRUE");
76+
});
77+
}

0 commit comments

Comments
 (0)