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 <mrnugget@gmail.com>
This commit is contained in:
Tim Masliuchenko 2024-03-20 08:35:42 +00:00 committed by GitHub
parent d5e0817fbc
commit 7855b9e9a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 313 additions and 17 deletions

View file

@ -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,

View file

@ -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, &region.pair.start) {
if buffer.contains_str_at(range.end, &region.pair.end) {
range.end += region.pair.end.len();
selection.start = range.start;
selection.end = range.end;
if buffer.contains_str_at(range.start, &region.pair.start)
&& buffer.contains_str_at(range.end, &region.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));

View file

@ -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, |_| {});

View file

@ -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(

View file

@ -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.