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.
This commit is contained in:
@@ -493,6 +493,10 @@ actions!(
|
|||||||
GoToTypeDefinition,
|
GoToTypeDefinition,
|
||||||
/// Goes to type definition in a split pane.
|
/// Goes to type definition in a split pane.
|
||||||
GoToTypeDefinitionSplit,
|
GoToTypeDefinitionSplit,
|
||||||
|
/// Goes to the next document highlight.
|
||||||
|
GoToNextDocumentHighlight,
|
||||||
|
/// Goes to the previous document highlight.
|
||||||
|
GoToPreviousDocumentHighlight,
|
||||||
/// Scrolls down by half a page.
|
/// Scrolls down by half a page.
|
||||||
HalfPageDown,
|
HalfPageDown,
|
||||||
/// Scrolls up by half a page.
|
/// Scrolls up by half a page.
|
||||||
|
|||||||
@@ -15948,6 +15948,87 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn go_to_next_document_highlight(
|
||||||
|
&mut self,
|
||||||
|
_: &GoToNextDocumentHighlight,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
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>,
|
||||||
|
) {
|
||||||
|
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<Editor>,
|
||||||
|
) {
|
||||||
|
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
|
||||||
|
let snapshot = self.snapshot(window, cx);
|
||||||
|
let buffer = &snapshot.buffer_snapshot;
|
||||||
|
let position = self.selections.newest::<Point>(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::<DocumentHighlightRead>()))
|
||||||
|
{
|
||||||
|
all_highlights.extend(read_highlights.iter());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((_, write_highlights)) = self
|
||||||
|
.background_highlights
|
||||||
|
.get(&HighlightKey::Type(TypeId::of::<DocumentHighlightWrite>()))
|
||||||
|
{
|
||||||
|
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<T: 'static>(
|
fn go_to_line<T: 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
position: Anchor,
|
position: Anchor,
|
||||||
|
|||||||
@@ -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::<DocumentHighlightRead>(
|
||||||
|
&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]
|
#[track_caller]
|
||||||
fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
|
fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
|
||||||
editor
|
editor
|
||||||
|
|||||||
@@ -381,6 +381,8 @@ impl EditorElement {
|
|||||||
register_action(editor, window, Editor::go_to_prev_diagnostic);
|
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_next_hunk);
|
||||||
register_action(editor, window, Editor::go_to_prev_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| {
|
register_action(editor, window, |editor, action, window, cx| {
|
||||||
editor
|
editor
|
||||||
.go_to_definition(action, window, cx)
|
.go_to_definition(action, window, cx)
|
||||||
|
|||||||
Reference in New Issue
Block a user