diff --git a/assets/settings/default.json b/assets/settings/default.json index 6576281554..1c759c6154 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -146,6 +146,10 @@ // opening parenthesis, bracket, brace, single or double quote characters. // For example, when you type (, Zed will add a closing ) at the correct position. "use_autoclose": true, + // Whether to automatically surround selected text when typing opening parenthesis, + // bracket, brace, single or double quote characters. + // For example, when you select text and type (, Zed will surround the text with (). + "use_auto_surround": 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. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e07e0df8f7..a1c344d886 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -532,6 +532,7 @@ pub struct Editor { next_editor_action_id: EditorActionId, editor_actions: Rc)>>>>, use_autoclose: bool, + use_auto_surround: bool, auto_replace_emoji_shortcode: bool, show_git_blame_gutter: bool, show_git_blame_inline: bool, @@ -1811,6 +1812,7 @@ impl Editor { use_modal_editing: mode == EditorMode::Full, read_only: false, use_autoclose: true, + use_auto_surround: true, auto_replace_emoji_shortcode: false, leader_peer_id: None, remote_id: None, @@ -2188,6 +2190,10 @@ impl Editor { self.use_autoclose = autoclose; } + pub fn set_use_auto_surround(&mut self, auto_surround: bool) { + self.use_auto_surround = auto_surround; + } + pub fn set_auto_replace_emoji_shortcode(&mut self, auto_replace: bool) { self.auto_replace_emoji_shortcode = auto_replace; } @@ -2887,7 +2893,7 @@ impl Editor { // `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 !pair.close { + if !pair.close && !pair.surround { continue; } @@ -2905,9 +2911,10 @@ impl Editor { } if let Some(bracket_pair) = bracket_pair { - let autoclose = self.use_autoclose - && snapshot.settings_at(selection.start, cx).use_autoclose; - + let snapshot_settings = snapshot.settings_at(selection.start, cx); + let autoclose = self.use_autoclose && snapshot_settings.use_autoclose; + let auto_surround = + self.use_auto_surround && snapshot_settings.use_auto_surround; if selection.is_empty() { if is_bracket_pair_start { let prefix_len = bracket_pair.start.len() - text.len(); @@ -2929,6 +2936,7 @@ impl Editor { &bracket_pair.start[..prefix_len], )); if autoclose + && bracket_pair.close && following_text_allows_autoclose && preceding_text_matches_prefix { @@ -2980,7 +2988,8 @@ impl Editor { } // If an opening bracket is 1 character long and is typed while // text is selected, then surround that text with the bracket pair. - else if autoclose + else if auto_surround + && bracket_pair.surround && is_bracket_pair_start && bracket_pair.start.chars().count() == 1 { @@ -12188,9 +12197,12 @@ impl ViewInputHandler for Editor { // Disable auto-closing when composing text (i.e. typing a `"` on a Brazilian keyboard) let use_autoclose = this.use_autoclose; + let use_auto_surround = this.use_auto_surround; this.set_use_autoclose(false); + this.set_use_auto_surround(false); this.handle_input(text, cx); this.set_use_autoclose(use_autoclose); + this.set_use_auto_surround(use_auto_surround); if let Some(new_selected_range) = new_selected_range_utf16 { let snapshot = this.buffer.read(cx).read(cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 36a4b3064c..105b07c8ec 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -4635,12 +4635,14 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { start: "{".to_string(), end: "}".to_string(), close: false, + surround: false, newline: true, }, BracketPair { start: "(".to_string(), end: ")".to_string(), close: false, + surround: false, newline: true, }, ], @@ -4684,7 +4686,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { +async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; @@ -4697,32 +4699,44 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { start: "{".to_string(), end: "}".to_string(), close: true, + surround: true, newline: true, }, BracketPair { start: "(".to_string(), end: ")".to_string(), close: true, + surround: true, newline: true, }, BracketPair { start: "/*".to_string(), end: " */".to_string(), close: true, + surround: true, newline: true, }, BracketPair { start: "[".to_string(), end: "]".to_string(), close: false, + surround: false, newline: true, }, BracketPair { start: "\"".to_string(), end: "\"".to_string(), close: true, + surround: true, newline: false, }, + BracketPair { + start: "<".to_string(), + end: ">".to_string(), + close: false, + surround: true, + newline: true, + }, ], ..Default::default() }, @@ -4850,6 +4864,16 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { cx.assert_editor_state("a\"ˇ\""); cx.update_editor(|view, cx| view.handle_input("\"", cx)); cx.assert_editor_state("a\"\"ˇ"); + + // Don't autoclose pair if autoclose is disabled + cx.set_state("ˇ"); + cx.update_editor(|view, cx| view.handle_input("<", cx)); + cx.assert_editor_state("<ˇ"); + + // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled + cx.set_state("«aˇ» b"); + cx.update_editor(|view, cx| view.handle_input("<", cx)); + cx.assert_editor_state("<«aˇ»> b"); } #[gpui::test] @@ -4868,18 +4892,21 @@ async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestA start: "{".to_string(), end: "}".to_string(), close: true, + surround: true, newline: true, }, BracketPair { start: "(".to_string(), end: ")".to_string(), close: true, + surround: true, newline: true, }, BracketPair { start: "[".to_string(), end: "]".to_string(), close: false, + surround: false, newline: true, }, ], @@ -5293,12 +5320,14 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { start: "{".to_string(), end: "}".to_string(), close: true, + surround: true, newline: true, }, BracketPair { start: "/* ".to_string(), end: "*/".to_string(), close: true, + surround: true, ..Default::default() }, ], @@ -5447,6 +5476,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { start: "{".to_string(), end: "}".to_string(), close: true, + surround: true, newline: true, }], ..Default::default() @@ -5558,18 +5588,21 @@ async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppC start: "{".to_string(), end: "}".to_string(), close: true, + surround: true, newline: true, }, BracketPair { start: "(".to_string(), end: ")".to_string(), close: true, + surround: true, newline: true, }, BracketPair { start: "[".to_string(), end: "]".to_string(), close: false, + surround: true, newline: true, }, ], @@ -7537,12 +7570,14 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { start: "{".to_string(), end: "}".to_string(), close: true, + surround: true, newline: true, }, BracketPair { start: "/* ".to_string(), end: " */".to_string(), close: true, + surround: true, newline: true, }, ], @@ -8344,6 +8379,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { start: "{".to_string(), end: "}".to_string(), close: true, + surround: true, newline: true, }], disabled_scopes_by_bracket_ix: Vec::new(), diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index ca905fee02..525d70f747 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -55,12 +55,14 @@ mod tests { start: "{".to_string(), end: "}".to_string(), close: false, + surround: false, newline: true, }, BracketPair { start: "(".to_string(), end: ")".to_string(), close: false, + surround: false, newline: true, }, ], diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index f509ce957d..fe13b62450 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -181,6 +181,7 @@ impl EditorLspTestContext { start: "{".to_string(), end: "}".to_string(), close: true, + surround: true, newline: true, }], disabled_scopes_by_bracket_ix: Default::default(), diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index fdde10ab74..c47ccbddd5 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1782,12 +1782,14 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) { start: "{".into(), end: "}".into(), close: true, + surround: true, newline: false, }, BracketPair { start: "'".into(), end: "'".into(), close: true, + surround: true, newline: false, }, ], @@ -1910,12 +1912,14 @@ fn test_language_scope_at_with_rust(cx: &mut AppContext) { start: "{".into(), end: "}".into(), close: true, + surround: true, newline: false, }, BracketPair { start: "'".into(), end: "'".into(), close: true, + surround: true, newline: false, }, ], diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 51e5773d81..d1bb7c36f7 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -61,6 +61,7 @@ use task::RunnableTag; pub use task_context::{ContextProvider, RunnableRange}; use theme::SyntaxTheme; use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore}; +use util::serde::default_true; pub use buffer::Operation; pub use buffer::*; @@ -803,6 +804,9 @@ pub struct BracketPair { pub end: String, /// True if `end` should be automatically inserted right after `start` characters. pub close: bool, + /// True if selected text should be surrounded by `start` and `end` characters. + #[serde(default = "default_true")] + pub surround: bool, /// True if an extra newline should be inserted while the cursor is in the middle /// of that bracket pair. pub newline: bool, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 1d5fd8713d..ebb6ae5059 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -112,6 +112,8 @@ pub struct LanguageSettings { pub inlay_hints: InlayHintSettings, /// Whether to automatically close brackets. pub use_autoclose: bool, + /// Whether to automatically surround text with brackets. + pub use_auto_surround: bool, // Controls how the editor handles the autoclosed characters. pub always_treat_brackets_as_autoclosed: bool, /// Which code actions to run on save @@ -315,6 +317,11 @@ pub struct LanguageSettingsContent { /// /// Default: true pub use_autoclose: Option, + /// Whether to automatically surround text with characters for you. For example, + /// when you select text and type (, Zed will automatically surround text with (). + /// + /// Default: true + pub use_auto_surround: 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. @@ -774,6 +781,7 @@ 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.use_auto_surround, src.use_auto_surround); merge( &mut settings.always_treat_brackets_as_autoclosed, src.always_treat_brackets_as_autoclosed, diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 4e4848742e..14093d896f 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -40,6 +40,7 @@ pub fn add_surrounds(text: Arc, target: SurroundsType, cx: &mut WindowConte start: text.to_string(), end: text.to_string(), close: true, + surround: true, newline: false, }, }; @@ -227,6 +228,7 @@ pub fn change_surrounds(text: Arc, target: Object, cx: &mut WindowContext) start: text.to_string(), end: text.to_string(), close: true, + surround: true, newline: false, }, }; @@ -388,54 +390,63 @@ fn all_support_surround_pair() -> Vec { start: "{".into(), end: "}".into(), close: true, + surround: true, newline: false, }, BracketPair { start: "'".into(), end: "'".into(), close: true, + surround: true, newline: false, }, BracketPair { start: "`".into(), end: "`".into(), close: true, + surround: true, newline: false, }, BracketPair { start: "\"".into(), end: "\"".into(), close: true, + surround: true, newline: false, }, BracketPair { start: "(".into(), end: ")".into(), close: true, + surround: true, newline: false, }, BracketPair { start: "|".into(), end: "|".into(), close: true, + surround: true, newline: false, }, BracketPair { start: "[".into(), end: "]".into(), close: true, + surround: true, newline: false, }, BracketPair { start: "{".into(), end: "}".into(), close: true, + surround: true, newline: false, }, BracketPair { start: "<".into(), end: ">".into(), close: true, + surround: true, newline: false, }, ]; @@ -461,48 +472,56 @@ fn object_to_bracket_pair(object: Object) -> Option { start: "'".to_string(), end: "'".to_string(), close: true, + surround: true, newline: false, }), Object::BackQuotes => Some(BracketPair { start: "`".to_string(), end: "`".to_string(), close: true, + surround: true, newline: false, }), Object::DoubleQuotes => Some(BracketPair { start: "\"".to_string(), end: "\"".to_string(), close: true, + surround: true, newline: false, }), Object::VerticalBars => Some(BracketPair { start: "|".to_string(), end: "|".to_string(), close: true, + surround: true, newline: false, }), Object::Parentheses => Some(BracketPair { start: "(".to_string(), end: ")".to_string(), close: true, + surround: true, newline: false, }), Object::SquareBrackets => Some(BracketPair { start: "[".to_string(), end: "]".to_string(), close: true, + surround: true, newline: false, }), Object::CurlyBrackets => Some(BracketPair { start: "{".to_string(), end: "}".to_string(), close: true, + surround: true, newline: false, }), Object::AngleBrackets => Some(BracketPair { start: "<".to_string(), end: ">".to_string(), close: true, + surround: true, newline: false, }), _ => None,