Skip to content

Commit dabe0ab

Browse files
committed
Set default version for positron as well
This is only for new projects, because positron defaults using the project-dependent R version otherwise.
1 parent 1691fcd commit dabe0ab

6 files changed

Lines changed: 246 additions & 66 deletions

File tree

src/common.rs

Lines changed: 234 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::path::Path;
66
use std::path::PathBuf;
77

88
use clap::ArgMatches;
9-
use jsonc_parser::cst::{CstInputValue, CstRootNode};
9+
use jsonc_parser::cst::{CstInputValue, CstObject, CstRootNode};
1010
use jsonc_parser::ParseOptions;
1111
use log::{debug, error, info, warn};
1212
use semver::Version;
@@ -555,91 +555,138 @@ fn get_project_version(path: &str) -> Result<Option<String>, Box<dyn Error>> {
555555

556556
// -- Positron ------------------------------------------------------------
557557

558-
pub fn ensure_positron_custom_root_folders() -> Result<(), Box<dyn Error>> {
559-
// Only user mode installs R outside of the directories Positron already
560-
// knows about.
561-
if get_mode()? != Mode::User {
558+
// Register rig's R installation root in Positron, and, if `default_ver` is
559+
// set, also make that R version Positron's default. Both settings live in the
560+
// same file, so we read, parse and write it once.
561+
pub fn ensure_positron_setup(default_ver: Option<&str>) -> Result<(), Box<dyn Error>> {
562+
let settings_path = match positron_settings_path()? {
563+
Some(x) => x,
564+
None => return Ok(()),
565+
};
566+
let r_root = get_r_root()?;
567+
let contents = read_positron_settings(&settings_path)?;
568+
569+
let root = CstRootNode::parse(&contents, &ParseOptions::default())?;
570+
let obj = root
571+
.object_value_or_create()
572+
.ok_or_else(|| SimpleError::new(format!("{} is not a JSON object", POSITRON_SETTINGS)))?;
573+
574+
let mut changed =
575+
add_to_json_string_array(&obj, POSITRON_ROOTS_KEY, &r_root, POSITRON_SETTINGS);
576+
577+
if let Some(ver) = default_ver {
578+
let rbin = positron_r_binary(ver)?;
579+
let rbin = rbin.to_string_lossy();
580+
changed |= set_json_string(
581+
&obj,
582+
POSITRON_DEFAULT_KEY,
583+
&rbin,
584+
&r_root,
585+
POSITRON_SETTINGS,
586+
);
587+
}
588+
589+
// Nothing to update
590+
if !changed {
562591
return Ok(());
563592
}
564593

594+
write_positron_settings(&settings_path, &root.to_string())?;
595+
// `rig default` is quiet about this, it only updates the default R version
596+
// that is already registered in Positron.
597+
if default_ver.is_none() {
598+
OUTPUT.success("Updated Positron settings");
599+
}
600+
601+
Ok(())
602+
}
603+
604+
const POSITRON_ROOTS_KEY: &str = "positron.r.customRootFolders";
605+
const POSITRON_DEFAULT_KEY: &str = "positron.r.interpreters.default";
606+
const POSITRON_SETTINGS: &str = "Positron settings.json";
607+
608+
// The R binary of `ver`, as Positron records it. Positron compares its default
609+
// R setting to the R binary paths it discovered, as plain strings, so we have
610+
// to write the exact same path here. It resolves symbolic links while
611+
// discovering, so the `Current` link is not an option, we need the versioned
612+
// path. On Windows it looks for `bin\x64\R.exe` first and only falls back to
613+
// `bin\R.exe` (which exists as well) if that is missing, e.g. on aarch64.
614+
#[cfg(target_os = "windows")]
615+
fn positron_r_binary(ver: &str) -> Result<PathBuf, Box<dyn Error>> {
616+
let x64 = get_r_binary_x64(ver)?;
617+
if x64.exists() {
618+
return Ok(x64);
619+
}
620+
get_r_binary(ver)
621+
}
622+
623+
#[cfg(not(target_os = "windows"))]
624+
fn positron_r_binary(ver: &str) -> Result<PathBuf, Box<dyn Error>> {
625+
get_r_binary(ver)
626+
}
627+
628+
// Path of Positron's user settings file, or `None` if no update.
629+
fn positron_settings_path() -> Result<Option<PathBuf>, Box<dyn Error>> {
630+
if get_mode()? != Mode::User {
631+
return Ok(None);
632+
}
633+
565634
if let Some(val) = crate::config::get_global_config_value("positron-setup")? {
566635
if val == "false" {
567636
debug!("Skipping Positron setup (positron-setup=false in rig config)");
568-
return Ok(());
637+
return Ok(None);
569638
}
570639
}
571640

572641
let positron_dir = positron_user_data_dir()?;
573642
if !positron_dir.exists() {
574643
debug!("Skipping Positron setup; Positron not found");
575-
return Ok(());
644+
return Ok(None);
576645
}
577-
let settings_path = positron_dir.join("User").join("settings.json");
578-
let r_root = get_r_root()?;
579646

580-
let contents = if settings_path.exists() {
581-
std::fs::read_to_string(&settings_path)?
582-
} else {
583-
"{}".to_string()
584-
};
647+
Ok(Some(positron_dir.join("User").join("settings.json")))
648+
}
585649

586-
let new_contents = match add_to_json_string_array(
587-
&contents,
588-
POSITRON_ROOTS_KEY,
589-
&r_root,
590-
"Positron settings.json",
591-
)? {
592-
Some(x) => x,
593-
// Already there, or the key has an unexpected type
594-
None => return Ok(()),
595-
};
650+
fn read_positron_settings(path: &Path) -> Result<String, Box<dyn Error>> {
651+
if path.exists() {
652+
Ok(std::fs::read_to_string(path)?)
653+
} else {
654+
Ok("{}".to_string())
655+
}
656+
}
596657

597-
if let Some(parent) = settings_path.parent() {
658+
fn write_positron_settings(path: &Path, contents: &str) -> Result<(), Box<dyn Error>> {
659+
if let Some(parent) = path.parent() {
598660
std::fs::create_dir_all(parent)?;
599661
}
600-
std::fs::write(&settings_path, new_contents)?;
601-
OUTPUT.success("Registered rig R versions in Positron");
602-
662+
std::fs::write(path, contents)?;
603663
Ok(())
604664
}
605665

606-
const POSITRON_ROOTS_KEY: &str = "positron.r.customRootFolders";
607-
608-
// Add `value` to the string array at `key` of a JSON object, creating the
609-
// array if needed. Returns the new file contents, or `None` if nothing needs
610-
// to change: either `value` is already in the array, or `key` is set to
611-
// something that is not an array, in which case we leave it alone.
666+
// Add `value` to the string array at `key` of `obj`, creating the array if
667+
// needed. Returns whether `obj` was changed: it is left alone if `value` is
668+
// already in the array, or if `key` is set to something that is not an array.
612669
//
613670
// Editor settings files are JSONC: they may contain comments and trailing
614-
// commas, and users care about their formatting. So we parse the text into a
615-
// concrete syntax tree and edit that in place, instead of re-serializing a
616-
// `serde_json::Value`, which would drop comments and reformat everything.
617-
fn add_to_json_string_array(
618-
contents: &str,
619-
key: &str,
620-
value: &str,
621-
what: &str,
622-
) -> Result<Option<String>, Box<dyn Error>> {
623-
let root = CstRootNode::parse(contents, &ParseOptions::default())?;
624-
let obj = root
625-
.object_value_or_create()
626-
.ok_or_else(|| SimpleError::new(format!("{} is not a JSON object", what)))?;
627-
671+
// commas, and users care about their formatting. So we edit the concrete
672+
// syntax tree in place, instead of re-serializing a `serde_json::Value`,
673+
// which would drop comments and reformat everything.
674+
fn add_to_json_string_array(obj: &CstObject, key: &str, value: &str, what: &str) -> bool {
628675
match obj.get(key) {
629676
Some(prop) => {
630677
let arr = match prop.array_value() {
631678
Some(arr) => arr,
632679
None => {
633680
// Unexpected type — leave it alone and inform
634-
info!(
681+
debug!(
635682
"{}: setting '{}' is not an array ({}); not modifying",
636683
what,
637684
key,
638685
prop.value()
639686
.map(|v| v.to_string())
640687
.unwrap_or_else(|| "".to_string())
641688
);
642-
return Ok(None);
689+
return false;
643690
}
644691
};
645692
// Already contains our value — nothing to do
@@ -650,21 +697,63 @@ fn add_to_json_string_array(
650697
== Some(value)
651698
});
652699
if have {
653-
return Ok(None);
700+
return false;
654701
}
655702
arr.append(CstInputValue::String(value.to_string()));
656-
info!("{}: appended \"{}\" to setting '{}'", what, value, key);
703+
debug!("{}: appended \"{}\" to setting '{}'", what, value, key);
657704
}
658705
None => {
659706
obj.append(
660707
key,
661708
CstInputValue::Array(vec![CstInputValue::String(value.to_string())]),
662709
);
663-
info!("{}: set setting '{}' = [\"{}\"]", what, key, value);
710+
debug!("{}: set setting '{}' = [\"{}\"]", what, key, value);
711+
}
712+
}
713+
714+
true
715+
}
716+
717+
// Set the string at `key` of `obj` to `value`, creating the key if needed.
718+
// Returns whether `obj` was changed: it is left alone if `key` is already set
719+
// to `value`, or if it is set to something rig does not manage, i.e. a value
720+
// that is not a string, or a path outside of `owned_dir`. The latter is the
721+
// user's own setting.
722+
fn set_json_string(obj: &CstObject, key: &str, value: &str, owned_dir: &str, what: &str) -> bool {
723+
match obj.get(key) {
724+
Some(prop) => {
725+
let old = prop
726+
.value()
727+
.and_then(|v| v.as_string_lit())
728+
.and_then(|s| s.decoded_value().ok());
729+
let old = match old {
730+
Some(old) => old,
731+
None => {
732+
// Unexpected type — leave it alone and inform
733+
debug!("{}: setting '{}' is not a string; not modifying", what, key);
734+
return false;
735+
}
736+
};
737+
if old == value {
738+
return false;
739+
}
740+
if !Path::new(&old).starts_with(owned_dir) {
741+
debug!(
742+
"{}: setting '{}' is \"{}\", which is not an R version managed \
743+
by rig; not modifying",
744+
what, key, old
745+
);
746+
return false;
747+
}
748+
prop.set_value(CstInputValue::String(value.to_string()));
749+
}
750+
None => {
751+
obj.append(key, CstInputValue::String(value.to_string()));
664752
}
665753
}
754+
debug!("{}: set setting '{}' = \"{}\"", what, key, value);
666755

667-
Ok(Some(root.to_string()))
756+
true
668757
}
669758

670759
fn positron_user_data_dir() -> Result<PathBuf, Box<dyn Error>> {
@@ -1055,9 +1144,22 @@ fn sc_available_rtools_versions(
10551144
mod tests {
10561145
use super::*;
10571146

1147+
// Apply `f` to the parsed `contents`, and return the new contents, or
1148+
// `None` if `f` did not change anything.
1149+
fn edit(contents: &str, f: impl Fn(&CstObject) -> bool) -> Option<String> {
1150+
let root = CstRootNode::parse(contents, &ParseOptions::default()).unwrap();
1151+
let obj = root.object_value_or_create().unwrap();
1152+
if f(&obj) {
1153+
Some(root.to_string())
1154+
} else {
1155+
None
1156+
}
1157+
}
1158+
10581159
fn add_root(contents: &str) -> Option<String> {
1059-
add_to_json_string_array(contents, POSITRON_ROOTS_KEY, "/home/u/r", "settings.json")
1060-
.unwrap()
1160+
edit(contents, |obj| {
1161+
add_to_json_string_array(obj, POSITRON_ROOTS_KEY, "/home/u/r", "settings.json")
1162+
})
10611163
}
10621164

10631165
#[test]
@@ -1115,6 +1217,82 @@ mod tests {
11151217
.contains("// only a comment"));
11161218
}
11171219

1220+
fn set_default(contents: &str) -> Option<String> {
1221+
edit(contents, |obj| {
1222+
set_json_string(
1223+
obj,
1224+
POSITRON_DEFAULT_KEY,
1225+
"/home/u/r/4.5.1/bin/R",
1226+
"/home/u/r",
1227+
"settings.json",
1228+
)
1229+
})
1230+
}
1231+
1232+
#[test]
1233+
fn test_json_string_new_key() {
1234+
assert_eq!(
1235+
set_default("{}").unwrap(),
1236+
"{\n \"positron.r.interpreters.default\": \"/home/u/r/4.5.1/bin/R\"\n}"
1237+
);
1238+
}
1239+
1240+
#[test]
1241+
fn test_json_string_updates_rig_version() {
1242+
let inp = r#"{
1243+
// R settings
1244+
"positron.r.interpreters.default": "/home/u/r/4.4.2/bin/R",
1245+
"editor.fontSize": 12
1246+
}
1247+
"#;
1248+
let out = set_default(inp).unwrap();
1249+
assert!(out.contains("// R settings"));
1250+
assert!(out.contains("\"positron.r.interpreters.default\": \"/home/u/r/4.5.1/bin/R\""));
1251+
assert!(!out.contains("4.4.2"));
1252+
}
1253+
1254+
#[test]
1255+
fn test_json_string_already_there() {
1256+
let inp = r#"{ "positron.r.interpreters.default": "/home/u/r/4.5.1/bin/R" }"#;
1257+
assert_eq!(set_default(inp), None);
1258+
}
1259+
1260+
#[test]
1261+
fn test_json_string_keeps_foreign_value() {
1262+
// Not one of our R versions, the user set this themselves
1263+
let inp = r#"{ "positron.r.interpreters.default": "/usr/lib/R/bin/R" }"#;
1264+
assert_eq!(set_default(inp), None);
1265+
// `/home/u/rig-other` is not within `/home/u/r`
1266+
let inp = r#"{ "positron.r.interpreters.default": "/home/u/rig-other/bin/R" }"#;
1267+
assert_eq!(set_default(inp), None);
1268+
}
1269+
1270+
#[test]
1271+
fn test_json_string_wrong_type() {
1272+
let inp = r#"{ "positron.r.interpreters.default": ["/home/u/r/4.5.1/bin/R"] }"#;
1273+
assert_eq!(set_default(inp), None);
1274+
}
1275+
1276+
#[test]
1277+
fn test_json_both_settings_in_one_pass() {
1278+
let out = edit("{\n // mine\n}\n", |obj| {
1279+
let roots =
1280+
add_to_json_string_array(obj, POSITRON_ROOTS_KEY, "/home/u/r", "settings.json");
1281+
let default = set_json_string(
1282+
obj,
1283+
POSITRON_DEFAULT_KEY,
1284+
"/home/u/r/4.5.1/bin/R",
1285+
"/home/u/r",
1286+
"settings.json",
1287+
);
1288+
roots || default
1289+
})
1290+
.unwrap();
1291+
assert!(out.contains("// mine"));
1292+
assert!(out.contains("\"positron.r.customRootFolders\": [\"/home/u/r\"]"));
1293+
assert!(out.contains("\"positron.r.interpreters.default\": \"/home/u/r/4.5.1/bin/R\""));
1294+
}
1295+
11181296
#[test]
11191297
fn test_normalize_rig_platform_short_linux() {
11201298
assert_eq!(normalize_rig_platform("ubuntu-24.04"), "linux-ubuntu-24.04");

0 commit comments

Comments
 (0)