1use std::ops::Range;
2
3use gpui::{impl_internal_actions, MutableAppContext, 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 MutableAppContext) {
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_weak(|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 if let Some(this) = this.upgrade(&cx) {
206 this.update(&mut cx, |this, cx| {
207 // Clear any existing highlights
208 this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
209 this.link_go_to_definition_state.kind = Some(definition_kind);
210 this.link_go_to_definition_state.symbol_range = result
211 .as_ref()
212 .and_then(|(symbol_range, _)| symbol_range.clone());
213
214 if let Some((symbol_range, definitions)) = result {
215 this.link_go_to_definition_state.definitions = definitions.clone();
216
217 let buffer_snapshot = buffer.read(cx).snapshot();
218
219 // Only show highlight if there exists a definition to jump to that doesn't contain
220 // the current location.
221 let any_definition_does_not_contain_current_location =
222 definitions.iter().any(|definition| {
223 let target = &definition.target;
224 if target.buffer == buffer {
225 let range = &target.range;
226 // Expand range by one character as lsp definition ranges include positions adjacent
227 // but not contained by the symbol range
228 let start = buffer_snapshot.clip_offset(
229 range.start.to_offset(&buffer_snapshot).saturating_sub(1),
230 Bias::Left,
231 );
232 let end = buffer_snapshot.clip_offset(
233 range.end.to_offset(&buffer_snapshot) + 1,
234 Bias::Right,
235 );
236 let offset = buffer_position.to_offset(&buffer_snapshot);
237 !(start <= offset && end >= offset)
238 } else {
239 true
240 }
241 });
242
243 if any_definition_does_not_contain_current_location {
244 // If no symbol range returned from language server, use the surrounding word.
245 let highlight_range = symbol_range.unwrap_or_else(|| {
246 let snapshot = &snapshot.buffer_snapshot;
247 let (offset_range, _) = snapshot.surrounding_word(trigger_point);
248
249 snapshot.anchor_before(offset_range.start)
250 ..snapshot.anchor_after(offset_range.end)
251 });
252
253 // Highlight symbol using theme link definition highlight style
254 let style = cx.global::<Settings>().theme.editor.link_definition;
255 this.highlight_text::<LinkGoToDefinitionState>(
256 vec![highlight_range],
257 style,
258 cx,
259 );
260 } else {
261 hide_link_definition(this, cx);
262 }
263 }
264 })
265 }
266
267 Ok::<_, anyhow::Error>(())
268 }
269 .log_err()
270 });
271
272 editor.link_go_to_definition_state.task = Some(task);
273}
274
275pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
276 if editor.link_go_to_definition_state.symbol_range.is_some()
277 || !editor.link_go_to_definition_state.definitions.is_empty()
278 {
279 editor.link_go_to_definition_state.symbol_range.take();
280 editor.link_go_to_definition_state.definitions.clear();
281 cx.notify();
282 }
283
284 editor.link_go_to_definition_state.task = None;
285
286 editor.clear_text_highlights::<LinkGoToDefinitionState>(cx);
287}
288
289pub fn go_to_fetched_definition(
290 workspace: &mut Workspace,
291 &GoToFetchedDefinition { point }: &GoToFetchedDefinition,
292 cx: &mut ViewContext<Workspace>,
293) {
294 go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, workspace, point, cx);
295}
296
297pub fn go_to_fetched_type_definition(
298 workspace: &mut Workspace,
299 &GoToFetchedTypeDefinition { point }: &GoToFetchedTypeDefinition,
300 cx: &mut ViewContext<Workspace>,
301) {
302 go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, workspace, point, cx);
303}
304
305fn go_to_fetched_definition_of_kind(
306 kind: LinkDefinitionKind,
307 workspace: &mut Workspace,
308 point: DisplayPoint,
309 cx: &mut ViewContext<Workspace>,
310) {
311 let active_item = workspace.active_item(cx);
312 let editor_handle = if let Some(editor) = active_item
313 .as_ref()
314 .and_then(|item| item.act_as::<Editor>(cx))
315 {
316 editor
317 } else {
318 return;
319 };
320
321 let (cached_definitions, cached_definitions_kind) = editor_handle.update(cx, |editor, cx| {
322 let definitions = editor.link_go_to_definition_state.definitions.clone();
323 hide_link_definition(editor, cx);
324 (definitions, editor.link_go_to_definition_state.kind)
325 });
326
327 let is_correct_kind = cached_definitions_kind == Some(kind);
328 if !cached_definitions.is_empty() && is_correct_kind {
329 editor_handle.update(cx, |editor, cx| {
330 if !editor.focused {
331 cx.focus_self();
332 }
333 });
334
335 Editor::navigate_to_definitions(workspace, editor_handle, cached_definitions, cx);
336 } else {
337 editor_handle.update(cx, |editor, cx| {
338 editor.select(
339 &Select(SelectPhase::Begin {
340 position: point,
341 add: false,
342 click_count: 1,
343 }),
344 cx,
345 );
346 });
347
348 match kind {
349 LinkDefinitionKind::Symbol => Editor::go_to_definition(workspace, &GoToDefinition, cx),
350
351 LinkDefinitionKind::Type => {
352 Editor::go_to_type_definition(workspace, &GoToTypeDefinition, cx)
353 }
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use futures::StreamExt;
361 use gpui::{Modifiers, ModifiersChangedEvent, View};
362 use indoc::indoc;
363 use lsp::request::{GotoDefinition, GotoTypeDefinition};
364
365 use crate::test::editor_lsp_test_context::EditorLspTestContext;
366
367 use super::*;
368
369 #[gpui::test]
370 async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
371 let mut cx = EditorLspTestContext::new_rust(
372 lsp::ServerCapabilities {
373 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
374 ..Default::default()
375 },
376 cx,
377 )
378 .await;
379
380 cx.set_state(indoc! {"
381 struct A;
382 let vˇariable = A;
383 "});
384
385 // Basic hold cmd+shift, expect highlight in region if response contains type definition
386 let hover_point = cx.display_point(indoc! {"
387 struct A;
388 let vˇariable = A;
389 "});
390 let symbol_range = cx.lsp_range(indoc! {"
391 struct A;
392 let «variable» = A;
393 "});
394 let target_range = cx.lsp_range(indoc! {"
395 struct «A»;
396 let variable = A;
397 "});
398
399 let mut requests =
400 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
401 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
402 lsp::LocationLink {
403 origin_selection_range: Some(symbol_range),
404 target_uri: url.clone(),
405 target_range,
406 target_selection_range: target_range,
407 },
408 ])))
409 });
410
411 // Press cmd+shift to trigger highlight
412 cx.update_editor(|editor, cx| {
413 update_go_to_definition_link(
414 editor,
415 &UpdateGoToDefinitionLink {
416 point: Some(hover_point),
417 cmd_held: true,
418 shift_held: true,
419 },
420 cx,
421 );
422 });
423 requests.next().await;
424 cx.foreground().run_until_parked();
425 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
426 struct A;
427 let «variable» = A;
428 "});
429
430 // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
431 cx.update_editor(|editor, cx| {
432 editor.modifiers_changed(
433 &gpui::ModifiersChangedEvent {
434 modifiers: Modifiers {
435 cmd: true,
436 ..Default::default()
437 },
438 ..Default::default()
439 },
440 cx,
441 );
442 });
443 // Assert no link highlights
444 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
445 struct A;
446 let variable = A;
447 "});
448
449 // Cmd+shift click without existing definition requests and jumps
450 let hover_point = cx.display_point(indoc! {"
451 struct A;
452 let vˇariable = A;
453 "});
454 let target_range = cx.lsp_range(indoc! {"
455 struct «A»;
456 let variable = A;
457 "});
458
459 let mut requests =
460 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
461 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
462 lsp::LocationLink {
463 origin_selection_range: None,
464 target_uri: url,
465 target_range,
466 target_selection_range: target_range,
467 },
468 ])))
469 });
470
471 cx.update_workspace(|workspace, cx| {
472 go_to_fetched_type_definition(
473 workspace,
474 &GoToFetchedTypeDefinition { point: hover_point },
475 cx,
476 );
477 });
478 requests.next().await;
479 cx.foreground().run_until_parked();
480
481 cx.assert_editor_state(indoc! {"
482 struct «Aˇ»;
483 let variable = A;
484 "});
485 }
486
487 #[gpui::test]
488 async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
489 let mut cx = EditorLspTestContext::new_rust(
490 lsp::ServerCapabilities {
491 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
492 ..Default::default()
493 },
494 cx,
495 )
496 .await;
497
498 cx.set_state(indoc! {"
499 fn ˇtest() { do_work(); }
500 fn do_work() { test(); }
501 "});
502
503 // Basic hold cmd, expect highlight in region if response contains definition
504 let hover_point = cx.display_point(indoc! {"
505 fn test() { do_wˇork(); }
506 fn do_work() { test(); }
507 "});
508 let symbol_range = cx.lsp_range(indoc! {"
509 fn test() { «do_work»(); }
510 fn do_work() { test(); }
511 "});
512 let target_range = cx.lsp_range(indoc! {"
513 fn test() { do_work(); }
514 fn «do_work»() { test(); }
515 "});
516
517 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
518 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
519 lsp::LocationLink {
520 origin_selection_range: Some(symbol_range),
521 target_uri: url.clone(),
522 target_range,
523 target_selection_range: target_range,
524 },
525 ])))
526 });
527
528 cx.update_editor(|editor, cx| {
529 update_go_to_definition_link(
530 editor,
531 &UpdateGoToDefinitionLink {
532 point: Some(hover_point),
533 cmd_held: true,
534 shift_held: false,
535 },
536 cx,
537 );
538 });
539 requests.next().await;
540 cx.foreground().run_until_parked();
541 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
542 fn test() { «do_work»(); }
543 fn do_work() { test(); }
544 "});
545
546 // Unpress cmd causes highlight to go away
547 cx.update_editor(|editor, cx| {
548 editor.modifiers_changed(&Default::default(), cx);
549 });
550
551 // Assert no link highlights
552 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
553 fn test() { do_work(); }
554 fn do_work() { test(); }
555 "});
556
557 // Response without source range still highlights word
558 cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
559 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
560 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
561 lsp::LocationLink {
562 // No origin range
563 origin_selection_range: None,
564 target_uri: url.clone(),
565 target_range,
566 target_selection_range: target_range,
567 },
568 ])))
569 });
570 cx.update_editor(|editor, cx| {
571 update_go_to_definition_link(
572 editor,
573 &UpdateGoToDefinitionLink {
574 point: Some(hover_point),
575 cmd_held: true,
576 shift_held: false,
577 },
578 cx,
579 );
580 });
581 requests.next().await;
582 cx.foreground().run_until_parked();
583
584 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
585 fn test() { «do_work»(); }
586 fn do_work() { test(); }
587 "});
588
589 // Moving mouse to location with no response dismisses highlight
590 let hover_point = cx.display_point(indoc! {"
591 fˇn test() { do_work(); }
592 fn do_work() { test(); }
593 "});
594 let mut requests = cx
595 .lsp
596 .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
597 // No definitions returned
598 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
599 });
600 cx.update_editor(|editor, cx| {
601 update_go_to_definition_link(
602 editor,
603 &UpdateGoToDefinitionLink {
604 point: Some(hover_point),
605 cmd_held: true,
606 shift_held: false,
607 },
608 cx,
609 );
610 });
611 requests.next().await;
612 cx.foreground().run_until_parked();
613
614 // Assert no link highlights
615 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
616 fn test() { do_work(); }
617 fn do_work() { test(); }
618 "});
619
620 // Move mouse without cmd and then pressing cmd triggers highlight
621 let hover_point = cx.display_point(indoc! {"
622 fn test() { do_work(); }
623 fn do_work() { teˇst(); }
624 "});
625 cx.update_editor(|editor, cx| {
626 update_go_to_definition_link(
627 editor,
628 &UpdateGoToDefinitionLink {
629 point: Some(hover_point),
630 cmd_held: false,
631 shift_held: false,
632 },
633 cx,
634 );
635 });
636 cx.foreground().run_until_parked();
637
638 // Assert no link highlights
639 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
640 fn test() { do_work(); }
641 fn do_work() { test(); }
642 "});
643
644 let symbol_range = cx.lsp_range(indoc! {"
645 fn test() { do_work(); }
646 fn do_work() { «test»(); }
647 "});
648 let target_range = cx.lsp_range(indoc! {"
649 fn «test»() { do_work(); }
650 fn do_work() { test(); }
651 "});
652
653 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
654 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
655 lsp::LocationLink {
656 origin_selection_range: Some(symbol_range),
657 target_uri: url,
658 target_range,
659 target_selection_range: target_range,
660 },
661 ])))
662 });
663 cx.update_editor(|editor, cx| {
664 editor.modifiers_changed(
665 &ModifiersChangedEvent {
666 modifiers: Modifiers {
667 cmd: true,
668 ..Default::default()
669 },
670 },
671 cx,
672 );
673 });
674 requests.next().await;
675 cx.foreground().run_until_parked();
676
677 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
678 fn test() { do_work(); }
679 fn do_work() { «test»(); }
680 "});
681
682 // Deactivating the window dismisses the highlight
683 cx.update_workspace(|workspace, cx| {
684 workspace.on_window_activation_changed(false, cx);
685 });
686 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
687 fn test() { do_work(); }
688 fn do_work() { test(); }
689 "});
690
691 // Moving the mouse restores the highlights.
692 cx.update_editor(|editor, cx| {
693 update_go_to_definition_link(
694 editor,
695 &UpdateGoToDefinitionLink {
696 point: Some(hover_point),
697 cmd_held: true,
698 shift_held: false,
699 },
700 cx,
701 );
702 });
703 cx.foreground().run_until_parked();
704 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
705 fn test() { do_work(); }
706 fn do_work() { «test»(); }
707 "});
708
709 // Moving again within the same symbol range doesn't re-request
710 let hover_point = cx.display_point(indoc! {"
711 fn test() { do_work(); }
712 fn do_work() { tesˇt(); }
713 "});
714 cx.update_editor(|editor, cx| {
715 update_go_to_definition_link(
716 editor,
717 &UpdateGoToDefinitionLink {
718 point: Some(hover_point),
719 cmd_held: true,
720 shift_held: false,
721 },
722 cx,
723 );
724 });
725 cx.foreground().run_until_parked();
726 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
727 fn test() { do_work(); }
728 fn do_work() { «test»(); }
729 "});
730
731 // Cmd click with existing definition doesn't re-request and dismisses highlight
732 cx.update_workspace(|workspace, cx| {
733 go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
734 });
735 // Assert selection moved to to definition
736 cx.lsp
737 .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
738 // Empty definition response to make sure we aren't hitting the lsp and using
739 // the cached location instead
740 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
741 });
742 cx.assert_editor_state(indoc! {"
743 fn «testˇ»() { do_work(); }
744 fn do_work() { test(); }
745 "});
746
747 // Assert no link highlights after jump
748 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
749 fn test() { do_work(); }
750 fn do_work() { test(); }
751 "});
752
753 // Cmd click without existing definition requests and jumps
754 let hover_point = cx.display_point(indoc! {"
755 fn test() { do_wˇork(); }
756 fn do_work() { test(); }
757 "});
758 let target_range = cx.lsp_range(indoc! {"
759 fn test() { do_work(); }
760 fn «do_work»() { test(); }
761 "});
762
763 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
764 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
765 lsp::LocationLink {
766 origin_selection_range: None,
767 target_uri: url,
768 target_range,
769 target_selection_range: target_range,
770 },
771 ])))
772 });
773 cx.update_workspace(|workspace, cx| {
774 go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
775 });
776 requests.next().await;
777 cx.foreground().run_until_parked();
778 cx.assert_editor_state(indoc! {"
779 fn test() { do_work(); }
780 fn «do_workˇ»() { test(); }
781 "});
782
783 // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
784 // 2. Selection is completed, hovering
785 let hover_point = cx.display_point(indoc! {"
786 fn test() { do_wˇork(); }
787 fn do_work() { test(); }
788 "});
789 let target_range = cx.lsp_range(indoc! {"
790 fn test() { do_work(); }
791 fn «do_work»() { test(); }
792 "});
793 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
794 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
795 lsp::LocationLink {
796 origin_selection_range: None,
797 target_uri: url,
798 target_range,
799 target_selection_range: target_range,
800 },
801 ])))
802 });
803
804 // create a pending selection
805 let selection_range = cx.ranges(indoc! {"
806 fn «test() { do_w»ork(); }
807 fn do_work() { test(); }
808 "})[0]
809 .clone();
810 cx.update_editor(|editor, cx| {
811 let snapshot = editor.buffer().read(cx).snapshot(cx);
812 let anchor_range = snapshot.anchor_before(selection_range.start)
813 ..snapshot.anchor_after(selection_range.end);
814 editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
815 s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
816 });
817 });
818 cx.update_editor(|editor, cx| {
819 update_go_to_definition_link(
820 editor,
821 &UpdateGoToDefinitionLink {
822 point: Some(hover_point),
823 cmd_held: true,
824 shift_held: false,
825 },
826 cx,
827 );
828 });
829 cx.foreground().run_until_parked();
830 assert!(requests.try_next().is_err());
831 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
832 fn test() { do_work(); }
833 fn do_work() { test(); }
834 "});
835 cx.foreground().run_until_parked();
836 }
837}