@@ -6,7 +6,7 @@ use std::path::Path;
66use std:: path:: PathBuf ;
77
88use clap:: ArgMatches ;
9- use jsonc_parser:: cst:: { CstInputValue , CstRootNode } ;
9+ use jsonc_parser:: cst:: { CstInputValue , CstObject , CstRootNode } ;
1010use jsonc_parser:: ParseOptions ;
1111use log:: { debug, error, info, warn} ;
1212use 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
670759fn positron_user_data_dir ( ) -> Result < PathBuf , Box < dyn Error > > {
@@ -1055,9 +1144,22 @@ fn sc_available_rtools_versions(
10551144mod 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