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