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 split: bool,
250 cx: &mut ViewContext<Editor>,
251) {
252 go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx);
253}
254
255pub fn go_to_fetched_type_definition(
256 editor: &mut Editor,
257 point: DisplayPoint,
258 split: bool,
259 cx: &mut ViewContext<Editor>,
260) {
261 go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx);
262}
263
264fn go_to_fetched_definition_of_kind(
265 kind: LinkDefinitionKind,
266 editor: &mut Editor,
267 point: DisplayPoint,
268 split: bool,
269 cx: &mut ViewContext<Editor>,
270) {
271 let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
272 hide_link_definition(editor, cx);
273 let cached_definitions_kind = editor.link_go_to_definition_state.kind;
274
275 let is_correct_kind = cached_definitions_kind == Some(kind);
276 if !cached_definitions.is_empty() && is_correct_kind {
277 if !editor.focused {
278 cx.focus_self();
279 }
280
281 editor.navigate_to_definitions(cached_definitions, split, cx);
282 } else {
283 editor.select(
284 SelectPhase::Begin {
285 position: point,
286 add: false,
287 click_count: 1,
288 },
289 cx,
290 );
291
292 match kind {
293 LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
294 LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
295 }
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
303 use futures::StreamExt;
304 use gpui::{
305 platform::{self, Modifiers, ModifiersChangedEvent},
306 View,
307 };
308 use indoc::indoc;
309 use lsp::request::{GotoDefinition, GotoTypeDefinition};
310
311 #[gpui::test]
312 async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
313 init_test(cx, |_| {});
314
315 let mut cx = EditorLspTestContext::new_rust(
316 lsp::ServerCapabilities {
317 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
318 type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
319 ..Default::default()
320 },
321 cx,
322 )
323 .await;
324
325 cx.set_state(indoc! {"
326 struct A;
327 let vˇariable = A;
328 "});
329
330 // Basic hold cmd+shift, expect highlight in region if response contains type definition
331 let hover_point = cx.display_point(indoc! {"
332 struct A;
333 let vˇariable = A;
334 "});
335 let symbol_range = cx.lsp_range(indoc! {"
336 struct A;
337 let «variable» = A;
338 "});
339 let target_range = cx.lsp_range(indoc! {"
340 struct «A»;
341 let variable = A;
342 "});
343
344 let mut requests =
345 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
346 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
347 lsp::LocationLink {
348 origin_selection_range: Some(symbol_range),
349 target_uri: url.clone(),
350 target_range,
351 target_selection_range: target_range,
352 },
353 ])))
354 });
355
356 // Press cmd+shift to trigger highlight
357 cx.update_editor(|editor, cx| {
358 update_go_to_definition_link(editor, Some(hover_point), true, true, cx);
359 });
360 requests.next().await;
361 cx.foreground().run_until_parked();
362 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
363 struct A;
364 let «variable» = A;
365 "});
366
367 // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
368 cx.update_editor(|editor, cx| {
369 editor.modifiers_changed(
370 &platform::ModifiersChangedEvent {
371 modifiers: Modifiers {
372 cmd: true,
373 ..Default::default()
374 },
375 ..Default::default()
376 },
377 cx,
378 );
379 });
380 // Assert no link highlights
381 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
382 struct A;
383 let variable = A;
384 "});
385
386 // Cmd+shift click without existing definition requests and jumps
387 let hover_point = cx.display_point(indoc! {"
388 struct A;
389 let vˇariable = A;
390 "});
391 let target_range = cx.lsp_range(indoc! {"
392 struct «A»;
393 let variable = A;
394 "});
395
396 let mut requests =
397 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
398 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
399 lsp::LocationLink {
400 origin_selection_range: None,
401 target_uri: url,
402 target_range,
403 target_selection_range: target_range,
404 },
405 ])))
406 });
407
408 cx.update_editor(|editor, cx| {
409 go_to_fetched_type_definition(editor, hover_point, false, cx);
410 });
411 requests.next().await;
412 cx.foreground().run_until_parked();
413
414 cx.assert_editor_state(indoc! {"
415 struct «Aˇ»;
416 let variable = A;
417 "});
418 }
419
420 #[gpui::test]
421 async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
422 init_test(cx, |_| {});
423
424 let mut cx = EditorLspTestContext::new_rust(
425 lsp::ServerCapabilities {
426 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
427 ..Default::default()
428 },
429 cx,
430 )
431 .await;
432
433 cx.set_state(indoc! {"
434 fn ˇtest() { do_work(); }
435 fn do_work() { test(); }
436 "});
437
438 // Basic hold cmd, expect highlight in region if response contains definition
439 let hover_point = cx.display_point(indoc! {"
440 fn test() { do_wˇork(); }
441 fn do_work() { test(); }
442 "});
443 let symbol_range = cx.lsp_range(indoc! {"
444 fn test() { «do_work»(); }
445 fn do_work() { test(); }
446 "});
447 let target_range = cx.lsp_range(indoc! {"
448 fn test() { do_work(); }
449 fn «do_work»() { test(); }
450 "});
451
452 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
453 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
454 lsp::LocationLink {
455 origin_selection_range: Some(symbol_range),
456 target_uri: url.clone(),
457 target_range,
458 target_selection_range: target_range,
459 },
460 ])))
461 });
462
463 cx.update_editor(|editor, cx| {
464 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
465 });
466 requests.next().await;
467 cx.foreground().run_until_parked();
468 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
469 fn test() { «do_work»(); }
470 fn do_work() { test(); }
471 "});
472
473 // Unpress cmd causes highlight to go away
474 cx.update_editor(|editor, cx| {
475 editor.modifiers_changed(&Default::default(), cx);
476 });
477
478 // Assert no link highlights
479 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
480 fn test() { do_work(); }
481 fn do_work() { test(); }
482 "});
483
484 // Response without source range still highlights word
485 cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
486 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
487 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
488 lsp::LocationLink {
489 // No origin range
490 origin_selection_range: None,
491 target_uri: url.clone(),
492 target_range,
493 target_selection_range: target_range,
494 },
495 ])))
496 });
497 cx.update_editor(|editor, cx| {
498 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
499 });
500 requests.next().await;
501 cx.foreground().run_until_parked();
502
503 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
504 fn test() { «do_work»(); }
505 fn do_work() { test(); }
506 "});
507
508 // Moving mouse to location with no response dismisses highlight
509 let hover_point = cx.display_point(indoc! {"
510 fˇn test() { do_work(); }
511 fn do_work() { test(); }
512 "});
513 let mut requests = cx
514 .lsp
515 .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
516 // No definitions returned
517 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
518 });
519 cx.update_editor(|editor, cx| {
520 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
521 });
522 requests.next().await;
523 cx.foreground().run_until_parked();
524
525 // Assert no link highlights
526 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
527 fn test() { do_work(); }
528 fn do_work() { test(); }
529 "});
530
531 // Move mouse without cmd and then pressing cmd triggers highlight
532 let hover_point = cx.display_point(indoc! {"
533 fn test() { do_work(); }
534 fn do_work() { teˇst(); }
535 "});
536 cx.update_editor(|editor, cx| {
537 update_go_to_definition_link(editor, Some(hover_point), false, false, cx);
538 });
539 cx.foreground().run_until_parked();
540
541 // Assert no link highlights
542 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
543 fn test() { do_work(); }
544 fn do_work() { test(); }
545 "});
546
547 let symbol_range = cx.lsp_range(indoc! {"
548 fn test() { do_work(); }
549 fn do_work() { «test»(); }
550 "});
551 let target_range = cx.lsp_range(indoc! {"
552 fn «test»() { do_work(); }
553 fn do_work() { test(); }
554 "});
555
556 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
557 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
558 lsp::LocationLink {
559 origin_selection_range: Some(symbol_range),
560 target_uri: url,
561 target_range,
562 target_selection_range: target_range,
563 },
564 ])))
565 });
566 cx.update_editor(|editor, cx| {
567 editor.modifiers_changed(
568 &ModifiersChangedEvent {
569 modifiers: Modifiers {
570 cmd: true,
571 ..Default::default()
572 },
573 },
574 cx,
575 );
576 });
577 requests.next().await;
578 cx.foreground().run_until_parked();
579
580 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
581 fn test() { do_work(); }
582 fn do_work() { «test»(); }
583 "});
584
585 // Deactivating the window dismisses the highlight
586 cx.update_workspace(|workspace, cx| {
587 workspace.on_window_activation_changed(false, cx);
588 });
589 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
590 fn test() { do_work(); }
591 fn do_work() { test(); }
592 "});
593
594 // Moving the mouse restores the highlights.
595 cx.update_editor(|editor, cx| {
596 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
597 });
598 cx.foreground().run_until_parked();
599 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
600 fn test() { do_work(); }
601 fn do_work() { «test»(); }
602 "});
603
604 // Moving again within the same symbol range doesn't re-request
605 let hover_point = cx.display_point(indoc! {"
606 fn test() { do_work(); }
607 fn do_work() { tesˇt(); }
608 "});
609 cx.update_editor(|editor, cx| {
610 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
611 });
612 cx.foreground().run_until_parked();
613 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
614 fn test() { do_work(); }
615 fn do_work() { «test»(); }
616 "});
617
618 // Cmd click with existing definition doesn't re-request and dismisses highlight
619 cx.update_editor(|editor, cx| {
620 go_to_fetched_definition(editor, hover_point, false, cx);
621 });
622 // Assert selection moved to to definition
623 cx.lsp
624 .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
625 // Empty definition response to make sure we aren't hitting the lsp and using
626 // the cached location instead
627 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
628 });
629 cx.assert_editor_state(indoc! {"
630 fn «testˇ»() { do_work(); }
631 fn do_work() { test(); }
632 "});
633
634 // Assert no link highlights after jump
635 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
636 fn test() { do_work(); }
637 fn do_work() { test(); }
638 "});
639
640 // Cmd click without existing definition requests and jumps
641 let hover_point = cx.display_point(indoc! {"
642 fn test() { do_wˇork(); }
643 fn do_work() { test(); }
644 "});
645 let target_range = cx.lsp_range(indoc! {"
646 fn test() { do_work(); }
647 fn «do_work»() { test(); }
648 "});
649
650 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
651 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
652 lsp::LocationLink {
653 origin_selection_range: None,
654 target_uri: url,
655 target_range,
656 target_selection_range: target_range,
657 },
658 ])))
659 });
660 cx.update_editor(|editor, cx| {
661 go_to_fetched_definition(editor, hover_point, false, cx);
662 });
663 requests.next().await;
664 cx.foreground().run_until_parked();
665 cx.assert_editor_state(indoc! {"
666 fn test() { do_work(); }
667 fn «do_workˇ»() { test(); }
668 "});
669
670 // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
671 // 2. Selection is completed, hovering
672 let hover_point = cx.display_point(indoc! {"
673 fn test() { do_wˇork(); }
674 fn do_work() { test(); }
675 "});
676 let target_range = cx.lsp_range(indoc! {"
677 fn test() { do_work(); }
678 fn «do_work»() { test(); }
679 "});
680 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
681 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
682 lsp::LocationLink {
683 origin_selection_range: None,
684 target_uri: url,
685 target_range,
686 target_selection_range: target_range,
687 },
688 ])))
689 });
690
691 // create a pending selection
692 let selection_range = cx.ranges(indoc! {"
693 fn «test() { do_w»ork(); }
694 fn do_work() { test(); }
695 "})[0]
696 .clone();
697 cx.update_editor(|editor, cx| {
698 let snapshot = editor.buffer().read(cx).snapshot(cx);
699 let anchor_range = snapshot.anchor_before(selection_range.start)
700 ..snapshot.anchor_after(selection_range.end);
701 editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
702 s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
703 });
704 });
705 cx.update_editor(|editor, cx| {
706 update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
707 });
708 cx.foreground().run_until_parked();
709 assert!(requests.try_next().is_err());
710 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
711 fn test() { do_work(); }
712 fn do_work() { test(); }
713 "});
714 cx.foreground().run_until_parked();
715 }
716}