link_go_to_definition.rs

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