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