1use std::ops::Range;
2
3use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
4use gpui::{Task, ViewContext};
5use language::{Bias, ToOffset};
6use project::LocationLink;
7use settings::Settings;
8use util::TryFutureExt;
9
10#[derive(Debug, Default)]
11pub struct LinkGoToDefinitionState {
12 pub last_mouse_location: Option<Anchor>,
13 pub symbol_range: Option<Range<Anchor>>,
14 pub kind: Option<LinkDefinitionKind>,
15 pub definitions: Vec<LocationLink>,
16 pub task: Option<Task<Option<()>>>,
17}
18
19pub fn update_go_to_definition_link(
20 editor: &mut Editor,
21 point: Option<DisplayPoint>,
22 cmd_held: bool,
23 shift_held: bool,
24 cx: &mut ViewContext<Editor>,
25) {
26 let pending_nonempty_selection = editor.has_pending_nonempty_selection();
27
28 // Store new mouse point as an anchor
29 let snapshot = editor.snapshot(cx);
30 let point = point.map(|point| {
31 snapshot
32 .buffer_snapshot
33 .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left))
34 });
35
36 // If the new point is the same as the previously stored one, return early
37 if let (Some(a), Some(b)) = (
38 &point,
39 &editor.link_go_to_definition_state.last_mouse_location,
40 ) {
41 if a.cmp(b, &snapshot.buffer_snapshot).is_eq() {
42 return;
43 }
44 }
45
46 editor.link_go_to_definition_state.last_mouse_location = point.clone();
47
48 if pending_nonempty_selection {
49 hide_link_definition(editor, cx);
50 return;
51 }
52
53 if cmd_held {
54 if let Some(point) = point {
55 let kind = if shift_held {
56 LinkDefinitionKind::Type
57 } else {
58 LinkDefinitionKind::Symbol
59 };
60
61 show_link_definition(kind, editor, point, snapshot, cx);
62 return;
63 }
64 }
65
66 hide_link_definition(editor, cx);
67}
68
69#[derive(Debug, Clone, Copy, PartialEq)]
70pub enum LinkDefinitionKind {
71 Symbol,
72 Type,
73}
74
75pub fn show_link_definition(
76 definition_kind: LinkDefinitionKind,
77 editor: &mut Editor,
78 trigger_point: Anchor,
79 snapshot: EditorSnapshot,
80 cx: &mut ViewContext<Editor>,
81) {
82 let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind);
83 if !same_kind {
84 hide_link_definition(editor, cx);
85 }
86
87 if editor.pending_rename.is_some() {
88 return;
89 }
90
91 let (buffer, buffer_position) = if let Some(output) = editor
92 .buffer
93 .read(cx)
94 .text_anchor_for_position(trigger_point.clone(), cx)
95 {
96 output
97 } else {
98 return;
99 };
100
101 let excerpt_id = if let Some((excerpt_id, _, _)) = editor
102 .buffer()
103 .read(cx)
104 .excerpt_containing(trigger_point.clone(), cx)
105 {
106 excerpt_id
107 } else {
108 return;
109 };
110
111 let project = if let Some(project) = editor.project.clone() {
112 project
113 } else {
114 return;
115 };
116
117 // Don't request again if the location is within the symbol region of a previous request with the same kind
118 if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
119 let point_after_start = symbol_range
120 .start
121 .cmp(&trigger_point, &snapshot.buffer_snapshot)
122 .is_le();
123
124 let point_before_end = symbol_range
125 .end
126 .cmp(&trigger_point, &snapshot.buffer_snapshot)
127 .is_ge();
128
129 let point_within_range = point_after_start && point_before_end;
130 if point_within_range && same_kind {
131 return;
132 }
133 }
134
135 let task = cx.spawn(|this, mut cx| {
136 async move {
137 // query the LSP for definition info
138 let definition_request = cx.update(|cx| {
139 project.update(cx, |project, cx| match definition_kind {
140 LinkDefinitionKind::Symbol => project.definition(&buffer, buffer_position, cx),
141
142 LinkDefinitionKind::Type => {
143 project.type_definition(&buffer, buffer_position, cx)
144 }
145 })
146 });
147
148 let result = definition_request.await.ok().map(|definition_result| {
149 (
150 definition_result.iter().find_map(|link| {
151 link.origin.as_ref().map(|origin| {
152 let start = snapshot
153 .buffer_snapshot
154 .anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
155 let end = snapshot
156 .buffer_snapshot
157 .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
158
159 start..end
160 })
161 }),
162 definition_result,
163 )
164 });
165
166 this.update(&mut cx, |this, cx| {
167 // Clear any existing highlights
168 this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
169 this.link_go_to_definition_state.kind = Some(definition_kind);
170 this.link_go_to_definition_state.symbol_range = result
171 .as_ref()
172 .and_then(|(symbol_range, _)| symbol_range.clone());
173
174 if let Some((symbol_range, definitions)) = result {
175 this.link_go_to_definition_state.definitions = definitions.clone();
176
177 let buffer_snapshot = buffer.read(cx).snapshot();
178
179 // Only show highlight if there exists a definition to jump to that doesn't contain
180 // the current location.
181 let any_definition_does_not_contain_current_location =
182 definitions.iter().any(|definition| {
183 let target = &definition.target;
184 if target.buffer == buffer {
185 let range = &target.range;
186 // Expand range by one character as lsp definition ranges include positions adjacent
187 // but not contained by the symbol range
188 let start = buffer_snapshot.clip_offset(
189 range.start.to_offset(&buffer_snapshot).saturating_sub(1),
190 Bias::Left,
191 );
192 let end = buffer_snapshot.clip_offset(
193 range.end.to_offset(&buffer_snapshot) + 1,
194 Bias::Right,
195 );
196 let offset = buffer_position.to_offset(&buffer_snapshot);
197 !(start <= offset && end >= offset)
198 } else {
199 true
200 }
201 });
202
203 if any_definition_does_not_contain_current_location {
204 // If no symbol range returned from language server, use the surrounding word.
205 let highlight_range = symbol_range.unwrap_or_else(|| {
206 let snapshot = &snapshot.buffer_snapshot;
207 let (offset_range, _) = snapshot.surrounding_word(trigger_point);
208
209 snapshot.anchor_before(offset_range.start)
210 ..snapshot.anchor_after(offset_range.end)
211 });
212
213 // Highlight symbol using theme link definition highlight style
214 let style = cx.global::<Settings>().theme.editor.link_definition;
215 this.highlight_text::<LinkGoToDefinitionState>(
216 vec![highlight_range],
217 style,
218 cx,
219 );
220 } else {
221 hide_link_definition(this, cx);
222 }
223 }
224 })?;
225
226 Ok::<_, anyhow::Error>(())
227 }
228 .log_err()
229 });
230
231 editor.link_go_to_definition_state.task = Some(task);
232}
233
234pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
235 if editor.link_go_to_definition_state.symbol_range.is_some()
236 || !editor.link_go_to_definition_state.definitions.is_empty()
237 {
238 editor.link_go_to_definition_state.symbol_range.take();
239 editor.link_go_to_definition_state.definitions.clear();
240 cx.notify();
241 }
242
243 editor.link_go_to_definition_state.task = None;
244
245 editor.clear_text_highlights::<LinkGoToDefinitionState>(cx);
246}
247
248pub fn go_to_fetched_definition(
249 editor: &mut Editor,
250 point: DisplayPoint,
251 cx: &mut ViewContext<Editor>,
252) {
253 go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, cx);
254}
255
256pub fn go_to_fetched_type_definition(
257 editor: &mut Editor,
258 point: DisplayPoint,
259 cx: &mut ViewContext<Editor>,
260) {
261 go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, cx);
262}
263
264fn go_to_fetched_definition_of_kind(
265 kind: LinkDefinitionKind,
266 editor: &mut Editor,
267 point: DisplayPoint,
268 cx: &mut ViewContext<Editor>,
269) {
270 let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
271 hide_link_definition(editor, cx);
272 let cached_definitions_kind = editor.link_go_to_definition_state.kind;
273
274 let is_correct_kind = cached_definitions_kind == Some(kind);
275 if !cached_definitions.is_empty() && is_correct_kind {
276 if !editor.focused {
277 cx.focus_self();
278 }
279
280 editor.navigate_to_definitions(cached_definitions, cx);
281 } else {
282 editor.select(
283 SelectPhase::Begin {
284 position: point,
285 add: false,
286 click_count: 1,
287 },
288 cx,
289 );
290
291 match kind {
292 LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
293 LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
294 }
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use futures::StreamExt;
301 use gpui::{
302 platform::{self, Modifiers, ModifiersChangedEvent},
303 View,
304 };
305 use indoc::indoc;
306 use lsp::request::{GotoDefinition, GotoTypeDefinition};
307
308 use crate::test::editor_lsp_test_context::EditorLspTestContext;
309
310 use super::*;
311
312 #[gpui::test]
313 async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
314 let mut cx = EditorLspTestContext::new_rust(
315 lsp::ServerCapabilities {
316 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
317 ..Default::default()
318 },
319 cx,
320 )
321 .await;
322
323 cx.set_state(indoc! {"
324 struct A;
325 let vˇariable = A;
326 "});
327
328 // Basic hold cmd+shift, expect highlight in region if response contains type definition
329 let hover_point = cx.display_point(indoc! {"
330 struct A;
331 let vˇariable = A;
332 "});
333 let symbol_range = cx.lsp_range(indoc! {"
334 struct A;
335 let «variable» = A;
336 "});
337 let target_range = cx.lsp_range(indoc! {"
338 struct «A»;
339 let variable = A;
340 "});
341
342 let mut requests =
343 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
344 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
345 lsp::LocationLink {
346 origin_selection_range: Some(symbol_range),
347 target_uri: url.clone(),
348 target_range,
349 target_selection_range: target_range,
350 },
351 ])))
352 });
353
354 // Press cmd+shift to trigger highlight
355 cx.update_editor(|editor, cx| {
356 update_go_to_definition_link(editor, Some(hover_point), true, true, cx);
357 });
358 requests.next().await;
359 cx.foreground().run_until_parked();
360 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
361 struct A;
362 let «variable» = A;
363 "});
364
365 // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
366 cx.update_editor(|editor, cx| {
367 editor.modifiers_changed(
368 &platform::ModifiersChangedEvent {
369 modifiers: Modifiers {
370 cmd: true,
371 ..Default::default()
372 },
373 ..Default::default()
374 },
375 cx,
376 );
377 });
378 // Assert no link highlights
379 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
380 struct A;
381 let variable = A;
382 "});
383
384 // Cmd+shift click without existing definition requests and jumps
385 let hover_point = cx.display_point(indoc! {"
386 struct A;
387 let vˇariable = A;
388 "});
389 let target_range = cx.lsp_range(indoc! {"
390 struct «A»;
391 let variable = A;
392 "});
393
394 let mut requests =
395 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
396 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
397 lsp::LocationLink {
398 origin_selection_range: None,
399 target_uri: url,
400 target_range,
401 target_selection_range: target_range,
402 },
403 ])))
404 });
405
406 cx.update_editor(|editor, cx| {
407 go_to_fetched_type_definition(editor, hover_point, cx);
408 });
409 requests.next().await;
410 cx.foreground().run_until_parked();
411
412 cx.assert_editor_state(indoc! {"
413 struct «Aˇ»;
414 let variable = A;
415 "});
416 }
417
418 #[gpui::test]
419 async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
420 let mut cx = EditorLspTestContext::new_rust(
421 lsp::ServerCapabilities {
422 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
423 ..Default::default()
424 },
425 cx,
426 )
427 .await;
428
429 cx.set_state(indoc! {"
430 fn ˇtest() { do_work(); }
431 fn do_work() { test(); }
432 "});
433
434 // Basic hold cmd, expect highlight in region if response contains definition
435 let hover_point = cx.display_point(indoc! {"
436 fn test() { do_wˇork(); }
437 fn do_work() { test(); }
438 "});
439 let symbol_range = cx.lsp_range(indoc! {"
440 fn test() { «do_work»(); }
441 fn do_work() { test(); }
442 "});
443 let target_range = cx.lsp_range(indoc! {"
444 fn test() { do_work(); }
445 fn «do_work»() { test(); }
446 "});
447
448 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
449 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
450 lsp::LocationLink {
451 origin_selection_range: Some(symbol_range),
452 target_uri: url.clone(),
453 target_range,
454 target_selection_range: target_range,
455 },
456 ])))
457 });
458
459 cx.update_editor(|editor, cx| {
460 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
461 });
462 requests.next().await;
463 cx.foreground().run_until_parked();
464 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
465 fn test() { «do_work»(); }
466 fn do_work() { test(); }
467 "});
468
469 // Unpress cmd causes highlight to go away
470 cx.update_editor(|editor, cx| {
471 editor.modifiers_changed(&Default::default(), cx);
472 });
473
474 // Assert no link highlights
475 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
476 fn test() { do_work(); }
477 fn do_work() { test(); }
478 "});
479
480 // Response without source range still highlights word
481 cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
482 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
483 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
484 lsp::LocationLink {
485 // No origin range
486 origin_selection_range: None,
487 target_uri: url.clone(),
488 target_range,
489 target_selection_range: target_range,
490 },
491 ])))
492 });
493 cx.update_editor(|editor, cx| {
494 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
495 });
496 requests.next().await;
497 cx.foreground().run_until_parked();
498
499 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
500 fn test() { «do_work»(); }
501 fn do_work() { test(); }
502 "});
503
504 // Moving mouse to location with no response dismisses highlight
505 let hover_point = cx.display_point(indoc! {"
506 fˇn test() { do_work(); }
507 fn do_work() { test(); }
508 "});
509 let mut requests = cx
510 .lsp
511 .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
512 // No definitions returned
513 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
514 });
515 cx.update_editor(|editor, cx| {
516 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
517 });
518 requests.next().await;
519 cx.foreground().run_until_parked();
520
521 // Assert no link highlights
522 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
523 fn test() { do_work(); }
524 fn do_work() { test(); }
525 "});
526
527 // Move mouse without cmd and then pressing cmd triggers highlight
528 let hover_point = cx.display_point(indoc! {"
529 fn test() { do_work(); }
530 fn do_work() { teˇst(); }
531 "});
532 cx.update_editor(|editor, cx| {
533 update_go_to_definition_link(editor, Some(hover_point), false, false, cx);
534 });
535 cx.foreground().run_until_parked();
536
537 // Assert no link highlights
538 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
539 fn test() { do_work(); }
540 fn do_work() { test(); }
541 "});
542
543 let symbol_range = cx.lsp_range(indoc! {"
544 fn test() { do_work(); }
545 fn do_work() { «test»(); }
546 "});
547 let target_range = cx.lsp_range(indoc! {"
548 fn «test»() { do_work(); }
549 fn do_work() { test(); }
550 "});
551
552 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
553 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
554 lsp::LocationLink {
555 origin_selection_range: Some(symbol_range),
556 target_uri: url,
557 target_range,
558 target_selection_range: target_range,
559 },
560 ])))
561 });
562 cx.update_editor(|editor, cx| {
563 editor.modifiers_changed(
564 &ModifiersChangedEvent {
565 modifiers: Modifiers {
566 cmd: true,
567 ..Default::default()
568 },
569 },
570 cx,
571 );
572 });
573 requests.next().await;
574 cx.foreground().run_until_parked();
575
576 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
577 fn test() { do_work(); }
578 fn do_work() { «test»(); }
579 "});
580
581 // Deactivating the window dismisses the highlight
582 cx.update_workspace(|workspace, cx| {
583 workspace.on_window_activation_changed(false, cx);
584 });
585 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
586 fn test() { do_work(); }
587 fn do_work() { test(); }
588 "});
589
590 // Moving the mouse restores the highlights.
591 cx.update_editor(|editor, cx| {
592 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
593 });
594 cx.foreground().run_until_parked();
595 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
596 fn test() { do_work(); }
597 fn do_work() { «test»(); }
598 "});
599
600 // Moving again within the same symbol range doesn't re-request
601 let hover_point = cx.display_point(indoc! {"
602 fn test() { do_work(); }
603 fn do_work() { tesˇt(); }
604 "});
605 cx.update_editor(|editor, cx| {
606 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
607 });
608 cx.foreground().run_until_parked();
609 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
610 fn test() { do_work(); }
611 fn do_work() { «test»(); }
612 "});
613
614 // Cmd click with existing definition doesn't re-request and dismisses highlight
615 cx.update_editor(|editor, cx| {
616 go_to_fetched_definition(editor, hover_point, cx);
617 });
618 // Assert selection moved to to definition
619 cx.lsp
620 .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
621 // Empty definition response to make sure we aren't hitting the lsp and using
622 // the cached location instead
623 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
624 });
625 cx.assert_editor_state(indoc! {"
626 fn «testˇ»() { do_work(); }
627 fn do_work() { test(); }
628 "});
629
630 // Assert no link highlights after jump
631 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
632 fn test() { do_work(); }
633 fn do_work() { test(); }
634 "});
635
636 // Cmd click without existing definition requests and jumps
637 let hover_point = cx.display_point(indoc! {"
638 fn test() { do_wˇork(); }
639 fn do_work() { test(); }
640 "});
641 let target_range = cx.lsp_range(indoc! {"
642 fn test() { do_work(); }
643 fn «do_work»() { test(); }
644 "});
645
646 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
647 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
648 lsp::LocationLink {
649 origin_selection_range: None,
650 target_uri: url,
651 target_range,
652 target_selection_range: target_range,
653 },
654 ])))
655 });
656 cx.update_editor(|editor, cx| {
657 go_to_fetched_definition(editor, hover_point, cx);
658 });
659 requests.next().await;
660 cx.foreground().run_until_parked();
661 cx.assert_editor_state(indoc! {"
662 fn test() { do_work(); }
663 fn «do_workˇ»() { test(); }
664 "});
665
666 // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
667 // 2. Selection is completed, hovering
668 let hover_point = cx.display_point(indoc! {"
669 fn test() { do_wˇork(); }
670 fn do_work() { test(); }
671 "});
672 let target_range = cx.lsp_range(indoc! {"
673 fn test() { do_work(); }
674 fn «do_work»() { test(); }
675 "});
676 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
677 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
678 lsp::LocationLink {
679 origin_selection_range: None,
680 target_uri: url,
681 target_range,
682 target_selection_range: target_range,
683 },
684 ])))
685 });
686
687 // create a pending selection
688 let selection_range = cx.ranges(indoc! {"
689 fn «test() { do_w»ork(); }
690 fn do_work() { test(); }
691 "})[0]
692 .clone();
693 cx.update_editor(|editor, cx| {
694 let snapshot = editor.buffer().read(cx).snapshot(cx);
695 let anchor_range = snapshot.anchor_before(selection_range.start)
696 ..snapshot.anchor_after(selection_range.end);
697 editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
698 s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
699 });
700 });
701 cx.update_editor(|editor, cx| {
702 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
703 });
704 cx.foreground().run_until_parked();
705 assert!(requests.try_next().is_err());
706 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
707 fn test() { do_work(); }
708 fn do_work() { test(); }
709 "});
710 cx.foreground().run_until_parked();
711 }
712}