editor: Go to previous and next symbol actions (#50777)
Closes discussion #34890 This is similar to the vim prev/next method/section motion, but more flexible because this follows the items in editor's outline (Tree sitter or LSP provided). Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - Added actions `editor::GoToPreviousSymbol` and `editor::GoToNextSymbol` actions to go to the previous and next outline symbol. This is either the tree sitter outline, or the LSP provided outline depending on the configuration.
This commit is contained in:
1 parent
9c8d9871c1
commit
002bb34dbc
4 files changed
+362
No files matched your search
@@ -568,6 +568,10 @@ actions!(
|
||||
GoToParentModule,
|
||||
/// Goes to the previous change in the file.
|
||||
GoToPreviousChange,
|
||||
/// Goes to the next symbol.
|
||||
GoToNextSymbol,
|
||||
/// Goes to the previous symbol.
|
||||
GoToPreviousSymbol,
|
||||
/// Goes to the next reference to the symbol under the cursor.
|
||||
GoToNextReference,
|
||||
/// Goes to the previous reference to the symbol under the cursor.
|
||||
|
||||
@@ -18125,6 +18125,108 @@ impl Editor {
|
||||
};
|
||||
}
|
||||
|
||||
fn go_to_symbol_by_offset(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
offset: i8,
|
||||
) -> Task<Result<()>> {
|
||||
let editor_snapshot = self.snapshot(window, cx);
|
||||
|
||||
// We don't care about multi-buffer symbols
|
||||
let Some((excerpt_id, _, _)) = editor_snapshot.as_singleton() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
let cursor_offset = self
|
||||
.selections
|
||||
.newest::<MultiBufferOffset>(&editor_snapshot.display_snapshot)
|
||||
.head();
|
||||
|
||||
cx.spawn_in(window, async move |editor, wcx| -> Result<()> {
|
||||
let Ok(Some(remote_id)) = editor.update(wcx, |ed, cx| {
|
||||
let buffer = ed.buffer.read(cx).as_singleton()?;
|
||||
Some(buffer.read(cx).remote_id())
|
||||
}) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let task = editor.update(wcx, |ed, cx| ed.buffer_outline_items(remote_id, cx))?;
|
||||
let outline_items: Vec<OutlineItem<text::Anchor>> = task.await;
|
||||
|
||||
let multi_snapshot = editor_snapshot.buffer();
|
||||
let buffer_range = |range: &Range<_>| {
|
||||
Anchor::range_in_buffer(excerpt_id, range.clone()).to_offset(multi_snapshot)
|
||||
};
|
||||
|
||||
wcx.update_window(wcx.window_handle(), |_, window, acx| {
|
||||
let current_idx = outline_items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, item)| {
|
||||
// Find the closest outline item by distance between outline text and cursor location
|
||||
let source_range = buffer_range(&item.source_range_for_text);
|
||||
let distance_to_closest_endpoint = cmp::min(
|
||||
(source_range.start.0 as isize - cursor_offset.0 as isize).abs(),
|
||||
(source_range.end.0 as isize - cursor_offset.0 as isize).abs(),
|
||||
);
|
||||
|
||||
let item_towards_offset =
|
||||
(source_range.start.0 as isize - cursor_offset.0 as isize).signum()
|
||||
== (offset as isize).signum();
|
||||
|
||||
let source_range_contains_cursor = source_range.contains(&cursor_offset);
|
||||
|
||||
// To pick the next outline to jump to, we should jump in the direction of the offset, and
|
||||
// we should not already be within the outline's source range. We then pick the closest outline
|
||||
// item.
|
||||
(item_towards_offset && !source_range_contains_cursor)
|
||||
.then_some((distance_to_closest_endpoint, idx))
|
||||
})
|
||||
.min()
|
||||
.map(|(_, idx)| idx);
|
||||
|
||||
let Some(idx) = current_idx else {
|
||||
return;
|
||||
};
|
||||
|
||||
let range = buffer_range(&outline_items[idx].source_range_for_text);
|
||||
let selection = [range.start..range.start];
|
||||
|
||||
let _ = editor
|
||||
.update(acx, |editor, ecx| {
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::newest()),
|
||||
window,
|
||||
ecx,
|
||||
|s| s.select_ranges(selection),
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn go_to_next_symbol(
|
||||
&mut self,
|
||||
_: &GoToNextSymbol,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.go_to_symbol_by_offset(window, cx, 1).detach();
|
||||
}
|
||||
|
||||
fn go_to_previous_symbol(
|
||||
&mut self,
|
||||
_: &GoToPreviousSymbol,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.go_to_symbol_by_offset(window, cx, -1).detach();
|
||||
}
|
||||
|
||||
pub fn go_to_reference_before_or_after_position(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
|
||||
@@ -19328,6 +19328,260 @@ fn test_split_words_for_snippet_prefix() {
|
||||
assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_syntax_node_relative_jumps(tcx: &mut TestAppContext) {
|
||||
init_test(tcx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new(
|
||||
Arc::into_inner(markdown_lang()).unwrap(),
|
||||
Default::default(),
|
||||
tcx,
|
||||
)
|
||||
.await;
|
||||
|
||||
async fn assert(offset: i8, before: &str, after: &str, cx: &mut EditorLspTestContext) {
|
||||
let _state_context = cx.set_state(before);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| editor.go_to_symbol_by_offset(window, cx, offset))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(after);
|
||||
}
|
||||
|
||||
const ABOVE: i8 = -1;
|
||||
const BELOW: i8 = 1;
|
||||
|
||||
assert(
|
||||
ABOVE,
|
||||
indoc! {"
|
||||
# Foo
|
||||
|
||||
ˇFoo foo foo
|
||||
|
||||
# Bar
|
||||
|
||||
Bar bar bar
|
||||
"},
|
||||
indoc! {"
|
||||
ˇ# Foo
|
||||
|
||||
Foo foo foo
|
||||
|
||||
# Bar
|
||||
|
||||
Bar bar bar
|
||||
"},
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert(
|
||||
ABOVE,
|
||||
indoc! {"
|
||||
ˇ# Foo
|
||||
|
||||
Foo foo foo
|
||||
|
||||
# Bar
|
||||
|
||||
Bar bar bar
|
||||
"},
|
||||
indoc! {"
|
||||
ˇ# Foo
|
||||
|
||||
Foo foo foo
|
||||
|
||||
# Bar
|
||||
|
||||
Bar bar bar
|
||||
"},
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert(
|
||||
BELOW,
|
||||
indoc! {"
|
||||
ˇ# Foo
|
||||
|
||||
Foo foo foo
|
||||
|
||||
# Bar
|
||||
|
||||
Bar bar bar
|
||||
"},
|
||||
indoc! {"
|
||||
# Foo
|
||||
|
||||
Foo foo foo
|
||||
|
||||
ˇ# Bar
|
||||
|
||||
Bar bar bar
|
||||
"},
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert(
|
||||
BELOW,
|
||||
indoc! {"
|
||||
# Foo
|
||||
|
||||
ˇFoo foo foo
|
||||
|
||||
# Bar
|
||||
|
||||
Bar bar bar
|
||||
"},
|
||||
indoc! {"
|
||||
# Foo
|
||||
|
||||
Foo foo foo
|
||||
|
||||
ˇ# Bar
|
||||
|
||||
Bar bar bar
|
||||
"},
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert(
|
||||
BELOW,
|
||||
indoc! {"
|
||||
# Foo
|
||||
|
||||
Foo foo foo
|
||||
|
||||
ˇ# Bar
|
||||
|
||||
Bar bar bar
|
||||
"},
|
||||
indoc! {"
|
||||
# Foo
|
||||
|
||||
Foo foo foo
|
||||
|
||||
ˇ# Bar
|
||||
|
||||
Bar bar bar
|
||||
"},
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert(
|
||||
BELOW,
|
||||
indoc! {"
|
||||
# Foo
|
||||
|
||||
Foo foo foo
|
||||
|
||||
# Bar
|
||||
ˇ
|
||||
Bar bar bar
|
||||
"},
|
||||
indoc! {"
|
||||
# Foo
|
||||
|
||||
Foo foo foo
|
||||
|
||||
# Bar
|
||||
ˇ
|
||||
Bar bar bar
|
||||
"},
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_syntax_node_relative_dead_zone(tcx: &mut TestAppContext) {
|
||||
init_test(tcx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new(
|
||||
Arc::into_inner(rust_lang()).unwrap(),
|
||||
Default::default(),
|
||||
tcx,
|
||||
)
|
||||
.await;
|
||||
|
||||
async fn assert(offset: i8, before: &str, after: &str, cx: &mut EditorLspTestContext) {
|
||||
let _state_context = cx.set_state(before);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| editor.go_to_symbol_by_offset(window, cx, offset))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(after);
|
||||
}
|
||||
|
||||
const ABOVE: i8 = -1;
|
||||
const BELOW: i8 = 1;
|
||||
|
||||
assert(
|
||||
ABOVE,
|
||||
indoc! {"
|
||||
fn foo() {
|
||||
// foo fn
|
||||
}
|
||||
|
||||
ˇ// this zone is not inside any top level outline node
|
||||
|
||||
fn bar() {
|
||||
// bar fn
|
||||
let _ = 2;
|
||||
}
|
||||
"},
|
||||
indoc! {"
|
||||
ˇfn foo() {
|
||||
// foo fn
|
||||
}
|
||||
|
||||
// this zone is not inside any top level outline node
|
||||
|
||||
fn bar() {
|
||||
// bar fn
|
||||
let _ = 2;
|
||||
}
|
||||
"},
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert(
|
||||
BELOW,
|
||||
indoc! {"
|
||||
fn foo() {
|
||||
// foo fn
|
||||
}
|
||||
|
||||
ˇ// this zone is not inside any top level outline node
|
||||
|
||||
fn bar() {
|
||||
// bar fn
|
||||
let _ = 2;
|
||||
}
|
||||
"},
|
||||
indoc! {"
|
||||
fn foo() {
|
||||
// foo fn
|
||||
}
|
||||
|
||||
// this zone is not inside any top level outline node
|
||||
|
||||
ˇfn bar() {
|
||||
// bar fn
|
||||
let _ = 2;
|
||||
}
|
||||
"},
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -540,6 +540,8 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::go_to_next_change);
|
||||
register_action(editor, window, Editor::go_to_prev_reference);
|
||||
register_action(editor, window, Editor::go_to_next_reference);
|
||||
register_action(editor, window, Editor::go_to_previous_symbol);
|
||||
register_action(editor, window, Editor::go_to_next_symbol);
|
||||
|
||||
register_action(editor, window, |editor, action, window, cx| {
|
||||
if let Some(task) = editor.format(action, window, cx) {
|
||||
|
||||
Reference in new issue
Block a user