hover_links.rs

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