link_go_to_definition.rs

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