diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 7cc41752f34d719c27f5954c41f26fa9febfde94..7451aaced9072d3f60483a3d1091caa38f92294b 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -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. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 204011412ec9b229ffdd49195e907369baa2d97f..da99dc45001548627317c3d2133859860a971a47 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -18125,6 +18125,108 @@ impl Editor { }; } + fn go_to_symbol_by_offset( + &mut self, + window: &mut Window, + cx: &mut Context, + offset: i8, + ) -> Task> { + 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::(&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> = 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.go_to_symbol_by_offset(window, cx, 1).detach(); + } + + fn go_to_previous_symbol( + &mut self, + _: &GoToPreviousSymbol, + window: &mut Window, + cx: &mut Context, + ) { + self.go_to_symbol_by_offset(window, cx, -1).detach(); + } + pub fn go_to_reference_before_or_after_position( &mut self, direction: Direction, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 683995e8ff0817e9f11c276fba1e85eef29eee7a..04d3babf20f5c866dd3f3447f0909f11becf724d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ab00de0df25ca209604c7052367f0ac6ce2142ae..7128c60b7f45147f99b6f46d3bd85b9428d358ef 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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) {