link_go_to_definition.rs

  1use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
  2use gpui::{Task, ViewContext};
  3use language::{Bias, ToOffset};
  4use project::LocationLink;
  5use std::ops::Range;
  6use util::TryFutureExt;
  7
  8#[derive(Debug, Default)]
  9pub struct LinkGoToDefinitionState {
 10    pub last_mouse_location: Option<Anchor>,
 11    pub symbol_range: Option<Range<Anchor>>,
 12    pub kind: Option<LinkDefinitionKind>,
 13    pub definitions: Vec<LocationLink>,
 14    pub task: Option<Task<Option<()>>>,
 15}
 16
 17pub fn update_go_to_definition_link(
 18    editor: &mut Editor,
 19    point: Option<DisplayPoint>,
 20    cmd_held: bool,
 21    shift_held: bool,
 22    cx: &mut ViewContext<Editor>,
 23) {
 24    let pending_nonempty_selection = editor.has_pending_nonempty_selection();
 25
 26    // Store new mouse point as an anchor
 27    let snapshot = editor.snapshot(cx);
 28    let point = point.map(|point| {
 29        snapshot
 30            .buffer_snapshot
 31            .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left))
 32    });
 33
 34    // If the new point is the same as the previously stored one, return early
 35    if let (Some(a), Some(b)) = (
 36        &point,
 37        &editor.link_go_to_definition_state.last_mouse_location,
 38    ) {
 39        if a.cmp(b, &snapshot.buffer_snapshot).is_eq() {
 40            return;
 41        }
 42    }
 43
 44    editor.link_go_to_definition_state.last_mouse_location = point.clone();
 45
 46    if pending_nonempty_selection {
 47        hide_link_definition(editor, cx);
 48        return;
 49    }
 50
 51    if cmd_held {
 52        if let Some(point) = point {
 53            let kind = if shift_held {
 54                LinkDefinitionKind::Type
 55            } else {
 56                LinkDefinitionKind::Symbol
 57            };
 58
 59            show_link_definition(kind, editor, point, snapshot, cx);
 60            return;
 61        }
 62    }
 63
 64    hide_link_definition(editor, cx);
 65}
 66
 67#[derive(Debug, Clone, Copy, PartialEq)]
 68pub enum LinkDefinitionKind {
 69    Symbol,
 70    Type,
 71}
 72
 73pub fn show_link_definition(
 74    definition_kind: LinkDefinitionKind,
 75    editor: &mut Editor,
 76    trigger_point: Anchor,
 77    snapshot: EditorSnapshot,
 78    cx: &mut ViewContext<Editor>,
 79) {
 80    let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind);
 81    if !same_kind {
 82        hide_link_definition(editor, cx);
 83    }
 84
 85    if editor.pending_rename.is_some() {
 86        return;
 87    }
 88
 89    let (buffer, buffer_position) = if let Some(output) = editor
 90        .buffer
 91        .read(cx)
 92        .text_anchor_for_position(trigger_point.clone(), cx)
 93    {
 94        output
 95    } else {
 96        return;
 97    };
 98
 99    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
100        .buffer()
101        .read(cx)
102        .excerpt_containing(trigger_point.clone(), cx)
103    {
104        excerpt_id
105    } else {
106        return;
107    };
108
109    let project = if let Some(project) = editor.project.clone() {
110        project
111    } else {
112        return;
113    };
114
115    // Don't request again if the location is within the symbol region of a previous request with the same kind
116    if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
117        let point_after_start = symbol_range
118            .start
119            .cmp(&trigger_point, &snapshot.buffer_snapshot)
120            .is_le();
121
122        let point_before_end = symbol_range
123            .end
124            .cmp(&trigger_point, &snapshot.buffer_snapshot)
125            .is_ge();
126
127        let point_within_range = point_after_start && point_before_end;
128        if point_within_range && same_kind {
129            return;
130        }
131    }
132
133    let task = cx.spawn(|this, mut cx| {
134        async move {
135            // query the LSP for definition info
136            let definition_request = cx.update(|cx| {
137                project.update(cx, |project, cx| match definition_kind {
138                    LinkDefinitionKind::Symbol => project.definition(&buffer, buffer_position, cx),
139
140                    LinkDefinitionKind::Type => {
141                        project.type_definition(&buffer, buffer_position, cx)
142                    }
143                })
144            });
145
146            let result = definition_request.await.ok().map(|definition_result| {
147                (
148                    definition_result.iter().find_map(|link| {
149                        link.origin.as_ref().map(|origin| {
150                            let start = snapshot
151                                .buffer_snapshot
152                                .anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
153                            let end = snapshot
154                                .buffer_snapshot
155                                .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
156
157                            start..end
158                        })
159                    }),
160                    definition_result,
161                )
162            });
163
164            this.update(&mut cx, |this, cx| {
165                // Clear any existing highlights
166                this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
167                this.link_go_to_definition_state.kind = Some(definition_kind);
168                this.link_go_to_definition_state.symbol_range = result
169                    .as_ref()
170                    .and_then(|(symbol_range, _)| symbol_range.clone());
171
172                if let Some((symbol_range, definitions)) = result {
173                    this.link_go_to_definition_state.definitions = definitions.clone();
174
175                    let buffer_snapshot = buffer.read(cx).snapshot();
176
177                    // Only show highlight if there exists a definition to jump to that doesn't contain
178                    // the current location.
179                    let any_definition_does_not_contain_current_location =
180                        definitions.iter().any(|definition| {
181                            let target = &definition.target;
182                            if target.buffer == buffer {
183                                let range = &target.range;
184                                // Expand range by one character as lsp definition ranges include positions adjacent
185                                // but not contained by the symbol range
186                                let start = buffer_snapshot.clip_offset(
187                                    range.start.to_offset(&buffer_snapshot).saturating_sub(1),
188                                    Bias::Left,
189                                );
190                                let end = buffer_snapshot.clip_offset(
191                                    range.end.to_offset(&buffer_snapshot) + 1,
192                                    Bias::Right,
193                                );
194                                let offset = buffer_position.to_offset(&buffer_snapshot);
195                                !(start <= offset && end >= offset)
196                            } else {
197                                true
198                            }
199                        });
200
201                    if any_definition_does_not_contain_current_location {
202                        // If no symbol range returned from language server, use the surrounding word.
203                        let highlight_range = symbol_range.unwrap_or_else(|| {
204                            let snapshot = &snapshot.buffer_snapshot;
205                            let (offset_range, _) = snapshot.surrounding_word(trigger_point);
206
207                            snapshot.anchor_before(offset_range.start)
208                                ..snapshot.anchor_after(offset_range.end)
209                        });
210
211                        // Highlight symbol using theme link definition highlight style
212                        let style = theme::current(cx).editor.link_definition;
213                        this.highlight_text::<LinkGoToDefinitionState>(
214                            vec![highlight_range],
215                            style,
216                            cx,
217                        );
218                    } else {
219                        hide_link_definition(this, cx);
220                    }
221                }
222            })?;
223
224            Ok::<_, anyhow::Error>(())
225        }
226        .log_err()
227    });
228
229    editor.link_go_to_definition_state.task = Some(task);
230}
231
232pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
233    if editor.link_go_to_definition_state.symbol_range.is_some()
234        || !editor.link_go_to_definition_state.definitions.is_empty()
235    {
236        editor.link_go_to_definition_state.symbol_range.take();
237        editor.link_go_to_definition_state.definitions.clear();
238        cx.notify();
239    }
240
241    editor.link_go_to_definition_state.task = None;
242
243    editor.clear_text_highlights::<LinkGoToDefinitionState>(cx);
244}
245
246pub fn go_to_fetched_definition(
247    editor: &mut Editor,
248    point: DisplayPoint,
249    cx: &mut ViewContext<Editor>,
250) {
251    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, cx);
252}
253
254pub fn go_to_fetched_type_definition(
255    editor: &mut Editor,
256    point: DisplayPoint,
257    cx: &mut ViewContext<Editor>,
258) {
259    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, cx);
260}
261
262fn go_to_fetched_definition_of_kind(
263    kind: LinkDefinitionKind,
264    editor: &mut Editor,
265    point: DisplayPoint,
266    cx: &mut ViewContext<Editor>,
267) {
268    let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
269    hide_link_definition(editor, cx);
270    let cached_definitions_kind = editor.link_go_to_definition_state.kind;
271
272    let is_correct_kind = cached_definitions_kind == Some(kind);
273    if !cached_definitions.is_empty() && is_correct_kind {
274        if !editor.focused {
275            cx.focus_self();
276        }
277
278        editor.navigate_to_definitions(cached_definitions, cx);
279    } else {
280        editor.select(
281            SelectPhase::Begin {
282                position: point,
283                add: false,
284                click_count: 1,
285            },
286            cx,
287        );
288
289        match kind {
290            LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
291            LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
292        }
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
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    #[gpui::test]
309    async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
310        init_test(cx, |_| {});
311
312        let mut cx = EditorLspTestContext::new_rust(
313            lsp::ServerCapabilities {
314                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
315                ..Default::default()
316            },
317            cx,
318        )
319        .await;
320
321        cx.set_state(indoc! {"
322            struct A;
323            let vˇariable = A;
324        "});
325
326        // Basic hold cmd+shift, expect highlight in region if response contains type definition
327        let hover_point = cx.display_point(indoc! {"
328            struct A;
329            let vˇariable = A;
330        "});
331        let symbol_range = cx.lsp_range(indoc! {"
332            struct A;
333            let «variable» = A;
334        "});
335        let target_range = cx.lsp_range(indoc! {"
336            struct «A»;
337            let variable = A;
338        "});
339
340        let mut requests =
341            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
342                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
343                    lsp::LocationLink {
344                        origin_selection_range: Some(symbol_range),
345                        target_uri: url.clone(),
346                        target_range,
347                        target_selection_range: target_range,
348                    },
349                ])))
350            });
351
352        // Press cmd+shift to trigger highlight
353        cx.update_editor(|editor, cx| {
354            update_go_to_definition_link(editor, Some(hover_point), true, true, cx);
355        });
356        requests.next().await;
357        cx.foreground().run_until_parked();
358        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
359            struct A;
360            let «variable» = A;
361        "});
362
363        // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
364        cx.update_editor(|editor, cx| {
365            editor.modifiers_changed(
366                &platform::ModifiersChangedEvent {
367                    modifiers: Modifiers {
368                        cmd: true,
369                        ..Default::default()
370                    },
371                    ..Default::default()
372                },
373                cx,
374            );
375        });
376        // Assert no link highlights
377        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
378            struct A;
379            let variable = A;
380        "});
381
382        // Cmd+shift click without existing definition requests and jumps
383        let hover_point = cx.display_point(indoc! {"
384            struct A;
385            let vˇariable = A;
386        "});
387        let target_range = cx.lsp_range(indoc! {"
388            struct «A»;
389            let variable = A;
390        "});
391
392        let mut requests =
393            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
394                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
395                    lsp::LocationLink {
396                        origin_selection_range: None,
397                        target_uri: url,
398                        target_range,
399                        target_selection_range: target_range,
400                    },
401                ])))
402            });
403
404        cx.update_editor(|editor, cx| {
405            go_to_fetched_type_definition(editor, hover_point, cx);
406        });
407        requests.next().await;
408        cx.foreground().run_until_parked();
409
410        cx.assert_editor_state(indoc! {"
411            struct «Aˇ»;
412            let variable = A;
413        "});
414    }
415
416    #[gpui::test]
417    async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
418        init_test(cx, |_| {});
419
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}