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    split: bool,
250    cx: &mut ViewContext<Editor>,
251) {
252    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx);
253}
254
255pub fn go_to_fetched_type_definition(
256    editor: &mut Editor,
257    point: DisplayPoint,
258    split: bool,
259    cx: &mut ViewContext<Editor>,
260) {
261    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx);
262}
263
264fn go_to_fetched_definition_of_kind(
265    kind: LinkDefinitionKind,
266    editor: &mut Editor,
267    point: DisplayPoint,
268    split: bool,
269    cx: &mut ViewContext<Editor>,
270) {
271    let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
272    hide_link_definition(editor, cx);
273    let cached_definitions_kind = editor.link_go_to_definition_state.kind;
274
275    let is_correct_kind = cached_definitions_kind == Some(kind);
276    if !cached_definitions.is_empty() && is_correct_kind {
277        if !editor.focused {
278            cx.focus_self();
279        }
280
281        editor.navigate_to_definitions(cached_definitions, split, cx);
282    } else {
283        editor.select(
284            SelectPhase::Begin {
285                position: point,
286                add: false,
287                click_count: 1,
288            },
289            cx,
290        );
291
292        match kind {
293            LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
294            LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
303    use futures::StreamExt;
304    use gpui::{
305        platform::{self, Modifiers, ModifiersChangedEvent},
306        View,
307    };
308    use indoc::indoc;
309    use lsp::request::{GotoDefinition, GotoTypeDefinition};
310
311    #[gpui::test]
312    async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
313        init_test(cx, |_| {});
314
315        let mut cx = EditorLspTestContext::new_rust(
316            lsp::ServerCapabilities {
317                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
318                type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
319                ..Default::default()
320            },
321            cx,
322        )
323        .await;
324
325        cx.set_state(indoc! {"
326            struct A;
327            let vˇariable = A;
328        "});
329
330        // Basic hold cmd+shift, expect highlight in region if response contains type definition
331        let hover_point = cx.display_point(indoc! {"
332            struct A;
333            let vˇariable = A;
334        "});
335        let symbol_range = cx.lsp_range(indoc! {"
336            struct A;
337            let «variable» = A;
338        "});
339        let target_range = cx.lsp_range(indoc! {"
340            struct «A»;
341            let variable = A;
342        "});
343
344        let mut requests =
345            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
346                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
347                    lsp::LocationLink {
348                        origin_selection_range: Some(symbol_range),
349                        target_uri: url.clone(),
350                        target_range,
351                        target_selection_range: target_range,
352                    },
353                ])))
354            });
355
356        // Press cmd+shift to trigger highlight
357        cx.update_editor(|editor, cx| {
358            update_go_to_definition_link(editor, Some(hover_point), true, true, cx);
359        });
360        requests.next().await;
361        cx.foreground().run_until_parked();
362        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
363            struct A;
364            let «variable» = A;
365        "});
366
367        // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
368        cx.update_editor(|editor, cx| {
369            editor.modifiers_changed(
370                &platform::ModifiersChangedEvent {
371                    modifiers: Modifiers {
372                        cmd: true,
373                        ..Default::default()
374                    },
375                    ..Default::default()
376                },
377                cx,
378            );
379        });
380        // Assert no link highlights
381        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
382            struct A;
383            let variable = A;
384        "});
385
386        // Cmd+shift click without existing definition requests and jumps
387        let hover_point = cx.display_point(indoc! {"
388            struct A;
389            let vˇariable = A;
390        "});
391        let target_range = cx.lsp_range(indoc! {"
392            struct «A»;
393            let variable = A;
394        "});
395
396        let mut requests =
397            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
398                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
399                    lsp::LocationLink {
400                        origin_selection_range: None,
401                        target_uri: url,
402                        target_range,
403                        target_selection_range: target_range,
404                    },
405                ])))
406            });
407
408        cx.update_editor(|editor, cx| {
409            go_to_fetched_type_definition(editor, hover_point, false, cx);
410        });
411        requests.next().await;
412        cx.foreground().run_until_parked();
413
414        cx.assert_editor_state(indoc! {"
415            struct «Aˇ»;
416            let variable = A;
417        "});
418    }
419
420    #[gpui::test]
421    async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
422        init_test(cx, |_| {});
423
424        let mut cx = EditorLspTestContext::new_rust(
425            lsp::ServerCapabilities {
426                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
427                ..Default::default()
428            },
429            cx,
430        )
431        .await;
432
433        cx.set_state(indoc! {"
434            fn ˇtest() { do_work(); }
435            fn do_work() { test(); }
436        "});
437
438        // Basic hold cmd, expect highlight in region if response contains definition
439        let hover_point = cx.display_point(indoc! {"
440            fn test() { do_wˇork(); }
441            fn do_work() { test(); }
442        "});
443        let symbol_range = cx.lsp_range(indoc! {"
444            fn test() { «do_work»(); }
445            fn do_work() { test(); }
446        "});
447        let target_range = cx.lsp_range(indoc! {"
448            fn test() { do_work(); }
449            fn «do_work»() { test(); }
450        "});
451
452        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
453            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
454                lsp::LocationLink {
455                    origin_selection_range: Some(symbol_range),
456                    target_uri: url.clone(),
457                    target_range,
458                    target_selection_range: target_range,
459                },
460            ])))
461        });
462
463        cx.update_editor(|editor, cx| {
464            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
465        });
466        requests.next().await;
467        cx.foreground().run_until_parked();
468        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
469            fn test() { «do_work»(); }
470            fn do_work() { test(); }
471        "});
472
473        // Unpress cmd causes highlight to go away
474        cx.update_editor(|editor, cx| {
475            editor.modifiers_changed(&Default::default(), cx);
476        });
477
478        // Assert no link highlights
479        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
480            fn test() { do_work(); }
481            fn do_work() { test(); }
482        "});
483
484        // Response without source range still highlights word
485        cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
486        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
487            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
488                lsp::LocationLink {
489                    // No origin range
490                    origin_selection_range: None,
491                    target_uri: url.clone(),
492                    target_range,
493                    target_selection_range: target_range,
494                },
495            ])))
496        });
497        cx.update_editor(|editor, cx| {
498            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
499        });
500        requests.next().await;
501        cx.foreground().run_until_parked();
502
503        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
504            fn test() { «do_work»(); }
505            fn do_work() { test(); }
506        "});
507
508        // Moving mouse to location with no response dismisses highlight
509        let hover_point = cx.display_point(indoc! {"
510            fˇn test() { do_work(); }
511            fn do_work() { test(); }
512        "});
513        let mut requests = cx
514            .lsp
515            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
516                // No definitions returned
517                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
518            });
519        cx.update_editor(|editor, cx| {
520            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
521        });
522        requests.next().await;
523        cx.foreground().run_until_parked();
524
525        // Assert no link highlights
526        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
527            fn test() { do_work(); }
528            fn do_work() { test(); }
529        "});
530
531        // Move mouse without cmd and then pressing cmd triggers highlight
532        let hover_point = cx.display_point(indoc! {"
533            fn test() { do_work(); }
534            fn do_work() { teˇst(); }
535        "});
536        cx.update_editor(|editor, cx| {
537            update_go_to_definition_link(editor, Some(hover_point), false, false, cx);
538        });
539        cx.foreground().run_until_parked();
540
541        // Assert no link highlights
542        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
543            fn test() { do_work(); }
544            fn do_work() { test(); }
545        "});
546
547        let symbol_range = cx.lsp_range(indoc! {"
548            fn test() { do_work(); }
549            fn do_work() { «test»(); }
550        "});
551        let target_range = cx.lsp_range(indoc! {"
552            fn «test»() { do_work(); }
553            fn do_work() { test(); }
554        "});
555
556        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
557            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
558                lsp::LocationLink {
559                    origin_selection_range: Some(symbol_range),
560                    target_uri: url,
561                    target_range,
562                    target_selection_range: target_range,
563                },
564            ])))
565        });
566        cx.update_editor(|editor, cx| {
567            editor.modifiers_changed(
568                &ModifiersChangedEvent {
569                    modifiers: Modifiers {
570                        cmd: true,
571                        ..Default::default()
572                    },
573                },
574                cx,
575            );
576        });
577        requests.next().await;
578        cx.foreground().run_until_parked();
579
580        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
581            fn test() { do_work(); }
582            fn do_work() { «test»(); }
583        "});
584
585        // Deactivating the window dismisses the highlight
586        cx.update_workspace(|workspace, cx| {
587            workspace.on_window_activation_changed(false, cx);
588        });
589        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
590            fn test() { do_work(); }
591            fn do_work() { test(); }
592        "});
593
594        // Moving the mouse restores the highlights.
595        cx.update_editor(|editor, cx| {
596            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
597        });
598        cx.foreground().run_until_parked();
599        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
600            fn test() { do_work(); }
601            fn do_work() { «test»(); }
602        "});
603
604        // Moving again within the same symbol range doesn't re-request
605        let hover_point = cx.display_point(indoc! {"
606            fn test() { do_work(); }
607            fn do_work() { tesˇt(); }
608        "});
609        cx.update_editor(|editor, cx| {
610            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
611        });
612        cx.foreground().run_until_parked();
613        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
614            fn test() { do_work(); }
615            fn do_work() { «test»(); }
616        "});
617
618        // Cmd click with existing definition doesn't re-request and dismisses highlight
619        cx.update_editor(|editor, cx| {
620            go_to_fetched_definition(editor, hover_point, false, cx);
621        });
622        // Assert selection moved to to definition
623        cx.lsp
624            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
625                // Empty definition response to make sure we aren't hitting the lsp and using
626                // the cached location instead
627                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
628            });
629        cx.assert_editor_state(indoc! {"
630            fn «testˇ»() { do_work(); }
631            fn do_work() { test(); }
632        "});
633
634        // Assert no link highlights after jump
635        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
636            fn test() { do_work(); }
637            fn do_work() { test(); }
638        "});
639
640        // Cmd click without existing definition requests and jumps
641        let hover_point = cx.display_point(indoc! {"
642            fn test() { do_wˇork(); }
643            fn do_work() { test(); }
644        "});
645        let target_range = cx.lsp_range(indoc! {"
646            fn test() { do_work(); }
647            fn «do_work»() { test(); }
648        "});
649
650        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
651            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
652                lsp::LocationLink {
653                    origin_selection_range: None,
654                    target_uri: url,
655                    target_range,
656                    target_selection_range: target_range,
657                },
658            ])))
659        });
660        cx.update_editor(|editor, cx| {
661            go_to_fetched_definition(editor, hover_point, false, cx);
662        });
663        requests.next().await;
664        cx.foreground().run_until_parked();
665        cx.assert_editor_state(indoc! {"
666            fn test() { do_work(); }
667            fn «do_workˇ»() { test(); }
668        "});
669
670        // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
671        // 2. Selection is completed, hovering
672        let hover_point = cx.display_point(indoc! {"
673            fn test() { do_wˇork(); }
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        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
681            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
682                lsp::LocationLink {
683                    origin_selection_range: None,
684                    target_uri: url,
685                    target_range,
686                    target_selection_range: target_range,
687                },
688            ])))
689        });
690
691        // create a pending selection
692        let selection_range = cx.ranges(indoc! {"
693            fn «test() { do_w»ork(); }
694            fn do_work() { test(); }
695        "})[0]
696            .clone();
697        cx.update_editor(|editor, cx| {
698            let snapshot = editor.buffer().read(cx).snapshot(cx);
699            let anchor_range = snapshot.anchor_before(selection_range.start)
700                ..snapshot.anchor_after(selection_range.end);
701            editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
702                s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
703            });
704        });
705        cx.update_editor(|editor, cx| {
706            update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
707        });
708        cx.foreground().run_until_parked();
709        assert!(requests.try_next().is_err());
710        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
711            fn test() { do_work(); }
712            fn do_work() { test(); }
713        "});
714        cx.foreground().run_until_parked();
715    }
716}