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