link_go_to_definition.rs

  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}