editor: Honor lsp_results_location on cmd-click (#64039)
# Objective Fixes #62916. With `"lsp_results_location": "picker"`, `cmd-b` opens the LSP locations picker but cmd-clicking the same symbol still opens a `Definitions for …` multibuffer. #61187 fixed this for the go-to-definition *fallback*; the definition path itself was left behind. The setting is only consulted by `handle_nav_action` in `lsp_locations`, which runs when one of the navigation *actions* is dispatched. Cmd-click never dispatches them: `cmd_click_reveal_task` either navigates the cached hover links itself, or calls `go_to_definition_of_kind` / `go_to_type_definition` directly. ## Solution Both cmd-click paths now dispatch the matching action (`GoToDefinition`, `GoToTypeDefinition`, …) when `lsp_results_location` is `picker` and the click is not a split one, so the picker gets its chance; everything else is untouched: - with the default `multi_buffer` setting `picker_action` returns `None` and the old code runs unchanged; - alt (split) clicks keep their current behavior, since the split actions have no picker handler; - URL and file links keep opening directly — the action is only dispatched when every resolved link is a `HoverLink::Text`. The dispatch is deferred to after the current editor update via `cx.spawn_in`: `LspLocationsPicker::open_for_editor` reads the editor, so dispatching inline panics with `cannot read editor::Editor while it is already being updated` (this is also why the existing fallback dispatch happens from a spawn). ## Testing `cargo test -p lsp_locations` — two new tests, one per cmd-click path: a cold cmd-click, and a cmd-click on a link already resolved by hovering with the modifier held (the path users actually hit). Both fail on `main` with the fix reverted and pass with it. Also ran `cargo test -p editor --lib hover` (68 passed) and `cargo clippy -p editor -p lsp_locations --all-targets`. Not yet verified by hand in a running Zed build — the automated tests above are what backs this. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Release Notes: - Fixed cmd-click on a symbol ignoring the `lsp_results_location` setting: with `"picker"` it opened a definitions multibuffer instead of the picker that `cmd-b` opens ([#62916](https://github.com/zed-industries/zed/issues/62916)). --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
1 parent
d9abcde22a
commit
2f10cba7c6
4 files changed
+866
-107
No files matched your search
@@ -11285,7 +11285,7 @@ impl Editor {
|
||||
self.read_scroll_position_from_db(item_id, workspace_id, window, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn lsp_data_enabled(&self) -> bool {
|
||||
pub fn lsp_data_enabled(&self) -> bool {
|
||||
self.enable_lsp_data && self.mode().is_full()
|
||||
}
|
||||
|
||||
|
||||
@@ -417,6 +417,9 @@ impl EditorElement {
|
||||
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);
|
||||
if editor.read(cx).lsp_data_enabled() {
|
||||
register_action(editor, window, Editor::open_definition_locations);
|
||||
}
|
||||
register_action(editor, window, |editor, action, window, cx| {
|
||||
editor
|
||||
.go_to_definition(action, window, cx)
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
use crate::{
|
||||
Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinitionSplit,
|
||||
GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind, HighlightKey, Navigated,
|
||||
PointForPosition, SelectPhase, editor_settings::GoToDefinitionFallback, scroll::ScrollAmount,
|
||||
Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GotoDefinitionKind,
|
||||
HighlightKey, Navigated, PointForPosition, SelectPhase,
|
||||
editor_settings::GoToDefinitionFallback, scroll::ScrollAmount,
|
||||
};
|
||||
use gpui::{
|
||||
App, AsyncWindowContext, Context, Entity, Focusable, HighlightStyle, Modifiers, Pixels, Task,
|
||||
UnderlineStyle, Window, px,
|
||||
Action, App, AsyncWindowContext, Context, Entity, Focusable, HighlightStyle, Modifiers, Pixels,
|
||||
Task, UnderlineStyle, WeakEntity, Window, px,
|
||||
};
|
||||
use language::{Bias, ToOffset};
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{InlayId, LocationLink, Project, ResolvedPath};
|
||||
use project::{InlayId, Location, LocationLink, Project, ResolvedPath};
|
||||
use regex::Regex;
|
||||
use settings::Settings;
|
||||
use std::{ops::Range, str::FromStr as _, sync::LazyLock};
|
||||
use settings::{OpenResultsIn, Settings};
|
||||
use std::{
|
||||
ops::Range,
|
||||
str::FromStr as _,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
use text::OffsetRangeExt;
|
||||
use theme::ActiveTheme as _;
|
||||
use util::{
|
||||
ResultExt, TryFutureExt as _, markdown::source_position_from_fragment, paths::PathWithPosition,
|
||||
};
|
||||
use workspace::pane::NavigationEntry;
|
||||
|
||||
#[derive(Clone, Action)]
|
||||
#[action(no_json, no_register)]
|
||||
pub struct OpenDefinitionLocations(pub Arc<DefinitionLocations>);
|
||||
|
||||
pub struct DefinitionLocations {
|
||||
pub editor: WeakEntity<Editor>,
|
||||
pub position: Anchor,
|
||||
pub kind: GotoDefinitionKind,
|
||||
pub locations: Vec<Location>,
|
||||
pub origin: Option<NavigationEntry>,
|
||||
}
|
||||
|
||||
impl PartialEq for OpenDefinitionLocations {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HoveredLinkState {
|
||||
@@ -206,24 +229,127 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
let reveal_task = self.cmd_click_reveal_task(point, modifiers, window, cx);
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
let definition_revealed = reveal_task.await.log_err().unwrap_or(Navigated::No);
|
||||
if definition_revealed == Navigated::Yes {
|
||||
let kind = if modifiers.shift {
|
||||
GotoDefinitionKind::Type
|
||||
} else {
|
||||
GotoDefinitionKind::Symbol
|
||||
};
|
||||
let split = Self::is_alt_pressed(&modifiers, cx);
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
let position = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_before(point.next_valid.to_point(&snapshot.display_snapshot));
|
||||
let origin = self.navigation_entry(position, cx);
|
||||
let cached = self.hovered_link_state.take();
|
||||
self.hide_hovered_link(cx);
|
||||
let refresh = cached.as_ref().is_none_or(|state| {
|
||||
state.links.is_empty()
|
||||
|| (matches!(state.last_trigger_point, TriggerPoint::Text(_))
|
||||
&& (state.preferred_kind != kind
|
||||
|| state.task.as_ref().is_some_and(|task| !task.is_ready())))
|
||||
});
|
||||
let mut links = cached.map(|state| state.links).unwrap_or_default();
|
||||
if refresh || !self.lsp_data_enabled() {
|
||||
links.retain(|link| {
|
||||
matches!(link, HoverLink::Url(_) | HoverLink::File(_))
|
||||
|| (self.lsp_data_enabled() && matches!(link, HoverLink::LspLocation(..)))
|
||||
});
|
||||
}
|
||||
if refresh && point.as_valid().is_some() {
|
||||
self.select(
|
||||
SelectPhase::Begin {
|
||||
position: point.next_valid,
|
||||
add: false,
|
||||
click_count: 1,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
self.select(SelectPhase::End, window, cx);
|
||||
let Some((buffer, anchor)) = self.buffer.read(cx).text_anchor_for_position(position, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
links.retain(|link| match link {
|
||||
HoverLink::Text(link) => exclude_link_to_position(&buffer, &anchor, link, cx),
|
||||
_ => true,
|
||||
});
|
||||
let definitions = (refresh && self.lsp_data_enabled() && point.as_valid().is_some())
|
||||
.then(|| {
|
||||
self.semantics_provider
|
||||
.as_ref()?
|
||||
.definitions(&buffer, anchor, kind, cx)
|
||||
})
|
||||
.flatten();
|
||||
if let Some(definitions) = definitions {
|
||||
let workspace = self.workspace().map(|workspace| workspace.downgrade());
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
let definitions = definitions.await.log_err().flatten().unwrap_or_default();
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
if editor.workspace().map(|workspace| workspace.downgrade()) != workspace
|
||||
|| !editor.buffer.read(cx).snapshot(cx).can_resolve(&position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
cx.update(|window, cx| {
|
||||
match EditorSettings::get_global(cx).go_to_definition_fallback {
|
||||
GoToDefinitionFallback::None => {}
|
||||
GoToDefinitionFallback::FindAllReferences => {
|
||||
focus_handle.dispatch_action(&FindAllReferences::default(), window, cx);
|
||||
}
|
||||
if editor.lsp_data_enabled() {
|
||||
links.extend(
|
||||
definitions
|
||||
.into_iter()
|
||||
.filter(|link| {
|
||||
exclude_link_to_position(&buffer, &anchor, link, cx)
|
||||
})
|
||||
.map(HoverLink::Text),
|
||||
);
|
||||
} else {
|
||||
links.retain(|link| {
|
||||
matches!(link, HoverLink::Url(_) | HoverLink::File(_))
|
||||
});
|
||||
}
|
||||
editor
|
||||
.reveal_clicked_links(kind, links, position, origin, split, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
self.reveal_clicked_links(kind, links, position, origin, split, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_definition_locations(
|
||||
&mut self,
|
||||
action: &OpenDefinitionLocations,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let action = &action.0;
|
||||
if action.editor != cx.weak_entity() {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
if !self.lsp_data_enabled()
|
||||
|| !self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.can_resolve(&action.position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let links = action
|
||||
.locations
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|target| {
|
||||
HoverLink::Text(LocationLink {
|
||||
origin: None,
|
||||
target,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.open_clicked_links(action.kind, links, action.origin.clone(), false, window, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_hover(
|
||||
@@ -250,90 +376,86 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_click_reveal_task(
|
||||
fn reveal_clicked_links(
|
||||
&mut self,
|
||||
point: PointForPosition,
|
||||
modifiers: Modifiers,
|
||||
kind: GotoDefinitionKind,
|
||||
links: Vec<HoverLink>,
|
||||
position: Anchor,
|
||||
origin: Option<NavigationEntry>,
|
||||
split: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<anyhow::Result<Navigated>> {
|
||||
if let Some(hovered_link_state) = self.hovered_link_state.take() {
|
||||
self.hide_hovered_link(cx);
|
||||
if !hovered_link_state.links.is_empty() {
|
||||
if !self.focus_handle.is_focused(window) {
|
||||
window.focus(&self.focus_handle, cx);
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !split
|
||||
&& self.lsp_data_enabled()
|
||||
&& self.workspace().is_some()
|
||||
&& EditorSettings::get_global(cx).lsp_results_location == OpenResultsIn::Picker
|
||||
&& links.iter().all(|link| matches!(link, HoverLink::Text(_)))
|
||||
{
|
||||
let action = OpenDefinitionLocations(Arc::new(DefinitionLocations {
|
||||
editor: cx.weak_entity(),
|
||||
position,
|
||||
kind,
|
||||
locations: links
|
||||
.into_iter()
|
||||
.filter_map(|link| match link {
|
||||
HoverLink::Text(link) => Some(link.target),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
origin,
|
||||
}));
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
window.defer(cx, move |window, cx| {
|
||||
if window.is_action_available_in(&action, &focus_handle) {
|
||||
focus_handle.dispatch_action(&action, window, cx);
|
||||
} else {
|
||||
action
|
||||
.0
|
||||
.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.open_definition_locations(&action, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self.open_clicked_links(kind, links, origin, split, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// exclude links pointing back to the current anchor
|
||||
let current_position = point
|
||||
.next_valid
|
||||
.to_point(&self.snapshot(window, cx).display_snapshot);
|
||||
let Some((buffer, anchor)) = self
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.text_anchor_for_position(current_position, cx)
|
||||
else {
|
||||
return Task::ready(Ok(Navigated::No));
|
||||
};
|
||||
let Some(multi_buffer_anchor) = self
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.anchor_in_excerpt(anchor)
|
||||
else {
|
||||
return Task::ready(Ok(Navigated::No));
|
||||
};
|
||||
let links = hovered_link_state
|
||||
.links
|
||||
.into_iter()
|
||||
.filter(|link| {
|
||||
if let HoverLink::Text(location) = link {
|
||||
exclude_link_to_position(&buffer, &anchor, location, cx)
|
||||
} else {
|
||||
true
|
||||
fn open_clicked_links(
|
||||
&mut self,
|
||||
kind: GotoDefinitionKind,
|
||||
links: Vec<HoverLink>,
|
||||
origin: Option<NavigationEntry>,
|
||||
split: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let navigation = self.navigate_to_hover_links(Some(kind), links, origin, split, window, cx);
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
if navigation.await.log_err().unwrap_or(Navigated::No) == Navigated::Yes {
|
||||
return;
|
||||
}
|
||||
let focus_handle = editor
|
||||
.read_with(cx, |editor, cx| {
|
||||
editor.lsp_data_enabled().then(|| editor.focus_handle(cx))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(focus_handle) = focus_handle {
|
||||
cx.update(|window, cx| {
|
||||
if EditorSettings::get_global(cx).go_to_definition_fallback
|
||||
== GoToDefinitionFallback::FindAllReferences
|
||||
{
|
||||
focus_handle.dispatch_action(&FindAllReferences::default(), window, cx);
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let nav_entry = self.navigation_entry(multi_buffer_anchor, cx);
|
||||
let split = Self::is_alt_pressed(&modifiers, cx);
|
||||
let navigate_task =
|
||||
self.navigate_to_hover_links(None, links, nav_entry, split, window, cx);
|
||||
self.select(SelectPhase::End, window, cx);
|
||||
return navigate_task;
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have the correct kind of link cached, set the selection on
|
||||
// click and immediately trigger GoToDefinition.
|
||||
self.select(
|
||||
SelectPhase::Begin {
|
||||
position: point.next_valid,
|
||||
add: false,
|
||||
click_count: 1,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let navigate_task = if point.as_valid().is_some() {
|
||||
let split = Self::is_alt_pressed(&modifiers, cx);
|
||||
match (modifiers.shift, split) {
|
||||
(true, true) => {
|
||||
self.go_to_type_definition_split(&GoToTypeDefinitionSplit, window, cx)
|
||||
}
|
||||
(true, false) => {
|
||||
self.go_to_type_definition(&GoToTypeDefinition::default(), window, cx)
|
||||
}
|
||||
(false, true) => self.go_to_definition_split(&GoToDefinitionSplit, window, cx),
|
||||
(false, false) => {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, window, cx)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Task::ready(Ok(Navigated::No))
|
||||
};
|
||||
self.select(SelectPhase::End, window, cx);
|
||||
navigate_task
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1072,7 +1194,8 @@ mod tests {
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Modifiers, MouseButton, MouseDownEvent, MousePressureEvent, MouseUpEvent, PressureStage,
|
||||
Modifiers, MouseButton, MouseDownEvent, MousePressureEvent, MouseUpEvent, PlatformInput,
|
||||
PressureStage,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::Point;
|
||||
@@ -1713,6 +1836,7 @@ mod tests {
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
definition_provider: Some(lsp::OneOf::Left(true)),
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
execute_command_provider: Some(lsp::ExecuteCommandOptions {
|
||||
commands: vec!["upgrade".to_string()],
|
||||
@@ -1910,13 +2034,57 @@ mod tests {
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.simulate_click(hover_point, Modifiers::secondary_key());
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
let destination = indoc! {"
|
||||
struct «TestStructˇ»;
|
||||
|
||||
fn main() {
|
||||
let variable = TestStruct;
|
||||
}
|
||||
"});
|
||||
"};
|
||||
cx.assert_editor_state(destination);
|
||||
|
||||
let source = cx.buffer_text().replace("= TestStruct", "= TeˇstStruct");
|
||||
let click_point = cx.pixel_position(&source);
|
||||
let hover_point = cx.pixel_position_for(midpoint);
|
||||
let mut definitions =
|
||||
cx.set_request_handler::<GotoDefinition, _, _>(move |uri, params, _| async move {
|
||||
let params = params.text_document_position_params;
|
||||
assert_eq!(params.text_document.uri, uri);
|
||||
assert_eq!(params.position, lsp::Position::new(3, 21));
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
|
||||
lsp::Location::new(uri, target_range),
|
||||
)))
|
||||
});
|
||||
let editor = cx.editor.clone();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
window.simulate_mouse_move(hover_point, cx);
|
||||
let state = editor.read(cx).hovered_link_state.as_ref().expect("hover");
|
||||
assert!(matches!(
|
||||
state.last_trigger_point,
|
||||
TriggerPoint::InlayHint(..)
|
||||
));
|
||||
assert!(state.links.is_empty());
|
||||
assert!(!state.task.as_ref().expect("hover task").is_ready());
|
||||
let down = MouseDownEvent {
|
||||
position: click_point,
|
||||
modifiers: Modifiers::secondary_key(),
|
||||
button: MouseButton::Left,
|
||||
click_count: 1,
|
||||
first_mouse: false,
|
||||
};
|
||||
let up = MouseUpEvent {
|
||||
position: down.position,
|
||||
modifiers: down.modifiers,
|
||||
button: down.button,
|
||||
click_count: down.click_count,
|
||||
};
|
||||
window.dispatch_event(PlatformInput::MouseDown(down), cx);
|
||||
window.dispatch_event(PlatformInput::MouseUp(up), cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(definitions.try_recv().ok(), Some(()));
|
||||
cx.assert_editor_state(destination);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1953,6 +2121,159 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reveal_clicked_links_picker_fallback(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
cx.update(|cx| {
|
||||
let mut settings = EditorSettings::get_global(cx).clone();
|
||||
settings.lsp_results_location = OpenResultsIn::Picker;
|
||||
settings.excerpt_context_lines = 0;
|
||||
EditorSettings::override_global(settings, cx);
|
||||
});
|
||||
for disabled in [false, true] {
|
||||
let mut cx =
|
||||
EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
|
||||
let source = cx.editor.clone();
|
||||
let (buffer, selections) = cx.update_editor(|editor, window, cx| {
|
||||
editor.set_text("origin\n\nfirst\n\n\n\n\n\nsecond\n", window, cx);
|
||||
(
|
||||
editor.buffer.read(cx).as_singleton().expect("buffer"),
|
||||
editor.selections.disjoint_anchors_arc(),
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
let links = [(2, 5), (8, 6)].map(|(row, end)| {
|
||||
HoverLink::Text(LocationLink {
|
||||
origin: None,
|
||||
target: Location {
|
||||
buffer: buffer.clone(),
|
||||
range: buffer.read(cx).anchor_before(Point::new(row, 0))
|
||||
..buffer.read(cx).anchor_after(Point::new(row, end)),
|
||||
},
|
||||
})
|
||||
});
|
||||
let position = editor.selections.newest_anchor().head();
|
||||
let origin = editor.navigation_entry(position, cx);
|
||||
editor.reveal_clicked_links(
|
||||
GotoDefinitionKind::Symbol,
|
||||
links.to_vec(),
|
||||
position,
|
||||
origin,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if disabled {
|
||||
editor.disable_lsp_data();
|
||||
}
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.editor(|editor, _, _| {
|
||||
assert_eq!(editor.selections.disjoint_anchors_arc(), selections);
|
||||
});
|
||||
cx.update_workspace(|workspace, window, cx| {
|
||||
assert!(!workspace.has_active_modal(window, cx));
|
||||
assert_eq!(workspace.items(cx).count(), if disabled { 1 } else { 2 });
|
||||
let active = workspace.active_item_as::<Editor>(cx).expect("editor");
|
||||
if disabled {
|
||||
assert_eq!(active, source);
|
||||
return;
|
||||
}
|
||||
assert_ne!(active, source);
|
||||
let multibuffer = active.read(cx).buffer().read(cx);
|
||||
assert!(!multibuffer.is_singleton());
|
||||
assert_eq!(
|
||||
multibuffer.all_buffers_iter().collect::<Vec<_>>(),
|
||||
std::slice::from_ref(&buffer)
|
||||
);
|
||||
assert_eq!(
|
||||
multibuffer
|
||||
.snapshot(cx)
|
||||
.excerpts()
|
||||
.map(|excerpt| excerpt.primary.to_offset(buffer.read(cx)))
|
||||
.collect::<Vec<_>>(),
|
||||
[8..13, 19..25],
|
||||
);
|
||||
});
|
||||
cx.update(|window, _| window.remove_window());
|
||||
cx.run_until_parked();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cmd_click_cached_links_survive_lsp_disable(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let capabilities = lsp::ServerCapabilities {
|
||||
definition_provider: Some(lsp::OneOf::Left(true)),
|
||||
type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
let delay = std::time::Duration::from_secs(1);
|
||||
let mut opened = Vec::new();
|
||||
for marked in ["hˇttps://zed.dev/", "fˇile2.rs"] {
|
||||
let mut cx = EditorLspTestContext::new_rust(capabilities.clone(), cx).await;
|
||||
let fs =
|
||||
cx.update_workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
|
||||
fs.as_fake()
|
||||
.insert_file(path!("/root/dir/file2.rs"), b"target".to_vec())
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
let source = format!("{marked}\nfirst\nsecond\n");
|
||||
cx.set_state(&source);
|
||||
let mut symbols =
|
||||
cx.set_request_handler::<GotoDefinition, _, _>(|_, _, _| async { Ok(None) });
|
||||
let mut types =
|
||||
cx.set_request_handler::<GotoTypeDefinition, _, _>(move |uri, _, app| async move {
|
||||
app.background_executor().timer(delay).await;
|
||||
let locations = [1, 2].map(|line| {
|
||||
let start = lsp::Position::new(line, 0);
|
||||
let end = lsp::Position::new(line, 1);
|
||||
lsp::Location::new(uri.clone(), lsp::Range::new(start, end))
|
||||
});
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Array(locations.to_vec())))
|
||||
});
|
||||
let position = cx.pixel_position(&source);
|
||||
cx.simulate_mouse_move(position, None, Modifiers::secondary_key());
|
||||
assert_eq!(symbols.next().await, Some(()));
|
||||
cx.run_until_parked();
|
||||
cx.editor(|editor, _, _| {
|
||||
let state = editor.hovered_link_state.as_ref().expect("cached link");
|
||||
assert_eq!(state.preferred_kind, GotoDefinitionKind::Symbol);
|
||||
assert!(state.task.as_ref().expect("hover task").is_ready());
|
||||
assert_eq!(state.links.len(), 1, "{marked}");
|
||||
});
|
||||
let mut modifiers = Modifiers::secondary_key();
|
||||
modifiers.shift = true;
|
||||
cx.simulate_click(position, modifiers);
|
||||
assert!(types.try_recv().is_err());
|
||||
cx.update_editor(|editor, _, _| editor.disable_lsp_data());
|
||||
cx.executor().advance_clock(delay);
|
||||
assert_eq!(types.next().await, Some(()));
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(&source);
|
||||
let url = cx.opened_url();
|
||||
opened.push(cx.update_workspace(|workspace, window, cx| {
|
||||
assert!(!workspace.has_active_modal(window, cx));
|
||||
if marked.starts_with("h") {
|
||||
return url;
|
||||
}
|
||||
let editor = workspace.active_item_as::<Editor>(cx).expect("editor");
|
||||
let buffer = editor.read(cx).buffer().read(cx);
|
||||
let buffer = buffer.as_singleton().expect("buffer");
|
||||
let file = buffer.read(cx).file().expect("file");
|
||||
let path = file.as_local().expect("local").abs_path(cx);
|
||||
Some(path.to_string_lossy().into_owned())
|
||||
}));
|
||||
cx.update(|window, _| window.remove_window());
|
||||
cx.run_until_parked();
|
||||
}
|
||||
assert_eq!(
|
||||
opened,
|
||||
["https://zed.dev/", path!("/root/dir/file2.rs")].map(|target| Some(target.to_owned()))
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hover_preconditions(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::any::TypeId;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -5,17 +6,18 @@ use collections::HashMap;
|
||||
use editor::actions::{
|
||||
FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation, GoToTypeDefinition,
|
||||
};
|
||||
use editor::hover_links::{HoverLink, OpenDefinitionLocations};
|
||||
use editor::{Editor, EditorSettings, GotoDefinitionKind, OpenResultsIn};
|
||||
use file_icons::FileIcons;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity,
|
||||
prelude::*,
|
||||
AnyElement, App, AppContext, AsyncWindowContext, Context, DismissEvent, DispatchPhase, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, HighlightStyle, StyledText, Subscription, Task,
|
||||
TextStyle, WeakEntity, prelude::*,
|
||||
};
|
||||
use language::{Buffer, HighlightId, LanguageAwareStyling};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{Location, Project, ProjectPath};
|
||||
use project::{Location, LocationLink, Project, ProjectPath};
|
||||
use settings::{GoToDefinitionFallback, Settings as _};
|
||||
use text::{Anchor, Point};
|
||||
use theme_settings::ThemeSettings;
|
||||
@@ -24,6 +26,7 @@ use ui::{ListItem, ListItemSpacing, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::item::ItemSettings;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::pane::NavigationEntry;
|
||||
use workspace::{ModalView, Toast, Workspace};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -38,6 +41,24 @@ fn register(editor: &mut Editor, _window: Option<&mut Window>, cx: &mut Context<
|
||||
if !editor.mode().is_full() {
|
||||
return;
|
||||
}
|
||||
editor
|
||||
.register_action_renderer(|editor, window, _| {
|
||||
if editor.lsp_data_enabled() {
|
||||
window.on_action(
|
||||
TypeId::of::<OpenDefinitionLocations>(),
|
||||
|action, phase, window, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
handle_definition_locations(
|
||||
action.downcast_ref().expect("definition action"),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
let handle = cx.entity().downgrade();
|
||||
editor
|
||||
.register_action({
|
||||
@@ -127,6 +148,65 @@ fn handle_nav_action(
|
||||
LspLocationsPicker::open_for_editor(kind, editor.clone(), window, cx);
|
||||
}
|
||||
|
||||
fn handle_definition_locations(
|
||||
action: &OpenDefinitionLocations,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let action = &action.0;
|
||||
let Some(editor) = action.editor.upgrade() else {
|
||||
return;
|
||||
};
|
||||
if !editor.read(cx).lsp_data_enabled()
|
||||
|| !editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.can_resolve(&action.position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(workspace) = editor.read(cx).workspace() else {
|
||||
cx.propagate();
|
||||
return;
|
||||
};
|
||||
if EditorSettings::get_global(cx).lsp_results_location != OpenResultsIn::Picker
|
||||
|| action
|
||||
.locations
|
||||
.iter()
|
||||
.any(|location| location.buffer.read(cx).file().is_none())
|
||||
{
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
let matches = build_location_matches(&action.locations, cx);
|
||||
if matches.len() < 2 {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
let kind = match action.kind {
|
||||
GotoDefinitionKind::Symbol => LspPickerKind::Definition,
|
||||
GotoDefinitionKind::Type => LspPickerKind::TypeDefinition,
|
||||
GotoDefinitionKind::Declaration => LspPickerKind::Declaration,
|
||||
GotoDefinitionKind::Implementation => LspPickerKind::Implementation,
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
LspLocationsPicker::new(
|
||||
kind,
|
||||
matches,
|
||||
project,
|
||||
action.editor.clone(),
|
||||
action.origin.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs the LSP query for `kind` and returns the raw locations. Returns `None`
|
||||
/// (and reports any error) when there is nothing to query, so the caller stops
|
||||
/// without an empty-results toast. Deduplication and dropping fileless results
|
||||
@@ -335,7 +415,7 @@ impl LspLocationsPicker {
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
Self::new(kind, matches, project, editor, window, cx)
|
||||
Self::new(kind, matches, project, editor, None, window, cx)
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
@@ -348,11 +428,12 @@ impl LspLocationsPicker {
|
||||
matches: Vec<LocationMatch>,
|
||||
project: Entity<Project>,
|
||||
editor: WeakEntity<Editor>,
|
||||
origin: Option<NavigationEntry>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let preview = picker_preview::editor_preview(project.clone(), window, cx);
|
||||
let delegate = LspLocationsDelegate::new(kind, matches, project, editor);
|
||||
let delegate = LspLocationsDelegate::new(kind, matches, project, editor, origin);
|
||||
let picker = cx.new(|cx| Picker::list_with_preview(delegate, preview, window, cx));
|
||||
let subscription = cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
@@ -403,6 +484,7 @@ struct LspLocationsDelegate {
|
||||
kind: LspPickerKind,
|
||||
project: Entity<Project>,
|
||||
editor: WeakEntity<Editor>,
|
||||
origin: Option<NavigationEntry>,
|
||||
all_matches: Vec<LocationMatch>,
|
||||
candidates: Arc<[StringMatchCandidate]>,
|
||||
matches: Vec<usize>,
|
||||
@@ -417,6 +499,7 @@ impl LspLocationsDelegate {
|
||||
all_matches: Vec<LocationMatch>,
|
||||
project: Entity<Project>,
|
||||
editor: WeakEntity<Editor>,
|
||||
origin: Option<NavigationEntry>,
|
||||
) -> Self {
|
||||
// Match against the line text and the file path, mirroring the fuzzy
|
||||
// matching every other Zed picker uses.
|
||||
@@ -439,6 +522,7 @@ impl LspLocationsDelegate {
|
||||
kind,
|
||||
project,
|
||||
editor,
|
||||
origin,
|
||||
all_matches,
|
||||
candidates,
|
||||
matches,
|
||||
@@ -500,7 +584,21 @@ impl LspLocationsDelegate {
|
||||
};
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
if let Some(origin) = self.origin.take() {
|
||||
editor.navigate_to_hover_links(
|
||||
None,
|
||||
vec![HoverLink::Text(LocationLink {
|
||||
origin: None,
|
||||
target: location,
|
||||
})],
|
||||
Some(origin),
|
||||
split,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
editor.open_location(location, split, window, cx)
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
cx.emit(DismissEvent);
|
||||
@@ -818,8 +916,11 @@ fn render_matched_line(location_match: &LocationMatch, cx: &App) -> StyledText {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{Modifiers, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::{sync::Mutex, time::Duration};
|
||||
use text::OffsetRangeExt as _;
|
||||
use workspace::Item as _;
|
||||
|
||||
async fn rust_cx(
|
||||
@@ -1055,6 +1156,111 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cmd_click_cold_locations(cx: &mut TestAppContext) {
|
||||
cx.update(crate::init);
|
||||
for results in [OpenResultsIn::Picker, OpenResultsIn::MultiBuffer] {
|
||||
for shift in [false, true] {
|
||||
for split in [false, true] {
|
||||
assert_cmd_click(cx, None, shift, false, results, split, false).await;
|
||||
}
|
||||
}
|
||||
assert_cmd_click(cx, None, false, false, results, false, true).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cmd_click_cached_locations(cx: &mut TestAppContext) {
|
||||
cx.update(crate::init);
|
||||
for results in [OpenResultsIn::Picker, OpenResultsIn::MultiBuffer] {
|
||||
for shift in [false, true] {
|
||||
for split in [false, true] {
|
||||
assert_cmd_click(cx, Some(shift), shift, false, results, split, false).await;
|
||||
}
|
||||
for pending in [false, true] {
|
||||
assert_cmd_click(cx, Some(!shift), shift, pending, results, false, false).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cmd_click_empty_hover_requeries(cx: &mut TestAppContext) {
|
||||
cx.update(crate::init);
|
||||
for count in [1, 0, 2] {
|
||||
let targets = vec![(2, 14, 17); count];
|
||||
let (mut cx, requests) = cmd_click_request(cx, true, &targets).await;
|
||||
let target = SOURCE.replace('ˇ', "").replace("= abc", "= «abcˇ»");
|
||||
cx.assert_editor_state(if count == 0 { SOURCE } else { &target });
|
||||
assert!(active_picker(&mut cx).is_none());
|
||||
cx.update_workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
|
||||
assert_eq!(requests.load(Ordering::SeqCst), 2);
|
||||
cx.update(|window, _| window.remove_window());
|
||||
cx.run_until_parked();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cmd_click_pending_hidden_source(cx: &mut TestAppContext) {
|
||||
cx.update(crate::init);
|
||||
let (mut cx, requests) = cmd_click_request(cx, false, &[(2, 14, 17)]).await;
|
||||
let other = cx
|
||||
.update_workspace(Editor::new_in_workspace)
|
||||
.await
|
||||
.expect("tab");
|
||||
cx.update(|window, cx| window.draw(cx).clear(cx));
|
||||
cx.update_workspace(|workspace, _, cx| {
|
||||
assert_eq!(workspace.active_item_as::<Editor>(cx), Some(other.clone()));
|
||||
});
|
||||
cx.executor().advance_clock(Duration::from_secs(1));
|
||||
cx.run_until_parked();
|
||||
cx.update_workspace(|workspace, _, cx| {
|
||||
assert_eq!(workspace.active_item_as::<Editor>(cx), Some(other));
|
||||
});
|
||||
cx.assert_editor_state(&SOURCE.replace('ˇ', "").replace("= abc", "= «abcˇ»"));
|
||||
assert_eq!(requests.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cmd_click_pending_preserves_tag_origin(cx: &mut TestAppContext) {
|
||||
cx.update(crate::init);
|
||||
let (mut cx, requests) = cmd_click_request(cx, false, &[(0, 3, 7), (2, 14, 17)]).await;
|
||||
let moved = format!("ˇ{}", SOURCE.replace('ˇ', ""));
|
||||
cx.set_selections_state(&moved);
|
||||
assert!(active_picker(&mut cx).is_none());
|
||||
cx.assert_editor_state(&moved);
|
||||
cx.executor().advance_clock(Duration::from_secs(1));
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(&moved);
|
||||
let modal = active_picker(&mut cx).expect("locations picker");
|
||||
cx.update(|window, cx| {
|
||||
modal.read(cx).picker.clone().update(cx, |picker, cx| {
|
||||
let matches = &picker.delegate.all_matches;
|
||||
let ranges = matches.iter().map(|location| location.range.clone());
|
||||
assert_eq!(ranges.collect::<Vec<_>>(), vec![3..7, 45..48]);
|
||||
picker.delegate.confirm(false, window, cx);
|
||||
});
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(&SOURCE.replace('ˇ', "").replace("main", "«mainˇ»"));
|
||||
cx.dispatch_action(workspace::ReopenLastPicker);
|
||||
cx.run_until_parked();
|
||||
let modal = active_picker(&mut cx).expect("reopened locations picker");
|
||||
cx.update(|window, cx| {
|
||||
modal.read(cx).picker.clone().update(cx, |picker, cx| {
|
||||
picker.set_selected_index(2, None, true, window, cx);
|
||||
picker.delegate.confirm(false, window, cx);
|
||||
});
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(&SOURCE.replace('ˇ', "").replace("= abc", "= «abcˇ»"));
|
||||
cx.dispatch_action(workspace::pane::GoToOlderTag);
|
||||
cx.assert_editor_state(&SOURCE.replace('ˇ', "").replace("main", "mainˇ"));
|
||||
cx.dispatch_action(workspace::pane::GoToOlderTag);
|
||||
cx.assert_editor_state(SOURCE);
|
||||
assert_eq!(requests.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_type_definition_honors_lsp_results_location(cx: &mut TestAppContext) {
|
||||
cx.update(crate::init);
|
||||
@@ -1140,4 +1346,233 @@ mod tests {
|
||||
"declaration should open the picker when lsp_results_location is picker"
|
||||
);
|
||||
}
|
||||
|
||||
async fn assert_cmd_click(
|
||||
cx: &mut TestAppContext,
|
||||
hover: Option<bool>,
|
||||
shift: bool,
|
||||
pending: bool,
|
||||
results: OpenResultsIn,
|
||||
split: bool,
|
||||
disabled: bool,
|
||||
) {
|
||||
let mut cx = rust_cx(
|
||||
lsp::ServerCapabilities {
|
||||
definition_provider: Some(lsp::OneOf::Left(true)),
|
||||
type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let scenario = format!(
|
||||
"hover={hover:?}, shift={shift}, pending={pending}, results={results:?}, split={split}, disabled={disabled}"
|
||||
);
|
||||
cx.update_global::<settings::SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings(cx, |settings| {
|
||||
settings.editor.lsp_results_location = Some(results);
|
||||
settings.editor.excerpt_context_lines = Some(0);
|
||||
});
|
||||
});
|
||||
let source = indoc! {r#"
|
||||
fn main() {
|
||||
let foo = ();
|
||||
|
||||
let foo = ();
|
||||
let bar = fˇoo;
|
||||
}
|
||||
"#}
|
||||
.replace("\n let bar", "\n\n\n\n\n\n let bar");
|
||||
cx.set_state(&source);
|
||||
if disabled {
|
||||
cx.update_editor(|editor, _, _| {
|
||||
editor.set_mode(editor::EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
})
|
||||
});
|
||||
}
|
||||
let position = cx
|
||||
.editor(|editor, _, cx| editor.pixel_position_of_cursor(cx))
|
||||
.expect("click pixel");
|
||||
cx.set_selections_state(&source.replace('ˇ', "").replace("main", "ˇmain"));
|
||||
cx.assert_editor_selections(vec![3..3]);
|
||||
let buffer = cx.multibuffer(|buffer, _| buffer.as_singleton().expect("source buffer"));
|
||||
let requests = Arc::new(Mutex::new(Vec::new()));
|
||||
let completed = Arc::new(AtomicUsize::new(0));
|
||||
let delay = Duration::from_secs(1);
|
||||
for kind in [false, true] {
|
||||
let requests = requests.clone();
|
||||
let completed = completed.clone();
|
||||
let handler = move |params: lsp::GotoDefinitionParams, app: gpui::AsyncApp| {
|
||||
let requests = requests.clone();
|
||||
let completed = completed.clone();
|
||||
async move {
|
||||
let params = params.text_document_position_params;
|
||||
requests.lock().expect("lock").push((kind, params.clone()));
|
||||
if pending && kind == shift {
|
||||
app.background_executor().timer(delay).await;
|
||||
}
|
||||
completed.fetch_add(1, Ordering::SeqCst);
|
||||
let (start, end) = if kind { (14, 16) } else { (8, 11) };
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Array(references(
|
||||
params.text_document.uri,
|
||||
&[(1, start, end), (3, start, end)],
|
||||
))))
|
||||
}
|
||||
};
|
||||
if kind {
|
||||
cx.lsp
|
||||
.set_request_handler::<lsp::request::GotoTypeDefinition, _, _>(handler);
|
||||
} else {
|
||||
cx.lsp
|
||||
.set_request_handler::<lsp::request::GotoDefinition, _, _>(handler);
|
||||
}
|
||||
}
|
||||
let modifiers = Modifiers {
|
||||
shift,
|
||||
alt: split,
|
||||
..Modifiers::secondary_key()
|
||||
};
|
||||
let mut expected_requests = Vec::new();
|
||||
let query = lsp::TextDocumentPositionParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(cx.buffer_lsp_url.clone()),
|
||||
position: lsp::Position::new(9, 15),
|
||||
};
|
||||
if let Some(shift) = hover {
|
||||
let mut modifiers = modifiers;
|
||||
modifiers.shift = shift;
|
||||
cx.simulate_mouse_move(position, None, modifiers);
|
||||
expected_requests.push((shift, query.clone()));
|
||||
}
|
||||
if pending {
|
||||
cx.simulate_mouse_move(position, None, modifiers);
|
||||
expected_requests.push((shift, query.clone()));
|
||||
}
|
||||
assert_eq!(*requests.lock().expect("requests"), expected_requests);
|
||||
assert_eq!(
|
||||
completed.load(Ordering::SeqCst),
|
||||
usize::from(hover.is_some())
|
||||
);
|
||||
cx.simulate_click(position, modifiers);
|
||||
if pending {
|
||||
cx.executor().advance_clock(delay);
|
||||
}
|
||||
cx.run_until_parked();
|
||||
if !disabled && hover != Some(shift) {
|
||||
expected_requests.push((shift, query));
|
||||
}
|
||||
assert_eq!(
|
||||
*requests.lock().expect("requests"),
|
||||
expected_requests,
|
||||
"{scenario}"
|
||||
);
|
||||
let picker = active_picker(&mut cx);
|
||||
let source_editor = cx.editor.clone();
|
||||
let (start, end) = if shift { (14, 16) } else { (8, 11) };
|
||||
let expected_ranges = vec![
|
||||
Point::new(1, start)..Point::new(1, end),
|
||||
Point::new(3, start)..Point::new(3, end),
|
||||
];
|
||||
cx.update_workspace(|workspace, _, cx| {
|
||||
assert_eq!(workspace.panes().len(), 1 + usize::from(split));
|
||||
let active = workspace.active_item_as::<Editor>(cx).expect("editor");
|
||||
if disabled {
|
||||
assert!(picker.is_none());
|
||||
assert_eq!(active, source_editor);
|
||||
assert_eq!(workspace.items(cx).count(), 1);
|
||||
return;
|
||||
}
|
||||
let actual_ranges = if results == OpenResultsIn::Picker && !split {
|
||||
let picker = picker.expect("locations picker");
|
||||
let delegate = &picker.read(cx).picker.read(cx).delegate;
|
||||
let kind = match shift {
|
||||
true => LspPickerKind::TypeDefinition,
|
||||
false => LspPickerKind::Definition,
|
||||
};
|
||||
assert_eq!(delegate.kind, kind);
|
||||
assert_eq!(workspace.items(cx).count(), 1);
|
||||
delegate
|
||||
.all_matches
|
||||
.iter()
|
||||
.map(|location| {
|
||||
assert_eq!(location.buffer, buffer);
|
||||
location.anchor_range.to_point(buffer.read(cx))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
assert!(picker.is_none());
|
||||
let multibuffer = active.read(cx).buffer().read(cx);
|
||||
assert!(!multibuffer.is_singleton());
|
||||
assert_eq!(
|
||||
multibuffer.all_buffers_iter().collect::<Vec<_>>(),
|
||||
vec![buffer.clone()]
|
||||
);
|
||||
assert_eq!(workspace.items(cx).count(), 2);
|
||||
multibuffer
|
||||
.snapshot(cx)
|
||||
.excerpts()
|
||||
.map(|excerpt| excerpt.primary.to_point(buffer.read(cx)))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(actual_ranges, expected_ranges);
|
||||
});
|
||||
cx.update(|window, _| window.remove_window());
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
async fn cmd_click_request(
|
||||
cx: &mut TestAppContext,
|
||||
empty_hover: bool,
|
||||
targets: &[(u32, u32, u32)],
|
||||
) -> (EditorLspTestContext, Arc<AtomicUsize>) {
|
||||
let mut cx = rust_cx(
|
||||
lsp::ServerCapabilities {
|
||||
definition_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
cx.update_global::<settings::SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings(cx, |settings| {
|
||||
settings.editor.lsp_results_location = Some(OpenResultsIn::Picker);
|
||||
settings.editor.go_to_definition_fallback = Some(GoToDefinitionFallback::None);
|
||||
});
|
||||
});
|
||||
cx.set_state(SOURCE);
|
||||
let requests = Arc::new(AtomicUsize::new(0));
|
||||
let locations = references(cx.buffer_lsp_url.clone(), targets);
|
||||
cx.set_request_handler::<lsp::request::GotoDefinition, _, _>({
|
||||
let requests = requests.clone();
|
||||
move |uri, params, app| {
|
||||
let params = params.text_document_position_params;
|
||||
assert_eq!(params.text_document.uri, uri);
|
||||
assert_eq!(params.position, lsp::Position::new(1, 9));
|
||||
let empty = requests.fetch_add(1, Ordering::SeqCst) == 0 && empty_hover;
|
||||
let locations = locations.clone();
|
||||
async move {
|
||||
if !empty_hover {
|
||||
app.background_executor()
|
||||
.timer(Duration::from_secs(1))
|
||||
.await;
|
||||
}
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Array(if empty {
|
||||
Vec::new()
|
||||
} else {
|
||||
locations
|
||||
})))
|
||||
}
|
||||
}
|
||||
});
|
||||
let position = cx
|
||||
.editor(|editor, _, cx| editor.pixel_position_of_cursor(cx))
|
||||
.expect("click pixel");
|
||||
if empty_hover {
|
||||
cx.simulate_mouse_move(position, None, Modifiers::secondary_key());
|
||||
}
|
||||
cx.simulate_click(position, Modifiers::secondary_key());
|
||||
cx.simulate_modifiers_change(Modifiers::none());
|
||||
(cx, requests)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user