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_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::{
362 platform::{self, Modifiers, ModifiersChangedEvent},
363 View,
364 };
365 use indoc::indoc;
366 use lsp::request::{GotoDefinition, GotoTypeDefinition};
367
368 use crate::test::editor_lsp_test_context::EditorLspTestContext;
369
370 use super::*;
371
372 #[gpui::test]
373 async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
374 let mut cx = EditorLspTestContext::new_rust(
375 lsp::ServerCapabilities {
376 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
377 ..Default::default()
378 },
379 cx,
380 )
381 .await;
382
383 cx.set_state(indoc! {"
384 struct A;
385 let vˇariable = A;
386 "});
387
388 // Basic hold cmd+shift, expect highlight in region if response contains type definition
389 let hover_point = cx.display_point(indoc! {"
390 struct A;
391 let vˇariable = A;
392 "});
393 let symbol_range = cx.lsp_range(indoc! {"
394 struct A;
395 let «variable» = A;
396 "});
397 let target_range = cx.lsp_range(indoc! {"
398 struct «A»;
399 let variable = A;
400 "});
401
402 let mut requests =
403 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
404 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
405 lsp::LocationLink {
406 origin_selection_range: Some(symbol_range),
407 target_uri: url.clone(),
408 target_range,
409 target_selection_range: target_range,
410 },
411 ])))
412 });
413
414 // Press cmd+shift to trigger highlight
415 cx.update_editor(|editor, cx| {
416 update_go_to_definition_link(
417 editor,
418 &UpdateGoToDefinitionLink {
419 point: Some(hover_point),
420 cmd_held: true,
421 shift_held: true,
422 },
423 cx,
424 );
425 });
426 requests.next().await;
427 cx.foreground().run_until_parked();
428 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
429 struct A;
430 let «variable» = A;
431 "});
432
433 // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
434 cx.update_editor(|editor, cx| {
435 editor.modifiers_changed(
436 &platform::ModifiersChangedEvent {
437 modifiers: Modifiers {
438 cmd: true,
439 ..Default::default()
440 },
441 ..Default::default()
442 },
443 cx,
444 );
445 });
446 // Assert no link highlights
447 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
448 struct A;
449 let variable = A;
450 "});
451
452 // Cmd+shift click without existing definition requests and jumps
453 let hover_point = cx.display_point(indoc! {"
454 struct A;
455 let vˇariable = A;
456 "});
457 let target_range = cx.lsp_range(indoc! {"
458 struct «A»;
459 let variable = A;
460 "});
461
462 let mut requests =
463 cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
464 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
465 lsp::LocationLink {
466 origin_selection_range: None,
467 target_uri: url,
468 target_range,
469 target_selection_range: target_range,
470 },
471 ])))
472 });
473
474 cx.update_workspace(|workspace, cx| {
475 go_to_fetched_type_definition(
476 workspace,
477 &GoToFetchedTypeDefinition { point: hover_point },
478 cx,
479 );
480 });
481 requests.next().await;
482 cx.foreground().run_until_parked();
483
484 cx.assert_editor_state(indoc! {"
485 struct «Aˇ»;
486 let variable = A;
487 "});
488 }
489
490 #[gpui::test]
491 async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
492 let mut cx = EditorLspTestContext::new_rust(
493 lsp::ServerCapabilities {
494 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
495 ..Default::default()
496 },
497 cx,
498 )
499 .await;
500
501 cx.set_state(indoc! {"
502 fn ˇtest() { do_work(); }
503 fn do_work() { test(); }
504 "});
505
506 // Basic hold cmd, expect highlight in region if response contains definition
507 let hover_point = cx.display_point(indoc! {"
508 fn test() { do_wˇork(); }
509 fn do_work() { test(); }
510 "});
511 let symbol_range = cx.lsp_range(indoc! {"
512 fn test() { «do_work»(); }
513 fn do_work() { test(); }
514 "});
515 let target_range = cx.lsp_range(indoc! {"
516 fn test() { do_work(); }
517 fn «do_work»() { test(); }
518 "});
519
520 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
521 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
522 lsp::LocationLink {
523 origin_selection_range: Some(symbol_range),
524 target_uri: url.clone(),
525 target_range,
526 target_selection_range: target_range,
527 },
528 ])))
529 });
530
531 cx.update_editor(|editor, cx| {
532 update_go_to_definition_link(
533 editor,
534 &UpdateGoToDefinitionLink {
535 point: Some(hover_point),
536 cmd_held: true,
537 shift_held: false,
538 },
539 cx,
540 );
541 });
542 requests.next().await;
543 cx.foreground().run_until_parked();
544 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
545 fn test() { «do_work»(); }
546 fn do_work() { test(); }
547 "});
548
549 // Unpress cmd causes highlight to go away
550 cx.update_editor(|editor, cx| {
551 editor.modifiers_changed(&Default::default(), cx);
552 });
553
554 // Assert no link highlights
555 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
556 fn test() { do_work(); }
557 fn do_work() { test(); }
558 "});
559
560 // Response without source range still highlights word
561 cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
562 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
563 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
564 lsp::LocationLink {
565 // No origin range
566 origin_selection_range: None,
567 target_uri: url.clone(),
568 target_range,
569 target_selection_range: target_range,
570 },
571 ])))
572 });
573 cx.update_editor(|editor, cx| {
574 update_go_to_definition_link(
575 editor,
576 &UpdateGoToDefinitionLink {
577 point: Some(hover_point),
578 cmd_held: true,
579 shift_held: false,
580 },
581 cx,
582 );
583 });
584 requests.next().await;
585 cx.foreground().run_until_parked();
586
587 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
588 fn test() { «do_work»(); }
589 fn do_work() { test(); }
590 "});
591
592 // Moving mouse to location with no response dismisses highlight
593 let hover_point = cx.display_point(indoc! {"
594 fˇn test() { do_work(); }
595 fn do_work() { test(); }
596 "});
597 let mut requests = cx
598 .lsp
599 .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
600 // No definitions returned
601 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
602 });
603 cx.update_editor(|editor, cx| {
604 update_go_to_definition_link(
605 editor,
606 &UpdateGoToDefinitionLink {
607 point: Some(hover_point),
608 cmd_held: true,
609 shift_held: false,
610 },
611 cx,
612 );
613 });
614 requests.next().await;
615 cx.foreground().run_until_parked();
616
617 // Assert no link highlights
618 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
619 fn test() { do_work(); }
620 fn do_work() { test(); }
621 "});
622
623 // Move mouse without cmd and then pressing cmd triggers highlight
624 let hover_point = cx.display_point(indoc! {"
625 fn test() { do_work(); }
626 fn do_work() { teˇst(); }
627 "});
628 cx.update_editor(|editor, cx| {
629 update_go_to_definition_link(
630 editor,
631 &UpdateGoToDefinitionLink {
632 point: Some(hover_point),
633 cmd_held: false,
634 shift_held: false,
635 },
636 cx,
637 );
638 });
639 cx.foreground().run_until_parked();
640
641 // Assert no link highlights
642 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
643 fn test() { do_work(); }
644 fn do_work() { test(); }
645 "});
646
647 let symbol_range = cx.lsp_range(indoc! {"
648 fn test() { do_work(); }
649 fn do_work() { «test»(); }
650 "});
651 let target_range = cx.lsp_range(indoc! {"
652 fn «test»() { do_work(); }
653 fn do_work() { test(); }
654 "});
655
656 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
657 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
658 lsp::LocationLink {
659 origin_selection_range: Some(symbol_range),
660 target_uri: url,
661 target_range,
662 target_selection_range: target_range,
663 },
664 ])))
665 });
666 cx.update_editor(|editor, cx| {
667 editor.modifiers_changed(
668 &ModifiersChangedEvent {
669 modifiers: Modifiers {
670 cmd: true,
671 ..Default::default()
672 },
673 },
674 cx,
675 );
676 });
677 requests.next().await;
678 cx.foreground().run_until_parked();
679
680 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
681 fn test() { do_work(); }
682 fn do_work() { «test»(); }
683 "});
684
685 // Deactivating the window dismisses the highlight
686 cx.update_workspace(|workspace, cx| {
687 workspace.on_window_activation_changed(false, cx);
688 });
689 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
690 fn test() { do_work(); }
691 fn do_work() { test(); }
692 "});
693
694 // Moving the mouse restores the highlights.
695 cx.update_editor(|editor, cx| {
696 update_go_to_definition_link(
697 editor,
698 &UpdateGoToDefinitionLink {
699 point: Some(hover_point),
700 cmd_held: true,
701 shift_held: false,
702 },
703 cx,
704 );
705 });
706 cx.foreground().run_until_parked();
707 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
708 fn test() { do_work(); }
709 fn do_work() { «test»(); }
710 "});
711
712 // Moving again within the same symbol range doesn't re-request
713 let hover_point = cx.display_point(indoc! {"
714 fn test() { do_work(); }
715 fn do_work() { tesˇt(); }
716 "});
717 cx.update_editor(|editor, cx| {
718 update_go_to_definition_link(
719 editor,
720 &UpdateGoToDefinitionLink {
721 point: Some(hover_point),
722 cmd_held: true,
723 shift_held: false,
724 },
725 cx,
726 );
727 });
728 cx.foreground().run_until_parked();
729 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
730 fn test() { do_work(); }
731 fn do_work() { «test»(); }
732 "});
733
734 // Cmd click with existing definition doesn't re-request and dismisses highlight
735 cx.update_workspace(|workspace, cx| {
736 go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
737 });
738 // Assert selection moved to to definition
739 cx.lsp
740 .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
741 // Empty definition response to make sure we aren't hitting the lsp and using
742 // the cached location instead
743 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
744 });
745 cx.assert_editor_state(indoc! {"
746 fn «testˇ»() { do_work(); }
747 fn do_work() { test(); }
748 "});
749
750 // Assert no link highlights after jump
751 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
752 fn test() { do_work(); }
753 fn do_work() { test(); }
754 "});
755
756 // Cmd click without existing definition requests and jumps
757 let hover_point = cx.display_point(indoc! {"
758 fn test() { do_wˇork(); }
759 fn do_work() { test(); }
760 "});
761 let target_range = cx.lsp_range(indoc! {"
762 fn test() { do_work(); }
763 fn «do_work»() { test(); }
764 "});
765
766 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
767 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
768 lsp::LocationLink {
769 origin_selection_range: None,
770 target_uri: url,
771 target_range,
772 target_selection_range: target_range,
773 },
774 ])))
775 });
776 cx.update_workspace(|workspace, cx| {
777 go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
778 });
779 requests.next().await;
780 cx.foreground().run_until_parked();
781 cx.assert_editor_state(indoc! {"
782 fn test() { do_work(); }
783 fn «do_workˇ»() { test(); }
784 "});
785
786 // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
787 // 2. Selection is completed, hovering
788 let hover_point = cx.display_point(indoc! {"
789 fn test() { do_wˇork(); }
790 fn do_work() { test(); }
791 "});
792 let target_range = cx.lsp_range(indoc! {"
793 fn test() { do_work(); }
794 fn «do_work»() { test(); }
795 "});
796 let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
797 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
798 lsp::LocationLink {
799 origin_selection_range: None,
800 target_uri: url,
801 target_range,
802 target_selection_range: target_range,
803 },
804 ])))
805 });
806
807 // create a pending selection
808 let selection_range = cx.ranges(indoc! {"
809 fn «test() { do_w»ork(); }
810 fn do_work() { test(); }
811 "})[0]
812 .clone();
813 cx.update_editor(|editor, cx| {
814 let snapshot = editor.buffer().read(cx).snapshot(cx);
815 let anchor_range = snapshot.anchor_before(selection_range.start)
816 ..snapshot.anchor_after(selection_range.end);
817 editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
818 s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
819 });
820 });
821 cx.update_editor(|editor, cx| {
822 update_go_to_definition_link(
823 editor,
824 &UpdateGoToDefinitionLink {
825 point: Some(hover_point),
826 cmd_held: true,
827 shift_held: false,
828 },
829 cx,
830 );
831 });
832 cx.foreground().run_until_parked();
833 assert!(requests.try_next().is_err());
834 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
835 fn test() { do_work(); }
836 fn do_work() { test(); }
837 "});
838 cx.foreground().run_until_parked();
839 }
840}