1use crate::{
2 display_map::DisplaySnapshot,
3 element::PointForPosition,
4 hover_popover::{self, InlayHover},
5 Anchor, DisplayPoint, Editor, EditorSnapshot, InlayId, SelectPhase,
6};
7use gpui::{Task, ViewContext};
8use language::{Bias, ToOffset};
9use lsp::LanguageServerId;
10use project::{
11 HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink,
12 ResolveState,
13};
14use std::ops::Range;
15use util::TryFutureExt;
16
17#[derive(Debug, Default)]
18pub struct LinkGoToDefinitionState {
19 pub last_trigger_point: Option<TriggerPoint>,
20 pub symbol_range: Option<RangeInEditor>,
21 pub kind: Option<LinkDefinitionKind>,
22 pub definitions: Vec<GoToDefinitionLink>,
23 pub task: Option<Task<Option<()>>>,
24}
25
26#[derive(Debug, Eq, PartialEq, Clone)]
27pub enum RangeInEditor {
28 Text(Range<Anchor>),
29 Inlay(InlayHighlight),
30}
31
32impl RangeInEditor {
33 pub fn as_text_range(&self) -> Option<Range<Anchor>> {
34 match self {
35 Self::Text(range) => Some(range.clone()),
36 Self::Inlay(_) => None,
37 }
38 }
39
40 fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
41 match (self, trigger_point) {
42 (Self::Text(range), TriggerPoint::Text(point)) => {
43 let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
44 point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge()
45 }
46 (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => {
47 highlight.inlay == point.inlay
48 && highlight.range.contains(&point.range.start)
49 && highlight.range.contains(&point.range.end)
50 }
51 (Self::Inlay(_), TriggerPoint::Text(_))
52 | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
53 }
54 }
55}
56
57#[derive(Debug)]
58pub enum GoToDefinitionTrigger {
59 Text(DisplayPoint),
60 InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
61}
62
63#[derive(Debug, Clone)]
64pub enum GoToDefinitionLink {
65 Text(LocationLink),
66 InlayHint(lsp::Location, LanguageServerId),
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct InlayHighlight {
71 pub inlay: InlayId,
72 pub inlay_position: Anchor,
73 pub range: Range<usize>,
74}
75
76#[derive(Debug, Clone)]
77pub enum TriggerPoint {
78 Text(Anchor),
79 InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
80}
81
82impl TriggerPoint {
83 pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
84 match self {
85 TriggerPoint::Text(_) => {
86 if shift {
87 LinkDefinitionKind::Type
88 } else {
89 LinkDefinitionKind::Symbol
90 }
91 }
92 TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type,
93 }
94 }
95
96 fn anchor(&self) -> &Anchor {
97 match self {
98 TriggerPoint::Text(anchor) => anchor,
99 TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position,
100 }
101 }
102}
103
104pub fn update_go_to_definition_link(
105 editor: &mut Editor,
106 origin: Option<GoToDefinitionTrigger>,
107 cmd_held: bool,
108 shift_held: bool,
109 cx: &mut ViewContext<Editor>,
110) {
111 let pending_nonempty_selection = editor.has_pending_nonempty_selection();
112
113 // Store new mouse point as an anchor
114 let snapshot = editor.snapshot(cx);
115 let trigger_point = match origin {
116 Some(GoToDefinitionTrigger::Text(p)) => {
117 Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before(
118 p.to_offset(&snapshot.display_snapshot, Bias::Left),
119 )))
120 }
121 Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => {
122 Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id))
123 }
124 None => None,
125 };
126
127 // If the new point is the same as the previously stored one, return early
128 if let (Some(a), Some(b)) = (
129 &trigger_point,
130 &editor.link_go_to_definition_state.last_trigger_point,
131 ) {
132 match (a, b) {
133 (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => {
134 if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() {
135 return;
136 }
137 }
138 (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => {
139 if range_a == range_b {
140 return;
141 }
142 }
143 _ => {}
144 }
145 }
146
147 editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone();
148
149 if pending_nonempty_selection {
150 hide_link_definition(editor, cx);
151 return;
152 }
153
154 if cmd_held {
155 if let Some(trigger_point) = trigger_point {
156 let kind = trigger_point.definition_kind(shift_held);
157 show_link_definition(kind, editor, trigger_point, snapshot, cx);
158 return;
159 }
160 }
161
162 hide_link_definition(editor, cx);
163}
164
165pub fn update_inlay_link_and_hover_points(
166 snapshot: &DisplaySnapshot,
167 point_for_position: PointForPosition,
168 editor: &mut Editor,
169 cmd_held: bool,
170 shift_held: bool,
171 cx: &mut ViewContext<'_, '_, Editor>,
172) {
173 let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
174 Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
175 } else {
176 None
177 };
178 let mut go_to_definition_updated = false;
179 let mut hover_updated = false;
180 if let Some(hovered_offset) = hovered_offset {
181 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
182 let previous_valid_anchor = buffer_snapshot.anchor_at(
183 point_for_position.previous_valid.to_point(snapshot),
184 Bias::Left,
185 );
186 let next_valid_anchor = buffer_snapshot.anchor_at(
187 point_for_position.next_valid.to_point(snapshot),
188 Bias::Right,
189 );
190 if let Some(hovered_hint) = editor
191 .visible_inlay_hints(cx)
192 .into_iter()
193 .skip_while(|hint| {
194 hint.position
195 .cmp(&previous_valid_anchor, &buffer_snapshot)
196 .is_lt()
197 })
198 .take_while(|hint| {
199 hint.position
200 .cmp(&next_valid_anchor, &buffer_snapshot)
201 .is_le()
202 })
203 .max_by_key(|hint| hint.id)
204 {
205 let inlay_hint_cache = editor.inlay_hint_cache();
206 let excerpt_id = previous_valid_anchor.excerpt_id;
207 if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
208 match cached_hint.resolve_state {
209 ResolveState::CanResolve(_, _) => {
210 if let Some(buffer_id) = previous_valid_anchor.buffer_id {
211 inlay_hint_cache.spawn_hint_resolve(
212 buffer_id,
213 excerpt_id,
214 hovered_hint.id,
215 cx,
216 );
217 }
218 }
219 ResolveState::Resolved => {
220 let mut extra_shift_left = 0;
221 let mut extra_shift_right = 0;
222 if cached_hint.padding_left {
223 extra_shift_left += 1;
224 extra_shift_right += 1;
225 }
226 if cached_hint.padding_right {
227 extra_shift_right += 1;
228 }
229 match cached_hint.label {
230 project::InlayHintLabel::String(_) => {
231 if let Some(tooltip) = cached_hint.tooltip {
232 hover_popover::hover_at_inlay(
233 editor,
234 InlayHover {
235 excerpt: excerpt_id,
236 tooltip: match tooltip {
237 InlayHintTooltip::String(text) => HoverBlock {
238 text,
239 kind: HoverBlockKind::PlainText,
240 },
241 InlayHintTooltip::MarkupContent(content) => {
242 HoverBlock {
243 text: content.value,
244 kind: content.kind,
245 }
246 }
247 },
248 range: InlayHighlight {
249 inlay: hovered_hint.id,
250 inlay_position: hovered_hint.position,
251 range: extra_shift_left
252 ..hovered_hint.text.len() + extra_shift_right,
253 },
254 },
255 cx,
256 );
257 hover_updated = true;
258 }
259 }
260 project::InlayHintLabel::LabelParts(label_parts) => {
261 let hint_start =
262 snapshot.anchor_to_inlay_offset(hovered_hint.position);
263 if let Some((hovered_hint_part, part_range)) =
264 hover_popover::find_hovered_hint_part(
265 label_parts,
266 hint_start,
267 hovered_offset,
268 )
269 {
270 let highlight_start =
271 (part_range.start - hint_start).0 + extra_shift_left;
272 let highlight_end =
273 (part_range.end - hint_start).0 + extra_shift_right;
274 let highlight = InlayHighlight {
275 inlay: hovered_hint.id,
276 inlay_position: hovered_hint.position,
277 range: highlight_start..highlight_end,
278 };
279 if let Some(tooltip) = hovered_hint_part.tooltip {
280 hover_popover::hover_at_inlay(
281 editor,
282 InlayHover {
283 excerpt: excerpt_id,
284 tooltip: match tooltip {
285 InlayHintLabelPartTooltip::String(text) => {
286 HoverBlock {
287 text,
288 kind: HoverBlockKind::PlainText,
289 }
290 }
291 InlayHintLabelPartTooltip::MarkupContent(
292 content,
293 ) => HoverBlock {
294 text: content.value,
295 kind: content.kind,
296 },
297 },
298 range: highlight.clone(),
299 },
300 cx,
301 );
302 hover_updated = true;
303 }
304 if let Some((language_server_id, location)) =
305 hovered_hint_part.location
306 {
307 go_to_definition_updated = true;
308 update_go_to_definition_link(
309 editor,
310 Some(GoToDefinitionTrigger::InlayHint(
311 highlight,
312 location,
313 language_server_id,
314 )),
315 cmd_held,
316 shift_held,
317 cx,
318 );
319 }
320 }
321 }
322 };
323 }
324 ResolveState::Resolving => {}
325 }
326 }
327 }
328 }
329
330 if !go_to_definition_updated {
331 update_go_to_definition_link(editor, None, cmd_held, shift_held, cx);
332 }
333 if !hover_updated {
334 hover_popover::hover_at(editor, None, cx);
335 }
336}
337
338#[derive(Debug, Clone, Copy, PartialEq)]
339pub enum LinkDefinitionKind {
340 Symbol,
341 Type,
342}
343
344pub fn show_link_definition(
345 definition_kind: LinkDefinitionKind,
346 editor: &mut Editor,
347 trigger_point: TriggerPoint,
348 snapshot: EditorSnapshot,
349 cx: &mut ViewContext<Editor>,
350) {
351 let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind);
352 if !same_kind {
353 hide_link_definition(editor, cx);
354 }
355
356 if editor.pending_rename.is_some() {
357 return;
358 }
359
360 let trigger_anchor = trigger_point.anchor();
361 let (buffer, buffer_position) = if let Some(output) = editor
362 .buffer
363 .read(cx)
364 .text_anchor_for_position(trigger_anchor.clone(), cx)
365 {
366 output
367 } else {
368 return;
369 };
370
371 let excerpt_id = if let Some((excerpt_id, _, _)) = editor
372 .buffer()
373 .read(cx)
374 .excerpt_containing(trigger_anchor.clone(), cx)
375 {
376 excerpt_id
377 } else {
378 return;
379 };
380
381 let project = if let Some(project) = editor.project.clone() {
382 project
383 } else {
384 return;
385 };
386
387 // Don't request again if the location is within the symbol region of a previous request with the same kind
388 if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
389 if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) {
390 return;
391 }
392 }
393
394 let task = cx.spawn(|this, mut cx| {
395 async move {
396 let result = match &trigger_point {
397 TriggerPoint::Text(_) => {
398 // query the LSP for definition info
399 cx.update(|cx| {
400 project.update(cx, |project, cx| match definition_kind {
401 LinkDefinitionKind::Symbol => {
402 project.definition(&buffer, buffer_position, cx)
403 }
404
405 LinkDefinitionKind::Type => {
406 project.type_definition(&buffer, buffer_position, cx)
407 }
408 })
409 })
410 .await
411 .ok()
412 .map(|definition_result| {
413 (
414 definition_result.iter().find_map(|link| {
415 link.origin.as_ref().map(|origin| {
416 let start = snapshot
417 .buffer_snapshot
418 .anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
419 let end = snapshot
420 .buffer_snapshot
421 .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
422 RangeInEditor::Text(start..end)
423 })
424 }),
425 definition_result
426 .into_iter()
427 .map(GoToDefinitionLink::Text)
428 .collect(),
429 )
430 })
431 }
432 TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
433 Some(RangeInEditor::Inlay(highlight.clone())),
434 vec![GoToDefinitionLink::InlayHint(
435 lsp_location.clone(),
436 *server_id,
437 )],
438 )),
439 };
440
441 this.update(&mut cx, |this, cx| {
442 // Clear any existing highlights
443 this.clear_highlights::<LinkGoToDefinitionState>(cx);
444 this.link_go_to_definition_state.kind = Some(definition_kind);
445 this.link_go_to_definition_state.symbol_range = result
446 .as_ref()
447 .and_then(|(symbol_range, _)| symbol_range.clone());
448
449 if let Some((symbol_range, definitions)) = result {
450 this.link_go_to_definition_state.definitions = definitions.clone();
451
452 let buffer_snapshot = buffer.read(cx).snapshot();
453
454 // Only show highlight if there exists a definition to jump to that doesn't contain
455 // the current location.
456 let any_definition_does_not_contain_current_location =
457 definitions.iter().any(|definition| {
458 match &definition {
459 GoToDefinitionLink::Text(link) => {
460 if link.target.buffer == buffer {
461 let range = &link.target.range;
462 // Expand range by one character as lsp definition ranges include positions adjacent
463 // but not contained by the symbol range
464 let start = buffer_snapshot.clip_offset(
465 range
466 .start
467 .to_offset(&buffer_snapshot)
468 .saturating_sub(1),
469 Bias::Left,
470 );
471 let end = buffer_snapshot.clip_offset(
472 range.end.to_offset(&buffer_snapshot) + 1,
473 Bias::Right,
474 );
475 let offset = buffer_position.to_offset(&buffer_snapshot);
476 !(start <= offset && end >= offset)
477 } else {
478 true
479 }
480 }
481 GoToDefinitionLink::InlayHint(_, _) => true,
482 }
483 });
484
485 if any_definition_does_not_contain_current_location {
486 // Highlight symbol using theme link definition highlight style
487 let style = theme::current(cx).editor.link_definition;
488 let highlight_range =
489 symbol_range.unwrap_or_else(|| match &trigger_point {
490 TriggerPoint::Text(trigger_anchor) => {
491 let snapshot = &snapshot.buffer_snapshot;
492 // If no symbol range returned from language server, use the surrounding word.
493 let (offset_range, _) =
494 snapshot.surrounding_word(*trigger_anchor);
495 RangeInEditor::Text(
496 snapshot.anchor_before(offset_range.start)
497 ..snapshot.anchor_after(offset_range.end),
498 )
499 }
500 TriggerPoint::InlayHint(highlight, _, _) => {
501 RangeInEditor::Inlay(highlight.clone())
502 }
503 });
504
505 match highlight_range {
506 RangeInEditor::Text(text_range) => this
507 .highlight_text::<LinkGoToDefinitionState>(
508 vec![text_range],
509 style,
510 cx,
511 ),
512 RangeInEditor::Inlay(highlight) => this
513 .highlight_inlays::<LinkGoToDefinitionState>(
514 vec![highlight],
515 style,
516 cx,
517 ),
518 }
519 } else {
520 hide_link_definition(this, cx);
521 }
522 }
523 })?;
524
525 Ok::<_, anyhow::Error>(())
526 }
527 .log_err()
528 });
529
530 editor.link_go_to_definition_state.task = Some(task);
531}
532
533pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
534 if editor.link_go_to_definition_state.symbol_range.is_some()
535 || !editor.link_go_to_definition_state.definitions.is_empty()
536 {
537 editor.link_go_to_definition_state.symbol_range.take();
538 editor.link_go_to_definition_state.definitions.clear();
539 cx.notify();
540 }
541
542 editor.link_go_to_definition_state.task = None;
543
544 editor.clear_highlights::<LinkGoToDefinitionState>(cx);
545}
546
547pub fn go_to_fetched_definition(
548 editor: &mut Editor,
549 point: PointForPosition,
550 split: bool,
551 cx: &mut ViewContext<Editor>,
552) {
553 go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx);
554}
555
556pub fn go_to_fetched_type_definition(
557 editor: &mut Editor,
558 point: PointForPosition,
559 split: bool,
560 cx: &mut ViewContext<Editor>,
561) {
562 go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx);
563}
564
565fn go_to_fetched_definition_of_kind(
566 kind: LinkDefinitionKind,
567 editor: &mut Editor,
568 point: PointForPosition,
569 split: bool,
570 cx: &mut ViewContext<Editor>,
571) {
572 let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
573 hide_link_definition(editor, cx);
574 let cached_definitions_kind = editor.link_go_to_definition_state.kind;
575
576 let is_correct_kind = cached_definitions_kind == Some(kind);
577 if !cached_definitions.is_empty() && is_correct_kind {
578 if !editor.focused {
579 cx.focus_self();
580 }
581
582 editor.navigate_to_definitions(cached_definitions, split, cx);
583 } else {
584 editor.select(
585 SelectPhase::Begin {
586 position: point.next_valid,
587 add: false,
588 click_count: 1,
589 },
590 cx,
591 );
592
593 if point.as_valid().is_some() {
594 match kind {
595 LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
596 LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
597 }
598 }
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use crate::{
606 display_map::ToDisplayPoint,
607 editor_tests::init_test,
608 inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
609 test::editor_lsp_test_context::EditorLspTestContext,
610 };
611 use futures::StreamExt;
612 use gpui::{
613 platform::{self, Modifiers, ModifiersChangedEvent},
614 View,
615 };
616 use indoc::indoc;
617 use language::language_settings::InlayHintSettings;
618 use lsp::request::{GotoDefinition, GotoTypeDefinition};
619 use util::assert_set_eq;
620
621 #[gpui::test]
622 async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
623 init_test(cx, |_| {});
624
625 let mut cx = EditorLspTestContext::new_rust(
626 lsp::ServerCapabilities {
627 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
628 type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
629 ..Default::default()
630 },
631 cx,
632 )
633 .await;
634
635 cx.set_state(indoc! {"
636 struct A;
637 let vˇariable = A;
638 "});
639
640 // Basic hold cmd+shift, expect highlight in region if response contains type definition
641 let hover_point = cx.display_point(indoc! {"
642 struct A;
643 let vˇariable = A;
644 "});
645 let symbol_range = cx.lsp_range(indoc! {"
646 struct A;
647 let «variable» = A;
648 "});
649 let target_range = cx.lsp_range(indoc! {"
650 struct «A»;
651 let variable = A;
652 "});
653
654 let mut requests =
655 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
656 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
657 lsp::LocationLink {
658 origin_selection_range: Some(symbol_range),
659 target_uri: url.clone(),
660 target_range,
661 target_selection_range: target_range,
662 },
663 ])))
664 });
665
666 // Press cmd+shift to trigger highlight
667 cx.update_editor(|editor, cx| {
668 update_go_to_definition_link(
669 editor,
670 Some(GoToDefinitionTrigger::Text(hover_point)),
671 true,
672 true,
673 cx,
674 );
675 });
676 requests.next().await;
677 cx.foreground().run_until_parked();
678 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
679 struct A;
680 let «variable» = A;
681 "});
682
683 // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
684 cx.update_editor(|editor, cx| {
685 editor.modifiers_changed(
686 &platform::ModifiersChangedEvent {
687 modifiers: Modifiers {
688 cmd: true,
689 ..Default::default()
690 },
691 ..Default::default()
692 },
693 cx,
694 );
695 });
696 // Assert no link highlights
697 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
698 struct A;
699 let variable = A;
700 "});
701
702 // Cmd+shift click without existing definition requests and jumps
703 let hover_point = cx.display_point(indoc! {"
704 struct A;
705 let vˇariable = A;
706 "});
707 let target_range = cx.lsp_range(indoc! {"
708 struct «A»;
709 let variable = A;
710 "});
711
712 let mut requests =
713 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
714 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
715 lsp::LocationLink {
716 origin_selection_range: None,
717 target_uri: url,
718 target_range,
719 target_selection_range: target_range,
720 },
721 ])))
722 });
723
724 cx.update_editor(|editor, cx| {
725 go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
726 });
727 requests.next().await;
728 cx.foreground().run_until_parked();
729
730 cx.assert_editor_state(indoc! {"
731 struct «Aˇ»;
732 let variable = A;
733 "});
734 }
735
736 #[gpui::test]
737 async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
738 init_test(cx, |_| {});
739
740 let mut cx = EditorLspTestContext::new_rust(
741 lsp::ServerCapabilities {
742 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
743 ..Default::default()
744 },
745 cx,
746 )
747 .await;
748
749 cx.set_state(indoc! {"
750 fn ˇtest() { do_work(); }
751 fn do_work() { test(); }
752 "});
753
754 // Basic hold cmd, expect highlight in region if response contains definition
755 let hover_point = cx.display_point(indoc! {"
756 fn test() { do_wˇork(); }
757 fn do_work() { test(); }
758 "});
759 let symbol_range = cx.lsp_range(indoc! {"
760 fn test() { «do_work»(); }
761 fn do_work() { test(); }
762 "});
763 let target_range = cx.lsp_range(indoc! {"
764 fn test() { do_work(); }
765 fn «do_work»() { test(); }
766 "});
767
768 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
769 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
770 lsp::LocationLink {
771 origin_selection_range: Some(symbol_range),
772 target_uri: url.clone(),
773 target_range,
774 target_selection_range: target_range,
775 },
776 ])))
777 });
778
779 cx.update_editor(|editor, cx| {
780 update_go_to_definition_link(
781 editor,
782 Some(GoToDefinitionTrigger::Text(hover_point)),
783 true,
784 false,
785 cx,
786 );
787 });
788 requests.next().await;
789 cx.foreground().run_until_parked();
790 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
791 fn test() { «do_work»(); }
792 fn do_work() { test(); }
793 "});
794
795 // Unpress cmd causes highlight to go away
796 cx.update_editor(|editor, cx| {
797 editor.modifiers_changed(&Default::default(), cx);
798 });
799
800 // Assert no link highlights
801 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
802 fn test() { do_work(); }
803 fn do_work() { test(); }
804 "});
805
806 // Response without source range still highlights word
807 cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
808 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
809 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
810 lsp::LocationLink {
811 // No origin range
812 origin_selection_range: None,
813 target_uri: url.clone(),
814 target_range,
815 target_selection_range: target_range,
816 },
817 ])))
818 });
819 cx.update_editor(|editor, cx| {
820 update_go_to_definition_link(
821 editor,
822 Some(GoToDefinitionTrigger::Text(hover_point)),
823 true,
824 false,
825 cx,
826 );
827 });
828 requests.next().await;
829 cx.foreground().run_until_parked();
830
831 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
832 fn test() { «do_work»(); }
833 fn do_work() { test(); }
834 "});
835
836 // Moving mouse to location with no response dismisses highlight
837 let hover_point = cx.display_point(indoc! {"
838 fˇn test() { do_work(); }
839 fn do_work() { test(); }
840 "});
841 let mut requests = cx
842 .lsp
843 .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
844 // No definitions returned
845 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
846 });
847 cx.update_editor(|editor, cx| {
848 update_go_to_definition_link(
849 editor,
850 Some(GoToDefinitionTrigger::Text(hover_point)),
851 true,
852 false,
853 cx,
854 );
855 });
856 requests.next().await;
857 cx.foreground().run_until_parked();
858
859 // Assert no link highlights
860 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
861 fn test() { do_work(); }
862 fn do_work() { test(); }
863 "});
864
865 // Move mouse without cmd and then pressing cmd triggers highlight
866 let hover_point = cx.display_point(indoc! {"
867 fn test() { do_work(); }
868 fn do_work() { teˇst(); }
869 "});
870 cx.update_editor(|editor, cx| {
871 update_go_to_definition_link(
872 editor,
873 Some(GoToDefinitionTrigger::Text(hover_point)),
874 false,
875 false,
876 cx,
877 );
878 });
879 cx.foreground().run_until_parked();
880
881 // Assert no link highlights
882 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
883 fn test() { do_work(); }
884 fn do_work() { test(); }
885 "});
886
887 let symbol_range = cx.lsp_range(indoc! {"
888 fn test() { do_work(); }
889 fn do_work() { «test»(); }
890 "});
891 let target_range = cx.lsp_range(indoc! {"
892 fn «test»() { do_work(); }
893 fn do_work() { test(); }
894 "});
895
896 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
897 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
898 lsp::LocationLink {
899 origin_selection_range: Some(symbol_range),
900 target_uri: url,
901 target_range,
902 target_selection_range: target_range,
903 },
904 ])))
905 });
906 cx.update_editor(|editor, cx| {
907 editor.modifiers_changed(
908 &ModifiersChangedEvent {
909 modifiers: Modifiers {
910 cmd: true,
911 ..Default::default()
912 },
913 },
914 cx,
915 );
916 });
917 requests.next().await;
918 cx.foreground().run_until_parked();
919
920 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
921 fn test() { do_work(); }
922 fn do_work() { «test»(); }
923 "});
924
925 // Deactivating the window dismisses the highlight
926 cx.update_workspace(|workspace, cx| {
927 workspace.on_window_activation_changed(false, cx);
928 });
929 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
930 fn test() { do_work(); }
931 fn do_work() { test(); }
932 "});
933
934 // Moving the mouse restores the highlights.
935 cx.update_editor(|editor, cx| {
936 update_go_to_definition_link(
937 editor,
938 Some(GoToDefinitionTrigger::Text(hover_point)),
939 true,
940 false,
941 cx,
942 );
943 });
944 cx.foreground().run_until_parked();
945 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
946 fn test() { do_work(); }
947 fn do_work() { «test»(); }
948 "});
949
950 // Moving again within the same symbol range doesn't re-request
951 let hover_point = cx.display_point(indoc! {"
952 fn test() { do_work(); }
953 fn do_work() { tesˇt(); }
954 "});
955 cx.update_editor(|editor, cx| {
956 update_go_to_definition_link(
957 editor,
958 Some(GoToDefinitionTrigger::Text(hover_point)),
959 true,
960 false,
961 cx,
962 );
963 });
964 cx.foreground().run_until_parked();
965 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
966 fn test() { do_work(); }
967 fn do_work() { «test»(); }
968 "});
969
970 // Cmd click with existing definition doesn't re-request and dismisses highlight
971 cx.update_editor(|editor, cx| {
972 go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
973 });
974 // Assert selection moved to to definition
975 cx.lsp
976 .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
977 // Empty definition response to make sure we aren't hitting the lsp and using
978 // the cached location instead
979 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
980 });
981 cx.foreground().run_until_parked();
982 cx.assert_editor_state(indoc! {"
983 fn «testˇ»() { do_work(); }
984 fn do_work() { test(); }
985 "});
986
987 // Assert no link highlights after jump
988 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
989 fn test() { do_work(); }
990 fn do_work() { test(); }
991 "});
992
993 // Cmd click without existing definition requests and jumps
994 let hover_point = cx.display_point(indoc! {"
995 fn test() { do_wˇork(); }
996 fn do_work() { test(); }
997 "});
998 let target_range = cx.lsp_range(indoc! {"
999 fn test() { do_work(); }
1000 fn «do_work»() { test(); }
1001 "});
1002
1003 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
1004 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
1005 lsp::LocationLink {
1006 origin_selection_range: None,
1007 target_uri: url,
1008 target_range,
1009 target_selection_range: target_range,
1010 },
1011 ])))
1012 });
1013 cx.update_editor(|editor, cx| {
1014 go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
1015 });
1016 requests.next().await;
1017 cx.foreground().run_until_parked();
1018 cx.assert_editor_state(indoc! {"
1019 fn test() { do_work(); }
1020 fn «do_workˇ»() { test(); }
1021 "});
1022
1023 // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
1024 // 2. Selection is completed, hovering
1025 let hover_point = cx.display_point(indoc! {"
1026 fn test() { do_wˇork(); }
1027 fn do_work() { test(); }
1028 "});
1029 let target_range = cx.lsp_range(indoc! {"
1030 fn test() { do_work(); }
1031 fn «do_work»() { test(); }
1032 "});
1033 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
1034 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
1035 lsp::LocationLink {
1036 origin_selection_range: None,
1037 target_uri: url,
1038 target_range,
1039 target_selection_range: target_range,
1040 },
1041 ])))
1042 });
1043
1044 // create a pending selection
1045 let selection_range = cx.ranges(indoc! {"
1046 fn «test() { do_w»ork(); }
1047 fn do_work() { test(); }
1048 "})[0]
1049 .clone();
1050 cx.update_editor(|editor, cx| {
1051 let snapshot = editor.buffer().read(cx).snapshot(cx);
1052 let anchor_range = snapshot.anchor_before(selection_range.start)
1053 ..snapshot.anchor_after(selection_range.end);
1054 editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
1055 s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
1056 });
1057 });
1058 cx.update_editor(|editor, cx| {
1059 update_go_to_definition_link(
1060 editor,
1061 Some(GoToDefinitionTrigger::Text(hover_point)),
1062 true,
1063 false,
1064 cx,
1065 );
1066 });
1067 cx.foreground().run_until_parked();
1068 assert!(requests.try_next().is_err());
1069 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
1070 fn test() { do_work(); }
1071 fn do_work() { test(); }
1072 "});
1073 cx.foreground().run_until_parked();
1074 }
1075
1076 #[gpui::test]
1077 async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
1078 init_test(cx, |settings| {
1079 settings.defaults.inlay_hints = Some(InlayHintSettings {
1080 enabled: true,
1081 show_type_hints: true,
1082 show_parameter_hints: true,
1083 show_other_hints: true,
1084 })
1085 });
1086
1087 let mut cx = EditorLspTestContext::new_rust(
1088 lsp::ServerCapabilities {
1089 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1090 ..Default::default()
1091 },
1092 cx,
1093 )
1094 .await;
1095 cx.set_state(indoc! {"
1096 struct TestStruct;
1097
1098 fn main() {
1099 let variableˇ = TestStruct;
1100 }
1101 "});
1102 let hint_start_offset = cx.ranges(indoc! {"
1103 struct TestStruct;
1104
1105 fn main() {
1106 let variableˇ = TestStruct;
1107 }
1108 "})[0]
1109 .start;
1110 let hint_position = cx.to_lsp(hint_start_offset);
1111 let target_range = cx.lsp_range(indoc! {"
1112 struct «TestStruct»;
1113
1114 fn main() {
1115 let variable = TestStruct;
1116 }
1117 "});
1118
1119 let expected_uri = cx.buffer_lsp_url.clone();
1120 let hint_label = ": TestStruct";
1121 cx.lsp
1122 .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1123 let expected_uri = expected_uri.clone();
1124 async move {
1125 assert_eq!(params.text_document.uri, expected_uri);
1126 Ok(Some(vec![lsp::InlayHint {
1127 position: hint_position,
1128 label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1129 value: hint_label.to_string(),
1130 location: Some(lsp::Location {
1131 uri: params.text_document.uri,
1132 range: target_range,
1133 }),
1134 ..Default::default()
1135 }]),
1136 kind: Some(lsp::InlayHintKind::TYPE),
1137 text_edits: None,
1138 tooltip: None,
1139 padding_left: Some(false),
1140 padding_right: Some(false),
1141 data: None,
1142 }]))
1143 }
1144 })
1145 .next()
1146 .await;
1147 cx.foreground().run_until_parked();
1148 cx.update_editor(|editor, cx| {
1149 let expected_layers = vec![hint_label.to_string()];
1150 assert_eq!(expected_layers, cached_hint_labels(editor));
1151 assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1152 });
1153
1154 let inlay_range = cx
1155 .ranges(indoc! {"
1156 struct TestStruct;
1157
1158 fn main() {
1159 let variable« »= TestStruct;
1160 }
1161 "})
1162 .get(0)
1163 .cloned()
1164 .unwrap();
1165 let hint_hover_position = cx.update_editor(|editor, cx| {
1166 let snapshot = editor.snapshot(cx);
1167 let previous_valid = inlay_range.start.to_display_point(&snapshot);
1168 let next_valid = inlay_range.end.to_display_point(&snapshot);
1169 assert_eq!(previous_valid.row(), next_valid.row());
1170 assert!(previous_valid.column() < next_valid.column());
1171 let exact_unclipped = DisplayPoint::new(
1172 previous_valid.row(),
1173 previous_valid.column() + (hint_label.len() / 2) as u32,
1174 );
1175 PointForPosition {
1176 previous_valid,
1177 next_valid,
1178 exact_unclipped,
1179 column_overshoot_after_line_end: 0,
1180 }
1181 });
1182 // Press cmd to trigger highlight
1183 cx.update_editor(|editor, cx| {
1184 update_inlay_link_and_hover_points(
1185 &editor.snapshot(cx),
1186 hint_hover_position,
1187 editor,
1188 true,
1189 false,
1190 cx,
1191 );
1192 });
1193 cx.foreground().run_until_parked();
1194 cx.update_editor(|editor, cx| {
1195 let snapshot = editor.snapshot(cx);
1196 let actual_highlights = snapshot
1197 .inlay_highlights::<LinkGoToDefinitionState>()
1198 .into_iter()
1199 .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
1200 .collect::<Vec<_>>();
1201
1202 let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1203 let expected_highlight = InlayHighlight {
1204 inlay: InlayId::Hint(0),
1205 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1206 range: 0..hint_label.len(),
1207 };
1208 assert_set_eq!(actual_highlights, vec![&expected_highlight]);
1209 });
1210
1211 // Unpress cmd causes highlight to go away
1212 cx.update_editor(|editor, cx| {
1213 editor.modifiers_changed(
1214 &platform::ModifiersChangedEvent {
1215 modifiers: Modifiers {
1216 cmd: false,
1217 ..Default::default()
1218 },
1219 ..Default::default()
1220 },
1221 cx,
1222 );
1223 });
1224 // Assert no link highlights
1225 cx.update_editor(|editor, cx| {
1226 let snapshot = editor.snapshot(cx);
1227 let actual_ranges = snapshot
1228 .text_highlight_ranges::<LinkGoToDefinitionState>()
1229 .map(|ranges| ranges.as_ref().clone().1)
1230 .unwrap_or_default();
1231
1232 assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
1233 });
1234
1235 // Cmd+click without existing definition requests and jumps
1236 cx.update_editor(|editor, cx| {
1237 editor.modifiers_changed(
1238 &platform::ModifiersChangedEvent {
1239 modifiers: Modifiers {
1240 cmd: true,
1241 ..Default::default()
1242 },
1243 ..Default::default()
1244 },
1245 cx,
1246 );
1247 update_inlay_link_and_hover_points(
1248 &editor.snapshot(cx),
1249 hint_hover_position,
1250 editor,
1251 true,
1252 false,
1253 cx,
1254 );
1255 });
1256 cx.foreground().run_until_parked();
1257 cx.update_editor(|editor, cx| {
1258 go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
1259 });
1260 cx.foreground().run_until_parked();
1261 cx.assert_editor_state(indoc! {"
1262 struct «TestStructˇ»;
1263
1264 fn main() {
1265 let variable = TestStruct;
1266 }
1267 "});
1268 }
1269}