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,
|
// Whether to automatically type closing characters for you. For example,
|
||||||
// when you type (, Zed will automatically add a closing ) at the correct position.
|
// when you type (, Zed will automatically add a closing ) at the correct position.
|
||||||
"use_autoclose": true,
|
"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
|
// Controls whether copilot provides suggestion immediately
|
||||||
// or waits for a `copilot::Toggle`
|
// or waits for a `copilot::Toggle`
|
||||||
"show_copilot_suggestions": true,
|
"show_copilot_suggestions": true,
|
||||||
|
|
|
@ -2432,16 +2432,23 @@ impl Editor {
|
||||||
// bracket of any of this language's bracket pairs.
|
// bracket of any of this language's bracket pairs.
|
||||||
let mut bracket_pair = None;
|
let mut bracket_pair = None;
|
||||||
let mut is_bracket_pair_start = false;
|
let mut is_bracket_pair_start = false;
|
||||||
|
let mut is_bracket_pair_end = false;
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
// `text` can be empty when a user is using IME (e.g. Chinese Wubi Simplified)
|
// `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.
|
// and they are removing the character that triggered IME popup.
|
||||||
for (pair, enabled) in scope.brackets() {
|
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());
|
bracket_pair = Some(pair.clone());
|
||||||
is_bracket_pair_start = true;
|
is_bracket_pair_start = true;
|
||||||
break;
|
break;
|
||||||
} else if pair.end.as_str() == text.as_ref() {
|
}
|
||||||
|
if pair.end.as_str() == text.as_ref() {
|
||||||
bracket_pair = Some(pair.clone());
|
bracket_pair = Some(pair.clone());
|
||||||
|
is_bracket_pair_end = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2504,6 +2511,21 @@ impl Editor {
|
||||||
continue;
|
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
|
// If an opening bracket is 1 character long and is typed while
|
||||||
// text is selected, then surround that text with the bracket pair.
|
// 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>) {
|
fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let selections = self.selections.all::<usize>(cx);
|
let selections = self.selections.all::<usize>(cx);
|
||||||
let buffer = self.buffer.read(cx).read(cx);
|
let buffer = self.buffer.read(cx).read(cx);
|
||||||
let mut new_selections = Vec::new();
|
let new_selections = self
|
||||||
for (mut selection, region) in self.selections_with_autoclose_regions(selections, &buffer) {
|
.selections_with_autoclose_regions(selections, &buffer)
|
||||||
if let (Some(region), true) = (region, selection.is_empty()) {
|
.map(|(mut selection, region)| {
|
||||||
let mut range = region.range.to_offset(&buffer);
|
if !selection.is_empty() {
|
||||||
if selection.start == range.start {
|
return selection;
|
||||||
if range.start >= region.pair.start.len() {
|
}
|
||||||
|
|
||||||
|
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();
|
range.start -= region.pair.start.len();
|
||||||
if buffer.contains_str_at(range.start, ®ion.pair.start) {
|
if buffer.contains_str_at(range.start, ®ion.pair.start)
|
||||||
if buffer.contains_str_at(range.end, ®ion.pair.end) {
|
&& buffer.contains_str_at(range.end, ®ion.pair.end)
|
||||||
range.end += region.pair.end.len();
|
{
|
||||||
selection.start = range.start;
|
range.end += region.pair.end.len();
|
||||||
selection.end = range.end;
|
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);
|
drop(buffer);
|
||||||
self.change_selections(None, cx, |selections| selections.select(new_selections));
|
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\"\"ˇ");
|
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]
|
#[gpui::test]
|
||||||
async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
|
async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
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]
|
#[gpui::test]
|
||||||
async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
|
async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
|
|
@ -103,6 +103,8 @@ pub struct LanguageSettings {
|
||||||
pub inlay_hints: InlayHintSettings,
|
pub inlay_hints: InlayHintSettings,
|
||||||
/// Whether to automatically close brackets.
|
/// Whether to automatically close brackets.
|
||||||
pub use_autoclose: bool,
|
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
|
/// Which code actions to run on save
|
||||||
pub code_actions_on_format: HashMap<String, bool>,
|
pub code_actions_on_format: HashMap<String, bool>,
|
||||||
}
|
}
|
||||||
|
@ -231,7 +233,14 @@ pub struct LanguageSettingsContent {
|
||||||
///
|
///
|
||||||
/// Default: true
|
/// Default: true
|
||||||
pub use_autoclose: Option<bool>,
|
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
|
/// Which code actions to run on save
|
||||||
///
|
///
|
||||||
/// Default: {} (or {"source.organizeImports": true} for Go).
|
/// 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.hard_tabs, src.hard_tabs);
|
||||||
merge(&mut settings.soft_wrap, src.soft_wrap);
|
merge(&mut settings.soft_wrap, src.soft_wrap);
|
||||||
merge(&mut settings.use_autoclose, src.use_autoclose);
|
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.show_wrap_guides, src.show_wrap_guides);
|
||||||
merge(&mut settings.wrap_guides, src.wrap_guides.clone());
|
merge(&mut settings.wrap_guides, src.wrap_guides.clone());
|
||||||
merge(
|
merge(
|
||||||
|
|
|
@ -380,6 +380,26 @@ To override settings for a language, add an entry for that language server's nam
|
||||||
|
|
||||||
`boolean` values
|
`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
|
## File Types
|
||||||
|
|
||||||
- Setting: `file_types`
|
- Setting: `file_types`
|
||||||
|
@ -573,6 +593,8 @@ The following settings can be overridden for each specific language:
|
||||||
- `show_whitespaces`
|
- `show_whitespaces`
|
||||||
- `soft_wrap`
|
- `soft_wrap`
|
||||||
- `tab_size`
|
- `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.
|
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