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