Refactor editor's select inside/around delimiter actions (#59472)

# Objective

Both https://github.com/zed-industries/zed/pull/59005 and
https://github.com/zed-industries/zed/pull/53789 introduced actions, at
the editor context, which allow to select the content between
brackets/delimiters, with the latter also adding an action to include
the brackets/delimiters too.

Shipping both would mean we end up with three distinct, but similar
actions:

* `editor: select inside enclosing bracket`
* `editor: select inside delimiters`
* `editor: select around delimiters`

As such, in order to simplify this, the changes in this Pull Request
remove the first action, while updating the logic for both `editor:
select inside delimiters` and `editor: select around delimiters` in
order to support continuous outwards expansion, like `editor: select
inside enclosing bracket` supported.

## Solution

* Remove `editor::SelectInsideEnclosingBracket` action, it's related
method and tests
* Updated `editor::selection::Editor::select_delimiters_impl` to use
`MultiBufferSnapshot::enclosing_bracket_ranges` instead of
`MultiBufferSnapshot::innermost_enclosing_bracket_ranges` so we can
traverse all bracket ranges and expand to the next one

## Testing

Besides manual testing, a new test case was added specifically for the
selection expansion support, namely
`editor::editor_tests::test_select_delimiters_expansion`.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- N/A
This commit is contained in:
Dino authored and GitHub committed 2026-06-17 10:37:40 +00:00
1 parent f2fbe40b5d
commit 906b7e6612
5 files changed
+75 -150

No files matched your search

+9 -6
View File
@@ -654,11 +654,16 @@ actions!(
MoveToBeginning,
/// Moves cursor to the enclosing bracket.
MoveToEnclosingBracket,
/// Selects the content inside the nearest enclosing delimiters
/// (brackets, braces, parentheses, or quotes).
/// Selects the content within the nearest enclosing delimiters
/// (brackets, braces, parentheses, or quotes), excluding the
/// delimiters.
/// Repeating the action expands the selection to the next enclosing
/// pair.
SelectInsideDelimiters,
/// Selects the content inside the nearest surrounding delimiters,
/// including the delimiters themselves.
/// Selects the nearest enclosing delimiters (brackets, braces,
/// parentheses, or quotes) together with the content between them.
/// Repeating the action expands the selection to the next enclosing
/// pair.
SelectAroundDelimiters,
/// Moves cursor to the end of the document.
MoveToEnd,
@@ -779,8 +784,6 @@ actions!(
SelectDown,
/// Selects the enclosing symbol.
SelectEnclosingSymbol,
/// Selects inside the innermost enclosing bracket pair.
SelectInsideEnclosingBracket,
/// Selects to the start of the next larger syntax node.
SelectToStartOfLargerSyntaxNode,
/// Selects to the end of the next larger syntax node.
-45
View File
@@ -7529,51 +7529,6 @@ impl Editor {
}
}
pub fn select_inside_delimiters(
&mut self,
_: &SelectInsideDelimiters,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.select_delimiters_impl(false, window, cx);
}
pub fn select_around_delimiters(
&mut self,
_: &SelectAroundDelimiters,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.select_delimiters_impl(true, window, cx);
}
fn select_delimiters_impl(
&mut self,
include_brackets: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.change_selections(Default::default(), window, cx, |s| {
s.move_offsets_with(&mut |snapshot, selection| {
let Some((open, close)) = snapshot
.innermost_enclosing_bracket_ranges(selection.start..selection.end, None)
else {
return;
};
let (start, end) = if include_brackets {
(open.start, close.end)
} else {
(open.end, close.start)
};
selection.start = start;
selection.end = end;
selection.reversed = false;
selection.goal = SelectionGoal::None;
})
});
}
pub fn rename(
&mut self,
_: &Rename,
+33 -91
View File
@@ -21303,88 +21303,6 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_select_inside_enclosing_bracket(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
#[track_caller]
fn assert_after_runs(before: &str, after: &str, runs: usize, cx: &mut EditorLspTestContext) {
let _state_context = cx.set_state(before);
cx.run_until_parked();
for _ in 0..runs {
cx.update_editor(|editor, window, cx| {
editor.select_inside_enclosing_bracket(&SelectInsideEnclosingBracket, window, cx)
});
}
cx.run_until_parked();
cx.assert_editor_state(after);
}
#[track_caller]
fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
assert_after_runs(before, after, 1, cx);
}
assert("console.log(ˇvar);", "console.log(«varˇ»);", &mut cx);
assert("console.logˇ(var);", "console.log(«varˇ»);", &mut cx);
assert("console.log(var)ˇ;", "console.log(«varˇ»);", &mut cx);
assert(
"let numbers = [1, ˇ2, 3];",
"let numbers = [«1, 2, 3ˇ»];",
&mut cx,
);
assert(
"const object = { foo: ˇbar };",
"const object = {« foo: bar ˇ»};",
&mut cx,
);
assert(
r#"const doubleQuoted = "foo ˇbar";"#,
r#"const doubleQuoted = "«foo barˇ»";"#,
&mut cx,
);
assert(
"const singleQuoted = 'foo ˇbar';",
"const singleQuoted = '«foo barˇ»';",
&mut cx,
);
assert(
"const template = `foo ˇbar`;",
"const template = `«foo barˇ»`;",
&mut cx,
);
assert(
"let result = foo(bar(ˇbaz));",
"let result = foo(bar(«bazˇ»));",
&mut cx,
);
assert(
"let result = foo(«barˇ»(baz));",
"let result = foo(«bar(baz)ˇ»);",
&mut cx,
);
assert_after_runs(
"let result = foo(bar(ˇbaz));",
"let result = foo(«bar(baz)ˇ»);",
2,
&mut cx,
);
assert_after_runs(
r#"let result = (xx[xxx{xxˇx}] xx"xxx"xx);"#,
r#"let result = («xx[xxx{xxx}] xx"xxx"xxˇ»);"#,
3,
&mut cx,
);
assert("let plain = ˇvalue;", "let plain = ˇvalue;", &mut cx);
assert(
"foo(ˇone); bar(ˇtwo);",
"foo(«oneˇ»); bar(«twoˇ»);",
&mut cx,
);
}
#[gpui::test]
async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -38423,17 +38341,13 @@ async fn test_toggle_markdown_block_quote(cx: &mut TestAppContext) {
#[track_caller]
fn assert_select_delimiters(around: bool, before: &str, after: &str, cx: &mut EditorTestContext) {
let _state_context = cx.set_state(before);
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
if around {
editor.select_around_delimiters(&SelectAroundDelimiters, window, cx);
} else {
editor.select_inside_delimiters(&SelectInsideDelimiters, window, cx);
}
});
if around {
cx.dispatch_action(SelectAroundDelimiters);
} else {
cx.dispatch_action(SelectInsideDelimiters);
}
cx.run_until_parked();
cx.assert_editor_state(after);
}
@@ -38572,3 +38486,31 @@ async fn test_select_delimiters_in_markdown(cx: &mut TestAppContext) {
&mut cx,
);
}
#[gpui::test]
async fn test_select_delimiters_expansion(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
let _state_context = cx.set_state("foo([1, ˇ2, 3]);");
cx.dispatch_action(SelectInsideDelimiters);
cx.assert_editor_state("foo([«1, 2, 3ˇ»]);");
cx.dispatch_action(SelectInsideDelimiters);
cx.assert_editor_state("foo(«[1, 2, 3]ˇ»);");
let _state_context = cx.set_state("foo([1, ˇ2, 3]);");
cx.dispatch_action(SelectInsideDelimiters);
cx.assert_editor_state("foo([«1, 2, 3ˇ»]);");
cx.dispatch_action(SelectAroundDelimiters);
cx.assert_editor_state("foo(«[1, 2, 3]ˇ»);");
cx.dispatch_action(SelectAroundDelimiters);
cx.assert_editor_state("foo«([1, 2, 3])ˇ»;");
let _state_context = cx.set_state("foo(x, { ˇa: 1 });");
cx.dispatch_action(SelectInsideDelimiters);
cx.assert_editor_state("foo(x, {« a: 1 ˇ»});");
cx.dispatch_action(SelectAroundDelimiters);
cx.assert_editor_state("foo(x, «{ a: 1 }ˇ»);");
cx.dispatch_action(SelectInsideDelimiters);
cx.assert_editor_state("foo(«x, { a: 1 }ˇ»);");
}
-1
View File
@@ -335,7 +335,6 @@ impl EditorElement {
register_action(editor, window, Editor::move_to_start_of_larger_syntax_node);
register_action(editor, window, Editor::move_to_end_of_larger_syntax_node);
register_action(editor, window, Editor::select_enclosing_symbol);
register_action(editor, window, Editor::select_inside_enclosing_bracket);
register_action(editor, window, Editor::move_to_enclosing_bracket);
register_action(editor, window, Editor::select_inside_delimiters);
register_action(editor, window, Editor::select_around_delimiters);
+33 -7
View File
@@ -966,9 +966,27 @@ impl Editor {
self.select_to_syntax_nodes(window, cx, true);
}
pub fn select_inside_enclosing_bracket(
pub fn select_inside_delimiters(
&mut self,
_: &SelectInsideEnclosingBracket,
_: &SelectInsideDelimiters,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.select_delimiters_impl(false, window, cx);
}
pub fn select_around_delimiters(
&mut self,
_: &SelectAroundDelimiters,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.select_delimiters_impl(true, window, cx);
}
fn select_delimiters_impl(
&mut self,
include_brackets: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -984,20 +1002,28 @@ impl Editor {
let mut best_length = usize::MAX;
for (open, close) in enclosing_bracket_ranges {
let inside = open.end..close.start;
if inside == (selection.start..selection.end) {
let range = if include_brackets {
open.start..close.end
} else {
open.end..close.start
};
// Skip any bracket pair that is already covered by the
// selection so repeated uses of delimiters selection only
// evere expands outwards to the next pair.
if (selection.start..selection.end).contains_inclusive(&range) {
continue;
}
let length = close.end - open.start;
if length < best_length {
best_length = length;
best = Some(inside);
best = Some(range);
}
}
if let Some(inside) = best {
selection.set_head_tail(inside.end, inside.start, SelectionGoal::None);
if let Some(range) = best {
selection.set_head_tail(range.end, range.start, SelectionGoal::None);
}
})
});