hover_links.rs

   1use crate::{
   2    hover_popover::{self, InlayHover},
   3    scroll::ScrollAmount,
   4    Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition,
   5    GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase,
   6};
   7use gpui::{px, AppContext, AsyncWindowContext, Model, Modifiers, Task, ViewContext};
   8use language::{Bias, ToOffset};
   9use linkify::{LinkFinder, LinkKind};
  10use lsp::LanguageServerId;
  11use project::{
  12    HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project,
  13    ResolveState, ResolvedPath,
  14};
  15use std::ops::Range;
  16use theme::ActiveTheme as _;
  17use util::{maybe, ResultExt, TryFutureExt as _};
  18
  19#[derive(Debug)]
  20pub struct HoveredLinkState {
  21    pub last_trigger_point: TriggerPoint,
  22    pub preferred_kind: GotoDefinitionKind,
  23    pub symbol_range: Option<RangeInEditor>,
  24    pub links: Vec<HoverLink>,
  25    pub task: Option<Task<Option<()>>>,
  26}
  27
  28#[derive(Debug, Eq, PartialEq, Clone)]
  29pub enum RangeInEditor {
  30    Text(Range<Anchor>),
  31    Inlay(InlayHighlight),
  32}
  33
  34impl RangeInEditor {
  35    pub fn as_text_range(&self) -> Option<Range<Anchor>> {
  36        match self {
  37            Self::Text(range) => Some(range.clone()),
  38            Self::Inlay(_) => None,
  39        }
  40    }
  41
  42    pub fn point_within_range(
  43        &self,
  44        trigger_point: &TriggerPoint,
  45        snapshot: &EditorSnapshot,
  46    ) -> bool {
  47        match (self, trigger_point) {
  48            (Self::Text(range), TriggerPoint::Text(point)) => {
  49                let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
  50                point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge()
  51            }
  52            (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => {
  53                highlight.inlay == point.inlay
  54                    && highlight.range.contains(&point.range.start)
  55                    && highlight.range.contains(&point.range.end)
  56            }
  57            (Self::Inlay(_), TriggerPoint::Text(_))
  58            | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
  59        }
  60    }
  61}
  62
  63#[derive(Debug, Clone)]
  64pub enum HoverLink {
  65    Url(String),
  66    File(ResolvedPath),
  67    Text(LocationLink),
  68    InlayHint(lsp::Location, LanguageServerId),
  69}
  70
  71#[derive(Debug, Clone, PartialEq, Eq)]
  72pub(crate) struct InlayHighlight {
  73    pub inlay: InlayId,
  74    pub inlay_position: Anchor,
  75    pub range: Range<usize>,
  76}
  77
  78#[derive(Debug, Clone, PartialEq)]
  79pub enum TriggerPoint {
  80    Text(Anchor),
  81    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
  82}
  83
  84impl TriggerPoint {
  85    fn anchor(&self) -> &Anchor {
  86        match self {
  87            TriggerPoint::Text(anchor) => anchor,
  88            TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position,
  89        }
  90    }
  91}
  92
  93pub fn exclude_link_to_position(
  94    buffer: &Model<language::Buffer>,
  95    current_position: &text::Anchor,
  96    location: &LocationLink,
  97    cx: &AppContext,
  98) -> bool {
  99    // Exclude definition links that points back to cursor position.
 100    // (i.e., currently cursor upon definition).
 101    let snapshot = buffer.read(cx).snapshot();
 102    !(buffer == &location.target.buffer
 103        && current_position
 104            .bias_right(&snapshot)
 105            .cmp(&location.target.range.start, &snapshot)
 106            .is_ge()
 107        && current_position
 108            .cmp(&location.target.range.end, &snapshot)
 109            .is_le())
 110}
 111
 112impl Editor {
 113    pub(crate) fn update_hovered_link(
 114        &mut self,
 115        point_for_position: PointForPosition,
 116        snapshot: &EditorSnapshot,
 117        modifiers: Modifiers,
 118        cx: &mut ViewContext<Self>,
 119    ) {
 120        if !modifiers.secondary() || self.has_pending_selection() {
 121            self.hide_hovered_link(cx);
 122            return;
 123        }
 124
 125        match point_for_position.as_valid() {
 126            Some(point) => {
 127                let trigger_point = TriggerPoint::Text(
 128                    snapshot
 129                        .buffer_snapshot
 130                        .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)),
 131                );
 132
 133                show_link_definition(modifiers.shift, self, trigger_point, snapshot, cx);
 134            }
 135            None => {
 136                update_inlay_link_and_hover_points(
 137                    snapshot,
 138                    point_for_position,
 139                    self,
 140                    modifiers.secondary(),
 141                    modifiers.shift,
 142                    cx,
 143                );
 144            }
 145        }
 146    }
 147
 148    pub(crate) fn hide_hovered_link(&mut self, cx: &mut ViewContext<Self>) {
 149        self.hovered_link_state.take();
 150        self.clear_highlights::<HoveredLinkState>(cx);
 151    }
 152
 153    pub(crate) fn handle_click_hovered_link(
 154        &mut self,
 155        point: PointForPosition,
 156        modifiers: Modifiers,
 157        cx: &mut ViewContext<Editor>,
 158    ) {
 159        let reveal_task = self.cmd_click_reveal_task(point, modifiers, cx);
 160        cx.spawn(|editor, mut cx| async move {
 161            let definition_revealed = reveal_task.await.log_err().unwrap_or(Navigated::No);
 162            let find_references = editor
 163                .update(&mut cx, |editor, cx| {
 164                    if definition_revealed == Navigated::Yes {
 165                        return None;
 166                    }
 167                    editor.find_all_references(&FindAllReferences, cx)
 168                })
 169                .ok()
 170                .flatten();
 171            if let Some(find_references) = find_references {
 172                find_references.await.log_err();
 173            }
 174        })
 175        .detach();
 176    }
 177
 178    pub fn scroll_hover(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) -> bool {
 179        let selection = self.selections.newest_anchor().head();
 180        let snapshot = self.snapshot(cx);
 181
 182        let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
 183            popover
 184                .symbol_range
 185                .point_within_range(&TriggerPoint::Text(selection), &snapshot)
 186        }) else {
 187            return false;
 188        };
 189        popover.scroll(amount, cx);
 190        true
 191    }
 192
 193    fn cmd_click_reveal_task(
 194        &mut self,
 195        point: PointForPosition,
 196        modifiers: Modifiers,
 197        cx: &mut ViewContext<Editor>,
 198    ) -> Task<anyhow::Result<Navigated>> {
 199        if let Some(hovered_link_state) = self.hovered_link_state.take() {
 200            self.hide_hovered_link(cx);
 201            if !hovered_link_state.links.is_empty() {
 202                if !self.focus_handle.is_focused(cx) {
 203                    cx.focus(&self.focus_handle);
 204                }
 205
 206                // exclude links pointing back to the current anchor
 207                let current_position = point
 208                    .next_valid
 209                    .to_point(&self.snapshot(cx).display_snapshot);
 210                let Some((buffer, anchor)) = self
 211                    .buffer()
 212                    .read(cx)
 213                    .text_anchor_for_position(current_position, cx)
 214                else {
 215                    return Task::ready(Ok(Navigated::No));
 216                };
 217                let links = hovered_link_state
 218                    .links
 219                    .into_iter()
 220                    .filter(|link| {
 221                        if let HoverLink::Text(location) = link {
 222                            exclude_link_to_position(&buffer, &anchor, location, cx)
 223                        } else {
 224                            true
 225                        }
 226                    })
 227                    .collect();
 228
 229                return self.navigate_to_hover_links(None, links, modifiers.alt, cx);
 230            }
 231        }
 232
 233        // We don't have the correct kind of link cached, set the selection on
 234        // click and immediately trigger GoToDefinition.
 235        self.select(
 236            SelectPhase::Begin {
 237                position: point.next_valid,
 238                add: false,
 239                click_count: 1,
 240            },
 241            cx,
 242        );
 243
 244        if point.as_valid().is_some() {
 245            if modifiers.shift {
 246                self.go_to_type_definition(&GoToTypeDefinition, cx)
 247            } else {
 248                self.go_to_definition(&GoToDefinition, cx)
 249            }
 250        } else {
 251            Task::ready(Ok(Navigated::No))
 252        }
 253    }
 254}
 255
 256pub fn update_inlay_link_and_hover_points(
 257    snapshot: &EditorSnapshot,
 258    point_for_position: PointForPosition,
 259    editor: &mut Editor,
 260    secondary_held: bool,
 261    shift_held: bool,
 262    cx: &mut ViewContext<'_, Editor>,
 263) {
 264    let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
 265        Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
 266    } else {
 267        None
 268    };
 269    let mut go_to_definition_updated = false;
 270    let mut hover_updated = false;
 271    if let Some(hovered_offset) = hovered_offset {
 272        let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
 273        let previous_valid_anchor = buffer_snapshot.anchor_at(
 274            point_for_position.previous_valid.to_point(snapshot),
 275            Bias::Left,
 276        );
 277        let next_valid_anchor = buffer_snapshot.anchor_at(
 278            point_for_position.next_valid.to_point(snapshot),
 279            Bias::Right,
 280        );
 281        if let Some(hovered_hint) = editor
 282            .visible_inlay_hints(cx)
 283            .into_iter()
 284            .skip_while(|hint| {
 285                hint.position
 286                    .cmp(&previous_valid_anchor, &buffer_snapshot)
 287                    .is_lt()
 288            })
 289            .take_while(|hint| {
 290                hint.position
 291                    .cmp(&next_valid_anchor, &buffer_snapshot)
 292                    .is_le()
 293            })
 294            .max_by_key(|hint| hint.id)
 295        {
 296            let inlay_hint_cache = editor.inlay_hint_cache();
 297            let excerpt_id = previous_valid_anchor.excerpt_id;
 298            if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
 299                match cached_hint.resolve_state {
 300                    ResolveState::CanResolve(_, _) => {
 301                        if let Some(buffer_id) = previous_valid_anchor.buffer_id {
 302                            inlay_hint_cache.spawn_hint_resolve(
 303                                buffer_id,
 304                                excerpt_id,
 305                                hovered_hint.id,
 306                                cx,
 307                            );
 308                        }
 309                    }
 310                    ResolveState::Resolved => {
 311                        let mut extra_shift_left = 0;
 312                        let mut extra_shift_right = 0;
 313                        if cached_hint.padding_left {
 314                            extra_shift_left += 1;
 315                            extra_shift_right += 1;
 316                        }
 317                        if cached_hint.padding_right {
 318                            extra_shift_right += 1;
 319                        }
 320                        match cached_hint.label {
 321                            project::InlayHintLabel::String(_) => {
 322                                if let Some(tooltip) = cached_hint.tooltip {
 323                                    hover_popover::hover_at_inlay(
 324                                        editor,
 325                                        InlayHover {
 326                                            tooltip: match tooltip {
 327                                                InlayHintTooltip::String(text) => HoverBlock {
 328                                                    text,
 329                                                    kind: HoverBlockKind::PlainText,
 330                                                },
 331                                                InlayHintTooltip::MarkupContent(content) => {
 332                                                    HoverBlock {
 333                                                        text: content.value,
 334                                                        kind: content.kind,
 335                                                    }
 336                                                }
 337                                            },
 338                                            range: InlayHighlight {
 339                                                inlay: hovered_hint.id,
 340                                                inlay_position: hovered_hint.position,
 341                                                range: extra_shift_left
 342                                                    ..hovered_hint.text.len() + extra_shift_right,
 343                                            },
 344                                        },
 345                                        cx,
 346                                    );
 347                                    hover_updated = true;
 348                                }
 349                            }
 350                            project::InlayHintLabel::LabelParts(label_parts) => {
 351                                let hint_start =
 352                                    snapshot.anchor_to_inlay_offset(hovered_hint.position);
 353                                if let Some((hovered_hint_part, part_range)) =
 354                                    hover_popover::find_hovered_hint_part(
 355                                        label_parts,
 356                                        hint_start,
 357                                        hovered_offset,
 358                                    )
 359                                {
 360                                    let highlight_start =
 361                                        (part_range.start - hint_start).0 + extra_shift_left;
 362                                    let highlight_end =
 363                                        (part_range.end - hint_start).0 + extra_shift_right;
 364                                    let highlight = InlayHighlight {
 365                                        inlay: hovered_hint.id,
 366                                        inlay_position: hovered_hint.position,
 367                                        range: highlight_start..highlight_end,
 368                                    };
 369                                    if let Some(tooltip) = hovered_hint_part.tooltip {
 370                                        hover_popover::hover_at_inlay(
 371                                            editor,
 372                                            InlayHover {
 373                                                tooltip: match tooltip {
 374                                                    InlayHintLabelPartTooltip::String(text) => {
 375                                                        HoverBlock {
 376                                                            text,
 377                                                            kind: HoverBlockKind::PlainText,
 378                                                        }
 379                                                    }
 380                                                    InlayHintLabelPartTooltip::MarkupContent(
 381                                                        content,
 382                                                    ) => HoverBlock {
 383                                                        text: content.value,
 384                                                        kind: content.kind,
 385                                                    },
 386                                                },
 387                                                range: highlight.clone(),
 388                                            },
 389                                            cx,
 390                                        );
 391                                        hover_updated = true;
 392                                    }
 393                                    if let Some((language_server_id, location)) =
 394                                        hovered_hint_part.location
 395                                    {
 396                                        if secondary_held
 397                                            && !editor.has_pending_nonempty_selection()
 398                                        {
 399                                            go_to_definition_updated = true;
 400                                            show_link_definition(
 401                                                shift_held,
 402                                                editor,
 403                                                TriggerPoint::InlayHint(
 404                                                    highlight,
 405                                                    location,
 406                                                    language_server_id,
 407                                                ),
 408                                                snapshot,
 409                                                cx,
 410                                            );
 411                                        }
 412                                    }
 413                                }
 414                            }
 415                        };
 416                    }
 417                    ResolveState::Resolving => {}
 418                }
 419            }
 420        }
 421    }
 422
 423    if !go_to_definition_updated {
 424        editor.hide_hovered_link(cx)
 425    }
 426    if !hover_updated {
 427        hover_popover::hover_at(editor, None, cx);
 428    }
 429}
 430
 431pub fn show_link_definition(
 432    shift_held: bool,
 433    editor: &mut Editor,
 434    trigger_point: TriggerPoint,
 435    snapshot: &EditorSnapshot,
 436    cx: &mut ViewContext<Editor>,
 437) {
 438    let preferred_kind = match trigger_point {
 439        TriggerPoint::Text(_) if !shift_held => GotoDefinitionKind::Symbol,
 440        _ => GotoDefinitionKind::Type,
 441    };
 442
 443    let (mut hovered_link_state, is_cached) =
 444        if let Some(existing) = editor.hovered_link_state.take() {
 445            (existing, true)
 446        } else {
 447            (
 448                HoveredLinkState {
 449                    last_trigger_point: trigger_point.clone(),
 450                    symbol_range: None,
 451                    preferred_kind,
 452                    links: vec![],
 453                    task: None,
 454                },
 455                false,
 456            )
 457        };
 458
 459    if editor.pending_rename.is_some() {
 460        return;
 461    }
 462
 463    let trigger_anchor = trigger_point.anchor();
 464    let Some((buffer, buffer_position)) = editor
 465        .buffer
 466        .read(cx)
 467        .text_anchor_for_position(*trigger_anchor, cx)
 468    else {
 469        return;
 470    };
 471
 472    let Some((excerpt_id, _, _)) = editor
 473        .buffer()
 474        .read(cx)
 475        .excerpt_containing(*trigger_anchor, cx)
 476    else {
 477        return;
 478    };
 479
 480    let same_kind = hovered_link_state.preferred_kind == preferred_kind
 481        || hovered_link_state
 482            .links
 483            .first()
 484            .is_some_and(|d| matches!(d, HoverLink::Url(_)));
 485
 486    if same_kind {
 487        if is_cached && (hovered_link_state.last_trigger_point == trigger_point)
 488            || hovered_link_state
 489                .symbol_range
 490                .as_ref()
 491                .is_some_and(|symbol_range| {
 492                    symbol_range.point_within_range(&trigger_point, snapshot)
 493                })
 494        {
 495            editor.hovered_link_state = Some(hovered_link_state);
 496            return;
 497        }
 498    } else {
 499        editor.hide_hovered_link(cx)
 500    }
 501    let project = editor.project.clone();
 502    let provider = editor.semantics_provider.clone();
 503
 504    let snapshot = snapshot.buffer_snapshot.clone();
 505    hovered_link_state.task = Some(cx.spawn(|this, mut cx| {
 506        async move {
 507            let result = match &trigger_point {
 508                TriggerPoint::Text(_) => {
 509                    if let Some((url_range, url)) = find_url(&buffer, buffer_position, cx.clone()) {
 510                        this.update(&mut cx, |_, _| {
 511                            let range = maybe!({
 512                                let start =
 513                                    snapshot.anchor_in_excerpt(excerpt_id, url_range.start)?;
 514                                let end = snapshot.anchor_in_excerpt(excerpt_id, url_range.end)?;
 515                                Some(RangeInEditor::Text(start..end))
 516                            });
 517                            (range, vec![HoverLink::Url(url)])
 518                        })
 519                        .ok()
 520                    } else if let Some((filename_range, filename)) =
 521                        find_file(&buffer, project.clone(), buffer_position, &mut cx).await
 522                    {
 523                        let range = maybe!({
 524                            let start =
 525                                snapshot.anchor_in_excerpt(excerpt_id, filename_range.start)?;
 526                            let end = snapshot.anchor_in_excerpt(excerpt_id, filename_range.end)?;
 527                            Some(RangeInEditor::Text(start..end))
 528                        });
 529
 530                        Some((range, vec![HoverLink::File(filename)]))
 531                    } else if let Some(provider) = provider {
 532                        let task = cx.update(|cx| {
 533                            provider.definitions(&buffer, buffer_position, preferred_kind, cx)
 534                        })?;
 535                        if let Some(task) = task {
 536                            task.await.ok().map(|definition_result| {
 537                                (
 538                                    definition_result.iter().find_map(|link| {
 539                                        link.origin.as_ref().and_then(|origin| {
 540                                            let start = snapshot.anchor_in_excerpt(
 541                                                excerpt_id,
 542                                                origin.range.start,
 543                                            )?;
 544                                            let end = snapshot
 545                                                .anchor_in_excerpt(excerpt_id, origin.range.end)?;
 546                                            Some(RangeInEditor::Text(start..end))
 547                                        })
 548                                    }),
 549                                    definition_result.into_iter().map(HoverLink::Text).collect(),
 550                                )
 551                            })
 552                        } else {
 553                            None
 554                        }
 555                    } else {
 556                        None
 557                    }
 558                }
 559                TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
 560                    Some(RangeInEditor::Inlay(highlight.clone())),
 561                    vec![HoverLink::InlayHint(lsp_location.clone(), *server_id)],
 562                )),
 563            };
 564
 565            this.update(&mut cx, |editor, cx| {
 566                // Clear any existing highlights
 567                editor.clear_highlights::<HoveredLinkState>(cx);
 568                let Some(hovered_link_state) = editor.hovered_link_state.as_mut() else {
 569                    editor.hide_hovered_link(cx);
 570                    return;
 571                };
 572                hovered_link_state.preferred_kind = preferred_kind;
 573                hovered_link_state.symbol_range = result
 574                    .as_ref()
 575                    .and_then(|(symbol_range, _)| symbol_range.clone());
 576
 577                if let Some((symbol_range, definitions)) = result {
 578                    hovered_link_state.links = definitions;
 579
 580                    let underline_hovered_link = !hovered_link_state.links.is_empty()
 581                        || hovered_link_state.symbol_range.is_some();
 582
 583                    if underline_hovered_link {
 584                        let style = gpui::HighlightStyle {
 585                            underline: Some(gpui::UnderlineStyle {
 586                                thickness: px(1.),
 587                                ..Default::default()
 588                            }),
 589                            color: Some(cx.theme().colors().link_text_hover),
 590                            ..Default::default()
 591                        };
 592                        let highlight_range =
 593                            symbol_range.unwrap_or_else(|| match &trigger_point {
 594                                TriggerPoint::Text(trigger_anchor) => {
 595                                    // If no symbol range returned from language server, use the surrounding word.
 596                                    let (offset_range, _) =
 597                                        snapshot.surrounding_word(*trigger_anchor, false);
 598                                    RangeInEditor::Text(
 599                                        snapshot.anchor_before(offset_range.start)
 600                                            ..snapshot.anchor_after(offset_range.end),
 601                                    )
 602                                }
 603                                TriggerPoint::InlayHint(highlight, _, _) => {
 604                                    RangeInEditor::Inlay(highlight.clone())
 605                                }
 606                            });
 607
 608                        match highlight_range {
 609                            RangeInEditor::Text(text_range) => editor
 610                                .highlight_text::<HoveredLinkState>(vec![text_range], style, cx),
 611                            RangeInEditor::Inlay(highlight) => editor
 612                                .highlight_inlays::<HoveredLinkState>(vec![highlight], style, cx),
 613                        }
 614                    }
 615                } else {
 616                    editor.hide_hovered_link(cx);
 617                }
 618            })?;
 619
 620            Ok::<_, anyhow::Error>(())
 621        }
 622        .log_err()
 623    }));
 624
 625    editor.hovered_link_state = Some(hovered_link_state);
 626}
 627
 628pub(crate) fn find_url(
 629    buffer: &Model<language::Buffer>,
 630    position: text::Anchor,
 631    mut cx: AsyncWindowContext,
 632) -> Option<(Range<text::Anchor>, String)> {
 633    const LIMIT: usize = 2048;
 634
 635    let Ok(snapshot) = buffer.update(&mut cx, |buffer, _| buffer.snapshot()) else {
 636        return None;
 637    };
 638
 639    let offset = position.to_offset(&snapshot);
 640    let mut token_start = offset;
 641    let mut token_end = offset;
 642    let mut found_start = false;
 643    let mut found_end = false;
 644
 645    for ch in snapshot.reversed_chars_at(offset).take(LIMIT) {
 646        if ch.is_whitespace() {
 647            found_start = true;
 648            break;
 649        }
 650        token_start -= ch.len_utf8();
 651    }
 652    // Check if we didn't find the starting whitespace or if we didn't reach the start of the buffer
 653    if !found_start && token_start != 0 {
 654        return None;
 655    }
 656
 657    for ch in snapshot
 658        .chars_at(offset)
 659        .take(LIMIT - (offset - token_start))
 660    {
 661        if ch.is_whitespace() {
 662            found_end = true;
 663            break;
 664        }
 665        token_end += ch.len_utf8();
 666    }
 667    // Check if we didn't find the ending whitespace or if we read more or equal than LIMIT
 668    // which at this point would happen only if we reached the end of buffer
 669    if !found_end && (token_end - token_start >= LIMIT) {
 670        return None;
 671    }
 672
 673    let mut finder = LinkFinder::new();
 674    finder.kinds(&[LinkKind::Url]);
 675    let input = snapshot
 676        .text_for_range(token_start..token_end)
 677        .collect::<String>();
 678
 679    let relative_offset = offset - token_start;
 680    for link in finder.links(&input) {
 681        if link.start() <= relative_offset && link.end() >= relative_offset {
 682            let range = snapshot.anchor_before(token_start + link.start())
 683                ..snapshot.anchor_after(token_start + link.end());
 684            return Some((range, link.as_str().to_string()));
 685        }
 686    }
 687    None
 688}
 689
 690pub(crate) async fn find_file(
 691    buffer: &Model<language::Buffer>,
 692    project: Option<Model<Project>>,
 693    position: text::Anchor,
 694    cx: &mut AsyncWindowContext,
 695) -> Option<(Range<text::Anchor>, ResolvedPath)> {
 696    let project = project?;
 697    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()).ok()?;
 698    let scope = snapshot.language_scope_at(position);
 699    let (range, candidate_file_path) = surrounding_filename(snapshot, position)?;
 700
 701    async fn check_path(
 702        candidate_file_path: &str,
 703        project: &Model<Project>,
 704        buffer: &Model<language::Buffer>,
 705        cx: &mut AsyncWindowContext,
 706    ) -> Option<ResolvedPath> {
 707        project
 708            .update(cx, |project, cx| {
 709                project.resolve_existing_file_path(&candidate_file_path, buffer, cx)
 710            })
 711            .ok()?
 712            .await
 713    }
 714
 715    if let Some(existing_path) = check_path(&candidate_file_path, &project, buffer, cx).await {
 716        return Some((range, existing_path));
 717    }
 718
 719    if let Some(scope) = scope {
 720        for suffix in scope.path_suffixes() {
 721            if candidate_file_path.ends_with(format!(".{suffix}").as_str()) {
 722                continue;
 723            }
 724
 725            let suffixed_candidate = format!("{candidate_file_path}.{suffix}");
 726            if let Some(existing_path) = check_path(&suffixed_candidate, &project, buffer, cx).await
 727            {
 728                return Some((range, existing_path));
 729            }
 730        }
 731    }
 732
 733    None
 734}
 735
 736fn surrounding_filename(
 737    snapshot: language::BufferSnapshot,
 738    position: text::Anchor,
 739) -> Option<(Range<text::Anchor>, String)> {
 740    const LIMIT: usize = 2048;
 741
 742    let offset = position.to_offset(&snapshot);
 743    let mut token_start = offset;
 744    let mut token_end = offset;
 745    let mut found_start = false;
 746    let mut found_end = false;
 747    let mut inside_quotes = false;
 748
 749    let mut filename = String::new();
 750
 751    let mut backwards = snapshot.reversed_chars_at(offset).take(LIMIT).peekable();
 752    while let Some(ch) = backwards.next() {
 753        // Escaped whitespace
 754        if ch.is_whitespace() && backwards.peek() == Some(&'\\') {
 755            filename.push(ch);
 756            token_start -= ch.len_utf8();
 757            backwards.next();
 758            token_start -= '\\'.len_utf8();
 759            continue;
 760        }
 761        if ch.is_whitespace() {
 762            found_start = true;
 763            break;
 764        }
 765        if (ch == '"' || ch == '\'') && !inside_quotes {
 766            found_start = true;
 767            inside_quotes = true;
 768            break;
 769        }
 770
 771        filename.push(ch);
 772        token_start -= ch.len_utf8();
 773    }
 774    if !found_start && token_start != 0 {
 775        return None;
 776    }
 777
 778    filename = filename.chars().rev().collect();
 779
 780    let mut forwards = snapshot
 781        .chars_at(offset)
 782        .take(LIMIT - (offset - token_start))
 783        .peekable();
 784    while let Some(ch) = forwards.next() {
 785        // Skip escaped whitespace
 786        if ch == '\\' && forwards.peek().map_or(false, |ch| ch.is_whitespace()) {
 787            token_end += ch.len_utf8();
 788            let whitespace = forwards.next().unwrap();
 789            token_end += whitespace.len_utf8();
 790            filename.push(whitespace);
 791            continue;
 792        }
 793
 794        if ch.is_whitespace() {
 795            found_end = true;
 796            break;
 797        }
 798        if ch == '"' || ch == '\'' {
 799            // If we're inside quotes, we stop when we come across the next quote
 800            if inside_quotes {
 801                found_end = true;
 802                break;
 803            } else {
 804                // Otherwise, we skip the quote
 805                inside_quotes = true;
 806                continue;
 807            }
 808        }
 809        filename.push(ch);
 810        token_end += ch.len_utf8();
 811    }
 812
 813    if !found_end && (token_end - token_start >= LIMIT) {
 814        return None;
 815    }
 816
 817    if filename.is_empty() {
 818        return None;
 819    }
 820
 821    let range = snapshot.anchor_before(token_start)..snapshot.anchor_after(token_end);
 822
 823    Some((range, filename))
 824}
 825
 826#[cfg(test)]
 827mod tests {
 828    use super::*;
 829    use crate::{
 830        display_map::ToDisplayPoint,
 831        editor_tests::init_test,
 832        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
 833        test::editor_lsp_test_context::EditorLspTestContext,
 834        DisplayPoint,
 835    };
 836    use futures::StreamExt;
 837    use gpui::Modifiers;
 838    use indoc::indoc;
 839    use language::language_settings::InlayHintSettings;
 840    use lsp::request::{GotoDefinition, GotoTypeDefinition};
 841    use util::assert_set_eq;
 842    use workspace::item::Item;
 843
 844    #[gpui::test]
 845    async fn test_hover_type_links(cx: &mut gpui::TestAppContext) {
 846        init_test(cx, |_| {});
 847
 848        let mut cx = EditorLspTestContext::new_rust(
 849            lsp::ServerCapabilities {
 850                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 851                type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
 852                ..Default::default()
 853            },
 854            cx,
 855        )
 856        .await;
 857
 858        cx.set_state(indoc! {"
 859            struct A;
 860            let vˇariable = A;
 861        "});
 862        let screen_coord = cx.editor(|editor, cx| editor.pixel_position_of_cursor(cx));
 863
 864        // Basic hold cmd+shift, expect highlight in region if response contains type definition
 865        let symbol_range = cx.lsp_range(indoc! {"
 866            struct A;
 867            let «variable» = A;
 868        "});
 869        let target_range = cx.lsp_range(indoc! {"
 870            struct «A»;
 871            let variable = A;
 872        "});
 873
 874        cx.run_until_parked();
 875
 876        let mut requests =
 877            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
 878                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
 879                    lsp::LocationLink {
 880                        origin_selection_range: Some(symbol_range),
 881                        target_uri: url.clone(),
 882                        target_range,
 883                        target_selection_range: target_range,
 884                    },
 885                ])))
 886            });
 887
 888        let modifiers = if cfg!(target_os = "macos") {
 889            Modifiers::command_shift()
 890        } else {
 891            Modifiers::control_shift()
 892        };
 893
 894        cx.simulate_mouse_move(screen_coord.unwrap(), None, modifiers);
 895
 896        requests.next().await;
 897        cx.run_until_parked();
 898        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
 899            struct A;
 900            let «variable» = A;
 901        "});
 902
 903        cx.simulate_modifiers_change(Modifiers::secondary_key());
 904        cx.run_until_parked();
 905        // Assert no link highlights
 906        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
 907            struct A;
 908            let variable = A;
 909        "});
 910
 911        cx.simulate_click(screen_coord.unwrap(), modifiers);
 912
 913        cx.assert_editor_state(indoc! {"
 914            struct «Aˇ»;
 915            let variable = A;
 916        "});
 917    }
 918
 919    #[gpui::test]
 920    async fn test_hover_links(cx: &mut gpui::TestAppContext) {
 921        init_test(cx, |_| {});
 922
 923        let mut cx = EditorLspTestContext::new_rust(
 924            lsp::ServerCapabilities {
 925                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 926                definition_provider: Some(lsp::OneOf::Left(true)),
 927                ..Default::default()
 928            },
 929            cx,
 930        )
 931        .await;
 932
 933        cx.set_state(indoc! {"
 934                fn ˇtest() { do_work(); }
 935                fn do_work() { test(); }
 936            "});
 937
 938        // Basic hold cmd, expect highlight in region if response contains definition
 939        let hover_point = cx.pixel_position(indoc! {"
 940                fn test() { do_wˇork(); }
 941                fn do_work() { test(); }
 942            "});
 943        let symbol_range = cx.lsp_range(indoc! {"
 944                fn test() { «do_work»(); }
 945                fn do_work() { test(); }
 946            "});
 947        let target_range = cx.lsp_range(indoc! {"
 948                fn test() { do_work(); }
 949                fn «do_work»() { test(); }
 950            "});
 951
 952        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
 953            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
 954                lsp::LocationLink {
 955                    origin_selection_range: Some(symbol_range),
 956                    target_uri: url.clone(),
 957                    target_range,
 958                    target_selection_range: target_range,
 959                },
 960            ])))
 961        });
 962
 963        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
 964        requests.next().await;
 965        cx.background_executor.run_until_parked();
 966        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
 967                fn test() { «do_work»(); }
 968                fn do_work() { test(); }
 969            "});
 970
 971        // Unpress cmd causes highlight to go away
 972        cx.simulate_modifiers_change(Modifiers::none());
 973        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
 974                fn test() { do_work(); }
 975                fn do_work() { test(); }
 976            "});
 977
 978        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
 979            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
 980                lsp::LocationLink {
 981                    origin_selection_range: Some(symbol_range),
 982                    target_uri: url.clone(),
 983                    target_range,
 984                    target_selection_range: target_range,
 985                },
 986            ])))
 987        });
 988
 989        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
 990        requests.next().await;
 991        cx.background_executor.run_until_parked();
 992        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
 993                fn test() { «do_work»(); }
 994                fn do_work() { test(); }
 995            "});
 996
 997        // Moving mouse to location with no response dismisses highlight
 998        let hover_point = cx.pixel_position(indoc! {"
 999                fˇn test() { do_work(); }
1000                fn do_work() { test(); }
1001            "});
1002        let mut requests = cx
1003            .lsp
1004            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
1005                // No definitions returned
1006                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
1007            });
1008        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
1009
1010        requests.next().await;
1011        cx.background_executor.run_until_parked();
1012
1013        // Assert no link highlights
1014        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1015                fn test() { do_work(); }
1016                fn do_work() { test(); }
1017            "});
1018
1019        // // Move mouse without cmd and then pressing cmd triggers highlight
1020        let hover_point = cx.pixel_position(indoc! {"
1021                fn test() { do_work(); }
1022                fn do_work() { teˇst(); }
1023            "});
1024        cx.simulate_mouse_move(hover_point, None, Modifiers::none());
1025
1026        // Assert no link highlights
1027        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1028                fn test() { do_work(); }
1029                fn do_work() { test(); }
1030            "});
1031
1032        let symbol_range = cx.lsp_range(indoc! {"
1033                fn test() { do_work(); }
1034                fn do_work() { «test»(); }
1035            "});
1036        let target_range = cx.lsp_range(indoc! {"
1037                fn «test»() { do_work(); }
1038                fn do_work() { test(); }
1039            "});
1040
1041        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
1042            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
1043                lsp::LocationLink {
1044                    origin_selection_range: Some(symbol_range),
1045                    target_uri: url,
1046                    target_range,
1047                    target_selection_range: target_range,
1048                },
1049            ])))
1050        });
1051
1052        cx.simulate_modifiers_change(Modifiers::secondary_key());
1053
1054        requests.next().await;
1055        cx.background_executor.run_until_parked();
1056
1057        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1058                fn test() { do_work(); }
1059                fn do_work() { «test»(); }
1060            "});
1061
1062        cx.deactivate_window();
1063        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1064                fn test() { do_work(); }
1065                fn do_work() { test(); }
1066            "});
1067
1068        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
1069        cx.background_executor.run_until_parked();
1070        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1071                fn test() { do_work(); }
1072                fn do_work() { «test»(); }
1073            "});
1074
1075        // Moving again within the same symbol range doesn't re-request
1076        let hover_point = cx.pixel_position(indoc! {"
1077                fn test() { do_work(); }
1078                fn do_work() { tesˇt(); }
1079            "});
1080        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
1081        cx.background_executor.run_until_parked();
1082        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1083                fn test() { do_work(); }
1084                fn do_work() { «test»(); }
1085            "});
1086
1087        // Cmd click with existing definition doesn't re-request and dismisses highlight
1088        cx.simulate_click(hover_point, Modifiers::secondary_key());
1089        cx.lsp
1090            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
1091                // Empty definition response to make sure we aren't hitting the lsp and using
1092                // the cached location instead
1093                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
1094            });
1095        cx.background_executor.run_until_parked();
1096        cx.assert_editor_state(indoc! {"
1097                fn «testˇ»() { do_work(); }
1098                fn do_work() { test(); }
1099            "});
1100
1101        // Assert no link highlights after jump
1102        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1103                fn test() { do_work(); }
1104                fn do_work() { test(); }
1105            "});
1106
1107        // Cmd click without existing definition requests and jumps
1108        let hover_point = cx.pixel_position(indoc! {"
1109                fn test() { do_wˇork(); }
1110                fn do_work() { test(); }
1111            "});
1112        let target_range = cx.lsp_range(indoc! {"
1113                fn test() { do_work(); }
1114                fn «do_work»() { test(); }
1115            "});
1116
1117        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
1118            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
1119                lsp::LocationLink {
1120                    origin_selection_range: None,
1121                    target_uri: url,
1122                    target_range,
1123                    target_selection_range: target_range,
1124                },
1125            ])))
1126        });
1127        cx.simulate_click(hover_point, Modifiers::secondary_key());
1128        requests.next().await;
1129        cx.background_executor.run_until_parked();
1130        cx.assert_editor_state(indoc! {"
1131                fn test() { do_work(); }
1132                fn «do_workˇ»() { test(); }
1133            "});
1134
1135        // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
1136        // 2. Selection is completed, hovering
1137        let hover_point = cx.pixel_position(indoc! {"
1138                fn test() { do_wˇork(); }
1139                fn do_work() { test(); }
1140            "});
1141        let target_range = cx.lsp_range(indoc! {"
1142                fn test() { do_work(); }
1143                fn «do_work»() { test(); }
1144            "});
1145        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
1146            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
1147                lsp::LocationLink {
1148                    origin_selection_range: None,
1149                    target_uri: url,
1150                    target_range,
1151                    target_selection_range: target_range,
1152                },
1153            ])))
1154        });
1155
1156        // create a pending selection
1157        let selection_range = cx.ranges(indoc! {"
1158                fn «test() { do_w»ork(); }
1159                fn do_work() { test(); }
1160            "})[0]
1161            .clone();
1162        cx.update_editor(|editor, cx| {
1163            let snapshot = editor.buffer().read(cx).snapshot(cx);
1164            let anchor_range = snapshot.anchor_before(selection_range.start)
1165                ..snapshot.anchor_after(selection_range.end);
1166            editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
1167                s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
1168            });
1169        });
1170        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
1171        cx.background_executor.run_until_parked();
1172        assert!(requests.try_next().is_err());
1173        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1174                fn test() { do_work(); }
1175                fn do_work() { test(); }
1176            "});
1177        cx.background_executor.run_until_parked();
1178    }
1179
1180    #[gpui::test]
1181    async fn test_inlay_hover_links(cx: &mut gpui::TestAppContext) {
1182        init_test(cx, |settings| {
1183            settings.defaults.inlay_hints = Some(InlayHintSettings {
1184                enabled: true,
1185                edit_debounce_ms: 0,
1186                scroll_debounce_ms: 0,
1187                show_type_hints: true,
1188                show_parameter_hints: true,
1189                show_other_hints: true,
1190                show_background: false,
1191            })
1192        });
1193
1194        let mut cx = EditorLspTestContext::new_rust(
1195            lsp::ServerCapabilities {
1196                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1197                ..Default::default()
1198            },
1199            cx,
1200        )
1201        .await;
1202        cx.set_state(indoc! {"
1203                struct TestStruct;
1204
1205                fn main() {
1206                    let variableˇ = TestStruct;
1207                }
1208            "});
1209        let hint_start_offset = cx.ranges(indoc! {"
1210                struct TestStruct;
1211
1212                fn main() {
1213                    let variableˇ = TestStruct;
1214                }
1215            "})[0]
1216            .start;
1217        let hint_position = cx.to_lsp(hint_start_offset);
1218        let target_range = cx.lsp_range(indoc! {"
1219                struct «TestStruct»;
1220
1221                fn main() {
1222                    let variable = TestStruct;
1223                }
1224            "});
1225
1226        let expected_uri = cx.buffer_lsp_url.clone();
1227        let hint_label = ": TestStruct";
1228        cx.lsp
1229            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1230                let expected_uri = expected_uri.clone();
1231                async move {
1232                    assert_eq!(params.text_document.uri, expected_uri);
1233                    Ok(Some(vec![lsp::InlayHint {
1234                        position: hint_position,
1235                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1236                            value: hint_label.to_string(),
1237                            location: Some(lsp::Location {
1238                                uri: params.text_document.uri,
1239                                range: target_range,
1240                            }),
1241                            ..Default::default()
1242                        }]),
1243                        kind: Some(lsp::InlayHintKind::TYPE),
1244                        text_edits: None,
1245                        tooltip: None,
1246                        padding_left: Some(false),
1247                        padding_right: Some(false),
1248                        data: None,
1249                    }]))
1250                }
1251            })
1252            .next()
1253            .await;
1254        cx.background_executor.run_until_parked();
1255        cx.update_editor(|editor, cx| {
1256            let expected_layers = vec![hint_label.to_string()];
1257            assert_eq!(expected_layers, cached_hint_labels(editor));
1258            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1259        });
1260
1261        let inlay_range = cx
1262            .ranges(indoc! {"
1263                struct TestStruct;
1264
1265                fn main() {
1266                    let variable« »= TestStruct;
1267                }
1268            "})
1269            .first()
1270            .cloned()
1271            .unwrap();
1272        let midpoint = cx.update_editor(|editor, cx| {
1273            let snapshot = editor.snapshot(cx);
1274            let previous_valid = inlay_range.start.to_display_point(&snapshot);
1275            let next_valid = inlay_range.end.to_display_point(&snapshot);
1276            assert_eq!(previous_valid.row(), next_valid.row());
1277            assert!(previous_valid.column() < next_valid.column());
1278            DisplayPoint::new(
1279                previous_valid.row(),
1280                previous_valid.column() + (hint_label.len() / 2) as u32,
1281            )
1282        });
1283        // Press cmd to trigger highlight
1284        let hover_point = cx.pixel_position_for(midpoint);
1285        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
1286        cx.background_executor.run_until_parked();
1287        cx.update_editor(|editor, cx| {
1288            let snapshot = editor.snapshot(cx);
1289            let actual_highlights = snapshot
1290                .inlay_highlights::<HoveredLinkState>()
1291                .into_iter()
1292                .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
1293                .collect::<Vec<_>>();
1294
1295            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1296            let expected_highlight = InlayHighlight {
1297                inlay: InlayId::Hint(0),
1298                inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1299                range: 0..hint_label.len(),
1300            };
1301            assert_set_eq!(actual_highlights, vec![&expected_highlight]);
1302        });
1303
1304        cx.simulate_mouse_move(hover_point, None, Modifiers::none());
1305        // Assert no link highlights
1306        cx.update_editor(|editor, cx| {
1307                let snapshot = editor.snapshot(cx);
1308                let actual_ranges = snapshot
1309                    .text_highlight_ranges::<HoveredLinkState>()
1310                    .map(|ranges| ranges.as_ref().clone().1)
1311                    .unwrap_or_default();
1312
1313                assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
1314            });
1315
1316        cx.simulate_modifiers_change(Modifiers::secondary_key());
1317        cx.background_executor.run_until_parked();
1318        cx.simulate_click(hover_point, Modifiers::secondary_key());
1319        cx.background_executor.run_until_parked();
1320        cx.assert_editor_state(indoc! {"
1321                struct «TestStructˇ»;
1322
1323                fn main() {
1324                    let variable = TestStruct;
1325                }
1326            "});
1327    }
1328
1329    #[gpui::test]
1330    async fn test_urls(cx: &mut gpui::TestAppContext) {
1331        init_test(cx, |_| {});
1332        let mut cx = EditorLspTestContext::new_rust(
1333            lsp::ServerCapabilities {
1334                ..Default::default()
1335            },
1336            cx,
1337        )
1338        .await;
1339
1340        cx.set_state(indoc! {"
1341            Let's test a [complex](https://zed.dev/channel/had-(oops)) caseˇ.
1342        "});
1343
1344        let screen_coord = cx.pixel_position(indoc! {"
1345            Let's test a [complex](https://zed.dev/channel/had-(ˇoops)) case.
1346            "});
1347
1348        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1349        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1350            Let's test a [complex](«https://zed.dev/channel/had-(oops)ˇ») case.
1351        "});
1352
1353        cx.simulate_click(screen_coord, Modifiers::secondary_key());
1354        assert_eq!(
1355            cx.opened_url(),
1356            Some("https://zed.dev/channel/had-(oops)".into())
1357        );
1358    }
1359
1360    #[gpui::test]
1361    async fn test_urls_at_beginning_of_buffer(cx: &mut gpui::TestAppContext) {
1362        init_test(cx, |_| {});
1363        let mut cx = EditorLspTestContext::new_rust(
1364            lsp::ServerCapabilities {
1365                ..Default::default()
1366            },
1367            cx,
1368        )
1369        .await;
1370
1371        cx.set_state(indoc! {"https://zed.dev/releases is a cool ˇwebpage."});
1372
1373        let screen_coord =
1374            cx.pixel_position(indoc! {"https://zed.dev/relˇeases is a cool webpage."});
1375
1376        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1377        cx.assert_editor_text_highlights::<HoveredLinkState>(
1378            indoc! {"«https://zed.dev/releasesˇ» is a cool webpage."},
1379        );
1380
1381        cx.simulate_click(screen_coord, Modifiers::secondary_key());
1382        assert_eq!(cx.opened_url(), Some("https://zed.dev/releases".into()));
1383    }
1384
1385    #[gpui::test]
1386    async fn test_urls_at_end_of_buffer(cx: &mut gpui::TestAppContext) {
1387        init_test(cx, |_| {});
1388        let mut cx = EditorLspTestContext::new_rust(
1389            lsp::ServerCapabilities {
1390                ..Default::default()
1391            },
1392            cx,
1393        )
1394        .await;
1395
1396        cx.set_state(indoc! {"A cool ˇwebpage is https://zed.dev/releases"});
1397
1398        let screen_coord =
1399            cx.pixel_position(indoc! {"A cool webpage is https://zed.dev/releˇases"});
1400
1401        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1402        cx.assert_editor_text_highlights::<HoveredLinkState>(
1403            indoc! {"A cool webpage is «https://zed.dev/releasesˇ»"},
1404        );
1405
1406        cx.simulate_click(screen_coord, Modifiers::secondary_key());
1407        assert_eq!(cx.opened_url(), Some("https://zed.dev/releases".into()));
1408    }
1409
1410    #[gpui::test]
1411    async fn test_surrounding_filename(cx: &mut gpui::TestAppContext) {
1412        init_test(cx, |_| {});
1413        let mut cx = EditorLspTestContext::new_rust(
1414            lsp::ServerCapabilities {
1415                ..Default::default()
1416            },
1417            cx,
1418        )
1419        .await;
1420
1421        let test_cases = [
1422            ("file ˇ name", None),
1423            ("ˇfile name", Some("file")),
1424            ("file ˇname", Some("name")),
1425            ("fiˇle name", Some("file")),
1426            ("filenˇame", Some("filename")),
1427            // Absolute path
1428            ("foobar ˇ/home/user/f.txt", Some("/home/user/f.txt")),
1429            ("foobar /home/useˇr/f.txt", Some("/home/user/f.txt")),
1430            // Windows
1431            ("C:\\Useˇrs\\user\\f.txt", Some("C:\\Users\\user\\f.txt")),
1432            // Whitespace
1433            ("ˇfile\\ -\\ name.txt", Some("file - name.txt")),
1434            ("file\\ -\\ naˇme.txt", Some("file - name.txt")),
1435            // Tilde
1436            ("ˇ~/file.txt", Some("~/file.txt")),
1437            ("~/fiˇle.txt", Some("~/file.txt")),
1438            // Double quotes
1439            ("\"fˇile.txt\"", Some("file.txt")),
1440            ("ˇ\"file.txt\"", Some("file.txt")),
1441            ("ˇ\"fi\\ le.txt\"", Some("fi le.txt")),
1442            // Single quotes
1443            ("'fˇile.txt'", Some("file.txt")),
1444            ("ˇ'file.txt'", Some("file.txt")),
1445            ("ˇ'fi\\ le.txt'", Some("fi le.txt")),
1446        ];
1447
1448        for (input, expected) in test_cases {
1449            cx.set_state(input);
1450
1451            let (position, snapshot) = cx.editor(|editor, cx| {
1452                let positions = editor.selections.newest_anchor().head().text_anchor;
1453                let snapshot = editor
1454                    .buffer()
1455                    .clone()
1456                    .read(cx)
1457                    .as_singleton()
1458                    .unwrap()
1459                    .read(cx)
1460                    .snapshot();
1461                (positions, snapshot)
1462            });
1463
1464            let result = surrounding_filename(snapshot, position);
1465
1466            if let Some(expected) = expected {
1467                assert!(result.is_some(), "Failed to find file path: {}", input);
1468                let (_, path) = result.unwrap();
1469                assert_eq!(&path, expected, "Incorrect file path for input: {}", input);
1470            } else {
1471                assert!(
1472                    result.is_none(),
1473                    "Expected no result, but got one: {:?}",
1474                    result
1475                );
1476            }
1477        }
1478    }
1479
1480    #[gpui::test]
1481    async fn test_hover_filenames(cx: &mut gpui::TestAppContext) {
1482        init_test(cx, |_| {});
1483        let mut cx = EditorLspTestContext::new_rust(
1484            lsp::ServerCapabilities {
1485                ..Default::default()
1486            },
1487            cx,
1488        )
1489        .await;
1490
1491        // Insert a new file
1492        let fs = cx.update_workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
1493        fs.as_fake()
1494            .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
1495            .await;
1496
1497        cx.set_state(indoc! {"
1498            You can't go to a file that does_not_exist.txt.
1499            Go to file2.rs if you want.
1500            Or go to ../dir/file2.rs if you want.
1501            Or go to /root/dir/file2.rs if project is local.
1502            Or go to /root/dir/file2 if this is a Rust file.ˇ
1503        "});
1504
1505        // File does not exist
1506        let screen_coord = cx.pixel_position(indoc! {"
1507            You can't go to a file that dˇoes_not_exist.txt.
1508            Go to file2.rs if you want.
1509            Or go to ../dir/file2.rs if you want.
1510            Or go to /root/dir/file2.rs if project is local.
1511            Or go to /root/dir/file2 if this is a Rust file.
1512        "});
1513        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1514        // No highlight
1515        cx.update_editor(|editor, cx| {
1516            assert!(editor
1517                .snapshot(cx)
1518                .text_highlight_ranges::<HoveredLinkState>()
1519                .unwrap_or_default()
1520                .1
1521                .is_empty());
1522        });
1523
1524        // Moving the mouse over a file that does exist should highlight it.
1525        let screen_coord = cx.pixel_position(indoc! {"
1526            You can't go to a file that does_not_exist.txt.
1527            Go to fˇile2.rs if you want.
1528            Or go to ../dir/file2.rs if you want.
1529            Or go to /root/dir/file2.rs if project is local.
1530            Or go to /root/dir/file2 if this is a Rust file.
1531        "});
1532
1533        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1534        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1535            You can't go to a file that does_not_exist.txt.
1536            Go to «file2.rsˇ» if you want.
1537            Or go to ../dir/file2.rs if you want.
1538            Or go to /root/dir/file2.rs if project is local.
1539            Or go to /root/dir/file2 if this is a Rust file.
1540        "});
1541
1542        // Moving the mouse over a relative path that does exist should highlight it
1543        let screen_coord = cx.pixel_position(indoc! {"
1544            You can't go to a file that does_not_exist.txt.
1545            Go to file2.rs if you want.
1546            Or go to ../dir/fˇile2.rs if you want.
1547            Or go to /root/dir/file2.rs if project is local.
1548            Or go to /root/dir/file2 if this is a Rust file.
1549        "});
1550
1551        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1552        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1553            You can't go to a file that does_not_exist.txt.
1554            Go to file2.rs if you want.
1555            Or go to «../dir/file2.rsˇ» if you want.
1556            Or go to /root/dir/file2.rs if project is local.
1557            Or go to /root/dir/file2 if this is a Rust file.
1558        "});
1559
1560        // Moving the mouse over an absolute path that does exist should highlight it
1561        let screen_coord = cx.pixel_position(indoc! {"
1562            You can't go to a file that does_not_exist.txt.
1563            Go to file2.rs if you want.
1564            Or go to ../dir/file2.rs if you want.
1565            Or go to /root/diˇr/file2.rs if project is local.
1566            Or go to /root/dir/file2 if this is a Rust file.
1567        "});
1568
1569        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1570        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1571            You can't go to a file that does_not_exist.txt.
1572            Go to file2.rs if you want.
1573            Or go to ../dir/file2.rs if you want.
1574            Or go to «/root/dir/file2.rsˇ» if project is local.
1575            Or go to /root/dir/file2 if this is a Rust file.
1576        "});
1577
1578        // Moving the mouse over a path that exists, if we add the language-specific suffix, it should highlight it
1579        let screen_coord = cx.pixel_position(indoc! {"
1580            You can't go to a file that does_not_exist.txt.
1581            Go to file2.rs if you want.
1582            Or go to ../dir/file2.rs if you want.
1583            Or go to /root/dir/file2.rs if project is local.
1584            Or go to /root/diˇr/file2 if this is a Rust file.
1585        "});
1586
1587        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1588        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
1589            You can't go to a file that does_not_exist.txt.
1590            Go to file2.rs if you want.
1591            Or go to ../dir/file2.rs if you want.
1592            Or go to /root/dir/file2.rs if project is local.
1593            Or go to «/root/dir/file2ˇ» if this is a Rust file.
1594        "});
1595
1596        cx.simulate_click(screen_coord, Modifiers::secondary_key());
1597
1598        cx.update_workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
1599        cx.update_workspace(|workspace, cx| {
1600            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
1601
1602            let buffer = active_editor
1603                .read(cx)
1604                .buffer()
1605                .read(cx)
1606                .as_singleton()
1607                .unwrap();
1608
1609            let file = buffer.read(cx).file().unwrap();
1610            let file_path = file.as_local().unwrap().abs_path(cx);
1611
1612            assert_eq!(file_path.to_str().unwrap(), "/root/dir/file2.rs");
1613        });
1614    }
1615}