keymap: Add ability to update user keymaps (#33487)

Closes #ISSUE

The ability to update user keybindings in their keymap is required for
#32436. This PR adds the ability to do so, reusing much of the existing
infrastructure for updating settings JSON files.

However, the existing JSON update functionality was intended to work
only with objects, therefore, this PR simply wraps the object updating
code with non-general keymap-specific array updating logic, that only
works for top-level arrays and can only append or update entries in said
top-level arrays. This limited API is reflected in the limited
operations that the new `update_keymap` method on `KeymapFile` can take
as arguments.

Additionally, this PR pulls out the existing JSON updating code into its
own module (where array updating code has been added) and adds a
significant number of tests (hence the high line count in the diff)

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle 2025-06-26 20:52:26 -05:00 committed by GitHub
parent 2823771c06
commit ba1c05abf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 2138 additions and 379 deletions

2
Cargo.lock generated
View file

@ -14554,12 +14554,12 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"smallvec",
"streaming-iterator",
"tree-sitter",
"tree-sitter-json",
"unindent",
"util",
"workspace-hack",
"zlog",
]
[[package]]

View file

@ -33,11 +33,11 @@ serde_derive.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
smallvec.workspace = true
streaming-iterator.workspace = true
tree-sitter-json.workspace = true
tree-sitter.workspace = true
util.workspace = true
workspace-hack.workspace = true
zlog.workspace = true
[dev-dependencies]
fs = { workspace = true, features = ["test-support"] }

View file

@ -1,75 +0,0 @@
use schemars::schema::{
ArrayValidation, InstanceType, RootSchema, Schema, SchemaObject, SingleOrVec,
};
use serde_json::Value;
pub struct SettingsJsonSchemaParams<'a> {
pub language_names: &'a [String],
pub font_names: &'a [String],
}
impl SettingsJsonSchemaParams<'_> {
pub fn font_family_schema(&self) -> Schema {
let available_fonts: Vec<_> = self.font_names.iter().cloned().map(Value::String).collect();
SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(available_fonts),
..Default::default()
}
.into()
}
pub fn font_fallback_schema(&self) -> Schema {
SchemaObject {
instance_type: Some(SingleOrVec::Vec(vec![
InstanceType::Array,
InstanceType::Null,
])),
array: Some(Box::new(ArrayValidation {
items: Some(schemars::schema::SingleOrVec::Single(Box::new(
self.font_family_schema(),
))),
unique_items: Some(true),
..Default::default()
})),
..Default::default()
}
.into()
}
}
type PropertyName<'a> = &'a str;
type ReferencePath<'a> = &'a str;
/// Modifies the provided [`RootSchema`] by adding references to all of the specified properties.
///
/// # Examples
///
/// ```
/// # let root_schema = RootSchema::default();
/// add_references_to_properties(&mut root_schema, &[
/// ("property_a", "#/definitions/DefinitionA"),
/// ("property_b", "#/definitions/DefinitionB"),
/// ])
/// ```
pub fn add_references_to_properties(
root_schema: &mut RootSchema,
properties_with_references: &[(PropertyName, ReferencePath)],
) {
for (property, definition) in properties_with_references {
let Some(schema) = root_schema.schema.object().properties.get_mut(*property) else {
log::warn!("property '{property}' not found in JSON schema");
continue;
};
match schema {
Schema::Object(schema) => {
schema.reference = Some(definition.to_string());
}
Schema::Bool(_) => {
// Boolean schemas can't have references.
}
}
}
}

View file

@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context as _, Result};
use collections::{BTreeMap, HashMap, IndexMap};
use fs::Fs;
use gpui::{
@ -18,7 +18,10 @@ use util::{
markdown::{MarkdownEscaped, MarkdownInlineCode, MarkdownString},
};
use crate::{SettingsAssets, settings_store::parse_json_with_comments};
use crate::{
SettingsAssets, append_top_level_array_value_in_json_text, parse_json_with_comments,
replace_top_level_array_value_in_json_text,
};
pub trait KeyBindingValidator: Send + Sync {
fn action_type_id(&self) -> TypeId;
@ -218,7 +221,7 @@ impl KeymapFile {
key_bindings: Vec::new(),
};
}
let keymap_file = match parse_json_with_comments::<Self>(content) {
let keymap_file = match Self::parse(content) {
Ok(keymap_file) => keymap_file,
Err(error) => {
return KeymapFileLoadResult::JsonParseFailure { error };
@ -629,9 +632,145 @@ impl KeymapFile {
}
}
}
pub fn update_keybinding<'a>(
mut operation: KeybindUpdateOperation<'a>,
mut keymap_contents: String,
tab_size: usize,
) -> Result<String> {
// if trying to replace a keybinding that is not user-defined, treat it as an add operation
match operation {
KeybindUpdateOperation::Replace {
target_source,
source,
..
} if target_source != KeybindSource::User => {
operation = KeybindUpdateOperation::Add(source);
}
_ => {}
}
// Sanity check that keymap contents are valid, even though we only use it for Replace.
// We don't want to modify the file if it's invalid.
let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?;
if let KeybindUpdateOperation::Replace { source, target, .. } = operation {
let mut found_index = None;
let target_action_value = target
.action_value()
.context("Failed to generate target action JSON value")?;
let source_action_value = source
.action_value()
.context("Failed to generate source action JSON value")?;
'sections: for (index, section) in keymap.sections().enumerate() {
if section.context != target.context.unwrap_or("") {
continue;
}
if section.use_key_equivalents != target.use_key_equivalents {
continue;
}
let Some(bindings) = &section.bindings else {
continue;
};
for (keystrokes, action) in bindings {
if keystrokes != target.keystrokes {
continue;
}
if action.0 != target_action_value {
continue;
}
found_index = Some(index);
break 'sections;
}
}
if let Some(index) = found_index {
let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
&keymap_contents,
&["bindings", target.keystrokes],
Some(&source_action_value),
Some(source.keystrokes),
index,
tab_size,
)
.context("Failed to replace keybinding")?;
keymap_contents.replace_range(replace_range, &replace_value);
return Ok(keymap_contents);
} else {
log::warn!(
"Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead",
target.keystrokes,
target_action_value,
source.keystrokes,
source_action_value,
);
operation = KeybindUpdateOperation::Add(source);
}
}
if let KeybindUpdateOperation::Add(keybinding) = operation {
let mut value = serde_json::Map::with_capacity(4);
if let Some(context) = keybinding.context {
value.insert("context".to_string(), context.into());
}
if keybinding.use_key_equivalents {
value.insert("use_key_equivalents".to_string(), true.into());
}
value.insert("bindings".to_string(), {
let mut bindings = serde_json::Map::new();
let action = keybinding.action_value()?;
bindings.insert(keybinding.keystrokes.into(), action);
bindings.into()
});
let (replace_range, replace_value) = append_top_level_array_value_in_json_text(
&keymap_contents,
&value.into(),
tab_size,
)?;
keymap_contents.replace_range(replace_range, &replace_value);
}
return Ok(keymap_contents);
}
}
#[derive(Clone, Copy)]
pub enum KeybindUpdateOperation<'a> {
Replace {
/// Describes the keybind to create
source: KeybindUpdateTarget<'a>,
/// Describes the keybind to remove
target: KeybindUpdateTarget<'a>,
target_source: KeybindSource,
},
Add(KeybindUpdateTarget<'a>),
}
pub struct KeybindUpdateTarget<'a> {
context: Option<&'a str>,
keystrokes: &'a str,
action_name: &'a str,
use_key_equivalents: bool,
input: Option<&'a str>,
}
impl<'a> KeybindUpdateTarget<'a> {
fn action_value(&self) -> Result<Value> {
let action_name: Value = self.action_name.into();
let value = match self.input {
Some(input) => {
let input = serde_json::from_str::<Value>(input)
.context("Failed to parse action input as JSON")?;
serde_json::json!([action_name, input])
}
None => action_name,
};
return Ok(value);
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum KeybindSource {
User,
Default,
@ -688,7 +827,12 @@ impl From<KeybindSource> for KeyBindingMetaIndex {
#[cfg(test)]
mod tests {
use crate::KeymapFile;
use unindent::Unindent;
use crate::{
KeybindSource, KeymapFile,
keymap_file::{KeybindUpdateOperation, KeybindUpdateTarget},
};
#[test]
fn can_deserialize_keymap_with_trailing_comma() {
@ -704,4 +848,316 @@ mod tests {
};
KeymapFile::parse(json).unwrap();
}
#[test]
fn keymap_update() {
zlog::init_test();
#[track_caller]
fn check_keymap_update(
input: impl ToString,
operation: KeybindUpdateOperation,
expected: impl ToString,
) {
let result = KeymapFile::update_keybinding(operation, input.to_string(), 4)
.expect("Update succeeded");
pretty_assertions::assert_eq!(expected.to_string(), result);
}
check_keymap_update(
"[]",
KeybindUpdateOperation::Add(KeybindUpdateTarget {
keystrokes: "ctrl-a",
action_name: "zed::SomeAction",
context: None,
use_key_equivalents: false,
input: None,
}),
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
}
]"#
.unindent(),
);
check_keymap_update(
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
}
]"#
.unindent(),
KeybindUpdateOperation::Add(KeybindUpdateTarget {
keystrokes: "ctrl-b",
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
input: None,
}),
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
},
{
"bindings": {
"ctrl-b": "zed::SomeOtherAction"
}
}
]"#
.unindent(),
);
check_keymap_update(
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
}
]"#
.unindent(),
KeybindUpdateOperation::Add(KeybindUpdateTarget {
keystrokes: "ctrl-b",
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
input: Some(r#"{"foo": "bar"}"#),
}),
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
},
{
"bindings": {
"ctrl-b": [
"zed::SomeOtherAction",
{
"foo": "bar"
}
]
}
}
]"#
.unindent(),
);
check_keymap_update(
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
}
]"#
.unindent(),
KeybindUpdateOperation::Add(KeybindUpdateTarget {
keystrokes: "ctrl-b",
action_name: "zed::SomeOtherAction",
context: Some("Zed > Editor && some_condition = true"),
use_key_equivalents: true,
input: Some(r#"{"foo": "bar"}"#),
}),
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
},
{
"context": "Zed > Editor && some_condition = true",
"use_key_equivalents": true,
"bindings": {
"ctrl-b": [
"zed::SomeOtherAction",
{
"foo": "bar"
}
]
}
}
]"#
.unindent(),
);
check_keymap_update(
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
}
]"#
.unindent(),
KeybindUpdateOperation::Replace {
target: KeybindUpdateTarget {
keystrokes: "ctrl-a",
action_name: "zed::SomeAction",
context: None,
use_key_equivalents: false,
input: None,
},
source: KeybindUpdateTarget {
keystrokes: "ctrl-b",
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
input: Some(r#"{"foo": "bar"}"#),
},
target_source: KeybindSource::Base,
},
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
},
{
"bindings": {
"ctrl-b": [
"zed::SomeOtherAction",
{
"foo": "bar"
}
]
}
}
]"#
.unindent(),
);
check_keymap_update(
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
}
]"#
.unindent(),
KeybindUpdateOperation::Replace {
target: KeybindUpdateTarget {
keystrokes: "ctrl-a",
action_name: "zed::SomeAction",
context: None,
use_key_equivalents: false,
input: None,
},
source: KeybindUpdateTarget {
keystrokes: "ctrl-b",
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
input: Some(r#"{"foo": "bar"}"#),
},
target_source: KeybindSource::User,
},
r#"[
{
"bindings": {
"ctrl-b": [
"zed::SomeOtherAction",
{
"foo": "bar"
}
]
}
}
]"#
.unindent(),
);
check_keymap_update(
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
}
]"#
.unindent(),
KeybindUpdateOperation::Replace {
target: KeybindUpdateTarget {
keystrokes: "ctrl-a",
action_name: "zed::SomeNonexistentAction",
context: None,
use_key_equivalents: false,
input: None,
},
source: KeybindUpdateTarget {
keystrokes: "ctrl-b",
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
input: None,
},
target_source: KeybindSource::User,
},
r#"[
{
"bindings": {
"ctrl-a": "zed::SomeAction"
}
},
{
"bindings": {
"ctrl-b": "zed::SomeOtherAction"
}
}
]"#
.unindent(),
);
check_keymap_update(
r#"[
{
"bindings": {
// some comment
"ctrl-a": "zed::SomeAction"
// some other comment
}
}
]"#
.unindent(),
KeybindUpdateOperation::Replace {
target: KeybindUpdateTarget {
keystrokes: "ctrl-a",
action_name: "zed::SomeAction",
context: None,
use_key_equivalents: false,
input: None,
},
source: KeybindUpdateTarget {
keystrokes: "ctrl-b",
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
input: Some(r#"{"foo": "bar"}"#),
},
target_source: KeybindSource::User,
},
r#"[
{
"bindings": {
// some comment
"ctrl-b": [
"zed::SomeOtherAction",
{
"foo": "bar"
}
]
// some other comment
}
}
]"#
.unindent(),
);
}
}

View file

@ -1,8 +1,8 @@
mod editable_setting_control;
mod json_schema;
mod key_equivalents;
mod keymap_file;
mod settings_file;
mod settings_json;
mod settings_store;
mod vscode_import;
@ -12,16 +12,16 @@ use std::{borrow::Cow, fmt, str};
use util::asset_str;
pub use editable_setting_control::*;
pub use json_schema::*;
pub use key_equivalents::*;
pub use keymap_file::{
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeymapFile,
KeymapFileLoadResult,
};
pub use settings_file::*;
pub use settings_json::*;
pub use settings_store::{
InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
SettingsStore, parse_json_with_comments,
SettingsStore,
};
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};

View file

@ -9,10 +9,9 @@ pub const EMPTY_THEME_NAME: &str = "empty-theme";
#[cfg(any(test, feature = "test-support"))]
pub fn test_settings() -> String {
let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>(
crate::default_settings().as_ref(),
)
.unwrap();
let mut value =
crate::parse_json_with_comments::<serde_json::Value>(crate::default_settings().as_ref())
.unwrap();
#[cfg(not(target_os = "windows"))]
util::merge_non_null_json_value_into(
serde_json::json!({

File diff suppressed because it is too large Load diff

View file

@ -16,17 +16,17 @@ use std::{
ops::Range,
path::{Path, PathBuf},
str::{self, FromStr},
sync::{Arc, LazyLock},
sync::Arc,
};
use streaming_iterator::StreamingIterator;
use tree_sitter::Query;
use util::RangeExt;
use util::{ResultExt as _, merge_non_null_json_value_into};
pub type EditorconfigProperties = ec4rs::Properties;
use crate::{SettingsJsonSchemaParams, VsCodeSettings, WorktreeId};
use crate::{
SettingsJsonSchemaParams, VsCodeSettings, WorktreeId, parse_json_with_comments,
update_value_in_json_text,
};
/// A value that can be defined as a user setting.
///
@ -1334,273 +1334,6 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
}
}
fn update_value_in_json_text<'a>(
text: &mut String,
key_path: &mut Vec<&'a str>,
tab_size: usize,
old_value: &'a Value,
new_value: &'a Value,
preserved_keys: &[&str],
edits: &mut Vec<(Range<usize>, String)>,
) {
// If the old and new values are both objects, then compare them key by key,
// preserving the comments and formatting of the unchanged parts. Otherwise,
// replace the old value with the new value.
if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) {
for (key, old_sub_value) in old_object.iter() {
key_path.push(key);
if let Some(new_sub_value) = new_object.get(key) {
// Key exists in both old and new, recursively update
update_value_in_json_text(
text,
key_path,
tab_size,
old_sub_value,
new_sub_value,
preserved_keys,
edits,
);
} else {
// Key was removed from new object, remove the entire key-value pair
let (range, replacement) = replace_value_in_json_text(text, key_path, 0, None);
text.replace_range(range.clone(), &replacement);
edits.push((range, replacement));
}
key_path.pop();
}
for (key, new_sub_value) in new_object.iter() {
key_path.push(key);
if !old_object.contains_key(key) {
update_value_in_json_text(
text,
key_path,
tab_size,
&Value::Null,
new_sub_value,
preserved_keys,
edits,
);
}
key_path.pop();
}
} else if key_path
.last()
.map_or(false, |key| preserved_keys.contains(key))
|| old_value != new_value
{
let mut new_value = new_value.clone();
if let Some(new_object) = new_value.as_object_mut() {
new_object.retain(|_, v| !v.is_null());
}
let (range, replacement) =
replace_value_in_json_text(text, key_path, tab_size, Some(&new_value));
text.replace_range(range.clone(), &replacement);
edits.push((range, replacement));
}
}
fn replace_value_in_json_text(
text: &str,
key_path: &[&str],
tab_size: usize,
new_value: Option<&Value>,
) -> (Range<usize>, String) {
static PAIR_QUERY: LazyLock<Query> = LazyLock::new(|| {
Query::new(
&tree_sitter_json::LANGUAGE.into(),
"(pair key: (string) @key value: (_) @value)",
)
.expect("Failed to create PAIR_QUERY")
});
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_json::LANGUAGE.into())
.unwrap();
let syntax_tree = parser.parse(text, None).unwrap();
let mut cursor = tree_sitter::QueryCursor::new();
let mut depth = 0;
let mut last_value_range = 0..0;
let mut first_key_start = None;
let mut existing_value_range = 0..text.len();
let mut matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
while let Some(mat) = matches.next() {
if mat.captures.len() != 2 {
continue;
}
let key_range = mat.captures[0].node.byte_range();
let value_range = mat.captures[1].node.byte_range();
// Don't enter sub objects until we find an exact
// match for the current keypath
if last_value_range.contains_inclusive(&value_range) {
continue;
}
last_value_range = value_range.clone();
if key_range.start > existing_value_range.end {
break;
}
first_key_start.get_or_insert(key_range.start);
let found_key = text
.get(key_range.clone())
.map(|key_text| {
depth < key_path.len() && key_text == format!("\"{}\"", key_path[depth])
})
.unwrap_or(false);
if found_key {
existing_value_range = value_range;
// Reset last value range when increasing in depth
last_value_range = existing_value_range.start..existing_value_range.start;
depth += 1;
if depth == key_path.len() {
break;
}
first_key_start = None;
}
}
// We found the exact key we want
if depth == key_path.len() {
if let Some(new_value) = new_value {
let new_val = to_pretty_json(new_value, tab_size, tab_size * depth);
(existing_value_range, new_val)
} else {
let mut removal_start = first_key_start.unwrap_or(existing_value_range.start);
let mut removal_end = existing_value_range.end;
// Find the actual key position by looking for the key in the pair
// We need to extend the range to include the key, not just the value
if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
if let Some(prev_key_start) = text[..key_start].rfind('"') {
removal_start = prev_key_start;
} else {
removal_start = key_start;
}
}
// Look backward for a preceding comma first
let preceding_text = text.get(0..removal_start).unwrap_or("");
if let Some(comma_pos) = preceding_text.rfind(',') {
// Check if there are only whitespace characters between the comma and our key
let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or("");
if between_comma_and_key.trim().is_empty() {
removal_start = comma_pos;
}
}
if let Some(remaining_text) = text.get(existing_value_range.end..) {
let mut chars = remaining_text.char_indices();
while let Some((offset, ch)) = chars.next() {
if ch == ',' {
removal_end = existing_value_range.end + offset + 1;
// Also consume whitespace after the comma
while let Some((_, next_ch)) = chars.next() {
if next_ch.is_whitespace() {
removal_end += next_ch.len_utf8();
} else {
break;
}
}
break;
} else if !ch.is_whitespace() {
break;
}
}
}
(removal_start..removal_end, String::new())
}
} 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_or(&serde_json::Value::Null)).unwrap();
for key in key_path[(depth + 1)..].iter().rev() {
new_value = serde_json::json!({ key.to_string(): new_value });
}
if let Some(first_key_start) = first_key_start {
let mut row = 0;
let mut column = 0;
for (ix, char) in text.char_indices() {
if ix == first_key_start {
break;
}
if char == '\n' {
row += 1;
column = 0;
} else {
column += char.len_utf8();
}
}
if row > 0 {
// depth is 0 based, but division needs to be 1 based.
let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
let space = ' ';
let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
(first_key_start..first_key_start, content)
} else {
let new_val = serde_json::to_string(&new_value).unwrap();
let mut content = format!(r#""{new_key}": {new_val},"#);
content.push(' ');
(first_key_start..first_key_start, content)
}
} else {
new_value = serde_json::json!({ new_key.to_string(): new_value });
let indent_prefix_len = 4 * depth;
let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
if depth == 0 {
new_val.push('\n');
}
(existing_value_range, new_val)
}
}
}
fn to_pretty_json(value: &impl Serialize, 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');
}
adjusted_text.pop();
adjusted_text
}
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
Ok(serde_json_lenient::from_str(content)?)
}
#[cfg(test)]
mod tests {
use crate::VsCodeSettingsSource;
@ -1784,6 +1517,22 @@ mod tests {
);
}
fn check_settings_update<T: Settings>(
store: &mut SettingsStore,
old_json: String,
update: fn(&mut T::FileContent),
expected_new_json: String,
cx: &mut App,
) {
store.set_user_settings(&old_json, cx).ok();
let edits = store.edits_for_update::<T>(&old_json, update);
let mut new_json = old_json;
for (range, replacement) in edits.into_iter() {
new_json.replace_range(range, &replacement);
}
pretty_assertions::assert_eq!(new_json, expected_new_json);
}
#[gpui::test]
fn test_setting_store_update(cx: &mut App) {
let mut store = SettingsStore::new(cx);
@ -1890,12 +1639,12 @@ mod tests {
&mut store,
r#"{
"user": { "age": 36, "name": "Max", "staff": true }
}"#
}"#
.unindent(),
|settings| settings.age = Some(37),
r#"{
"user": { "age": 37, "name": "Max", "staff": true }
}"#
}"#
.unindent(),
cx,
);
@ -2118,22 +1867,6 @@ mod tests {
);
}
fn check_settings_update<T: Settings>(
store: &mut SettingsStore,
old_json: String,
update: fn(&mut T::FileContent),
expected_new_json: String,
cx: &mut App,
) {
store.set_user_settings(&old_json, cx).ok();
let edits = store.edits_for_update::<T>(&old_json, update);
let mut new_json = old_json;
for (range, replacement) in edits.into_iter() {
new_json.replace_range(range, &replacement);
}
pretty_assertions::assert_eq!(new_json, expected_new_json);
}
fn check_vscode_import(
store: &mut SettingsStore,
old: String,