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