link_go_to_definition.rs

   1use crate::{
   2    display_map::DisplaySnapshot,
   3    element::PointForPosition,
   4    hover_popover::{self, InlayHover},
   5    Anchor, DisplayPoint, Editor, EditorSnapshot, InlayId, SelectPhase,
   6};
   7use gpui::{Task, ViewContext};
   8use language::{Bias, ToOffset};
   9use lsp::LanguageServerId;
  10use project::{
  11    HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink,
  12    ResolveState,
  13};
  14use std::ops::Range;
  15use util::TryFutureExt;
  16
  17#[derive(Debug, Default)]
  18pub struct LinkGoToDefinitionState {
  19    pub last_trigger_point: Option<TriggerPoint>,
  20    pub symbol_range: Option<RangeInEditor>,
  21    pub kind: Option<LinkDefinitionKind>,
  22    pub definitions: Vec<GoToDefinitionLink>,
  23    pub task: Option<Task<Option<()>>>,
  24}
  25
  26#[derive(Debug, Eq, PartialEq, Clone)]
  27pub enum RangeInEditor {
  28    Text(Range<Anchor>),
  29    Inlay(InlayHighlight),
  30}
  31
  32impl RangeInEditor {
  33    pub fn as_text_range(&self) -> Option<Range<Anchor>> {
  34        match self {
  35            Self::Text(range) => Some(range.clone()),
  36            Self::Inlay(_) => None,
  37        }
  38    }
  39
  40    fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
  41        match (self, trigger_point) {
  42            (Self::Text(range), TriggerPoint::Text(point)) => {
  43                let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
  44                point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge()
  45            }
  46            (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => {
  47                highlight.inlay == point.inlay
  48                    && highlight.range.contains(&point.range.start)
  49                    && highlight.range.contains(&point.range.end)
  50            }
  51            (Self::Inlay(_), TriggerPoint::Text(_))
  52            | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
  53        }
  54    }
  55}
  56
  57#[derive(Debug)]
  58pub enum GoToDefinitionTrigger {
  59    Text(DisplayPoint),
  60    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
  61}
  62
  63#[derive(Debug, Clone)]
  64pub enum GoToDefinitionLink {
  65    Text(LocationLink),
  66    InlayHint(lsp::Location, LanguageServerId),
  67}
  68
  69#[derive(Debug, Clone, PartialEq, Eq)]
  70pub struct InlayHighlight {
  71    pub inlay: InlayId,
  72    pub inlay_position: Anchor,
  73    pub range: Range<usize>,
  74}
  75
  76#[derive(Debug, Clone)]
  77pub enum TriggerPoint {
  78    Text(Anchor),
  79    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
  80}
  81
  82impl TriggerPoint {
  83    pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
  84        match self {
  85            TriggerPoint::Text(_) => {
  86                if shift {
  87                    LinkDefinitionKind::Type
  88                } else {
  89                    LinkDefinitionKind::Symbol
  90                }
  91            }
  92            TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type,
  93        }
  94    }
  95
  96    fn anchor(&self) -> &Anchor {
  97        match self {
  98            TriggerPoint::Text(anchor) => anchor,
  99            TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position,
 100        }
 101    }
 102}
 103
 104pub fn update_go_to_definition_link(
 105    editor: &mut Editor,
 106    origin: Option<GoToDefinitionTrigger>,
 107    cmd_held: bool,
 108    shift_held: bool,
 109    cx: &mut ViewContext<Editor>,
 110) {
 111    let pending_nonempty_selection = editor.has_pending_nonempty_selection();
 112
 113    // Store new mouse point as an anchor
 114    let snapshot = editor.snapshot(cx);
 115    let trigger_point = match origin {
 116        Some(GoToDefinitionTrigger::Text(p)) => {
 117            Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before(
 118                p.to_offset(&snapshot.display_snapshot, Bias::Left),
 119            )))
 120        }
 121        Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => {
 122            Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id))
 123        }
 124        None => None,
 125    };
 126
 127    // If the new point is the same as the previously stored one, return early
 128    if let (Some(a), Some(b)) = (
 129        &trigger_point,
 130        &editor.link_go_to_definition_state.last_trigger_point,
 131    ) {
 132        match (a, b) {
 133            (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => {
 134                if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() {
 135                    return;
 136                }
 137            }
 138            (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => {
 139                if range_a == range_b {
 140                    return;
 141                }
 142            }
 143            _ => {}
 144        }
 145    }
 146
 147    editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone();
 148
 149    if pending_nonempty_selection {
 150        hide_link_definition(editor, cx);
 151        return;
 152    }
 153
 154    if cmd_held {
 155        if let Some(trigger_point) = trigger_point {
 156            let kind = trigger_point.definition_kind(shift_held);
 157            show_link_definition(kind, editor, trigger_point, snapshot, cx);
 158            return;
 159        }
 160    }
 161
 162    hide_link_definition(editor, cx);
 163}
 164
 165pub fn update_inlay_link_and_hover_points(
 166    snapshot: &DisplaySnapshot,
 167    point_for_position: PointForPosition,
 168    editor: &mut Editor,
 169    cmd_held: bool,
 170    shift_held: bool,
 171    cx: &mut ViewContext<'_, '_, Editor>,
 172) {
 173    let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
 174        Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
 175    } else {
 176        None
 177    };
 178    let mut go_to_definition_updated = false;
 179    let mut hover_updated = false;
 180    if let Some(hovered_offset) = hovered_offset {
 181        let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
 182        let previous_valid_anchor = buffer_snapshot.anchor_at(
 183            point_for_position.previous_valid.to_point(snapshot),
 184            Bias::Left,
 185        );
 186        let next_valid_anchor = buffer_snapshot.anchor_at(
 187            point_for_position.next_valid.to_point(snapshot),
 188            Bias::Right,
 189        );
 190        if let Some(hovered_hint) = editor
 191            .visible_inlay_hints(cx)
 192            .into_iter()
 193            .skip_while(|hint| {
 194                hint.position
 195                    .cmp(&previous_valid_anchor, &buffer_snapshot)
 196                    .is_lt()
 197            })
 198            .take_while(|hint| {
 199                hint.position
 200                    .cmp(&next_valid_anchor, &buffer_snapshot)
 201                    .is_le()
 202            })
 203            .max_by_key(|hint| hint.id)
 204        {
 205            let inlay_hint_cache = editor.inlay_hint_cache();
 206            let excerpt_id = previous_valid_anchor.excerpt_id;
 207            if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
 208                match cached_hint.resolve_state {
 209                    ResolveState::CanResolve(_, _) => {
 210                        if let Some(buffer_id) = previous_valid_anchor.buffer_id {
 211                            inlay_hint_cache.spawn_hint_resolve(
 212                                buffer_id,
 213                                excerpt_id,
 214                                hovered_hint.id,
 215                                cx,
 216                            );
 217                        }
 218                    }
 219                    ResolveState::Resolved => {
 220                        let mut extra_shift_left = 0;
 221                        let mut extra_shift_right = 0;
 222                        if cached_hint.padding_left {
 223                            extra_shift_left += 1;
 224                            extra_shift_right += 1;
 225                        }
 226                        if cached_hint.padding_right {
 227                            extra_shift_right += 1;
 228                        }
 229                        match cached_hint.label {
 230                            project::InlayHintLabel::String(_) => {
 231                                if let Some(tooltip) = cached_hint.tooltip {
 232                                    hover_popover::hover_at_inlay(
 233                                        editor,
 234                                        InlayHover {
 235                                            excerpt: excerpt_id,
 236                                            tooltip: match tooltip {
 237                                                InlayHintTooltip::String(text) => HoverBlock {
 238                                                    text,
 239                                                    kind: HoverBlockKind::PlainText,
 240                                                },
 241                                                InlayHintTooltip::MarkupContent(content) => {
 242                                                    HoverBlock {
 243                                                        text: content.value,
 244                                                        kind: content.kind,
 245                                                    }
 246                                                }
 247                                            },
 248                                            range: InlayHighlight {
 249                                                inlay: hovered_hint.id,
 250                                                inlay_position: hovered_hint.position,
 251                                                range: extra_shift_left
 252                                                    ..hovered_hint.text.len() + extra_shift_right,
 253                                            },
 254                                        },
 255                                        cx,
 256                                    );
 257                                    hover_updated = true;
 258                                }
 259                            }
 260                            project::InlayHintLabel::LabelParts(label_parts) => {
 261                                let hint_start =
 262                                    snapshot.anchor_to_inlay_offset(hovered_hint.position);
 263                                if let Some((hovered_hint_part, part_range)) =
 264                                    hover_popover::find_hovered_hint_part(
 265                                        label_parts,
 266                                        hint_start,
 267                                        hovered_offset,
 268                                    )
 269                                {
 270                                    let highlight_start =
 271                                        (part_range.start - hint_start).0 + extra_shift_left;
 272                                    let highlight_end =
 273                                        (part_range.end - hint_start).0 + extra_shift_right;
 274                                    let highlight = InlayHighlight {
 275                                        inlay: hovered_hint.id,
 276                                        inlay_position: hovered_hint.position,
 277                                        range: highlight_start..highlight_end,
 278                                    };
 279                                    if let Some(tooltip) = hovered_hint_part.tooltip {
 280                                        hover_popover::hover_at_inlay(
 281                                            editor,
 282                                            InlayHover {
 283                                                excerpt: excerpt_id,
 284                                                tooltip: match tooltip {
 285                                                    InlayHintLabelPartTooltip::String(text) => {
 286                                                        HoverBlock {
 287                                                            text,
 288                                                            kind: HoverBlockKind::PlainText,
 289                                                        }
 290                                                    }
 291                                                    InlayHintLabelPartTooltip::MarkupContent(
 292                                                        content,
 293                                                    ) => HoverBlock {
 294                                                        text: content.value,
 295                                                        kind: content.kind,
 296                                                    },
 297                                                },
 298                                                range: highlight.clone(),
 299                                            },
 300                                            cx,
 301                                        );
 302                                        hover_updated = true;
 303                                    }
 304                                    if let Some((language_server_id, location)) =
 305                                        hovered_hint_part.location
 306                                    {
 307                                        go_to_definition_updated = true;
 308                                        update_go_to_definition_link(
 309                                            editor,
 310                                            Some(GoToDefinitionTrigger::InlayHint(
 311                                                highlight,
 312                                                location,
 313                                                language_server_id,
 314                                            )),
 315                                            cmd_held,
 316                                            shift_held,
 317                                            cx,
 318                                        );
 319                                    }
 320                                }
 321                            }
 322                        };
 323                    }
 324                    ResolveState::Resolving => {}
 325                }
 326            }
 327        }
 328    }
 329
 330    if !go_to_definition_updated {
 331        update_go_to_definition_link(editor, None, cmd_held, shift_held, cx);
 332    }
 333    if !hover_updated {
 334        hover_popover::hover_at(editor, None, cx);
 335    }
 336}
 337
 338#[derive(Debug, Clone, Copy, PartialEq)]
 339pub enum LinkDefinitionKind {
 340    Symbol,
 341    Type,
 342}
 343
 344pub fn show_link_definition(
 345    definition_kind: LinkDefinitionKind,
 346    editor: &mut Editor,
 347    trigger_point: TriggerPoint,
 348    snapshot: EditorSnapshot,
 349    cx: &mut ViewContext<Editor>,
 350) {
 351    let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind);
 352    if !same_kind {
 353        hide_link_definition(editor, cx);
 354    }
 355
 356    if editor.pending_rename.is_some() {
 357        return;
 358    }
 359
 360    let trigger_anchor = trigger_point.anchor();
 361    let (buffer, buffer_position) = if let Some(output) = editor
 362        .buffer
 363        .read(cx)
 364        .text_anchor_for_position(trigger_anchor.clone(), cx)
 365    {
 366        output
 367    } else {
 368        return;
 369    };
 370
 371    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
 372        .buffer()
 373        .read(cx)
 374        .excerpt_containing(trigger_anchor.clone(), cx)
 375    {
 376        excerpt_id
 377    } else {
 378        return;
 379    };
 380
 381    let project = if let Some(project) = editor.project.clone() {
 382        project
 383    } else {
 384        return;
 385    };
 386
 387    // Don't request again if the location is within the symbol region of a previous request with the same kind
 388    if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
 389        if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) {
 390            return;
 391        }
 392    }
 393
 394    let task = cx.spawn(|this, mut cx| {
 395        async move {
 396            let result = match &trigger_point {
 397                TriggerPoint::Text(_) => {
 398                    // query the LSP for definition info
 399                    cx.update(|cx| {
 400                        project.update(cx, |project, cx| match definition_kind {
 401                            LinkDefinitionKind::Symbol => {
 402                                project.definition(&buffer, buffer_position, cx)
 403                            }
 404
 405                            LinkDefinitionKind::Type => {
 406                                project.type_definition(&buffer, buffer_position, cx)
 407                            }
 408                        })
 409                    })
 410                    .await
 411                    .ok()
 412                    .map(|definition_result| {
 413                        (
 414                            definition_result.iter().find_map(|link| {
 415                                link.origin.as_ref().map(|origin| {
 416                                    let start = snapshot
 417                                        .buffer_snapshot
 418                                        .anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
 419                                    let end = snapshot
 420                                        .buffer_snapshot
 421                                        .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
 422                                    RangeInEditor::Text(start..end)
 423                                })
 424                            }),
 425                            definition_result
 426                                .into_iter()
 427                                .map(GoToDefinitionLink::Text)
 428                                .collect(),
 429                        )
 430                    })
 431                }
 432                TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
 433                    Some(RangeInEditor::Inlay(highlight.clone())),
 434                    vec![GoToDefinitionLink::InlayHint(
 435                        lsp_location.clone(),
 436                        *server_id,
 437                    )],
 438                )),
 439            };
 440
 441            this.update(&mut cx, |this, cx| {
 442                // Clear any existing highlights
 443                this.clear_highlights::<LinkGoToDefinitionState>(cx);
 444                this.link_go_to_definition_state.kind = Some(definition_kind);
 445                this.link_go_to_definition_state.symbol_range = result
 446                    .as_ref()
 447                    .and_then(|(symbol_range, _)| symbol_range.clone());
 448
 449                if let Some((symbol_range, definitions)) = result {
 450                    this.link_go_to_definition_state.definitions = definitions.clone();
 451
 452                    let buffer_snapshot = buffer.read(cx).snapshot();
 453
 454                    // Only show highlight if there exists a definition to jump to that doesn't contain
 455                    // the current location.
 456                    let any_definition_does_not_contain_current_location =
 457                        definitions.iter().any(|definition| {
 458                            match &definition {
 459                                GoToDefinitionLink::Text(link) => {
 460                                    if link.target.buffer == buffer {
 461                                        let range = &link.target.range;
 462                                        // Expand range by one character as lsp definition ranges include positions adjacent
 463                                        // but not contained by the symbol range
 464                                        let start = buffer_snapshot.clip_offset(
 465                                            range
 466                                                .start
 467                                                .to_offset(&buffer_snapshot)
 468                                                .saturating_sub(1),
 469                                            Bias::Left,
 470                                        );
 471                                        let end = buffer_snapshot.clip_offset(
 472                                            range.end.to_offset(&buffer_snapshot) + 1,
 473                                            Bias::Right,
 474                                        );
 475                                        let offset = buffer_position.to_offset(&buffer_snapshot);
 476                                        !(start <= offset && end >= offset)
 477                                    } else {
 478                                        true
 479                                    }
 480                                }
 481                                GoToDefinitionLink::InlayHint(_, _) => true,
 482                            }
 483                        });
 484
 485                    if any_definition_does_not_contain_current_location {
 486                        // Highlight symbol using theme link definition highlight style
 487                        let style = theme::current(cx).editor.link_definition;
 488                        let highlight_range =
 489                            symbol_range.unwrap_or_else(|| match &trigger_point {
 490                                TriggerPoint::Text(trigger_anchor) => {
 491                                    let snapshot = &snapshot.buffer_snapshot;
 492                                    // If no symbol range returned from language server, use the surrounding word.
 493                                    let (offset_range, _) =
 494                                        snapshot.surrounding_word(*trigger_anchor);
 495                                    RangeInEditor::Text(
 496                                        snapshot.anchor_before(offset_range.start)
 497                                            ..snapshot.anchor_after(offset_range.end),
 498                                    )
 499                                }
 500                                TriggerPoint::InlayHint(highlight, _, _) => {
 501                                    RangeInEditor::Inlay(highlight.clone())
 502                                }
 503                            });
 504
 505                        match highlight_range {
 506                            RangeInEditor::Text(text_range) => this
 507                                .highlight_text::<LinkGoToDefinitionState>(
 508                                    vec![text_range],
 509                                    style,
 510                                    cx,
 511                                ),
 512                            RangeInEditor::Inlay(highlight) => this
 513                                .highlight_inlays::<LinkGoToDefinitionState>(
 514                                    vec![highlight],
 515                                    style,
 516                                    cx,
 517                                ),
 518                        }
 519                    } else {
 520                        hide_link_definition(this, cx);
 521                    }
 522                }
 523            })?;
 524
 525            Ok::<_, anyhow::Error>(())
 526        }
 527        .log_err()
 528    });
 529
 530    editor.link_go_to_definition_state.task = Some(task);
 531}
 532
 533pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
 534    if editor.link_go_to_definition_state.symbol_range.is_some()
 535        || !editor.link_go_to_definition_state.definitions.is_empty()
 536    {
 537        editor.link_go_to_definition_state.symbol_range.take();
 538        editor.link_go_to_definition_state.definitions.clear();
 539        cx.notify();
 540    }
 541
 542    editor.link_go_to_definition_state.task = None;
 543
 544    editor.clear_highlights::<LinkGoToDefinitionState>(cx);
 545}
 546
 547pub fn go_to_fetched_definition(
 548    editor: &mut Editor,
 549    point: PointForPosition,
 550    split: bool,
 551    cx: &mut ViewContext<Editor>,
 552) {
 553    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx);
 554}
 555
 556pub fn go_to_fetched_type_definition(
 557    editor: &mut Editor,
 558    point: PointForPosition,
 559    split: bool,
 560    cx: &mut ViewContext<Editor>,
 561) {
 562    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx);
 563}
 564
 565fn go_to_fetched_definition_of_kind(
 566    kind: LinkDefinitionKind,
 567    editor: &mut Editor,
 568    point: PointForPosition,
 569    split: bool,
 570    cx: &mut ViewContext<Editor>,
 571) {
 572    let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
 573    hide_link_definition(editor, cx);
 574    let cached_definitions_kind = editor.link_go_to_definition_state.kind;
 575
 576    let is_correct_kind = cached_definitions_kind == Some(kind);
 577    if !cached_definitions.is_empty() && is_correct_kind {
 578        if !editor.focused {
 579            cx.focus_self();
 580        }
 581
 582        editor.navigate_to_definitions(cached_definitions, split, cx);
 583    } else {
 584        editor.select(
 585            SelectPhase::Begin {
 586                position: point.next_valid,
 587                add: false,
 588                click_count: 1,
 589            },
 590            cx,
 591        );
 592
 593        if point.as_valid().is_some() {
 594            match kind {
 595                LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
 596                LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
 597            }
 598        }
 599    }
 600}
 601
 602#[cfg(test)]
 603mod tests {
 604    use super::*;
 605    use crate::{
 606        display_map::ToDisplayPoint,
 607        editor_tests::init_test,
 608        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
 609        test::editor_lsp_test_context::EditorLspTestContext,
 610    };
 611    use futures::StreamExt;
 612    use gpui::{
 613        platform::{self, Modifiers, ModifiersChangedEvent},
 614        View,
 615    };
 616    use indoc::indoc;
 617    use language::language_settings::InlayHintSettings;
 618    use lsp::request::{GotoDefinition, GotoTypeDefinition};
 619    use util::assert_set_eq;
 620
 621    #[gpui::test]
 622    async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
 623        init_test(cx, |_| {});
 624
 625        let mut cx = EditorLspTestContext::new_rust(
 626            lsp::ServerCapabilities {
 627                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 628                type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
 629                ..Default::default()
 630            },
 631            cx,
 632        )
 633        .await;
 634
 635        cx.set_state(indoc! {"
 636            struct A;
 637            let vˇariable = A;
 638        "});
 639
 640        // Basic hold cmd+shift, expect highlight in region if response contains type definition
 641        let hover_point = cx.display_point(indoc! {"
 642            struct A;
 643            let vˇariable = A;
 644        "});
 645        let symbol_range = cx.lsp_range(indoc! {"
 646            struct A;
 647            let «variable» = A;
 648        "});
 649        let target_range = cx.lsp_range(indoc! {"
 650            struct «A»;
 651            let variable = A;
 652        "});
 653
 654        let mut requests =
 655            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
 656                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
 657                    lsp::LocationLink {
 658                        origin_selection_range: Some(symbol_range),
 659                        target_uri: url.clone(),
 660                        target_range,
 661                        target_selection_range: target_range,
 662                    },
 663                ])))
 664            });
 665
 666        // Press cmd+shift to trigger highlight
 667        cx.update_editor(|editor, cx| {
 668            update_go_to_definition_link(
 669                editor,
 670                Some(GoToDefinitionTrigger::Text(hover_point)),
 671                true,
 672                true,
 673                cx,
 674            );
 675        });
 676        requests.next().await;
 677        cx.foreground().run_until_parked();
 678        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 679            struct A;
 680            let «variable» = A;
 681        "});
 682
 683        // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
 684        cx.update_editor(|editor, cx| {
 685            editor.modifiers_changed(
 686                &platform::ModifiersChangedEvent {
 687                    modifiers: Modifiers {
 688                        cmd: true,
 689                        ..Default::default()
 690                    },
 691                    ..Default::default()
 692                },
 693                cx,
 694            );
 695        });
 696        // Assert no link highlights
 697        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 698            struct A;
 699            let variable = A;
 700        "});
 701
 702        // Cmd+shift click without existing definition requests and jumps
 703        let hover_point = cx.display_point(indoc! {"
 704            struct A;
 705            let vˇariable = A;
 706        "});
 707        let target_range = cx.lsp_range(indoc! {"
 708            struct «A»;
 709            let variable = A;
 710        "});
 711
 712        let mut requests =
 713            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
 714                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
 715                    lsp::LocationLink {
 716                        origin_selection_range: None,
 717                        target_uri: url,
 718                        target_range,
 719                        target_selection_range: target_range,
 720                    },
 721                ])))
 722            });
 723
 724        cx.update_editor(|editor, cx| {
 725            go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
 726        });
 727        requests.next().await;
 728        cx.foreground().run_until_parked();
 729
 730        cx.assert_editor_state(indoc! {"
 731            struct «Aˇ»;
 732            let variable = A;
 733        "});
 734    }
 735
 736    #[gpui::test]
 737    async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
 738        init_test(cx, |_| {});
 739
 740        let mut cx = EditorLspTestContext::new_rust(
 741            lsp::ServerCapabilities {
 742                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 743                ..Default::default()
 744            },
 745            cx,
 746        )
 747        .await;
 748
 749        cx.set_state(indoc! {"
 750            fn ˇtest() { do_work(); }
 751            fn do_work() { test(); }
 752        "});
 753
 754        // Basic hold cmd, expect highlight in region if response contains definition
 755        let hover_point = cx.display_point(indoc! {"
 756            fn test() { do_wˇork(); }
 757            fn do_work() { test(); }
 758        "});
 759        let symbol_range = cx.lsp_range(indoc! {"
 760            fn test() { «do_work»(); }
 761            fn do_work() { test(); }
 762        "});
 763        let target_range = cx.lsp_range(indoc! {"
 764            fn test() { do_work(); }
 765            fn «do_work»() { test(); }
 766        "});
 767
 768        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
 769            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
 770                lsp::LocationLink {
 771                    origin_selection_range: Some(symbol_range),
 772                    target_uri: url.clone(),
 773                    target_range,
 774                    target_selection_range: target_range,
 775                },
 776            ])))
 777        });
 778
 779        cx.update_editor(|editor, cx| {
 780            update_go_to_definition_link(
 781                editor,
 782                Some(GoToDefinitionTrigger::Text(hover_point)),
 783                true,
 784                false,
 785                cx,
 786            );
 787        });
 788        requests.next().await;
 789        cx.foreground().run_until_parked();
 790        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 791            fn test() { «do_work»(); }
 792            fn do_work() { test(); }
 793        "});
 794
 795        // Unpress cmd causes highlight to go away
 796        cx.update_editor(|editor, cx| {
 797            editor.modifiers_changed(&Default::default(), cx);
 798        });
 799
 800        // Assert no link highlights
 801        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 802            fn test() { do_work(); }
 803            fn do_work() { test(); }
 804        "});
 805
 806        // Response without source range still highlights word
 807        cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
 808        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
 809            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
 810                lsp::LocationLink {
 811                    // No origin range
 812                    origin_selection_range: None,
 813                    target_uri: url.clone(),
 814                    target_range,
 815                    target_selection_range: target_range,
 816                },
 817            ])))
 818        });
 819        cx.update_editor(|editor, cx| {
 820            update_go_to_definition_link(
 821                editor,
 822                Some(GoToDefinitionTrigger::Text(hover_point)),
 823                true,
 824                false,
 825                cx,
 826            );
 827        });
 828        requests.next().await;
 829        cx.foreground().run_until_parked();
 830
 831        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 832            fn test() { «do_work»(); }
 833            fn do_work() { test(); }
 834        "});
 835
 836        // Moving mouse to location with no response dismisses highlight
 837        let hover_point = cx.display_point(indoc! {"
 838            fˇn test() { do_work(); }
 839            fn do_work() { test(); }
 840        "});
 841        let mut requests = cx
 842            .lsp
 843            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
 844                // No definitions returned
 845                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
 846            });
 847        cx.update_editor(|editor, cx| {
 848            update_go_to_definition_link(
 849                editor,
 850                Some(GoToDefinitionTrigger::Text(hover_point)),
 851                true,
 852                false,
 853                cx,
 854            );
 855        });
 856        requests.next().await;
 857        cx.foreground().run_until_parked();
 858
 859        // Assert no link highlights
 860        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 861            fn test() { do_work(); }
 862            fn do_work() { test(); }
 863        "});
 864
 865        // Move mouse without cmd and then pressing cmd triggers highlight
 866        let hover_point = cx.display_point(indoc! {"
 867            fn test() { do_work(); }
 868            fn do_work() { teˇst(); }
 869        "});
 870        cx.update_editor(|editor, cx| {
 871            update_go_to_definition_link(
 872                editor,
 873                Some(GoToDefinitionTrigger::Text(hover_point)),
 874                false,
 875                false,
 876                cx,
 877            );
 878        });
 879        cx.foreground().run_until_parked();
 880
 881        // Assert no link highlights
 882        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 883            fn test() { do_work(); }
 884            fn do_work() { test(); }
 885        "});
 886
 887        let symbol_range = cx.lsp_range(indoc! {"
 888            fn test() { do_work(); }
 889            fn do_work() { «test»(); }
 890        "});
 891        let target_range = cx.lsp_range(indoc! {"
 892            fn «test»() { do_work(); }
 893            fn do_work() { test(); }
 894        "});
 895
 896        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
 897            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
 898                lsp::LocationLink {
 899                    origin_selection_range: Some(symbol_range),
 900                    target_uri: url,
 901                    target_range,
 902                    target_selection_range: target_range,
 903                },
 904            ])))
 905        });
 906        cx.update_editor(|editor, cx| {
 907            editor.modifiers_changed(
 908                &ModifiersChangedEvent {
 909                    modifiers: Modifiers {
 910                        cmd: true,
 911                        ..Default::default()
 912                    },
 913                },
 914                cx,
 915            );
 916        });
 917        requests.next().await;
 918        cx.foreground().run_until_parked();
 919
 920        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 921            fn test() { do_work(); }
 922            fn do_work() { «test»(); }
 923        "});
 924
 925        // Deactivating the window dismisses the highlight
 926        cx.update_workspace(|workspace, cx| {
 927            workspace.on_window_activation_changed(false, cx);
 928        });
 929        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 930            fn test() { do_work(); }
 931            fn do_work() { test(); }
 932        "});
 933
 934        // Moving the mouse restores the highlights.
 935        cx.update_editor(|editor, cx| {
 936            update_go_to_definition_link(
 937                editor,
 938                Some(GoToDefinitionTrigger::Text(hover_point)),
 939                true,
 940                false,
 941                cx,
 942            );
 943        });
 944        cx.foreground().run_until_parked();
 945        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 946            fn test() { do_work(); }
 947            fn do_work() { «test»(); }
 948        "});
 949
 950        // Moving again within the same symbol range doesn't re-request
 951        let hover_point = cx.display_point(indoc! {"
 952            fn test() { do_work(); }
 953            fn do_work() { tesˇt(); }
 954        "});
 955        cx.update_editor(|editor, cx| {
 956            update_go_to_definition_link(
 957                editor,
 958                Some(GoToDefinitionTrigger::Text(hover_point)),
 959                true,
 960                false,
 961                cx,
 962            );
 963        });
 964        cx.foreground().run_until_parked();
 965        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 966            fn test() { do_work(); }
 967            fn do_work() { «test»(); }
 968        "});
 969
 970        // Cmd click with existing definition doesn't re-request and dismisses highlight
 971        cx.update_editor(|editor, cx| {
 972            go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
 973        });
 974        // Assert selection moved to to definition
 975        cx.lsp
 976            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
 977                // Empty definition response to make sure we aren't hitting the lsp and using
 978                // the cached location instead
 979                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
 980            });
 981        cx.foreground().run_until_parked();
 982        cx.assert_editor_state(indoc! {"
 983            fn «testˇ»() { do_work(); }
 984            fn do_work() { test(); }
 985        "});
 986
 987        // Assert no link highlights after jump
 988        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
 989            fn test() { do_work(); }
 990            fn do_work() { test(); }
 991        "});
 992
 993        // Cmd click without existing definition requests and jumps
 994        let hover_point = cx.display_point(indoc! {"
 995            fn test() { do_wˇork(); }
 996            fn do_work() { test(); }
 997        "});
 998        let target_range = cx.lsp_range(indoc! {"
 999            fn test() { do_work(); }
1000            fn «do_work»() { test(); }
1001        "});
1002
1003        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
1004            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
1005                lsp::LocationLink {
1006                    origin_selection_range: None,
1007                    target_uri: url,
1008                    target_range,
1009                    target_selection_range: target_range,
1010                },
1011            ])))
1012        });
1013        cx.update_editor(|editor, cx| {
1014            go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
1015        });
1016        requests.next().await;
1017        cx.foreground().run_until_parked();
1018        cx.assert_editor_state(indoc! {"
1019            fn test() { do_work(); }
1020            fn «do_workˇ»() { test(); }
1021        "});
1022
1023        // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
1024        // 2. Selection is completed, hovering
1025        let hover_point = cx.display_point(indoc! {"
1026            fn test() { do_wˇork(); }
1027            fn do_work() { test(); }
1028        "});
1029        let target_range = cx.lsp_range(indoc! {"
1030            fn test() { do_work(); }
1031            fn «do_work»() { test(); }
1032        "});
1033        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
1034            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
1035                lsp::LocationLink {
1036                    origin_selection_range: None,
1037                    target_uri: url,
1038                    target_range,
1039                    target_selection_range: target_range,
1040                },
1041            ])))
1042        });
1043
1044        // create a pending selection
1045        let selection_range = cx.ranges(indoc! {"
1046            fn «test() { do_w»ork(); }
1047            fn do_work() { test(); }
1048        "})[0]
1049            .clone();
1050        cx.update_editor(|editor, cx| {
1051            let snapshot = editor.buffer().read(cx).snapshot(cx);
1052            let anchor_range = snapshot.anchor_before(selection_range.start)
1053                ..snapshot.anchor_after(selection_range.end);
1054            editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
1055                s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
1056            });
1057        });
1058        cx.update_editor(|editor, cx| {
1059            update_go_to_definition_link(
1060                editor,
1061                Some(GoToDefinitionTrigger::Text(hover_point)),
1062                true,
1063                false,
1064                cx,
1065            );
1066        });
1067        cx.foreground().run_until_parked();
1068        assert!(requests.try_next().is_err());
1069        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
1070            fn test() { do_work(); }
1071            fn do_work() { test(); }
1072        "});
1073        cx.foreground().run_until_parked();
1074    }
1075
1076    #[gpui::test]
1077    async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
1078        init_test(cx, |settings| {
1079            settings.defaults.inlay_hints = Some(InlayHintSettings {
1080                enabled: true,
1081                show_type_hints: true,
1082                show_parameter_hints: true,
1083                show_other_hints: true,
1084            })
1085        });
1086
1087        let mut cx = EditorLspTestContext::new_rust(
1088            lsp::ServerCapabilities {
1089                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1090                ..Default::default()
1091            },
1092            cx,
1093        )
1094        .await;
1095        cx.set_state(indoc! {"
1096            struct TestStruct;
1097
1098            fn main() {
1099                let variableˇ = TestStruct;
1100            }
1101        "});
1102        let hint_start_offset = cx.ranges(indoc! {"
1103            struct TestStruct;
1104
1105            fn main() {
1106                let variableˇ = TestStruct;
1107            }
1108        "})[0]
1109            .start;
1110        let hint_position = cx.to_lsp(hint_start_offset);
1111        let target_range = cx.lsp_range(indoc! {"
1112            struct «TestStruct»;
1113
1114            fn main() {
1115                let variable = TestStruct;
1116            }
1117        "});
1118
1119        let expected_uri = cx.buffer_lsp_url.clone();
1120        let hint_label = ": TestStruct";
1121        cx.lsp
1122            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1123                let expected_uri = expected_uri.clone();
1124                async move {
1125                    assert_eq!(params.text_document.uri, expected_uri);
1126                    Ok(Some(vec![lsp::InlayHint {
1127                        position: hint_position,
1128                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1129                            value: hint_label.to_string(),
1130                            location: Some(lsp::Location {
1131                                uri: params.text_document.uri,
1132                                range: target_range,
1133                            }),
1134                            ..Default::default()
1135                        }]),
1136                        kind: Some(lsp::InlayHintKind::TYPE),
1137                        text_edits: None,
1138                        tooltip: None,
1139                        padding_left: Some(false),
1140                        padding_right: Some(false),
1141                        data: None,
1142                    }]))
1143                }
1144            })
1145            .next()
1146            .await;
1147        cx.foreground().run_until_parked();
1148        cx.update_editor(|editor, cx| {
1149            let expected_layers = vec![hint_label.to_string()];
1150            assert_eq!(expected_layers, cached_hint_labels(editor));
1151            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1152        });
1153
1154        let inlay_range = cx
1155            .ranges(indoc! {"
1156            struct TestStruct;
1157
1158            fn main() {
1159                let variable« »= TestStruct;
1160            }
1161        "})
1162            .get(0)
1163            .cloned()
1164            .unwrap();
1165        let hint_hover_position = cx.update_editor(|editor, cx| {
1166            let snapshot = editor.snapshot(cx);
1167            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1168            let next_valid = inlay_range.end.to_display_point(&snapshot);
1169            assert_eq!(previous_valid.row(), next_valid.row());
1170            assert!(previous_valid.column() < next_valid.column());
1171            let exact_unclipped = DisplayPoint::new(
1172                previous_valid.row(),
1173                previous_valid.column() + (hint_label.len() / 2) as u32,
1174            );
1175            PointForPosition {
1176                previous_valid,
1177                next_valid,
1178                exact_unclipped,
1179                column_overshoot_after_line_end: 0,
1180            }
1181        });
1182        // Press cmd to trigger highlight
1183        cx.update_editor(|editor, cx| {
1184            update_inlay_link_and_hover_points(
1185                &editor.snapshot(cx),
1186                hint_hover_position,
1187                editor,
1188                true,
1189                false,
1190                cx,
1191            );
1192        });
1193        cx.foreground().run_until_parked();
1194        cx.update_editor(|editor, cx| {
1195            let snapshot = editor.snapshot(cx);
1196            let actual_highlights = snapshot
1197                .inlay_highlights::<LinkGoToDefinitionState>()
1198                .into_iter()
1199                .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
1200                .collect::<Vec<_>>();
1201
1202            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1203            let expected_highlight = InlayHighlight {
1204                inlay: InlayId::Hint(0),
1205                inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1206                range: 0..hint_label.len(),
1207            };
1208            assert_set_eq!(actual_highlights, vec![&expected_highlight]);
1209        });
1210
1211        // Unpress cmd causes highlight to go away
1212        cx.update_editor(|editor, cx| {
1213            editor.modifiers_changed(
1214                &platform::ModifiersChangedEvent {
1215                    modifiers: Modifiers {
1216                        cmd: false,
1217                        ..Default::default()
1218                    },
1219                    ..Default::default()
1220                },
1221                cx,
1222            );
1223        });
1224        // Assert no link highlights
1225        cx.update_editor(|editor, cx| {
1226            let snapshot = editor.snapshot(cx);
1227            let actual_ranges = snapshot
1228                .text_highlight_ranges::<LinkGoToDefinitionState>()
1229                .map(|ranges| ranges.as_ref().clone().1)
1230                .unwrap_or_default();
1231
1232            assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
1233        });
1234
1235        // Cmd+click without existing definition requests and jumps
1236        cx.update_editor(|editor, cx| {
1237            editor.modifiers_changed(
1238                &platform::ModifiersChangedEvent {
1239                    modifiers: Modifiers {
1240                        cmd: true,
1241                        ..Default::default()
1242                    },
1243                    ..Default::default()
1244                },
1245                cx,
1246            );
1247            update_inlay_link_and_hover_points(
1248                &editor.snapshot(cx),
1249                hint_hover_position,
1250                editor,
1251                true,
1252                false,
1253                cx,
1254            );
1255        });
1256        cx.foreground().run_until_parked();
1257        cx.update_editor(|editor, cx| {
1258            go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
1259        });
1260        cx.foreground().run_until_parked();
1261        cx.assert_editor_state(indoc! {"
1262            struct «TestStructˇ»;
1263
1264            fn main() {
1265                let variable = TestStruct;
1266            }
1267        "});
1268    }
1269}