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