From 3a7b16da5dbc961bea1390c32ef768d2c4172585 Mon Sep 17 00:00:00 2001 From: Dom Porada Date: Mon, 21 Sep 2026 13:08:53 +0000 Subject: [PATCH] Add support for `"..."` in `edit_predictions.disabled_globs` (#64540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Objective Let users and projects add edit prediction exclusion patterns without repeating defaults or inherited patterns. Part of #64399. ## Solution Extend the `"..."` convention from #62769, #64222, #64440, and #64484 to `edit_predictions.disabled_globs` using `SplicingVec`. Inherited patterns are inserted at `"..."`, and duplicates retain their first occurrence. Omitting the marker replaces the list, `[]` clears it, and omitting the setting preserves inheritance. Opening an absent setting now initializes it with `["..."]` instead of clearing inherited patterns, and the selection is located by parsing the JSON instead of a regex, so a commented-out or nested `"disabled_globs"` can no longer capture it. Cursor imports append `"..."` like the `files.exclude` import does, so Zed's sensitive-file defaults survive an import; empty lists and empty or literal `"..."` entries are dropped, matching `enabled_patterns`. ## Testing Tested on macOS with settings merge and loading tests, Cursor import tests, the UI settings update and selection path (including commented-out and nested decoy keys), the new `settings_json` key-path lookup, and Copilot integration coverage. To check manually, configure custom disabled patterns alongside `"..."` and confirm that both custom patterns and inherited sensitive-file patterns remain disabled. Repeat without `"..."` and with `[]` to verify replacement and clearing. Opening the setting when it is absent should preserve inherited patterns. ## Self-Review Checklist: - [x] I’ve reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed’s UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Added support for `"..."` in `edit_predictions.disabled_globs` so custom patterns can extend inherited exclusions, and fixed "Configure Excluded Files" selecting the wrong list when a commented-out `disabled_globs` precedes it. --------- Co-authored-by: Kirill Bulatov --- Cargo.lock | 1 - assets/settings/default.json | 21 +- .../src/copilot_edit_prediction_delegate.rs | 4 +- crates/edit_prediction_ui/Cargo.toml | 1 - .../src/edit_prediction_button.rs | 161 ++++++++++-- crates/language/src/language_settings.rs | 230 +++++++++++++++++- crates/settings/src/vscode_import.rs | 107 +++++++- crates/settings_content/src/language.rs | 30 ++- crates/settings_content/src/project.rs | 8 +- crates/settings_content/src/workspace.rs | 2 +- crates/settings_json/src/settings_json.rs | 65 +++++ docs/src/reference/all-settings.md | 36 ++- 12 files changed, 598 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37512c4c33..e601b4d31c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5556,7 +5556,6 @@ dependencies = [ "multi_buffer", "paths", "project", - "regex", "settings", "telemetry", "text", diff --git a/assets/settings/default.json b/assets/settings/default.json index a7bae4d564..f10ab796a2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -586,7 +586,7 @@ // Whether to show code action buttons in the editor toolbar. "code_actions": false, }, - // Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). + // Whether to allow windows to tab together based on the user's tabbing preference (macOS only). "use_system_window_tabs": false, // Which fullscreen mode the `zed::ToggleFullScreen` action enters (macOS only). // @@ -1873,11 +1873,20 @@ "edit_predictions": { // Which edit prediction provider to use. "provider": "zed", - // A list of globs representing files that edit predictions should be disabled for. - // There's a sensible default list of globs already included. - // Any addition to this list will be merged with the default list. - // Globs are matched relative to the worktree root, - // except when starting with a slash (/) or equivalent in Windows. + // Disable edit predictions for files matching these glob patterns. + // + // Use `"..."` to add patterns without repeating Zed's defaults. In project + // settings, it extends the user or parent configuration value. Omit + // `"..."` to replace the inherited list. + // + // Inherited patterns are inserted at `"..."`, and duplicates keep their first + // occurrence. + // + // Set `[]` to clear the inherited list. Omit this setting to inherit it unchanged. + // + // Relative patterns are matched against paths relative to the worktree root. + // Absolute patterns are matched against absolute paths. + // A leading `~` is expanded to your home folder. "disabled_globs": [ "**/.env*", "**/*.pem", diff --git a/crates/copilot/src/copilot_edit_prediction_delegate.rs b/crates/copilot/src/copilot_edit_prediction_delegate.rs index dfb8ebcdb7..02b319c189 100644 --- a/crates/copilot/src/copilot_edit_prediction_delegate.rs +++ b/crates/copilot/src/copilot_edit_prediction_delegate.rs @@ -246,7 +246,7 @@ mod tests { use lsp::Uri; use project::Project; use serde_json::json; - use settings::{AllLanguageSettingsContent, DelayMs, SettingsStore}; + use settings::{AllLanguageSettingsContent, DelayMs, SettingsStore, SplicingVec}; use std::future::Future; use util::{ path, @@ -979,7 +979,7 @@ mod tests { settings .edit_predictions .get_or_insert(Default::default()) - .disabled_globs = Some(vec![".env*".to_string()]); + .disabled_globs = Some(SplicingVec::from(vec![".env*".to_string()])); }); let (copilot, copilot_lsp) = Copilot::fake(cx); diff --git a/crates/edit_prediction_ui/Cargo.toml b/crates/edit_prediction_ui/Cargo.toml index 911cd359c0..d0e892024c 100644 --- a/crates/edit_prediction_ui/Cargo.toml +++ b/crates/edit_prediction_ui/Cargo.toml @@ -36,7 +36,6 @@ menu.workspace = true multi_buffer.workspace = true paths.workspace = true project.workspace = true -regex.workspace = true settings.workspace = true telemetry.workspace = true text.workspace = true diff --git a/crates/edit_prediction_ui/src/edit_prediction_button.rs b/crates/edit_prediction_ui/src/edit_prediction_button.rs index bd029b0a21..6a5cc13b8c 100644 --- a/crates/edit_prediction_ui/src/edit_prediction_button.rs +++ b/crates/edit_prediction_ui/src/edit_prediction_button.rs @@ -23,13 +23,11 @@ use language::{ }, }; use project::{DisableAiSettings, Project}; -use regex::Regex; -use settings::{Settings, SettingsStore, update_settings_file}; -use std::{ - rc::Rc, - sync::{Arc, LazyLock}, - time::Duration, +use settings::{ + Settings, SettingsContent, SettingsStore, SplicingVec, find_value_range_in_json_text, + update_settings_file, }; +use std::{ops::Range, rc::Rc, sync::Arc, time::Duration}; use ui::{ Clickable, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*, @@ -1402,6 +1400,25 @@ impl StatusItemView for EditPredictionButton { } } +fn initialize_disabled_globs_setting(file: &mut SettingsContent) { + file.project + .all_languages + .edit_predictions + .get_or_insert_with(Default::default) + .disabled_globs + .get_or_insert_with(|| SplicingVec::from(vec![SplicingVec::REST.to_string()])); +} + +fn disabled_globs_content_range(text: &str) -> Option> { + let array_range = find_value_range_in_json_text(text, &["edit_predictions", "disabled_globs"])?; + let content = text + .get(array_range.clone())? + .strip_prefix('[')? + .strip_suffix(']')?; + let start = array_range.start + 1 + (content.len() - content.trim_start().len()); + Some(start..start + content.trim().len()) +} + async fn open_disabled_globs_setting_in_editor( workspace: WeakEntity, cx: &mut AsyncWindowContext, @@ -1423,16 +1440,8 @@ async fn open_disabled_globs_setting_in_editor( let settings = cx.global::(); - // Ensure that we always have "edit_predictions { "disabled_globs": [] }" let Some(edits) = settings - .edits_for_update(&text, |file| { - file.project - .all_languages - .edit_predictions - .get_or_insert_with(Default::default) - .disabled_globs - .get_or_insert_with(Vec::new); - }) + .edits_for_update(&text, initialize_disabled_globs_setting) .log_err() else { return; @@ -1449,16 +1458,7 @@ async fn open_disabled_globs_setting_in_editor( let text = item.buffer().read(cx).snapshot(cx).text(); - static DISABLED_GLOBS_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r#""disabled_globs":\s*\[\s*(?P(?:.|\n)*?)\s*\]"#).unwrap() - }); - // Only capture [...] - let range = DISABLED_GLOBS_REGEX.captures(&text).and_then(|captures| { - captures - .name("content") - .map(|inner_match| inner_match.start()..inner_match.end()) - }); - if let Some(range) = range { + if let Some(range) = disabled_globs_content_range(&text) { let range = MultiBufferOffset(range.start)..MultiBufferOffset(range.end); item.change_selections( SelectionEffects::scroll(Autoscroll::newest()), @@ -1686,7 +1686,118 @@ fn copilot_settings_url(enterprise_uri: Option<&str>) -> Arc { #[cfg(test)] mod tests { use super::*; + use anyhow::Context as _; use gpui::TestAppContext; + use settings::RootUserSettings; + + #[test] + fn test_disabled_globs_selection_and_replacement() -> Result<()> { + for contents in [ + r#""[.][.][.]""#, + r#""**/[ab]/**", "...""#, + r#""escaped\"]", "backslash\\", "...""#, + r#""escaped\\]", "line\nbreak]", "unicode\u005d""#, + "\n \"é[ab]\",\n \"...\"\n ", + "// Ignore filenames containing a \" character\n \"**/private/**\"\n", + "/* Ignore \" and ] */\n \"**/private/**\"\n", + "\"**/private/**\", // Keep \" and ]\n \"...\"\n", + r#""**/build/**", "...""#, + "", + "\n ", + ] { + let mut text = format!( + r#"// "disabled_globs": ["commented/**"] +{{"languages":{{"Rust":{{"disabled_globs":["nested/**"]}}}}, +/* "disabled_globs": ["commented/**"] */ +"edit_predictions":{{"disabled_globs":[{contents}],"mode":"subtle"}}}}"# + ); + let mut expected = SettingsContent::parse_json_with_comments(&text)?; + let range = disabled_globs_content_range(&text).context("disabled globs selection")?; + assert_eq!(text.get(range.clone()), Some(contents.trim())); + + text.replace_range(range, r#""**/replacement/**", "...""#); + expected + .project + .all_languages + .edit_predictions + .as_mut() + .context("edit prediction settings")? + .disabled_globs = Some(SplicingVec::from(vec![ + "**/replacement/**".to_string(), + SplicingVec::REST.to_string(), + ])); + assert_eq!(SettingsContent::parse_json_with_comments(&text)?, expected); + } + Ok(()) + } + + #[gpui::test] + fn test_initialize_disabled_globs_setting(cx: &mut App) { + let store = SettingsStore::new(cx, &settings::default_settings()); + + let cases: &[(&str, &[&str])] = &[ + ("", &[SplicingVec::REST]), + (r#"{}"#, &[SplicingVec::REST]), + (r#"{"edit_predictions":{}}"#, &[SplicingVec::REST]), + ( + r#"{"edit_predictions":{"mode":"subtle"}}"#, + &[SplicingVec::REST], + ), + (r#"{"edit_predictions":{"disabled_globs":[]}}"#, &[]), + ( + r#"{"edit_predictions":{"disabled_globs":["**/build/**"]}}"#, + &["**/build/**"], + ), + ( + r#"{"edit_predictions":{"disabled_globs":["...","**/build/**"]}}"#, + &[SplicingVec::REST, "**/build/**"], + ), + ( + r#"{"edit_predictions":{"disabled_globs":["[.][.][.]"]}}"#, + &["[.][.][.]"], + ), + ]; + for &(content, expected_globs) in cases { + let original = if content.is_empty() { "{}" } else { content }; + let mut expected_content = SettingsContent::parse_json_with_comments(original) + .expect("settings content parses"); + let original_globs = &mut expected_content + .project + .all_languages + .edit_predictions + .get_or_insert_with(Default::default) + .disabled_globs; + let already_configured = original_globs.is_some(); + *original_globs = Some(SplicingVec::from( + expected_globs + .iter() + .map(|glob| glob.to_string()) + .collect::>(), + )); + + let edits = store + .edits_for_update(content, initialize_disabled_globs_setting) + .expect("settings edits are generated"); + assert_eq!(edits.is_empty(), already_configured, "{content}"); + let mut updated = content.to_string(); + for (range, replacement) in edits { + updated.replace_range(range, &replacement); + } + assert_eq!( + SettingsContent::parse_json_with_comments(&updated) + .expect("updated settings parse"), + expected_content, + "{content}", + ); + assert!( + store + .edits_for_update(&updated, initialize_disabled_globs_setting) + .expect("repeated settings edits are generated") + .is_empty(), + "{content}", + ); + } + } #[gpui::test] async fn test_copilot_settings_url_with_enterprise_uri(cx: &mut TestAppContext) { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index d415eeb9e9..e22b5f9428 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -487,9 +487,28 @@ impl InlayHintSettings { pub struct EditPredictionSettings { /// The provider that supplies edit predictions. pub provider: settings::EditPredictionProvider, - /// A list of globs representing files that edit predictions should be disabled for. - /// This list adds to a pre-existing, sensible default set of globs. - /// Any additional ones you add are combined with them. + /// Disable edit predictions for files matching these glob patterns. + /// + /// Use `"..."` to add patterns without repeating Zed's defaults. In project + /// settings, it extends the user or parent configuration value. Omit + /// `"..."` to replace the inherited list. + /// + /// ```json + /// { + /// "edit_predictions": { + /// "disabled_globs": ["**/build/**", "..."] + /// } + /// } + /// ``` + /// + /// Inherited patterns are inserted at `"..."`, and duplicates keep their first + /// occurrence. + /// + /// Set `[]` to clear the inherited list. Omit this setting to inherit it unchanged. + /// + /// Relative patterns are matched against paths relative to the worktree root. + /// Absolute patterns are matched against absolute paths. A leading `~` is + /// expanded to your home folder. pub disabled_globs: Vec, /// Configures how edit predictions are displayed in the buffer. pub mode: settings::EditPredictionsMode, @@ -918,6 +937,7 @@ impl settings::Settings for AllLanguageSettings { .disabled_globs .as_ref() .unwrap() + .0 .iter() .collect(); @@ -1096,6 +1116,210 @@ mod tests { }); } + #[gpui::test] + fn test_edit_predictions_disabled_globs_replace_and_clear(cx: &mut App) { + let mut store = SettingsStore::new(cx, &settings::default_settings()); + store.register_setting::(); + + for (content, sensitive_enabled, custom_enabled) in [ + (r#"{}"#, false, true), + ( + r#"{"edit_predictions":{"disabled_globs":["**/build/**"]}}"#, + true, + false, + ), + (r#"{"edit_predictions":{"disabled_globs":[]}}"#, true, true), + (r#"{"edit_predictions":{}}"#, false, true), + ] { + store + .set_user_settings(content, cx) + .expect("user settings should load"); + let settings = &store.get::(None).edit_predictions; + for (path, expected_enabled) in [ + (".env", sensitive_enabled), + ("build/output.rs", custom_enabled), + ("certificates/private.pem", sensitive_enabled), + ("config/secrets.yml", sensitive_enabled), + ("src/main.rs", true), + ] { + let file: Arc = Arc::new(crate::TestFile { + path: rel_path(path).into(), + root_name: "project".to_string(), + local_root: None, + }); + assert_eq!( + settings.enabled_for_file(&file, cx), + expected_enabled, + "path: {path}, settings: {content}" + ); + } + } + } + + #[gpui::test] + fn test_edit_predictions_disabled_globs_inherit_across_settings_files(cx: &mut App) { + let mut store = SettingsStore::new(cx, &settings::default_settings()); + store.register_setting::(); + let worktree_id = WorktreeId::from_usize(1); + let root = LocalSettingsPath::InWorktree(rel_path("root").into()); + let child = LocalSettingsPath::InWorktree(rel_path("root/child").into()); + + store + .set_user_settings( + r#"{"edit_predictions":{"disabled_globs":["**/user/**","..."]}}"#, + cx, + ) + .expect("user settings should load"); + store + .set_local_settings( + worktree_id, + root, + LocalSettingsKind::Settings, + Some(r#"{"edit_predictions":{"disabled_globs":["**/project/**","..."]}}"#), + cx, + ) + .expect("project settings should load"); + + for (content, inherits, child_enabled) in [ + ( + r#"{"edit_predictions":{"disabled_globs":["**/child/**","...","**/.env*","..."]}}"#, + true, + false, + ), + (r#"{"edit_predictions":{}}"#, true, true), + ( + r#"{"edit_predictions":{"disabled_globs":["**/child/**"]}}"#, + false, + false, + ), + (r#"{"edit_predictions":{"disabled_globs":[]}}"#, false, true), + ] { + store + .set_local_settings( + worktree_id, + child.clone(), + LocalSettingsKind::Settings, + Some(content), + cx, + ) + .expect("child settings should load"); + for (location, expected) in [ + (None, [false, true, true, false]), + ( + Some(SettingsLocation { + worktree_id, + path: rel_path("root/file.rs"), + }), + [false, true, false, false], + ), + ( + Some(SettingsLocation { + worktree_id, + path: rel_path("root/child/file.rs"), + }), + [!inherits, child_enabled, !inherits, !inherits], + ), + ] { + let settings = &store.get::(location).edit_predictions; + for (path, enabled) in [ + ".env", + "child/output.rs", + "project/output.rs", + "user/output.rs", + ] + .into_iter() + .zip(expected) + .chain([("...", true), ("src/main.rs", true)]) + { + let file: Arc = Arc::new(crate::TestFile { + path: rel_path(path).into(), + root_name: "project".to_string(), + local_root: None, + }); + assert_eq!( + settings.enabled_for_file(&file, cx), + enabled, + "path: {path}, settings: {content}" + ); + } + } + } + } + + #[gpui::test] + fn test_edit_predictions_disabled_globs_preserve_matching(cx: &mut App) { + let mut store = SettingsStore::new(cx, &settings::default_settings()); + store.register_setting::(); + let absolute_root = if cfg!(windows) { + "C:/absolute" + } else { + "/absolute" + }; + let absolute_pattern = format!("{absolute_root}/project/**/*.rs"); + let home = shellexpand::tilde("~").into_owned(); + for (patterns, path, local_root, enabled) in [ + ( + vec!["**/src/**", "..."], + "src/main.rs", + Some(absolute_root), + false, + ), + ( + vec!["**/src/**", "..."], + "other/main.rs", + Some(absolute_root), + true, + ), + ( + vec![absolute_pattern.as_str(), "..."], + "src/main.rs", + Some(absolute_root), + false, + ), + ( + vec![absolute_pattern.as_str(), "..."], + "src/main.rs", + None, + true, + ), + ( + vec!["~/project/**/*.rs", "..."], + "src/main.rs", + Some(home.as_str()), + false, + ), + ( + vec!["[", "**/src/**", "..."], + "src/main.rs", + Some(absolute_root), + false, + ), + (vec!["[", "..."], ".env", Some(absolute_root), false), + (vec!["[", "{"], ".env", Some(absolute_root), true), + (vec!["[", "{"], "src/main.rs", Some(absolute_root), true), + (vec!["[.][.][.]"], "...", Some(absolute_root), false), + ] { + store + .set_user_settings( + &serde_json::json!({"edit_predictions": {"disabled_globs": patterns}}) + .to_string(), + cx, + ) + .expect("user settings should load"); + let file: Arc = Arc::new(crate::TestFile { + path: rel_path(path).into(), + root_name: "project".to_string(), + local_root: local_root.map(std::path::PathBuf::from), + }); + let settings = &store.get::(None).edit_predictions; + assert_eq!( + settings.enabled_for_file(&file, cx), + enabled, + "path: {path}, patterns: {patterns:?}" + ); + } + } + #[gpui::test] fn test_edit_predictions_enabled_for_file(cx: &mut TestAppContext) { use crate::TestFile; diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 3bb2b86b9d..0fe5162a14 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -677,19 +677,22 @@ impl VsCodeSettings { } fn edit_predictions_settings_content(&self) -> Option { - let disabled_globs = self + let mut disabled_globs = self .read_value("cursor.general.globalCursorIgnoreList")? - .as_array()?; + .as_array()? + .iter() + .filter_map(Value::as_str) + .filter(|glob| !glob.is_empty() && *glob != SplicingVec::REST) + .map(str::to_owned) + .collect::>(); + if disabled_globs.is_empty() { + return None; + } + disabled_globs.push(SplicingVec::REST.to_owned()); - skip_default(EditPredictionSettingsContent { - disabled_globs: skip_default( - disabled_globs - .iter() - .filter_map(|glob| glob.as_str()) - .map(|s| s.to_string()) - .collect(), - ), - ..Default::default() + Some(EditPredictionSettingsContent { + disabled_globs: Some(SplicingVec::from(disabled_globs)), + ..EditPredictionSettingsContent::default() }) } @@ -1235,6 +1238,88 @@ mod tests { .reduce_motion } + #[test] + fn test_import_disabled_globs_extends_inherited_patterns() -> Result<()> { + for (ignore_list, expected_imported, expected_merged) in [ + ( + serde_json::json!(["**/build/**", "**/cache/**"]), + serde_json::json!(["**/build/**", "**/cache/**", "..."]), + serde_json::json!(["**/build/**", "**/cache/**", "**/inherited/**"]), + ), + ( + serde_json::json!(["**/build/**", "", false, null, 1, {}, []]), + serde_json::json!(["**/build/**", "..."]), + serde_json::json!(["**/build/**", "**/inherited/**"]), + ), + ( + serde_json::json!(["...", "**/build/**", false]), + serde_json::json!(["**/build/**", "..."]), + serde_json::json!(["**/build/**", "**/inherited/**"]), + ), + ( + serde_json::json!(["**/inherited/**", "**/build/**"]), + serde_json::json!(["**/inherited/**", "**/build/**", "..."]), + serde_json::json!(["**/inherited/**", "**/build/**"]), + ), + ] { + let content = serde_json::json!({ + "cursor.general.globalCursorIgnoreList": ignore_list, + }); + let imported = + VsCodeSettings::from_str(&content.to_string(), VsCodeSettingsSource::Cursor)? + .settings_content(); + let imported = imported + .project + .all_languages + .edit_predictions + .context("imported edit prediction settings")?; + assert_eq!( + serde_json::to_value(&imported.disabled_globs)?, + expected_imported + ); + + let mut inherited = EditPredictionSettingsContent { + disabled_globs: Some(SplicingVec::from(vec!["**/inherited/**".to_string()])), + ..Default::default() + }; + inherited.merge_from(&imported); + assert_eq!( + serde_json::to_value(&inherited.disabled_globs)?, + expected_merged + ); + } + Ok(()) + } + + #[test] + fn test_import_disabled_globs_omits_empty_results() -> Result<()> { + let inherited = AllLanguageSettingsContent { + edit_predictions: Some(EditPredictionSettingsContent { + disabled_globs: Some(SplicingVec::from(vec!["**/inherited/**".to_string()])), + ..Default::default() + }), + ..Default::default() + }; + for content in [ + r#"{"cursor.general.globalCursorIgnoreList": "**/build/**"}"#, + r#"{"cursor.general.globalCursorIgnoreList": ["..."]}"#, + r#"{"cursor.general.globalCursorIgnoreList": [""]}"#, + r#"{"cursor.general.globalCursorIgnoreList": ["...", "", false, null]}"#, + r#"{"cursor.general.globalCursorIgnoreList": []}"#, + r#"{"cursor.general.globalCursorIgnoreList": [false, null, 1, {}, []]}"#, + r#"{"cursor.general.globalCursorIgnoreList": null}"#, + r#"{}"#, + ] { + let imported = + VsCodeSettings::from_str(content, VsCodeSettingsSource::Cursor)?.settings_content(); + assert_eq!(imported.project.all_languages.edit_predictions, None); + let mut unchanged = inherited.clone(); + unchanged.merge_from(&imported.project.all_languages); + assert_eq!(unchanged.edit_predictions, inherited.edit_predictions); + } + Ok(()) + } + #[test] fn test_import_file_exclusions() -> Result<()> { let imported = VsCodeSettings::from_str( diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index e7e9e3a47f..7c072a2ab8 100644 --- a/crates/settings_content/src/language.rs +++ b/crates/settings_content/src/language.rs @@ -7,7 +7,8 @@ use settings_macros::{MergeFrom, with_fallible_options}; use std::sync::Arc; use crate::{ - DelayMs, DocumentFoldingRanges, DocumentSymbols, ExtendingSet, SemanticTokens, merge_from, + DelayMs, DocumentFoldingRanges, DocumentSymbols, ExtendingSet, SemanticTokens, SplicingVec, + merge_from, }; /// The state of the modifier keys at some point in time @@ -132,10 +133,29 @@ impl EditPredictionProvider { pub struct EditPredictionSettingsContent { /// Determines which edit prediction provider to use. pub provider: Option, - /// A list of globs representing files that edit predictions should be disabled for. - /// This list adds to a pre-existing, sensible default set of globs. - /// Any additional ones you add are combined with them. - pub disabled_globs: Option>, + /// Disable edit predictions for files matching these glob patterns. + /// + /// Use `"..."` to add patterns without repeating Zed's defaults. In project + /// settings, it extends the user or parent configuration value. Omit + /// `"..."` to replace the inherited list. + /// + /// ```json + /// { + /// "edit_predictions": { + /// "disabled_globs": ["**/build/**", "..."] + /// } + /// } + /// ``` + /// + /// Inherited patterns are inserted at `"..."`, and duplicates keep their first + /// occurrence. + /// + /// Set `[]` to clear the inherited list. Omit this setting to inherit it unchanged. + /// + /// Relative patterns are matched against paths relative to the worktree root. + /// Absolute patterns are matched against absolute paths. A leading `~` is + /// expanded to your home folder. + pub disabled_globs: Option, /// The mode used to display edit predictions in the buffer. /// Provider support required. pub mode: Option, diff --git a/crates/settings_content/src/project.rs b/crates/settings_content/src/project.rs index 37f9bc5b1c..49bd242d5f 100644 --- a/crates/settings_content/src/project.rs +++ b/crates/settings_content/src/project.rs @@ -151,7 +151,7 @@ pub struct WorktreeSettingsContent { /// } /// ``` /// - /// Use `"..."` to add patterns without repeating Zed’s defaults. In project + /// Use `"..."` to add patterns without repeating Zed's defaults. In project /// settings, it extends the user or parent configuration value. Omit /// `"..."` to replace the inherited list. /// @@ -177,7 +177,7 @@ pub struct WorktreeSettingsContent { /// } /// ``` /// - /// Use `"..."` to add patterns without repeating Zed’s defaults. In project + /// Use `"..."` to add patterns without repeating Zed's defaults. In project /// settings, it extends the user or parent configuration value. Omit /// `"..."` to replace the inherited list. /// @@ -224,7 +224,7 @@ pub struct WorktreeSettingsContent { /// } /// ``` /// - /// Use `"..."` to add patterns without repeating Zed’s defaults. In project + /// Use `"..."` to add patterns without repeating Zed's defaults. In project /// settings, it extends the user or parent configuration value. Omit /// `"..."` to replace the inherited list. /// @@ -253,7 +253,7 @@ pub struct WorktreeSettingsContent { /// } /// ``` /// - /// Use `"..."` to add patterns without repeating Zed’s defaults. In project + /// Use `"..."` to add patterns without repeating Zed's defaults. In project /// settings, it extends the user or parent configuration value. Omit /// `"..."` to replace the inherited list. /// diff --git a/crates/settings_content/src/workspace.rs b/crates/settings_content/src/workspace.rs index ab0666e3ce..dbcb6fc414 100644 --- a/crates/settings_content/src/workspace.rs +++ b/crates/settings_content/src/workspace.rs @@ -125,7 +125,7 @@ pub struct WorkspaceSettingsContent { /// /// Default: false pub close_on_file_delete: Option, - /// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). + /// Whether to allow windows to tab together based on the user's tabbing preference (macOS only). /// /// Default: false pub use_system_window_tabs: Option, diff --git a/crates/settings_json/src/settings_json.rs b/crates/settings_json/src/settings_json.rs index 0c36b56ec1..483cfd1f6b 100644 --- a/crates/settings_json/src/settings_json.rs +++ b/crates/settings_json/src/settings_json.rs @@ -71,6 +71,37 @@ pub fn update_value_in_json_text<'a>( } } +#[cfg(feature = "editing")] +pub fn find_value_range_in_json_text>( + text: &str, + key_path: &[T], +) -> Option> { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(&tree_sitter_json::LANGUAGE.into()) + .ok()?; + let syntax_tree = parser.parse(text, None)?; + let mut node = syntax_tree.root_node(); + if node.kind() == TS_DOCUMENT_KIND { + let mut cursor = node.walk(); + node = node + .named_children(&mut cursor) + .find(|child| child.kind() != TS_COMMENT_KIND)?; + } + for key in key_path { + let key_json = serde_json::to_string(key.as_ref()).ok()?; + let mut cursor = node.walk(); + let value = node.named_children(&mut cursor).find_map(|pair| { + let key_node = pair.child_by_field_name("key")?; + (text.get(key_node.byte_range()) == Some(key_json.as_str())) + .then(|| pair.child_by_field_name("value")) + .flatten() + })?; + node = value; + } + Some(node.byte_range()) +} + /// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`. #[cfg(feature = "editing")] pub fn replace_value_in_json_text>( @@ -2700,4 +2731,38 @@ mod tests { "#; parse_json_with_comments::>(trailing).unwrap_err(); } + + #[test] + fn test_find_value_range_in_json_text() { + let text = r#"// "edit_predictions": { "disabled_globs": ["commented/**"] }, + { + // "disabled_globs": ["commented/**"], + "languages": { "edit_predictions": { "disabled_globs": ["nested/**"] } }, + "edit_predictions": { + /* "disabled_globs": ["commented/**"], */ + "mode": "subtle", + "disabled_globs": ["live/**", /* ] */ "..."], // ] + } + }"#; + let range = find_value_range_in_json_text(text, &["edit_predictions", "disabled_globs"]) + .expect("value range"); + assert_eq!(&text[range], r#"["live/**", /* ] */ "..."]"#); + assert_eq!( + find_value_range_in_json_text(text, &["edit_predictions", "missing"]), + None + ); + assert_eq!(find_value_range_in_json_text(text, &["missing"]), None); + assert_eq!( + find_value_range_in_json_text("// only a comment", &["edit_predictions"]), + None + ); + assert_eq!( + find_value_range_in_json_text("", &["edit_predictions"]), + None + ); + assert_eq!( + find_value_range_in_json_text(r#"{"a": 1}"#, &["a", "b"]), + None + ); + } } diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index fe4451ae95..12ea64e57f 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -100,7 +100,7 @@ To configure panel sizing, open the Settings Editor and search for “Agent Pane ### Flexible Sizing {#agent-panel-flexible-sizing} -- Description: Whether the agent panel uses flexible (proportional) sizing when docked to the left or right. When enabled, `agent.default_width` does not control the panel width, and double-clicking the panel’s outer resize handle restores the default proportion. +- Description: Whether the agent panel uses flexible (proportional) sizing when docked to the left or right. When enabled, `agent.default_width` does not control the panel width, and double-clicking the panel's outer resize handle restores the default proportion. - Setting: `agent.flexible` - Default: `true` @@ -141,7 +141,7 @@ Open the Settings Editor and search for “Threads Sidebar Default Width”. Or } ``` -If you haven’t manually resized the sidebar, its width follows changes to this setting immediately. A manually resized width takes precedence until you double-click the divider to reset it. After resetting, the sidebar follows this setting again. +If you haven't manually resized the sidebar, its width follows changes to this setting immediately. A manually resized width takes precedence until you double-click the divider to reset it. After resetting, the sidebar follows this setting again. Widths saved by older versions of Zed are preserved if they differ from the previous default of 300 pixels. A saved width of 300 pixels uses this setting instead. @@ -789,6 +789,7 @@ For the case of "open", regular selection behavior can be achieved by holding `a - Default: ```json [settings] +{ "edit_predictions": { "disabled_globs": [ "**/.env*", @@ -803,6 +804,7 @@ For the case of "open", regular selection behavior can be achieved by holding `a "/**/zed/keymap.json" ] } +} ``` **Options** @@ -847,7 +849,7 @@ For the case of "open", regular selection behavior can be achieved by holding `a ### Disabled Globs -- Description: A list of globs for which edit predictions should be disabled for. This list adds to a pre-existing, sensible default set of globs. Any additional ones you add are combined with them. +- Description: Disable edit predictions for files matching these glob patterns. - Setting: `disabled_globs` - Default: `["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml", "**/.zed/settings.json", "/**/zed/settings.json", "/**/zed/keymap.json"]` @@ -855,6 +857,22 @@ For the case of "open", regular selection behavior can be achieved by holding `a List of `string` values. +Use `"..."` to add patterns without repeating Zed's defaults. In project settings, it extends the user or parent configuration value. Omit `"..."` to replace the inherited list. + +```json [settings] +{ + "edit_predictions": { + "disabled_globs": ["**/build/**", "..."] + } +} +``` + +Inherited patterns are inserted at `"..."`, and duplicates keep their first occurrence. + +Set `[]` to clear the inherited list. Omit this setting to inherit it unchanged. + +Relative patterns are matched against paths relative to the worktree root. Absolute patterns are matched against absolute paths. A leading `~` is expanded to your home folder. + ### Prediction Debounce - Description: How long Zed waits after you stop typing before automatically requesting an edit prediction. @@ -1878,13 +1896,13 @@ Each option controls displaying of a particular toolbar element. If all elements ## Use System Tabs -- Description: Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). +- Description: Whether to allow windows to tab together based on the user's tabbing preference (macOS only). - Setting: `use_system_window_tabs` - Default: `false` **Options** -This setting enables integration with macOS’s native window tabbing feature. When set to `true`, Zed windows can be grouped together as tabs in a single macOS window, following the system-wide tabbing preferences set by the user (such as "Always", "In Full Screen", or "Never"). This setting is only available on macOS. +This setting enables integration with macOS's native window tabbing feature. When set to `true`, Zed windows can be grouped together as tabs in a single macOS window, following the system-wide tabbing preferences set by the user (such as "Always", "In Full Screen", or "Never"). This setting is only available on macOS. ## Fullscreen Mode @@ -2353,7 +2371,7 @@ The result is still `)))` and not `))))))`, which is what it would be by default } ``` -Use `"..."` to add patterns without repeating Zed’s defaults. In project settings, it extends the user or parent configuration value. Omit `"..."` to replace the inherited list. +Use `"..."` to add patterns without repeating Zed's defaults. In project settings, it extends the user or parent configuration value. Omit `"..."` to replace the inherited list. ```json [settings] { @@ -2375,7 +2393,7 @@ Inherited patterns are inserted at `"..."`, and duplicates keep their first occu } ``` -Use `"..."` to add patterns without repeating Zed’s defaults. In project settings, it extends the user or parent configuration value. Omit `"..."` to replace the inherited list. +Use `"..."` to add patterns without repeating Zed's defaults. In project settings, it extends the user or parent configuration value. Omit `"..."` to replace the inherited list. ```json [settings] { @@ -2944,7 +2962,7 @@ Example: } ``` -Use `"..."` to add patterns without repeating Zed’s defaults. In project settings, it extends the user or parent configuration value. Omit `"..."` to replace the inherited list. +Use `"..."` to add patterns without repeating Zed's defaults. In project settings, it extends the user or parent configuration value. Omit `"..."` to replace the inherited list. ```json [settings] { @@ -3908,7 +3926,7 @@ List of `string` glob patterns } ``` -Use `"..."` to add patterns without repeating Zed’s defaults. In project settings, it extends the user or parent configuration value. Omit `"..."` to replace the inherited list. +Use `"..."` to add patterns without repeating Zed's defaults. In project settings, it extends the user or parent configuration value. Omit `"..."` to replace the inherited list. ```json [settings] {