@@ -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<Self>) {
let selections = self.selections.all::<usize>(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));
@@ -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, |_| {});
@@ -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<String, bool>,
}
@@ -231,7 +233,14 @@ pub struct LanguageSettingsContent {
///
/// Default: true
pub use_autoclose: Option<bool>,
-
+ // 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<bool>,
/// 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(