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