link_go_to_definition.rs

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