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:
parent
d5e0817fbc
commit
7855b9e9a8
5 changed files with 313 additions and 17 deletions
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue