From 7855b9e9a864935258cc20a18f317d22c3478fd2 Mon Sep 17 00:00:00 2001 From: Tim Masliuchenko Date: Wed, 20 Mar 2024 08:35:42 +0000 Subject: [PATCH] Allow to handle autoclosed characters differently (#8666) Adds the `always_treat_brackets_as_autoclosed` setting to control how the autoclosed characters are handled. The setting is off by default, meaning the behaviour stays the same (following how VSCode handles autoclosed characters). When set to `true`, the autoclosed characters are always skipped over and auto-removed no matter how they were inserted (following how Sublime Text/Xcode handle this). https://github.com/zed-industries/zed/assets/471335/304cd04a-59fe-450f-9c65-cc31b781b0db https://github.com/zed-industries/zed/assets/471335/0f5b09c2-260f-48d4-8528-23f122dee45f Release Notes: - Added the setting `always_treat_brackets_as_autoclosed` (default: `false`) to always treat brackets as "auto-closed" brackets, i.e. deleting the pair when deleting start/end, etc. ([#7146](https://github.com/zed-industries/zed/issues/7146)). --------- Co-authored-by: Thorsten Ball --- assets/settings/default.json | 6 + crates/editor/src/editor.rs | 88 ++++++++-- crates/editor/src/editor_tests.rs | 199 +++++++++++++++++++++++ crates/language/src/language_settings.rs | 15 +- docs/src/configuring_zed.md | 22 +++ 5 files changed, 313 insertions(+), 17 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f93e22e059861beffdfcc7fb6587e9f013704206..39c62b4ea2f32fa7ee98a1fe87b443819f9a0bbf 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -92,6 +92,12 @@ // Whether to automatically type closing characters for you. For example, // when you type (, Zed will automatically add a closing ) at the correct position. "use_autoclose": true, + // Controls how the editor handles the autoclosed characters. + // When set to `false`(default), skipping over and auto-removing of the closing characters + // happen only for auto-inserted characters. + // Otherwise(when `true`), the closing characters are always skipped over and auto-removed + // no matter how they were inserted. + "always_treat_brackets_as_autoclosed": false, // Controls whether copilot provides suggestion immediately // or waits for a `copilot::Toggle` "show_copilot_suggestions": true, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dbcbe67f82d4c9b539b4a703eeb5f2ef3268843c..05582f4441ba6655f54ae2cbb0d8005c86b244df 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2432,16 +2432,23 @@ impl Editor { // bracket of any of this language's bracket pairs. let mut bracket_pair = None; let mut is_bracket_pair_start = false; + let mut is_bracket_pair_end = false; if !text.is_empty() { // `text` can be empty when a user is using IME (e.g. Chinese Wubi Simplified) // and they are removing the character that triggered IME popup. for (pair, enabled) in scope.brackets() { - if enabled && pair.close && pair.start.ends_with(text.as_ref()) { + if !pair.close { + continue; + } + + if enabled && pair.start.ends_with(text.as_ref()) { bracket_pair = Some(pair.clone()); is_bracket_pair_start = true; break; - } else if pair.end.as_str() == text.as_ref() { + } + if pair.end.as_str() == text.as_ref() { bracket_pair = Some(pair.clone()); + is_bracket_pair_end = true; break; } } @@ -2504,6 +2511,21 @@ impl Editor { continue; } } + + let always_treat_brackets_as_autoclosed = snapshot + .settings_at(selection.start, cx) + .always_treat_brackets_as_autoclosed; + if always_treat_brackets_as_autoclosed + && is_bracket_pair_end + && snapshot.contains_str_at(selection.end, text.as_ref()) + { + // Otherwise, when `always_treat_brackets_as_autoclosed` is set to `true + // and the inserted text is a closing bracket and the selection is followed + // by the closing bracket then move the selection past the closing bracket. + let anchor = snapshot.anchor_after(selection.end); + new_selections.push((selection.map(|_| anchor), text.len())); + continue; + } } // If an opening bracket is 1 character long and is typed while // text is selected, then surround that text with the bracket pair. @@ -3024,25 +3046,59 @@ impl Editor { fn select_autoclose_pair(&mut self, cx: &mut ViewContext) { let selections = self.selections.all::(cx); let buffer = self.buffer.read(cx).read(cx); - let mut new_selections = Vec::new(); - for (mut selection, region) in self.selections_with_autoclose_regions(selections, &buffer) { - if let (Some(region), true) = (region, selection.is_empty()) { - let mut range = region.range.to_offset(&buffer); - if selection.start == range.start { - if range.start >= region.pair.start.len() { + let new_selections = self + .selections_with_autoclose_regions(selections, &buffer) + .map(|(mut selection, region)| { + if !selection.is_empty() { + return selection; + } + + if let Some(region) = region { + let mut range = region.range.to_offset(&buffer); + if selection.start == range.start && range.start >= region.pair.start.len() { range.start -= region.pair.start.len(); - if buffer.contains_str_at(range.start, ®ion.pair.start) { - if buffer.contains_str_at(range.end, ®ion.pair.end) { - range.end += region.pair.end.len(); - selection.start = range.start; - selection.end = range.end; + if buffer.contains_str_at(range.start, ®ion.pair.start) + && buffer.contains_str_at(range.end, ®ion.pair.end) + { + range.end += region.pair.end.len(); + selection.start = range.start; + selection.end = range.end; + + return selection; + } + } + } + + let always_treat_brackets_as_autoclosed = buffer + .settings_at(selection.start, cx) + .always_treat_brackets_as_autoclosed; + + if !always_treat_brackets_as_autoclosed { + return selection; + } + + if let Some(scope) = buffer.language_scope_at(selection.start) { + for (pair, enabled) in scope.brackets() { + if !enabled || !pair.close { + continue; + } + + if buffer.contains_str_at(selection.start, &pair.end) { + let pair_start_len = pair.start.len(); + if buffer.contains_str_at(selection.start - pair_start_len, &pair.start) + { + selection.start -= pair_start_len; + selection.end += pair.end.len(); + + return selection; } } } } - } - new_selections.push(selection); - } + + selection + }) + .collect(); drop(buffer); self.change_selections(None, cx, |selections| selections.select(new_selections)); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 44d08b23cade757d89ec06f4dce48a373bf40f64..fad6485a7aed182e31dfae664ad5a530c617227b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -4566,6 +4566,105 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { cx.assert_editor_state("a\"\"ˇ"); } +#[gpui::test] +async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.always_treat_brackets_as_autoclosed = Some(true); + }); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "[".to_string(), + end: "]".to_string(), + close: false, + newline: true, + }, + ], + ..Default::default() + }, + autoclose_before: "})]".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + cx.language_registry().add(language.clone()); + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(language), cx); + }); + + cx.set_state( + &" + ˇ + ˇ + ˇ + " + .unindent(), + ); + + // ensure only matching closing brackets are skipped over + cx.update_editor(|view, cx| { + view.handle_input("}", cx); + view.move_left(&MoveLeft, cx); + view.handle_input(")", cx); + view.move_left(&MoveLeft, cx); + }); + cx.assert_editor_state( + &" + ˇ)} + ˇ)} + ˇ)} + " + .unindent(), + ); + + // skip-over closing brackets at multiple cursors + cx.update_editor(|view, cx| { + view.handle_input(")", cx); + view.handle_input("}", cx); + }); + cx.assert_editor_state( + &" + )}ˇ + )}ˇ + )}ˇ + " + .unindent(), + ); + + // ignore non-close brackets + cx.update_editor(|view, cx| { + view.handle_input("]", cx); + view.move_left(&MoveLeft, cx); + view.handle_input("]", cx); + }); + cx.assert_editor_state( + &" + )}]ˇ] + )}]ˇ] + )}]ˇ] + " + .unindent(), + ); +} + #[gpui::test] async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -5163,6 +5262,106 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.always_treat_brackets_as_autoclosed = Some(true); + }); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new(Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "[".to_string(), + end: "]".to_string(), + close: false, + newline: true, + }, + ], + ..Default::default() + }, + autoclose_before: "})]".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + cx.language_registry().add(language.clone()); + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(language), cx); + }); + + cx.set_state( + &" + {(ˇ)} + [[ˇ]] + {(ˇ)} + " + .unindent(), + ); + + cx.update_editor(|view, cx| { + view.backspace(&Default::default(), cx); + view.backspace(&Default::default(), cx); + }); + + cx.assert_editor_state( + &" + ˇ + ˇ]] + ˇ + " + .unindent(), + ); + + cx.update_editor(|view, cx| { + view.handle_input("{", cx); + view.handle_input("{", cx); + view.move_right(&MoveRight, cx); + view.move_right(&MoveRight, cx); + view.move_left(&MoveLeft, cx); + view.move_left(&MoveLeft, cx); + view.backspace(&Default::default(), cx); + }); + + cx.assert_editor_state( + &" + {ˇ} + {ˇ}]] + {ˇ} + " + .unindent(), + ); + + cx.update_editor(|view, cx| { + view.backspace(&Default::default(), cx); + }); + + cx.assert_editor_state( + &" + ˇ + ˇ]] + ˇ + " + .unindent(), + ); +} + #[gpui::test] async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 6ee6e137f6b69c93265930489065c02424798e2f..38776141793633ce755786c0838a2ee10dab152c 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -103,6 +103,8 @@ pub struct LanguageSettings { pub inlay_hints: InlayHintSettings, /// Whether to automatically close brackets. pub use_autoclose: bool, + // Controls how the editor handles the autoclosed characters. + pub always_treat_brackets_as_autoclosed: bool, /// Which code actions to run on save pub code_actions_on_format: HashMap, } @@ -231,7 +233,14 @@ pub struct LanguageSettingsContent { /// /// Default: true pub use_autoclose: Option, - + // Controls how the editor handles the autoclosed characters. + // When set to `false`(default), skipping over and auto-removing of the closing characters + // happen only for auto-inserted characters. + // Otherwise(when `true`), the closing characters are always skipped over and auto-removed + // no matter how they were inserted. + /// + /// Default: false + pub always_treat_brackets_as_autoclosed: Option, /// Which code actions to run on save /// /// Default: {} (or {"source.organizeImports": true} for Go). @@ -602,6 +611,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent merge(&mut settings.hard_tabs, src.hard_tabs); merge(&mut settings.soft_wrap, src.soft_wrap); merge(&mut settings.use_autoclose, src.use_autoclose); + merge( + &mut settings.always_treat_brackets_as_autoclosed, + src.always_treat_brackets_as_autoclosed, + ); merge(&mut settings.show_wrap_guides, src.show_wrap_guides); merge(&mut settings.wrap_guides, src.wrap_guides.clone()); merge( diff --git a/docs/src/configuring_zed.md b/docs/src/configuring_zed.md index cec956347dbc022befc4fb83803d5cf93d287327..6d5b383468560b0b7281218ef434124b66468787 100644 --- a/docs/src/configuring_zed.md +++ b/docs/src/configuring_zed.md @@ -380,6 +380,26 @@ To override settings for a language, add an entry for that language server's nam `boolean` values +## Always Treat Brackets As Autoclosed + +- Description: Controls how the editor handles the autoclosed characters. +- Setting: `always_treat_brackets_as_autoclosed` +- Default: `false` + +**Options** + +`boolean` values + +**Example** + +If the setting is set to `true`: + +1. Enter in the editor: `)))` +2. Move the cursor to the start: `^)))` +3. Enter again: `)))` + +The result is still `)))` and not `))))))`, which is what it would be by default. + ## File Types - Setting: `file_types` @@ -573,6 +593,8 @@ The following settings can be overridden for each specific language: - `show_whitespaces` - `soft_wrap` - `tab_size` +- `use_autoclose` +- `always_treat_brackets_as_autoclosed` These values take in the same options as the root-level settings with the same name.