diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 229e11ff3a..501a88c42e 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -18,7 +18,7 @@ use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; -use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc}; +use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc}; use theme::{Theme, ThemeRegistry}; use tree_sitter::Query; use util::ResultExt as _; @@ -686,13 +686,25 @@ pub fn settings_file_json_schema( serde_json::to_value(root_schema).unwrap() } +fn merge(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } +} + +pub fn parse_json_with_comments(content: &str) -> Result { + Ok(serde_json::from_reader( + json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()), + )?) +} + /// Expects the key to be unquoted, and the value to be valid JSON /// (e.g. values should be unquoted for numbers and bools, quoted for strings) -pub fn write_settings_key( - mut settings_content: String, - top_level_key: &str, - new_val: &str, -) -> String { +pub fn write_settings_key( + settings_content: &mut String, + key_path: &[&str], + new_value: &T, +) { let mut parser = tree_sitter::Parser::new(); parser.set_language(tree_sitter_json::language()).unwrap(); let tree = parser.parse(&settings_content, None).unwrap(); @@ -702,56 +714,64 @@ pub fn write_settings_key( let query = Query::new( tree_sitter_json::language(), " - (document - (object - (pair - key: (string) @key - value: (_) @value))) - ", + (pair + key: (string) @key + value: (_) @value) + ", ) .unwrap(); + let mut depth = 0; let mut first_key_start = None; - let mut existing_value_range = None; + let mut existing_value_range = 0..settings_content.len(); let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes()); for mat in matches { if mat.captures.len() != 2 { continue; } - let key = mat.captures[0]; - let value = mat.captures[1]; + let key_range = mat.captures[0].node.byte_range(); + let value_range = mat.captures[1].node.byte_range(); - first_key_start.get_or_insert_with(|| key.node.start_byte()); + if key_range.start > existing_value_range.end { + break; + } - if let Some(key_text) = settings_content.get(key.node.byte_range()) { - if key_text == format!("\"{top_level_key}\"") { - existing_value_range = Some(value.node.byte_range()); + first_key_start.get_or_insert_with(|| key_range.start); + + let found_key = settings_content + .get(key_range.clone()) + .map(|key_text| key_text == format!("\"{}\"", key_path[depth])) + .unwrap_or(false); + + if found_key { + existing_value_range = value_range; + depth += 1; + + if depth == key_path.len() { break; + } else { + first_key_start = None; } } } - match (first_key_start, existing_value_range) { - (None, None) => { - // No document, create a new object and overwrite - settings_content.clear(); - write!( - settings_content, - "{{\n \"{}\": {new_val}\n}}\n", - top_level_key - ) - .unwrap(); + // We found the exact key we want, insert the new value + if depth == key_path.len() { + let new_val = serde_json::to_string_pretty(new_value) + .expect("Could not serialize new json field to string"); + settings_content.replace_range(existing_value_range, &new_val); + } else { + // We have key paths, construct the sub objects + let new_key = key_path[depth]; + + // We don't have the key, construct the nested objects + let mut new_value = serde_json::to_value(new_value).unwrap(); + for key in key_path[(depth + 1)..].iter().rev() { + new_value = serde_json::json!({ key.to_string(): new_value }); } - (_, Some(existing_value_range)) => { - // Existing theme key, overwrite - settings_content.replace_range(existing_value_range, &new_val); - } - - (Some(first_key_start), None) => { - // No existing theme key, but other settings. Prepend new theme settings and - // match style of first key + if let Some(first_key_start) = first_key_start { let mut row = 0; let mut column = 0; for (ix, char) in settings_content.char_indices() { @@ -766,70 +786,118 @@ pub fn write_settings_key( } } - let content = format!(r#""{top_level_key}": {new_val},"#); - settings_content.insert_str(first_key_start, &content); - if row > 0 { + let new_val = to_pretty_json(&new_value, column, column); + let content = format!(r#""{new_key}": {new_val},"#); + settings_content.insert_str(first_key_start, &content); + settings_content.insert_str( first_key_start + content.len(), &format!("\n{:width$}", ' ', width = column), ) } else { - settings_content.insert_str(first_key_start + content.len(), " ") + let new_val = serde_json::to_string(&new_value).unwrap(); + let mut content = format!(r#""{new_key}": {new_val},"#); + content.push(' '); + settings_content.insert_str(first_key_start, &content); + } + } else { + new_value = serde_json::json!({ new_key.to_string(): new_value }); + let indent_prefix_len = 4 * depth; + let new_val = to_pretty_json(&new_value, 4, indent_prefix_len); + + settings_content.replace_range(existing_value_range, &new_val); + if depth == 0 { + settings_content.push('\n'); } } } - - settings_content } -fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; +fn to_pretty_json( + value: &serde_json::Value, + indent_size: usize, + indent_prefix_len: usize, +) -> String { + const SPACES: [u8; 32] = [b' '; 32]; + + debug_assert!(indent_size <= SPACES.len()); + debug_assert!(indent_prefix_len <= SPACES.len()); + + let mut output = Vec::new(); + let mut ser = serde_json::Serializer::with_formatter( + &mut output, + serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]), + ); + + value.serialize(&mut ser).unwrap(); + let text = String::from_utf8(output).unwrap(); + + let mut adjusted_text = String::new(); + for (i, line) in text.split('\n').enumerate() { + if i > 0 { + adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap()); + } + adjusted_text.push_str(line); + adjusted_text.push('\n'); } -} - -pub fn parse_json_with_comments(content: &str) -> Result { - Ok(serde_json::from_reader( - json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()), - )?) + adjusted_text.pop(); + adjusted_text } pub fn update_settings_file( - old_text: String, + mut text: String, old_file_content: SettingsFileContent, update: impl FnOnce(&mut SettingsFileContent), ) -> String { let mut new_file_content = old_file_content.clone(); + update(&mut new_file_content); - let old_json = to_json_object(old_file_content); - let new_json = to_json_object(new_file_content); + let old_object = to_json_object(old_file_content); + let new_object = to_json_object(new_file_content); - // Find changed fields - let mut diffs = vec![]; - for (key, old_value) in old_json.iter() { - let new_value = new_json.get(key).unwrap(); - if old_value != new_value { - if matches!( - new_value, - &Value::Null | &Value::Object(_) | &Value::Array(_) - ) { - unimplemented!("We only support updating basic values at the top level"); + fn apply_changes_to_json_text( + old_object: &serde_json::Map, + new_object: &serde_json::Map, + current_key_path: Vec<&str>, + json_text: &mut String, + ) { + for (key, old_value) in old_object.iter() { + // We know that these two are from the same shape of object, so we can just unwrap + let new_value = new_object.get(key).unwrap(); + if old_value != new_value { + match new_value { + Value::Bool(_) | Value::Number(_) | Value::String(_) => { + let mut key_path = current_key_path.clone(); + key_path.push(key); + write_settings_key(json_text, &key_path, &new_value); + } + Value::Object(new_sub_object) => { + let mut key_path = current_key_path.clone(); + key_path.push(key); + if let Value::Object(old_sub_object) = old_value { + apply_changes_to_json_text( + old_sub_object, + new_sub_object, + key_path, + json_text, + ); + } else { + unimplemented!("This function doesn't support changing values from simple values to objects yet"); + } + } + Value::Null | Value::Array(_) => { + unimplemented!("We only support objects and simple values"); + } + } } - - let new_json = serde_json::to_string_pretty(new_value) - .expect("Could not serialize new json field to string"); - - diffs.push((key, new_json)); } } - let mut new_text = old_text; - for (key, new_value) in diffs { - new_text = write_settings_key(new_text, key, &new_value) - } - new_text + apply_changes_to_json_text(&old_object, &new_object, vec![], &mut text); + + text } fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map { @@ -851,7 +919,7 @@ mod tests { expected_new_json: S2, ) { let old_json = old_json.into(); - let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap(); + let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default(); let new_json = update_settings_file(old_json, old_content, update); assert_eq!(new_json, expected_new_json.into()); } @@ -859,23 +927,27 @@ mod tests { #[test] fn test_update_telemetry_setting_multiple_fields() { assert_new_settings( - r#"{ - "telemetry": { - "metrics": false, - "diagnostics": false + r#" + { + "telemetry": { + "metrics": false, + "diagnostics": false + } } - }"# + "# .unindent(), |settings| { settings.telemetry.set_diagnostics(true); settings.telemetry.set_metrics(true); }, - r#"{ - "telemetry": { - "metrics": true, - "diagnostics": true + r#" + { + "telemetry": { + "metrics": true, + "diagnostics": true + } } - }"# + "# .unindent(), ); } @@ -898,20 +970,45 @@ mod tests { #[test] fn test_update_telemetry_setting_other_fields() { assert_new_settings( - r#"{ - "telemetry": { - "metrics": false, - "diagnostics": true + r#" + { + "telemetry": { + "metrics": false, + "diagnostics": true + } } - }"# + "# .unindent(), |settings| settings.telemetry.set_diagnostics(false), - r#"{ - "telemetry": { - "metrics": false, - "diagnostics": false + r#" + { + "telemetry": { + "metrics": false, + "diagnostics": false + } } - }"# + "# + .unindent(), + ); + } + + #[test] + fn test_update_telemetry_setting_empty_telemetry() { + assert_new_settings( + r#" + { + "telemetry": {} + } + "# + .unindent(), + |settings| settings.telemetry.set_diagnostics(false), + r#" + { + "telemetry": { + "diagnostics": false + } + } + "# .unindent(), ); } @@ -919,18 +1016,22 @@ mod tests { #[test] fn test_update_telemetry_setting_pre_existing() { assert_new_settings( - r#"{ - "telemetry": { - "diagnostics": true + r#" + { + "telemetry": { + "diagnostics": true + } } - }"# + "# .unindent(), |settings| settings.telemetry.set_diagnostics(false), - r#"{ - "telemetry": { - "diagnostics": false + r#" + { + "telemetry": { + "diagnostics": false + } } - }"# + "# .unindent(), ); } @@ -940,111 +1041,119 @@ mod tests { assert_new_settings( "{}", |settings| settings.telemetry.set_diagnostics(true), - r#"{ - "telemetry": { - "diagnostics": true + r#" + { + "telemetry": { + "diagnostics": true + } } - }"# + "# + .unindent(), + ); + } + + #[test] + fn test_update_object_empty_doc() { + assert_new_settings( + "", + |settings| settings.telemetry.set_diagnostics(true), + r#" + { + "telemetry": { + "diagnostics": true + } + } + "# .unindent(), ); } #[test] fn test_write_theme_into_settings_with_theme() { - let settings = r#" - { - "theme": "One Dark" - } - "# - .unindent(); - - let new_settings = r#" - { - "theme": "summerfruit-light" - } - "# - .unindent(); - - let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); - - assert_eq!(settings_after_theme, new_settings) + assert_new_settings( + r#" + { + "theme": "One Dark" + } + "# + .unindent(), + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#" + { + "theme": "summerfruit-light" + } + "# + .unindent(), + ); } #[test] fn test_write_theme_into_empty_settings() { - let settings = r#" - { - } - "# - .unindent(); - - let new_settings = r#" - { - "theme": "summerfruit-light" - } - "# - .unindent(); - - let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); - - assert_eq!(settings_after_theme, new_settings) + assert_new_settings( + r#" + { + } + "# + .unindent(), + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#" + { + "theme": "summerfruit-light" + } + "# + .unindent(), + ); } #[test] - fn test_write_theme_into_no_settings() { - let settings = "".to_string(); - - let new_settings = r#" - { - "theme": "summerfruit-light" - } - "# - .unindent(); - - let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); - - assert_eq!(settings_after_theme, new_settings) + fn write_key_no_document() { + assert_new_settings( + "", + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#" + { + "theme": "summerfruit-light" + } + "# + .unindent(), + ); } #[test] fn test_write_theme_into_single_line_settings_without_theme() { - let settings = r#"{ "a": "", "ok": true }"#.to_string(); - let new_settings = r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#; - - let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); - - assert_eq!(settings_after_theme, new_settings) + assert_new_settings( + r#"{ "a": "", "ok": true }"#, + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#, + ); } #[test] fn test_write_theme_pre_object_whitespace() { - let settings = r#" { "a": "", "ok": true }"#.to_string(); - let new_settings = r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#; - - let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); - - assert_eq!(settings_after_theme, new_settings) + assert_new_settings( + r#" { "a": "", "ok": true }"#, + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(), + ); } #[test] fn test_write_theme_into_multi_line_settings_without_theme() { - let settings = r#" - { - "a": "b" - } - "# - .unindent(); - - let new_settings = r#" - { - "theme": "summerfruit-light", - "a": "b" - } - "# - .unindent(); - - let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); - - assert_eq!(settings_after_theme, new_settings) + assert_new_settings( + r#" + { + "a": "b" + } + "# + .unindent(), + |settings| settings.theme = Some("summerfruit-light".to_string()), + r#" + { + "theme": "summerfruit-light", + "a": "b" + } + "# + .unindent(), + ); } }