link_go_to_definition.rs

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