Detailed changes
@@ -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) {