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