From 14ffd7b53fb69cbee6e979efe5fc0ea5ece101f7 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 9 Sep 2025 11:38:38 -0700 Subject: [PATCH] editor: Implement Go to next/prev Document Highlight (#35994) Closes #21193 Closes #14703 Having the ability to navigate directly to the next symbolHighlight/reference lets you follow the data flow of a variable. If you highlight the function itself (depending on the LSP), you can also navigate to all returns. Note that this is a different feature from navigating to the next match, as that is not language-context aware. For example, if you have a var named foo it would also navigate to an unrelated variable fooBar. Here's how this patch works: - The editor struct has a background_highlights. - Collect all highlights with the keys [DocumentHighlightRead, DocumentHighlightWrite] - Depending on the direction, move the cursor to the next or previous highlight relative to the current position. Release Notes: - Added `editor::GoToNextDocumentHighlight` and `editor::GoToPreviousDocumentHighlight` to navigate to the next LSP document highlight. Useful for navigating to the next usage of a certain symbol. --- crates/editor/src/actions.rs | 4 ++ crates/editor/src/editor.rs | 81 ++++++++++++++++++++++++++ crates/editor/src/editor_tests.rs | 95 +++++++++++++++++++++++++++++++ crates/editor/src/element.rs | 2 + 4 files changed, 182 insertions(+) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index df467c4d5daf12c34fe546ad49fb60210ba56078..35510b36e9b80e3e8c4d9cd82a3b6a8f7da67429 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -493,6 +493,10 @@ actions!( GoToTypeDefinition, /// Goes to type definition in a split pane. GoToTypeDefinitionSplit, + /// Goes to the next document highlight. + GoToNextDocumentHighlight, + /// Goes to the previous document highlight. + GoToPreviousDocumentHighlight, /// Scrolls down by half a page. HalfPageDown, /// Scrolls up by half a page. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2991c47856a06098bbff0e56d67922a765d35efe..34134ad001afe3f0c7536f05582d32a6b234bf65 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -15948,6 +15948,87 @@ impl Editor { } } + pub fn go_to_next_document_highlight( + &mut self, + _: &GoToNextDocumentHighlight, + window: &mut Window, + cx: &mut Context, + ) { + self.go_to_document_highlight_before_or_after_position(Direction::Next, window, cx); + } + + pub fn go_to_prev_document_highlight( + &mut self, + _: &GoToPreviousDocumentHighlight, + window: &mut Window, + cx: &mut Context, + ) { + self.go_to_document_highlight_before_or_after_position(Direction::Prev, window, cx); + } + + pub fn go_to_document_highlight_before_or_after_position( + &mut self, + direction: Direction, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + let snapshot = self.snapshot(window, cx); + let buffer = &snapshot.buffer_snapshot; + let position = self.selections.newest::(cx).head(); + let anchor_position = buffer.anchor_after(position); + + // Get all document highlights (both read and write) + let mut all_highlights = Vec::new(); + + if let Some((_, read_highlights)) = self + .background_highlights + .get(&HighlightKey::Type(TypeId::of::())) + { + all_highlights.extend(read_highlights.iter()); + } + + if let Some((_, write_highlights)) = self + .background_highlights + .get(&HighlightKey::Type(TypeId::of::())) + { + all_highlights.extend(write_highlights.iter()); + } + + if all_highlights.is_empty() { + return; + } + + // Sort highlights by position + all_highlights.sort_by(|a, b| a.start.cmp(&b.start, buffer)); + + let target_highlight = match direction { + Direction::Next => { + // Find the first highlight after the current position + all_highlights + .iter() + .find(|highlight| highlight.start.cmp(&anchor_position, buffer).is_gt()) + } + Direction::Prev => { + // Find the last highlight before the current position + all_highlights + .iter() + .rev() + .find(|highlight| highlight.end.cmp(&anchor_position, buffer).is_lt()) + } + }; + + if let Some(highlight) = target_highlight { + let destination = highlight.start.to_point(buffer); + let autoscroll = Autoscroll::center(); + + self.unfold_ranges(&[destination..destination], false, false, cx); + self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| { + s.select_ranges([destination..destination]); + }); + } + } + fn go_to_line( &mut self, position: Anchor, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 567018baaf9e717c2d968644aabb6a594478d572..6cf911fb4c2bca8cd86a03729dc250377d1d12d5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -25603,6 +25603,101 @@ async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_next_prev_document_highlight(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state( + "let ˇvariable = 42; +let another = variable + 1; +let result = variable * 2;", + ); + + // Set up document highlights manually (simulating LSP response) + cx.update_editor(|editor, _window, cx| { + let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); + + // Create highlights for "variable" occurrences + let highlight_ranges = [ + Point::new(0, 4)..Point::new(0, 12), // First "variable" + Point::new(1, 14)..Point::new(1, 22), // Second "variable" + Point::new(2, 13)..Point::new(2, 21), // Third "variable" + ]; + + let anchor_ranges: Vec<_> = highlight_ranges + .iter() + .map(|range| range.clone().to_anchors(&buffer_snapshot)) + .collect(); + + editor.highlight_background::( + &anchor_ranges, + |theme| theme.colors().editor_document_highlight_read_background, + cx, + ); + }); + + // Go to next highlight - should move to second "variable" + cx.update_editor(|editor, window, cx| { + editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx); + }); + cx.assert_editor_state( + "let variable = 42; +let another = ˇvariable + 1; +let result = variable * 2;", + ); + + // Go to next highlight - should move to third "variable" + cx.update_editor(|editor, window, cx| { + editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx); + }); + cx.assert_editor_state( + "let variable = 42; +let another = variable + 1; +let result = ˇvariable * 2;", + ); + + // Go to next highlight - should stay at third "variable" (no wrap-around) + cx.update_editor(|editor, window, cx| { + editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx); + }); + cx.assert_editor_state( + "let variable = 42; +let another = variable + 1; +let result = ˇvariable * 2;", + ); + + // Now test going backwards from third position + cx.update_editor(|editor, window, cx| { + editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx); + }); + cx.assert_editor_state( + "let variable = 42; +let another = ˇvariable + 1; +let result = variable * 2;", + ); + + // Go to previous highlight - should move to first "variable" + cx.update_editor(|editor, window, cx| { + editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx); + }); + cx.assert_editor_state( + "let ˇvariable = 42; +let another = variable + 1; +let result = variable * 2;", + ); + + // Go to previous highlight - should stay on first "variable" + cx.update_editor(|editor, window, cx| { + editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx); + }); + cx.assert_editor_state( + "let ˇvariable = 42; +let another = variable + 1; +let result = variable * 2;", + ); +} + #[track_caller] fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec { editor diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a702d5b74f7d995a8aa29e35fecbed3cadabeba4..d25cf2e611b8f87f23f47802382ff822580cc882 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -381,6 +381,8 @@ impl EditorElement { register_action(editor, window, Editor::go_to_prev_diagnostic); register_action(editor, window, Editor::go_to_next_hunk); register_action(editor, window, Editor::go_to_prev_hunk); + register_action(editor, window, Editor::go_to_next_document_highlight); + register_action(editor, window, Editor::go_to_prev_document_highlight); register_action(editor, window, |editor, action, window, cx| { editor .go_to_definition(action, window, cx)