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 ..Default::default()
316 },
317 cx,
318 )
319 .await;
320
321 cx.set_state(indoc! {"
322 struct A;
323 let vˇariable = A;
324 "});
325
326 // Basic hold cmd+shift, expect highlight in region if response contains type definition
327 let hover_point = cx.display_point(indoc! {"
328 struct A;
329 let vˇariable = A;
330 "});
331 let symbol_range = cx.lsp_range(indoc! {"
332 struct A;
333 let «variable» = A;
334 "});
335 let target_range = cx.lsp_range(indoc! {"
336 struct «A»;
337 let variable = A;
338 "});
339
340 let mut requests =
341 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
342 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
343 lsp::LocationLink {
344 origin_selection_range: Some(symbol_range),
345 target_uri: url.clone(),
346 target_range,
347 target_selection_range: target_range,
348 },
349 ])))
350 });
351
352 // Press cmd+shift to trigger highlight
353 cx.update_editor(|editor, cx| {
354 update_go_to_definition_link(editor, Some(hover_point), true, true, cx);
355 });
356 requests.next().await;
357 cx.foreground().run_until_parked();
358 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
359 struct A;
360 let «variable» = A;
361 "});
362
363 // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
364 cx.update_editor(|editor, cx| {
365 editor.modifiers_changed(
366 &platform::ModifiersChangedEvent {
367 modifiers: Modifiers {
368 cmd: true,
369 ..Default::default()
370 },
371 ..Default::default()
372 },
373 cx,
374 );
375 });
376 // Assert no link highlights
377 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
378 struct A;
379 let variable = A;
380 "});
381
382 // Cmd+shift click without existing definition requests and jumps
383 let hover_point = cx.display_point(indoc! {"
384 struct A;
385 let vˇariable = A;
386 "});
387 let target_range = cx.lsp_range(indoc! {"
388 struct «A»;
389 let variable = A;
390 "});
391
392 let mut requests =
393 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
394 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
395 lsp::LocationLink {
396 origin_selection_range: None,
397 target_uri: url,
398 target_range,
399 target_selection_range: target_range,
400 },
401 ])))
402 });
403
404 cx.update_editor(|editor, cx| {
405 go_to_fetched_type_definition(editor, hover_point, cx);
406 });
407 requests.next().await;
408 cx.foreground().run_until_parked();
409
410 cx.assert_editor_state(indoc! {"
411 struct «Aˇ»;
412 let variable = A;
413 "});
414 }
415
416 #[gpui::test]
417 async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
418 init_test(cx, |_| {});
419
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}