hover_links.rs

   1use crate::{
   2    Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
   3    GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind,
   4    HighlightKey, Navigated, PointForPosition, SelectPhase,
   5    editor_settings::GoToDefinitionFallback, scroll::ScrollAmount,
   6};
   7use gpui::{App, AsyncWindowContext, Context, Entity, Modifiers, Pixels, Task, Window, px};
   8use language::{Bias, ToOffset};
   9use linkify::{LinkFinder, LinkKind};
  10use lsp::LanguageServerId;
  11use project::{InlayId, LocationLink, Project, ResolvedPath};
  12use regex::Regex;
  13use settings::Settings;
  14use std::{ops::Range, sync::LazyLock};
  15use text::OffsetRangeExt;
  16use theme::ActiveTheme as _;
  17use util::{ResultExt, TryFutureExt as _, maybe};
  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 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: &Entity<language::Buffer>,
  95    current_position: &text::Anchor,
  96    location: &LocationLink,
  97    cx: &App,
  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        mouse_position: Option<gpui::Point<Pixels>>,
 117        snapshot: &EditorSnapshot,
 118        modifiers: Modifiers,
 119        window: &mut Window,
 120        cx: &mut Context<Self>,
 121    ) {
 122        let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&modifiers, cx);
 123        if !hovered_link_modifier || self.has_pending_selection() || self.mouse_cursor_hidden {
 124            self.hide_hovered_link(cx);
 125            return;
 126        }
 127
 128        match point_for_position.as_valid() {
 129            Some(point) => {
 130                let trigger_point = TriggerPoint::Text(
 131                    snapshot
 132                        .buffer_snapshot()
 133                        .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)),
 134                );
 135
 136                show_link_definition(modifiers.shift, self, trigger_point, snapshot, window, cx);
 137            }
 138            None => {
 139                self.update_inlay_link_and_hover_points(
 140                    snapshot,
 141                    point_for_position,
 142                    mouse_position,
 143                    hovered_link_modifier,
 144                    modifiers.shift,
 145                    window,
 146                    cx,
 147                );
 148            }
 149        }
 150    }
 151
 152    pub(crate) fn hide_hovered_link(&mut self, cx: &mut Context<Self>) {
 153        self.hovered_link_state.take();
 154        self.clear_highlights(HighlightKey::HoveredLinkState, cx);
 155    }
 156
 157    pub(crate) fn handle_click_hovered_link(
 158        &mut self,
 159        point: PointForPosition,
 160        modifiers: Modifiers,
 161        window: &mut Window,
 162        cx: &mut Context<Editor>,
 163    ) {
 164        let reveal_task = self.cmd_click_reveal_task(point, modifiers, window, cx);
 165        cx.spawn_in(window, async move |editor, cx| {
 166            let definition_revealed = reveal_task.await.log_err().unwrap_or(Navigated::No);
 167            let find_references = editor
 168                .update_in(cx, |editor, window, cx| {
 169                    if definition_revealed == Navigated::Yes {
 170                        return None;
 171                    }
 172                    match EditorSettings::get_global(cx).go_to_definition_fallback {
 173                        GoToDefinitionFallback::None => None,
 174                        GoToDefinitionFallback::FindAllReferences => {
 175                            editor.find_all_references(&FindAllReferences::default(), window, cx)
 176                        }
 177                    }
 178                })
 179                .ok()
 180                .flatten();
 181            if let Some(find_references) = find_references {
 182                find_references.await.log_err();
 183            }
 184        })
 185        .detach();
 186    }
 187
 188    pub fn scroll_hover(
 189        &mut self,
 190        amount: ScrollAmount,
 191        window: &mut Window,
 192        cx: &mut Context<Self>,
 193    ) -> bool {
 194        let selection = self.selections.newest_anchor().head();
 195        let snapshot = self.snapshot(window, cx);
 196
 197        if let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
 198            popover
 199                .symbol_range
 200                .point_within_range(&TriggerPoint::Text(selection), &snapshot)
 201        }) {
 202            popover.scroll(amount, window, cx);
 203            true
 204        } else if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
 205            context_menu.scroll_aside(amount, window, cx);
 206            true
 207        } else {
 208            false
 209        }
 210    }
 211
 212    fn cmd_click_reveal_task(
 213        &mut self,
 214        point: PointForPosition,
 215        modifiers: Modifiers,
 216        window: &mut Window,
 217        cx: &mut Context<Editor>,
 218    ) -> Task<anyhow::Result<Navigated>> {
 219        if let Some(hovered_link_state) = self.hovered_link_state.take() {
 220            self.hide_hovered_link(cx);
 221            if !hovered_link_state.links.is_empty() {
 222                if !self.focus_handle.is_focused(window) {
 223                    window.focus(&self.focus_handle, cx);
 224                }
 225
 226                // exclude links pointing back to the current anchor
 227                let current_position = point
 228                    .next_valid
 229                    .to_point(&self.snapshot(window, cx).display_snapshot);
 230                let Some((buffer, anchor)) = self
 231                    .buffer()
 232                    .read(cx)
 233                    .text_anchor_for_position(current_position, cx)
 234                else {
 235                    return Task::ready(Ok(Navigated::No));
 236                };
 237                let Some(mb_anchor) = self
 238                    .buffer()
 239                    .read(cx)
 240                    .buffer_anchor_to_anchor(&buffer, anchor, cx)
 241                else {
 242                    return Task::ready(Ok(Navigated::No));
 243                };
 244                let links = hovered_link_state
 245                    .links
 246                    .into_iter()
 247                    .filter(|link| {
 248                        if let HoverLink::Text(location) = link {
 249                            exclude_link_to_position(&buffer, &anchor, location, cx)
 250                        } else {
 251                            true
 252                        }
 253                    })
 254                    .collect();
 255                let nav_entry = self.navigation_entry(mb_anchor, cx);
 256                let split = Self::is_alt_pressed(&modifiers, cx);
 257                let navigate_task =
 258                    self.navigate_to_hover_links(None, links, nav_entry, split, window, cx);
 259                self.select(SelectPhase::End, window, cx);
 260                return navigate_task;
 261            }
 262        }
 263
 264        // We don't have the correct kind of link cached, set the selection on
 265        // click and immediately trigger GoToDefinition.
 266        self.select(
 267            SelectPhase::Begin {
 268                position: point.next_valid,
 269                add: false,
 270                click_count: 1,
 271            },
 272            window,
 273            cx,
 274        );
 275
 276        let navigate_task = if point.as_valid().is_some() {
 277            let split = Self::is_alt_pressed(&modifiers, cx);
 278            match (modifiers.shift, split) {
 279                (true, true) => {
 280                    self.go_to_type_definition_split(&GoToTypeDefinitionSplit, window, cx)
 281                }
 282                (true, false) => self.go_to_type_definition(&GoToTypeDefinition, window, cx),
 283                (false, true) => self.go_to_definition_split(&GoToDefinitionSplit, window, cx),
 284                (false, false) => self.go_to_definition(&GoToDefinition, window, cx),
 285            }
 286        } else {
 287            Task::ready(Ok(Navigated::No))
 288        };
 289        self.select(SelectPhase::End, window, cx);
 290        navigate_task
 291    }
 292}
 293
 294pub fn show_link_definition(
 295    shift_held: bool,
 296    editor: &mut Editor,
 297    trigger_point: TriggerPoint,
 298    snapshot: &EditorSnapshot,
 299    window: &mut Window,
 300    cx: &mut Context<Editor>,
 301) {
 302    let preferred_kind = match trigger_point {
 303        TriggerPoint::Text(_) if !shift_held => GotoDefinitionKind::Symbol,
 304        _ => GotoDefinitionKind::Type,
 305    };
 306
 307    let (mut hovered_link_state, is_cached) =
 308        if let Some(existing) = editor.hovered_link_state.take() {
 309            (existing, true)
 310        } else {
 311            (
 312                HoveredLinkState {
 313                    last_trigger_point: trigger_point.clone(),
 314                    symbol_range: None,
 315                    preferred_kind,
 316                    links: vec![],
 317                    task: None,
 318                },
 319                false,
 320            )
 321        };
 322
 323    if editor.pending_rename.is_some() {
 324        return;
 325    }
 326
 327    let trigger_anchor = trigger_point.anchor();
 328    let anchor = snapshot.buffer_snapshot().anchor_before(*trigger_anchor);
 329    let Some(buffer) = editor.buffer().read(cx).buffer_for_anchor(anchor, cx) else {
 330        return;
 331    };
 332    let Anchor {
 333        excerpt_id,
 334        text_anchor,
 335        ..
 336    } = anchor;
 337    let same_kind = hovered_link_state.preferred_kind == preferred_kind
 338        || hovered_link_state
 339            .links
 340            .first()
 341            .is_some_and(|d| matches!(d, HoverLink::Url(_)));
 342
 343    if same_kind {
 344        if is_cached && (hovered_link_state.last_trigger_point == trigger_point)
 345            || hovered_link_state
 346                .symbol_range
 347                .as_ref()
 348                .is_some_and(|symbol_range| {
 349                    symbol_range.point_within_range(&trigger_point, snapshot)
 350                })
 351        {
 352            editor.hovered_link_state = Some(hovered_link_state);
 353            return;
 354        }
 355    } else {
 356        editor.hide_hovered_link(cx)
 357    }
 358    let project = editor.project.clone();
 359    let provider = editor.semantics_provider.clone();
 360
 361    let snapshot = snapshot.buffer_snapshot().clone();
 362    hovered_link_state.task = Some(cx.spawn_in(window, async move |this, cx| {
 363        async move {
 364            let result = match &trigger_point {
 365                TriggerPoint::Text(_) => {
 366                    if let Some((url_range, url)) = find_url(&buffer, text_anchor, cx.clone()) {
 367                        this.read_with(cx, |_, _| {
 368                            let range = maybe!({
 369                                let range =
 370                                    snapshot.anchor_range_in_excerpt(excerpt_id, url_range)?;
 371                                Some(RangeInEditor::Text(range))
 372                            });
 373                            (range, vec![HoverLink::Url(url)])
 374                        })
 375                        .ok()
 376                    } else if let Some((filename_range, filename)) =
 377                        find_file(&buffer, project.clone(), text_anchor, cx).await
 378                    {
 379                        let range = maybe!({
 380                            let range =
 381                                snapshot.anchor_range_in_excerpt(excerpt_id, filename_range)?;
 382                            Some(RangeInEditor::Text(range))
 383                        });
 384
 385                        Some((range, vec![HoverLink::File(filename)]))
 386                    } else if let Some(provider) = provider {
 387                        let task = cx.update(|_, cx| {
 388                            provider.definitions(&buffer, text_anchor, preferred_kind, cx)
 389                        })?;
 390                        if let Some(task) = task {
 391                            task.await.ok().flatten().map(|definition_result| {
 392                                (
 393                                    definition_result.iter().find_map(|link| {
 394                                        link.origin.as_ref().and_then(|origin| {
 395                                            let range = snapshot.anchor_range_in_excerpt(
 396                                                excerpt_id,
 397                                                origin.range.clone(),
 398                                            )?;
 399                                            Some(RangeInEditor::Text(range))
 400                                        })
 401                                    }),
 402                                    definition_result.into_iter().map(HoverLink::Text).collect(),
 403                                )
 404                            })
 405                        } else {
 406                            None
 407                        }
 408                    } else {
 409                        None
 410                    }
 411                }
 412                TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
 413                    Some(RangeInEditor::Inlay(highlight.clone())),
 414                    vec![HoverLink::InlayHint(lsp_location.clone(), *server_id)],
 415                )),
 416            };
 417
 418            this.update(cx, |editor, cx| {
 419                // Clear any existing highlights
 420                editor.clear_highlights(HighlightKey::HoveredLinkState, cx);
 421                let Some(hovered_link_state) = editor.hovered_link_state.as_mut() else {
 422                    editor.hide_hovered_link(cx);
 423                    return;
 424                };
 425                hovered_link_state.preferred_kind = preferred_kind;
 426                hovered_link_state.symbol_range = result
 427                    .as_ref()
 428                    .and_then(|(symbol_range, _)| symbol_range.clone());
 429
 430                if let Some((symbol_range, definitions)) = result {
 431                    hovered_link_state.links = definitions;
 432
 433                    let underline_hovered_link = !hovered_link_state.links.is_empty()
 434                        || hovered_link_state.symbol_range.is_some();
 435
 436                    if underline_hovered_link {
 437                        let style = gpui::HighlightStyle {
 438                            underline: Some(gpui::UnderlineStyle {
 439                                thickness: px(1.),
 440                                ..Default::default()
 441                            }),
 442                            color: Some(cx.theme().colors().link_text_hover),
 443                            ..Default::default()
 444                        };
 445                        let highlight_range =
 446                            symbol_range.unwrap_or_else(|| match &trigger_point {
 447                                TriggerPoint::Text(trigger_anchor) => {
 448                                    // If no symbol range returned from language server, use the surrounding word.
 449                                    let (offset_range, _) =
 450                                        snapshot.surrounding_word(*trigger_anchor, None);
 451                                    RangeInEditor::Text(
 452                                        snapshot.anchor_before(offset_range.start)
 453                                            ..snapshot.anchor_after(offset_range.end),
 454                                    )
 455                                }
 456                                TriggerPoint::InlayHint(highlight, _, _) => {
 457                                    RangeInEditor::Inlay(highlight.clone())
 458                                }
 459                            });
 460
 461                        match highlight_range {
 462                            RangeInEditor::Text(text_range) => editor.highlight_text(
 463                                HighlightKey::HoveredLinkState,
 464                                vec![text_range],
 465                                style,
 466                                cx,
 467                            ),
 468                            RangeInEditor::Inlay(highlight) => editor.highlight_inlays(
 469                                HighlightKey::HoveredLinkState,
 470                                vec![highlight],
 471                                style,
 472                                cx,
 473                            ),
 474                        }
 475                    }
 476                } else {
 477                    editor.hide_hovered_link(cx);
 478                }
 479            })?;
 480
 481            anyhow::Ok(())
 482        }
 483        .log_err()
 484        .await
 485    }));
 486
 487    editor.hovered_link_state = Some(hovered_link_state);
 488}
 489
 490pub(crate) fn find_url(
 491    buffer: &Entity<language::Buffer>,
 492    position: text::Anchor,
 493    cx: AsyncWindowContext,
 494) -> Option<(Range<text::Anchor>, String)> {
 495    const LIMIT: usize = 2048;
 496
 497    let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
 498
 499    let offset = position.to_offset(&snapshot);
 500    let mut token_start = offset;
 501    let mut token_end = offset;
 502    let mut found_start = false;
 503    let mut found_end = false;
 504
 505    for ch in snapshot.reversed_chars_at(offset).take(LIMIT) {
 506        if ch.is_whitespace() {
 507            found_start = true;
 508            break;
 509        }
 510        token_start -= ch.len_utf8();
 511    }
 512    // Check if we didn't find the starting whitespace or if we didn't reach the start of the buffer
 513    if !found_start && token_start != 0 {
 514        return None;
 515    }
 516
 517    for ch in snapshot
 518        .chars_at(offset)
 519        .take(LIMIT - (offset - token_start))
 520    {
 521        if ch.is_whitespace() {
 522            found_end = true;
 523            break;
 524        }
 525        token_end += ch.len_utf8();
 526    }
 527    // Check if we didn't find the ending whitespace or if we read more or equal than LIMIT
 528    // which at this point would happen only if we reached the end of buffer
 529    if !found_end && (token_end - token_start >= LIMIT) {
 530        return None;
 531    }
 532
 533    let mut finder = LinkFinder::new();
 534    finder.kinds(&[LinkKind::Url]);
 535    let input = snapshot
 536        .text_for_range(token_start..token_end)
 537        .collect::<String>();
 538
 539    let relative_offset = offset - token_start;
 540    for link in finder.links(&input) {
 541        if link.start() <= relative_offset && link.end() >= relative_offset {
 542            let range = snapshot.anchor_before(token_start + link.start())
 543                ..snapshot.anchor_after(token_start + link.end());
 544            return Some((range, link.as_str().to_string()));
 545        }
 546    }
 547    None
 548}
 549
 550pub(crate) fn find_url_from_range(
 551    buffer: &Entity<language::Buffer>,
 552    range: Range<text::Anchor>,
 553    cx: AsyncWindowContext,
 554) -> Option<String> {
 555    const LIMIT: usize = 2048;
 556
 557    let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
 558
 559    let start_offset = range.start.to_offset(&snapshot);
 560    let end_offset = range.end.to_offset(&snapshot);
 561
 562    let mut token_start = start_offset.min(end_offset);
 563    let mut token_end = start_offset.max(end_offset);
 564
 565    let range_len = token_end - token_start;
 566
 567    if range_len >= LIMIT {
 568        return None;
 569    }
 570
 571    // Skip leading whitespace
 572    for ch in snapshot.chars_at(token_start).take(range_len) {
 573        if !ch.is_whitespace() {
 574            break;
 575        }
 576        token_start += ch.len_utf8();
 577    }
 578
 579    // Skip trailing whitespace
 580    for ch in snapshot.reversed_chars_at(token_end).take(range_len) {
 581        if !ch.is_whitespace() {
 582            break;
 583        }
 584        token_end -= ch.len_utf8();
 585    }
 586
 587    if token_start >= token_end {
 588        return None;
 589    }
 590
 591    let text = snapshot
 592        .text_for_range(token_start..token_end)
 593        .collect::<String>();
 594
 595    let mut finder = LinkFinder::new();
 596    finder.kinds(&[LinkKind::Url]);
 597
 598    if let Some(link) = finder.links(&text).next()
 599        && link.start() == 0
 600        && link.end() == text.len()
 601    {
 602        return Some(link.as_str().to_string());
 603    }
 604
 605    None
 606}
 607
 608pub(crate) async fn find_file(
 609    buffer: &Entity<language::Buffer>,
 610    project: Option<Entity<Project>>,
 611    position: text::Anchor,
 612    cx: &mut AsyncWindowContext,
 613) -> Option<(Range<text::Anchor>, ResolvedPath)> {
 614    let project = project?;
 615    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
 616    let scope = snapshot.language_scope_at(position);
 617    let (range, candidate_file_path) = surrounding_filename(&snapshot, position)?;
 618    let candidate_len = candidate_file_path.len();
 619
 620    async fn check_path(
 621        candidate_file_path: &str,
 622        project: &Entity<Project>,
 623        buffer: &Entity<language::Buffer>,
 624        cx: &mut AsyncWindowContext,
 625    ) -> Option<ResolvedPath> {
 626        project
 627            .update(cx, |project, cx| {
 628                project.resolve_path_in_buffer(candidate_file_path, buffer, cx)
 629            })
 630            .await
 631            .filter(|s| s.is_file())
 632    }
 633
 634    let pattern_candidates = link_pattern_file_candidates(&candidate_file_path);
 635
 636    for (pattern_candidate, pattern_range) in &pattern_candidates {
 637        if let Some(existing_path) = check_path(&pattern_candidate, &project, buffer, cx).await {
 638            let offset_range = range.to_offset(&snapshot);
 639            let actual_start = offset_range.start + pattern_range.start;
 640            let actual_end = offset_range.end - (candidate_len - pattern_range.end);
 641            return Some((
 642                snapshot.anchor_before(actual_start)..snapshot.anchor_after(actual_end),
 643                existing_path,
 644            ));
 645        }
 646    }
 647    if let Some(scope) = scope {
 648        for (pattern_candidate, pattern_range) in pattern_candidates {
 649            for suffix in scope.path_suffixes() {
 650                if pattern_candidate.ends_with(format!(".{suffix}").as_str()) {
 651                    continue;
 652                }
 653
 654                let suffixed_candidate = format!("{pattern_candidate}.{suffix}");
 655                if let Some(existing_path) =
 656                    check_path(&suffixed_candidate, &project, buffer, cx).await
 657                {
 658                    let offset_range = range.to_offset(&snapshot);
 659                    let actual_start = offset_range.start + pattern_range.start;
 660                    let actual_end = offset_range.end - (candidate_len - pattern_range.end);
 661                    return Some((
 662                        snapshot.anchor_before(actual_start)..snapshot.anchor_after(actual_end),
 663                        existing_path,
 664                    ));
 665                }
 666            }
 667        }
 668    }
 669    None
 670}
 671
 672// Tries to capture potentially inlined links, like those found in markdown,
 673// e.g. [LinkTitle](link_file.txt)
 674// Since files can have parens, we should always return the full string
 675// (literally, [LinkTitle](link_file.txt)) as a candidate.
 676fn link_pattern_file_candidates(candidate: &str) -> Vec<(String, Range<usize>)> {
 677    static MD_LINK_REGEX: LazyLock<Regex> =
 678        LazyLock::new(|| Regex::new(r"]\(([^)]*)\)").expect("Failed to create REGEX"));
 679
 680    let candidate_len = candidate.len();
 681
 682    let mut candidates = vec![(candidate.to_string(), 0..candidate_len)];
 683
 684    if let Some(captures) = MD_LINK_REGEX.captures(candidate) {
 685        if let Some(link) = captures.get(1) {
 686            candidates.push((link.as_str().to_string(), link.range()));
 687        }
 688    }
 689    candidates
 690}
 691
 692fn surrounding_filename(
 693    snapshot: &language::BufferSnapshot,
 694    position: text::Anchor,
 695) -> Option<(Range<text::Anchor>, String)> {
 696    const LIMIT: usize = 2048;
 697
 698    let offset = position.to_offset(&snapshot);
 699    let mut token_start = offset;
 700    let mut token_end = offset;
 701    let mut found_start = false;
 702    let mut found_end = false;
 703    let mut inside_quotes = false;
 704
 705    let mut filename = String::new();
 706
 707    let mut backwards = snapshot.reversed_chars_at(offset).take(LIMIT).peekable();
 708    while let Some(ch) = backwards.next() {
 709        // Escaped whitespace
 710        if ch.is_whitespace() && backwards.peek() == Some(&'\\') {
 711            filename.push(ch);
 712            token_start -= ch.len_utf8();
 713            backwards.next();
 714            token_start -= '\\'.len_utf8();
 715            continue;
 716        }
 717        if ch.is_whitespace() {
 718            found_start = true;
 719            break;
 720        }
 721        if (ch == '"' || ch == '\'') && !inside_quotes {
 722            found_start = true;
 723            inside_quotes = true;
 724            break;
 725        }
 726
 727        filename.push(ch);
 728        token_start -= ch.len_utf8();
 729    }
 730    if !found_start && token_start != 0 {
 731        return None;
 732    }
 733
 734    filename = filename.chars().rev().collect();
 735
 736    let mut forwards = snapshot
 737        .chars_at(offset)
 738        .take(LIMIT - (offset - token_start))
 739        .peekable();
 740    while let Some(ch) = forwards.next() {
 741        // Skip escaped whitespace
 742        if ch == '\\' && forwards.peek().is_some_and(|ch| ch.is_whitespace()) {
 743            token_end += ch.len_utf8();
 744            let whitespace = forwards.next().unwrap();
 745            token_end += whitespace.len_utf8();
 746            filename.push(whitespace);
 747            continue;
 748        }
 749
 750        if ch.is_whitespace() {
 751            found_end = true;
 752            break;
 753        }
 754        if ch == '"' || ch == '\'' {
 755            // If we're inside quotes, we stop when we come across the next quote
 756            if inside_quotes {
 757                found_end = true;
 758                break;
 759            } else {
 760                // Otherwise, we skip the quote
 761                inside_quotes = true;
 762                token_end += ch.len_utf8();
 763                continue;
 764            }
 765        }
 766        filename.push(ch);
 767        token_end += ch.len_utf8();
 768    }
 769
 770    if !found_end && (token_end - token_start >= LIMIT) {
 771        return None;
 772    }
 773
 774    if filename.is_empty() {
 775        return None;
 776    }
 777
 778    let range = snapshot.anchor_before(token_start)..snapshot.anchor_after(token_end);
 779
 780    Some((range, filename))
 781}
 782
 783#[cfg(test)]
 784mod tests {
 785    use super::*;
 786    use crate::{
 787        DisplayPoint, HideMouseCursorOrigin,
 788        display_map::ToDisplayPoint,
 789        editor_tests::init_test,
 790        inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
 791        test::editor_lsp_test_context::EditorLspTestContext,
 792    };
 793    use futures::StreamExt;
 794    use gpui::{Modifiers, MousePressureEvent, PressureStage};
 795    use indoc::indoc;
 796    use lsp::request::{GotoDefinition, GotoTypeDefinition};
 797    use multi_buffer::MultiBufferOffset;
 798    use settings::InlayHintSettingsContent;
 799    use util::{assert_set_eq, path};
 800    use workspace::item::Item;
 801
 802    #[gpui::test]
 803    async fn test_hover_type_links(cx: &mut gpui::TestAppContext) {
 804        init_test(cx, |_| {});
 805
 806        let mut cx = EditorLspTestContext::new_rust(
 807            lsp::ServerCapabilities {
 808                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 809                type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
 810                ..Default::default()
 811            },
 812            cx,
 813        )
 814        .await;
 815
 816        cx.set_state(indoc! {"
 817            struct A;
 818            let vˇariable = A;
 819        "});
 820        let screen_coord = cx.editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
 821
 822        // Basic hold cmd+shift, expect highlight in region if response contains type definition
 823        let symbol_range = cx.lsp_range(indoc! {"
 824            struct A;
 825            let «variable» = A;
 826        "});
 827        let target_range = cx.lsp_range(indoc! {"
 828            struct «A»;
 829            let variable = A;
 830        "});
 831
 832        cx.run_until_parked();
 833
 834        let mut requests =
 835            cx.set_request_handler::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
 836                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
 837                    lsp::LocationLink {
 838                        origin_selection_range: Some(symbol_range),
 839                        target_uri: url.clone(),
 840                        target_range,
 841                        target_selection_range: target_range,
 842                    },
 843                ])))
 844            });
 845
 846        let modifiers = if cfg!(target_os = "macos") {
 847            Modifiers::command_shift()
 848        } else {
 849            Modifiers::control_shift()
 850        };
 851
 852        cx.simulate_mouse_move(screen_coord.unwrap(), None, modifiers);
 853
 854        requests.next().await;
 855        cx.run_until_parked();
 856        cx.assert_editor_text_highlights(
 857            HighlightKey::HoveredLinkState,
 858            indoc! {"
 859            struct A;
 860            let «variable» = A;
 861        "},
 862        );
 863
 864        cx.simulate_modifiers_change(Modifiers::secondary_key());
 865        cx.run_until_parked();
 866        // Assert no link highlights
 867        cx.assert_editor_text_highlights(
 868            HighlightKey::HoveredLinkState,
 869            indoc! {"
 870            struct A;
 871            let variable = A;
 872        "},
 873        );
 874
 875        cx.simulate_click(screen_coord.unwrap(), modifiers);
 876
 877        cx.assert_editor_state(indoc! {"
 878            struct «Aˇ»;
 879            let variable = A;
 880        "});
 881    }
 882
 883    #[gpui::test]
 884    async fn test_hover_links(cx: &mut gpui::TestAppContext) {
 885        init_test(cx, |_| {});
 886
 887        let mut cx = EditorLspTestContext::new_rust(
 888            lsp::ServerCapabilities {
 889                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
 890                definition_provider: Some(lsp::OneOf::Left(true)),
 891                ..Default::default()
 892            },
 893            cx,
 894        )
 895        .await;
 896
 897        cx.set_state(indoc! {"
 898                fn ˇtest() { do_work(); }
 899                fn do_work() { test(); }
 900            "});
 901
 902        // Basic hold cmd, expect highlight in region if response contains definition
 903        let hover_point = cx.pixel_position(indoc! {"
 904                fn test() { do_wˇork(); }
 905                fn do_work() { test(); }
 906            "});
 907        let symbol_range = cx.lsp_range(indoc! {"
 908                fn test() { «do_work»(); }
 909                fn do_work() { test(); }
 910            "});
 911        let target_range = cx.lsp_range(indoc! {"
 912                fn test() { do_work(); }
 913                fn «do_work»() { test(); }
 914            "});
 915
 916        let mut requests =
 917            cx.set_request_handler::<GotoDefinition, _, _>(move |url, _, _| async move {
 918                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
 919                    lsp::LocationLink {
 920                        origin_selection_range: Some(symbol_range),
 921                        target_uri: url.clone(),
 922                        target_range,
 923                        target_selection_range: target_range,
 924                    },
 925                ])))
 926            });
 927
 928        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
 929        requests.next().await;
 930        cx.background_executor.run_until_parked();
 931        cx.assert_editor_text_highlights(
 932            HighlightKey::HoveredLinkState,
 933            indoc! {"
 934                fn test() { «do_work»(); }
 935                fn do_work() { test(); }
 936            "},
 937        );
 938
 939        // Unpress cmd causes highlight to go away
 940        cx.simulate_modifiers_change(Modifiers::none());
 941        cx.assert_editor_text_highlights(
 942            HighlightKey::HoveredLinkState,
 943            indoc! {"
 944                fn test() { do_work(); }
 945                fn do_work() { test(); }
 946            "},
 947        );
 948
 949        let mut requests =
 950            cx.set_request_handler::<GotoDefinition, _, _>(move |url, _, _| async move {
 951                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
 952                    lsp::LocationLink {
 953                        origin_selection_range: Some(symbol_range),
 954                        target_uri: url.clone(),
 955                        target_range,
 956                        target_selection_range: target_range,
 957                    },
 958                ])))
 959            });
 960
 961        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
 962        requests.next().await;
 963        cx.background_executor.run_until_parked();
 964        cx.assert_editor_text_highlights(
 965            HighlightKey::HoveredLinkState,
 966            indoc! {"
 967                fn test() { «do_work»(); }
 968                fn do_work() { test(); }
 969            "},
 970        );
 971
 972        // Moving mouse to location with no response dismisses highlight
 973        let hover_point = cx.pixel_position(indoc! {"
 974                fˇn test() { do_work(); }
 975                fn do_work() { test(); }
 976            "});
 977        let mut requests =
 978            cx.lsp
 979                .set_request_handler::<GotoDefinition, _, _>(move |_, _| async move {
 980                    // No definitions returned
 981                    Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
 982                });
 983        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
 984
 985        requests.next().await;
 986        cx.background_executor.run_until_parked();
 987
 988        // Assert no link highlights
 989        cx.assert_editor_text_highlights(
 990            HighlightKey::HoveredLinkState,
 991            indoc! {"
 992                fn test() { do_work(); }
 993                fn do_work() { test(); }
 994            "},
 995        );
 996
 997        // // Move mouse without cmd and then pressing cmd triggers highlight
 998        let hover_point = cx.pixel_position(indoc! {"
 999                fn test() { do_work(); }
1000                fn do_work() { teˇst(); }
1001            "});
1002        cx.simulate_mouse_move(hover_point, None, Modifiers::none());
1003
1004        // Assert no link highlights
1005        cx.assert_editor_text_highlights(
1006            HighlightKey::HoveredLinkState,
1007            indoc! {"
1008                fn test() { do_work(); }
1009                fn do_work() { test(); }
1010            "},
1011        );
1012
1013        let symbol_range = cx.lsp_range(indoc! {"
1014                fn test() { do_work(); }
1015                fn do_work() { «test»(); }
1016            "});
1017        let target_range = cx.lsp_range(indoc! {"
1018                fn «test»() { do_work(); }
1019                fn do_work() { test(); }
1020            "});
1021
1022        let mut requests =
1023            cx.set_request_handler::<GotoDefinition, _, _>(move |url, _, _| async move {
1024                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
1025                    lsp::LocationLink {
1026                        origin_selection_range: Some(symbol_range),
1027                        target_uri: url,
1028                        target_range,
1029                        target_selection_range: target_range,
1030                    },
1031                ])))
1032            });
1033
1034        cx.simulate_modifiers_change(Modifiers::secondary_key());
1035
1036        requests.next().await;
1037        cx.background_executor.run_until_parked();
1038
1039        cx.assert_editor_text_highlights(
1040            HighlightKey::HoveredLinkState,
1041            indoc! {"
1042                fn test() { do_work(); }
1043                fn do_work() { «test»(); }
1044            "},
1045        );
1046
1047        cx.deactivate_window();
1048        cx.assert_editor_text_highlights(
1049            HighlightKey::HoveredLinkState,
1050            indoc! {"
1051                fn test() { do_work(); }
1052                fn do_work() { test(); }
1053            "},
1054        );
1055
1056        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
1057        cx.background_executor.run_until_parked();
1058        cx.assert_editor_text_highlights(
1059            HighlightKey::HoveredLinkState,
1060            indoc! {"
1061                fn test() { do_work(); }
1062                fn do_work() { «test»(); }
1063            "},
1064        );
1065
1066        // Moving again within the same symbol range doesn't re-request
1067        let hover_point = cx.pixel_position(indoc! {"
1068                fn test() { do_work(); }
1069                fn do_work() { tesˇt(); }
1070            "});
1071        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
1072        cx.background_executor.run_until_parked();
1073        cx.assert_editor_text_highlights(
1074            HighlightKey::HoveredLinkState,
1075            indoc! {"
1076                fn test() { do_work(); }
1077                fn do_work() { «test»(); }
1078            "},
1079        );
1080
1081        // Cmd click with existing definition doesn't re-request and dismisses highlight
1082        cx.simulate_click(hover_point, Modifiers::secondary_key());
1083        cx.lsp
1084            .set_request_handler::<GotoDefinition, _, _>(move |_, _| async move {
1085                // Empty definition response to make sure we aren't hitting the lsp and using
1086                // the cached location instead
1087                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
1088            });
1089        cx.background_executor.run_until_parked();
1090        cx.assert_editor_state(indoc! {"
1091                fn «testˇ»() { do_work(); }
1092                fn do_work() { test(); }
1093            "});
1094
1095        // Assert no link highlights after jump
1096        cx.assert_editor_text_highlights(
1097            HighlightKey::HoveredLinkState,
1098            indoc! {"
1099                fn test() { do_work(); }
1100                fn do_work() { test(); }
1101            "},
1102        );
1103
1104        // Cmd click without existing definition requests and jumps
1105        let hover_point = cx.pixel_position(indoc! {"
1106                fn test() { do_wˇork(); }
1107                fn do_work() { test(); }
1108            "});
1109        let target_range = cx.lsp_range(indoc! {"
1110                fn test() { do_work(); }
1111                fn «do_work»() { test(); }
1112            "});
1113
1114        let mut requests =
1115            cx.set_request_handler::<GotoDefinition, _, _>(move |url, _, _| async move {
1116                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
1117                    lsp::LocationLink {
1118                        origin_selection_range: None,
1119                        target_uri: url,
1120                        target_range,
1121                        target_selection_range: target_range,
1122                    },
1123                ])))
1124            });
1125        cx.simulate_click(hover_point, Modifiers::secondary_key());
1126        requests.next().await;
1127        cx.background_executor.run_until_parked();
1128        cx.assert_editor_state(indoc! {"
1129                fn test() { do_work(); }
1130                fn «do_workˇ»() { test(); }
1131            "});
1132
1133        // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
1134        // 2. Selection is completed, hovering
1135        let hover_point = cx.pixel_position(indoc! {"
1136                fn test() { do_wˇork(); }
1137                fn do_work() { test(); }
1138            "});
1139        let target_range = cx.lsp_range(indoc! {"
1140                fn test() { do_work(); }
1141                fn «do_work»() { test(); }
1142            "});
1143        let mut requests =
1144            cx.set_request_handler::<GotoDefinition, _, _>(move |url, _, _| async move {
1145                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
1146                    lsp::LocationLink {
1147                        origin_selection_range: None,
1148                        target_uri: url,
1149                        target_range,
1150                        target_selection_range: target_range,
1151                    },
1152                ])))
1153            });
1154
1155        // create a pending selection
1156        let selection_range = cx.ranges(indoc! {"
1157                fn «test() { do_w»ork(); }
1158                fn do_work() { test(); }
1159            "})[0]
1160            .clone();
1161        cx.update_editor(|editor, window, cx| {
1162            let snapshot = editor.buffer().read(cx).snapshot(cx);
1163            let anchor_range = snapshot.anchor_before(MultiBufferOffset(selection_range.start))
1164                ..snapshot.anchor_after(MultiBufferOffset(selection_range.end));
1165            editor.change_selections(Default::default(), window, cx, |s| {
1166                s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
1167            });
1168        });
1169        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
1170        cx.background_executor.run_until_parked();
1171        assert!(requests.try_next().is_err());
1172        cx.assert_editor_text_highlights(
1173            HighlightKey::HoveredLinkState,
1174            indoc! {"
1175                fn test() { do_work(); }
1176                fn do_work() { test(); }
1177            "},
1178        );
1179        cx.background_executor.run_until_parked();
1180    }
1181
1182    #[gpui::test]
1183    async fn test_inlay_hover_links(cx: &mut gpui::TestAppContext) {
1184        init_test(cx, |settings| {
1185            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
1186                enabled: Some(true),
1187                show_value_hints: Some(false),
1188                edit_debounce_ms: Some(0),
1189                scroll_debounce_ms: Some(0),
1190                show_type_hints: Some(true),
1191                show_parameter_hints: Some(true),
1192                show_other_hints: Some(true),
1193                show_background: Some(false),
1194                toggle_on_modifiers_press: None,
1195            })
1196        });
1197
1198        let mut cx = EditorLspTestContext::new_rust(
1199            lsp::ServerCapabilities {
1200                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1201                ..Default::default()
1202            },
1203            cx,
1204        )
1205        .await;
1206        cx.set_state(indoc! {"
1207                struct TestStruct;
1208
1209                fn main() {
1210                    let variableˇ = TestStruct;
1211                }
1212            "});
1213        let hint_start_offset = cx.ranges(indoc! {"
1214                struct TestStruct;
1215
1216                fn main() {
1217                    let variableˇ = TestStruct;
1218                }
1219            "})[0]
1220            .start;
1221        let hint_position = cx.to_lsp(MultiBufferOffset(hint_start_offset));
1222        let target_range = cx.lsp_range(indoc! {"
1223                struct «TestStruct»;
1224
1225                fn main() {
1226                    let variable = TestStruct;
1227                }
1228            "});
1229
1230        let expected_uri = cx.buffer_lsp_url.clone();
1231        let hint_label = ": TestStruct";
1232        cx.lsp
1233            .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1234                let expected_uri = expected_uri.clone();
1235                async move {
1236                    assert_eq!(params.text_document.uri, expected_uri);
1237                    Ok(Some(vec![lsp::InlayHint {
1238                        position: hint_position,
1239                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1240                            value: hint_label.to_string(),
1241                            location: Some(lsp::Location {
1242                                uri: params.text_document.uri,
1243                                range: target_range,
1244                            }),
1245                            ..Default::default()
1246                        }]),
1247                        kind: Some(lsp::InlayHintKind::TYPE),
1248                        text_edits: None,
1249                        tooltip: None,
1250                        padding_left: Some(false),
1251                        padding_right: Some(false),
1252                        data: None,
1253                    }]))
1254                }
1255            })
1256            .next()
1257            .await;
1258        cx.background_executor.run_until_parked();
1259        cx.update_editor(|editor, _window, cx| {
1260            let expected_layers = vec![hint_label.to_string()];
1261            assert_eq!(expected_layers, cached_hint_labels(editor, cx));
1262            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1263        });
1264
1265        let inlay_range = cx
1266            .ranges(indoc! {"
1267                struct TestStruct;
1268
1269                fn main() {
1270                    let variable« »= TestStruct;
1271                }
1272            "})
1273            .first()
1274            .cloned()
1275            .unwrap();
1276        let midpoint = cx.update_editor(|editor, window, cx| {
1277            let snapshot = editor.snapshot(window, cx);
1278            let previous_valid = MultiBufferOffset(inlay_range.start).to_display_point(&snapshot);
1279            let next_valid = MultiBufferOffset(inlay_range.end).to_display_point(&snapshot);
1280            assert_eq!(previous_valid.row(), next_valid.row());
1281            assert!(previous_valid.column() < next_valid.column());
1282            DisplayPoint::new(
1283                previous_valid.row(),
1284                previous_valid.column() + (hint_label.len() / 2) as u32,
1285            )
1286        });
1287        // Press cmd to trigger highlight
1288        let hover_point = cx.pixel_position_for(midpoint);
1289        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
1290        cx.background_executor.run_until_parked();
1291        cx.update_editor(|editor, window, cx| {
1292            let snapshot = editor.snapshot(window, cx);
1293            let actual_highlights = snapshot
1294                .inlay_highlights(HighlightKey::HoveredLinkState)
1295                .into_iter()
1296                .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
1297                .collect::<Vec<_>>();
1298
1299            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1300            let expected_highlight = InlayHighlight {
1301                inlay: InlayId::Hint(0),
1302                inlay_position: buffer_snapshot.anchor_after(MultiBufferOffset(inlay_range.start)),
1303                range: 0..hint_label.len(),
1304            };
1305            assert_set_eq!(actual_highlights, vec![&expected_highlight]);
1306        });
1307
1308        cx.simulate_mouse_move(hover_point, None, Modifiers::none());
1309        // Assert no link highlights
1310        cx.update_editor(|editor, window, cx| {
1311                let snapshot = editor.snapshot(window, cx);
1312                let actual_ranges = snapshot
1313                    .text_highlight_ranges(HighlightKey::HoveredLinkState)
1314                    .map(|ranges| ranges.as_ref().clone().1)
1315                    .unwrap_or_default();
1316
1317                assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
1318            });
1319
1320        cx.simulate_modifiers_change(Modifiers::secondary_key());
1321        cx.background_executor.run_until_parked();
1322        cx.simulate_click(hover_point, Modifiers::secondary_key());
1323        cx.background_executor.run_until_parked();
1324        cx.assert_editor_state(indoc! {"
1325                struct «TestStructˇ»;
1326
1327                fn main() {
1328                    let variable = TestStruct;
1329                }
1330            "});
1331    }
1332
1333    #[gpui::test]
1334    async fn test_urls(cx: &mut gpui::TestAppContext) {
1335        init_test(cx, |_| {});
1336        let mut cx = EditorLspTestContext::new_rust(
1337            lsp::ServerCapabilities {
1338                ..Default::default()
1339            },
1340            cx,
1341        )
1342        .await;
1343
1344        cx.set_state(indoc! {"
1345            Let's test a [complex](https://zed.dev/channel/had-(oops)) caseˇ.
1346        "});
1347
1348        let screen_coord = cx.pixel_position(indoc! {"
1349            Let's test a [complex](https://zed.dev/channel/had-(ˇoops)) case.
1350            "});
1351
1352        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1353        cx.assert_editor_text_highlights(
1354            HighlightKey::HoveredLinkState,
1355            indoc! {"
1356            Let's test a [complex](«https://zed.dev/channel/had-(oops)ˇ») case.
1357        "},
1358        );
1359
1360        cx.simulate_click(screen_coord, Modifiers::secondary_key());
1361        assert_eq!(
1362            cx.opened_url(),
1363            Some("https://zed.dev/channel/had-(oops)".into())
1364        );
1365    }
1366
1367    #[gpui::test]
1368    async fn test_hover_preconditions(cx: &mut gpui::TestAppContext) {
1369        init_test(cx, |_| {});
1370        let mut cx = EditorLspTestContext::new_rust(
1371            lsp::ServerCapabilities {
1372                ..Default::default()
1373            },
1374            cx,
1375        )
1376        .await;
1377
1378        macro_rules! assert_no_highlight {
1379            ($cx:expr) => {
1380                // No highlight
1381                $cx.update_editor(|editor, window, cx| {
1382                    assert!(
1383                        editor
1384                            .snapshot(window, cx)
1385                            .text_highlight_ranges(HighlightKey::HoveredLinkState)
1386                            .unwrap_or_default()
1387                            .1
1388                            .is_empty()
1389                    );
1390                });
1391            };
1392        }
1393
1394        // No link
1395        cx.set_state(indoc! {"
1396            Let's test a [complex](https://zed.dev/channel/) caseˇ.
1397        "});
1398        assert_no_highlight!(cx);
1399
1400        // No modifier
1401        let screen_coord = cx.pixel_position(indoc! {"
1402            Let's test a [complex](https://zed.dev/channel/ˇ) case.
1403            "});
1404        cx.simulate_mouse_move(screen_coord, None, Modifiers::none());
1405        assert_no_highlight!(cx);
1406
1407        // Modifier active
1408        let screen_coord = cx.pixel_position(indoc! {"
1409            Let's test a [complex](https://zed.dev/channeˇl/) case.
1410            "});
1411        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1412        cx.assert_editor_text_highlights(
1413            HighlightKey::HoveredLinkState,
1414            indoc! {"
1415            Let's test a [complex](«https://zed.dev/channel/ˇ») case.
1416        "},
1417        );
1418
1419        // Cursor hidden with secondary key
1420        let screen_coord = cx.pixel_position(indoc! {"
1421            Let's test a [complex](https://zed.dev/ˇchannel/) case.
1422            "});
1423        cx.simulate_mouse_move(screen_coord, None, Modifiers::none());
1424        cx.update_editor(|editor, _, cx| {
1425            editor.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
1426        });
1427        cx.simulate_modifiers_change(Modifiers::secondary_key());
1428        assert_no_highlight!(cx);
1429
1430        // Cursor active again
1431        let screen_coord = cx.pixel_position(indoc! {"
1432            Let's test a [complex](https://ˇzed.dev/channel/) case.
1433            "});
1434        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1435        cx.assert_editor_text_highlights(
1436            HighlightKey::HoveredLinkState,
1437            indoc! {"
1438            Let's test a [complex](«https://zed.dev/channel/ˇ») case.
1439        "},
1440        );
1441    }
1442
1443    #[gpui::test]
1444    async fn test_urls_at_beginning_of_buffer(cx: &mut gpui::TestAppContext) {
1445        init_test(cx, |_| {});
1446        let mut cx = EditorLspTestContext::new_rust(
1447            lsp::ServerCapabilities {
1448                ..Default::default()
1449            },
1450            cx,
1451        )
1452        .await;
1453
1454        cx.set_state(indoc! {"https://zed.dev/releases is a cool ˇwebpage."});
1455
1456        let screen_coord =
1457            cx.pixel_position(indoc! {"https://zed.dev/relˇeases is a cool webpage."});
1458
1459        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1460        cx.assert_editor_text_highlights(
1461            HighlightKey::HoveredLinkState,
1462            indoc! {"«https://zed.dev/releasesˇ» is a cool webpage."},
1463        );
1464
1465        cx.simulate_click(screen_coord, Modifiers::secondary_key());
1466        assert_eq!(cx.opened_url(), Some("https://zed.dev/releases".into()));
1467    }
1468
1469    #[gpui::test]
1470    async fn test_urls_at_end_of_buffer(cx: &mut gpui::TestAppContext) {
1471        init_test(cx, |_| {});
1472        let mut cx = EditorLspTestContext::new_rust(
1473            lsp::ServerCapabilities {
1474                ..Default::default()
1475            },
1476            cx,
1477        )
1478        .await;
1479
1480        cx.set_state(indoc! {"A cool ˇwebpage is https://zed.dev/releases"});
1481
1482        let screen_coord =
1483            cx.pixel_position(indoc! {"A cool webpage is https://zed.dev/releˇases"});
1484
1485        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1486        cx.assert_editor_text_highlights(
1487            HighlightKey::HoveredLinkState,
1488            indoc! {"A cool webpage is «https://zed.dev/releasesˇ»"},
1489        );
1490
1491        cx.simulate_click(screen_coord, Modifiers::secondary_key());
1492        assert_eq!(cx.opened_url(), Some("https://zed.dev/releases".into()));
1493    }
1494
1495    #[test]
1496    fn test_link_pattern_file_candidates() {
1497        let candidates: Vec<String> = link_pattern_file_candidates("[LinkTitle](link_file.txt)")
1498            .into_iter()
1499            .map(|(c, _)| c)
1500            .collect();
1501        assert_eq!(
1502            candidates,
1503            vec!["[LinkTitle](link_file.txt)", "link_file.txt",]
1504        );
1505        // Link title with spaces in it
1506        let candidates: Vec<String> = link_pattern_file_candidates("LinkTitle](link_file.txt)")
1507            .into_iter()
1508            .map(|(c, _)| c)
1509            .collect();
1510        assert_eq!(
1511            candidates,
1512            vec!["LinkTitle](link_file.txt)", "link_file.txt",]
1513        );
1514
1515        // Link with spaces
1516        let candidates: Vec<String> = link_pattern_file_candidates("LinkTitle](link\\ _file.txt)")
1517            .into_iter()
1518            .map(|(c, _)| c)
1519            .collect();
1520
1521        assert_eq!(
1522            candidates,
1523            vec!["LinkTitle](link\\ _file.txt)", "link\\ _file.txt",]
1524        );
1525        // Parentheses without preceding `]` should not extract inner content,
1526        // to avoid matching function calls like `do_work(file2)` as file paths.
1527        let candidates: Vec<String> = link_pattern_file_candidates("(link_file.txt)")
1528            .into_iter()
1529            .map(|(c, _)| c)
1530            .collect();
1531        assert_eq!(candidates, vec!["(link_file.txt)"]);
1532
1533        let candidates: Vec<String> = link_pattern_file_candidates("do_work(file2);")
1534            .into_iter()
1535            .map(|(c, _)| c)
1536            .collect();
1537        assert_eq!(candidates, vec!["do_work(file2);"]);
1538
1539        // Markdown links should still extract the path
1540        let candidates: Vec<String> = link_pattern_file_candidates("](readme.md)")
1541            .into_iter()
1542            .map(|(c, _)| c)
1543            .collect();
1544        assert_eq!(candidates, vec!["](readme.md)", "readme.md"]);
1545
1546        // No nesting
1547        let candidates: Vec<String> =
1548            link_pattern_file_candidates("LinkTitle](link_(link_file)file.txt)")
1549                .into_iter()
1550                .map(|(c, _)| c)
1551                .collect();
1552
1553        assert_eq!(
1554            candidates,
1555            vec!["LinkTitle](link_(link_file)file.txt)", "link_(link_file",]
1556        )
1557    }
1558
1559    #[gpui::test]
1560    async fn test_surrounding_filename(cx: &mut gpui::TestAppContext) {
1561        init_test(cx, |_| {});
1562        let mut cx = EditorLspTestContext::new_rust(
1563            lsp::ServerCapabilities {
1564                ..Default::default()
1565            },
1566            cx,
1567        )
1568        .await;
1569
1570        let test_cases = [
1571            ("file ˇ name", None),
1572            ("ˇfile name", Some("file")),
1573            ("file ˇname", Some("name")),
1574            ("fiˇle name", Some("file")),
1575            ("filenˇame", Some("filename")),
1576            // Absolute path
1577            ("foobar ˇ/home/user/f.txt", Some("/home/user/f.txt")),
1578            ("foobar /home/useˇr/f.txt", Some("/home/user/f.txt")),
1579            // Windows
1580            ("C:\\Useˇrs\\user\\f.txt", Some("C:\\Users\\user\\f.txt")),
1581            // Whitespace
1582            ("ˇfile\\ -\\ name.txt", Some("file - name.txt")),
1583            ("file\\ -\\ naˇme.txt", Some("file - name.txt")),
1584            // Tilde
1585            ("ˇ~/file.txt", Some("~/file.txt")),
1586            ("~/fiˇle.txt", Some("~/file.txt")),
1587            // Double quotes
1588            ("\"fˇile.txt\"", Some("file.txt")),
1589            ("ˇ\"file.txt\"", Some("file.txt")),
1590            ("ˇ\"fi\\ le.txt\"", Some("fi le.txt")),
1591            // Single quotes
1592            ("'fˇile.txt'", Some("file.txt")),
1593            ("ˇ'file.txt'", Some("file.txt")),
1594            ("ˇ'fi\\ le.txt'", Some("fi le.txt")),
1595            // Quoted multibyte characters
1596            (" ˇ\"\"", Some("")),
1597            (" \"ˇ常\"", Some("")),
1598            ("ˇ\"\"", Some("")),
1599        ];
1600
1601        for (input, expected) in test_cases {
1602            cx.set_state(input);
1603
1604            let (position, snapshot) = cx.editor(|editor, _, cx| {
1605                let positions = editor.selections.newest_anchor().head().text_anchor;
1606                let snapshot = editor
1607                    .buffer()
1608                    .clone()
1609                    .read(cx)
1610                    .as_singleton()
1611                    .unwrap()
1612                    .read(cx)
1613                    .snapshot();
1614                (positions, snapshot)
1615            });
1616
1617            let result = surrounding_filename(&snapshot, position);
1618
1619            if let Some(expected) = expected {
1620                assert!(result.is_some(), "Failed to find file path: {}", input);
1621                let (_, path) = result.unwrap();
1622                assert_eq!(&path, expected, "Incorrect file path for input: {}", input);
1623            } else {
1624                assert!(
1625                    result.is_none(),
1626                    "Expected no result, but got one: {:?}",
1627                    result
1628                );
1629            }
1630        }
1631    }
1632
1633    #[gpui::test]
1634    async fn test_hover_filenames(cx: &mut gpui::TestAppContext) {
1635        init_test(cx, |_| {});
1636        let mut cx = EditorLspTestContext::new_rust(
1637            lsp::ServerCapabilities {
1638                ..Default::default()
1639            },
1640            cx,
1641        )
1642        .await;
1643
1644        // Insert a new file
1645        let fs = cx.update_workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
1646        fs.as_fake()
1647            .insert_file(
1648                path!("/root/dir/file2.rs"),
1649                "This is file2.rs".as_bytes().to_vec(),
1650            )
1651            .await;
1652
1653        #[cfg(not(target_os = "windows"))]
1654        cx.set_state(indoc! {"
1655            You can't go to a file that does_not_exist.txt.
1656            Go to file2.rs if you want.
1657            Or go to ../dir/file2.rs if you want.
1658            Or go to /root/dir/file2.rs if project is local.
1659            Or go to /root/dir/file2 if this is a Rust file.ˇ
1660            "});
1661        #[cfg(target_os = "windows")]
1662        cx.set_state(indoc! {"
1663            You can't go to a file that does_not_exist.txt.
1664            Go to file2.rs if you want.
1665            Or go to ../dir/file2.rs if you want.
1666            Or go to C:/root/dir/file2.rs if project is local.
1667            Or go to C:/root/dir/file2 if this is a Rust file.ˇ
1668        "});
1669
1670        // File does not exist
1671        #[cfg(not(target_os = "windows"))]
1672        let screen_coord = cx.pixel_position(indoc! {"
1673            You can't go to a file that dˇoes_not_exist.txt.
1674            Go to file2.rs if you want.
1675            Or go to ../dir/file2.rs if you want.
1676            Or go to /root/dir/file2.rs if project is local.
1677            Or go to /root/dir/file2 if this is a Rust file.
1678        "});
1679        #[cfg(target_os = "windows")]
1680        let screen_coord = cx.pixel_position(indoc! {"
1681            You can't go to a file that dˇoes_not_exist.txt.
1682            Go to file2.rs if you want.
1683            Or go to ../dir/file2.rs if you want.
1684            Or go to C:/root/dir/file2.rs if project is local.
1685            Or go to C:/root/dir/file2 if this is a Rust file.
1686        "});
1687        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1688        // No highlight
1689        cx.update_editor(|editor, window, cx| {
1690            assert!(
1691                editor
1692                    .snapshot(window, cx)
1693                    .text_highlight_ranges(HighlightKey::HoveredLinkState)
1694                    .unwrap_or_default()
1695                    .1
1696                    .is_empty()
1697            );
1698        });
1699
1700        // Moving the mouse over a file that does exist should highlight it.
1701        #[cfg(not(target_os = "windows"))]
1702        let screen_coord = cx.pixel_position(indoc! {"
1703            You can't go to a file that does_not_exist.txt.
1704            Go to fˇile2.rs if you want.
1705            Or go to ../dir/file2.rs if you want.
1706            Or go to /root/dir/file2.rs if project is local.
1707            Or go to /root/dir/file2 if this is a Rust file.
1708        "});
1709        #[cfg(target_os = "windows")]
1710        let screen_coord = cx.pixel_position(indoc! {"
1711            You can't go to a file that does_not_exist.txt.
1712            Go to fˇile2.rs if you want.
1713            Or go to ../dir/file2.rs if you want.
1714            Or go to C:/root/dir/file2.rs if project is local.
1715            Or go to C:/root/dir/file2 if this is a Rust file.
1716        "});
1717
1718        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1719        #[cfg(not(target_os = "windows"))]
1720        cx.assert_editor_text_highlights(
1721            HighlightKey::HoveredLinkState,
1722            indoc! {"
1723            You can't go to a file that does_not_exist.txt.
1724            Go to «file2.rsˇ» if you want.
1725            Or go to ../dir/file2.rs if you want.
1726            Or go to /root/dir/file2.rs if project is local.
1727            Or go to /root/dir/file2 if this is a Rust file.
1728        "},
1729        );
1730        #[cfg(target_os = "windows")]
1731        cx.assert_editor_text_highlights(
1732            HighlightKey::HoveredLinkState,
1733            indoc! {"
1734            You can't go to a file that does_not_exist.txt.
1735            Go to «file2.rsˇ» if you want.
1736            Or go to ../dir/file2.rs if you want.
1737            Or go to C:/root/dir/file2.rs if project is local.
1738            Or go to C:/root/dir/file2 if this is a Rust file.
1739        "},
1740        );
1741
1742        // Moving the mouse over a relative path that does exist should highlight it
1743        #[cfg(not(target_os = "windows"))]
1744        let screen_coord = cx.pixel_position(indoc! {"
1745            You can't go to a file that does_not_exist.txt.
1746            Go to file2.rs if you want.
1747            Or go to ../dir/fˇile2.rs if you want.
1748            Or go to /root/dir/file2.rs if project is local.
1749            Or go to /root/dir/file2 if this is a Rust file.
1750        "});
1751        #[cfg(target_os = "windows")]
1752        let screen_coord = cx.pixel_position(indoc! {"
1753            You can't go to a file that does_not_exist.txt.
1754            Go to file2.rs if you want.
1755            Or go to ../dir/fˇile2.rs if you want.
1756            Or go to C:/root/dir/file2.rs if project is local.
1757            Or go to C:/root/dir/file2 if this is a Rust file.
1758        "});
1759
1760        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1761        #[cfg(not(target_os = "windows"))]
1762        cx.assert_editor_text_highlights(
1763            HighlightKey::HoveredLinkState,
1764            indoc! {"
1765            You can't go to a file that does_not_exist.txt.
1766            Go to file2.rs if you want.
1767            Or go to «../dir/file2.rsˇ» if you want.
1768            Or go to /root/dir/file2.rs if project is local.
1769            Or go to /root/dir/file2 if this is a Rust file.
1770        "},
1771        );
1772        #[cfg(target_os = "windows")]
1773        cx.assert_editor_text_highlights(
1774            HighlightKey::HoveredLinkState,
1775            indoc! {"
1776            You can't go to a file that does_not_exist.txt.
1777            Go to file2.rs if you want.
1778            Or go to «../dir/file2.rsˇ» if you want.
1779            Or go to C:/root/dir/file2.rs if project is local.
1780            Or go to C:/root/dir/file2 if this is a Rust file.
1781        "},
1782        );
1783
1784        // Moving the mouse over an absolute path that does exist should highlight it
1785        #[cfg(not(target_os = "windows"))]
1786        let screen_coord = cx.pixel_position(indoc! {"
1787            You can't go to a file that does_not_exist.txt.
1788            Go to file2.rs if you want.
1789            Or go to ../dir/file2.rs if you want.
1790            Or go to /root/diˇr/file2.rs if project is local.
1791            Or go to /root/dir/file2 if this is a Rust file.
1792        "});
1793
1794        #[cfg(target_os = "windows")]
1795        let screen_coord = cx.pixel_position(indoc! {"
1796            You can't go to a file that does_not_exist.txt.
1797            Go to file2.rs if you want.
1798            Or go to ../dir/file2.rs if you want.
1799            Or go to C:/root/diˇr/file2.rs if project is local.
1800            Or go to C:/root/dir/file2 if this is a Rust file.
1801        "});
1802
1803        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1804        #[cfg(not(target_os = "windows"))]
1805        cx.assert_editor_text_highlights(
1806            HighlightKey::HoveredLinkState,
1807            indoc! {"
1808            You can't go to a file that does_not_exist.txt.
1809            Go to file2.rs if you want.
1810            Or go to ../dir/file2.rs if you want.
1811            Or go to «/root/dir/file2.rsˇ» if project is local.
1812            Or go to /root/dir/file2 if this is a Rust file.
1813        "},
1814        );
1815        #[cfg(target_os = "windows")]
1816        cx.assert_editor_text_highlights(
1817            HighlightKey::HoveredLinkState,
1818            indoc! {"
1819            You can't go to a file that does_not_exist.txt.
1820            Go to file2.rs if you want.
1821            Or go to ../dir/file2.rs if you want.
1822            Or go to «C:/root/dir/file2.rsˇ» if project is local.
1823            Or go to C:/root/dir/file2 if this is a Rust file.
1824        "},
1825        );
1826
1827        // Moving the mouse over a path that exists, if we add the language-specific suffix, it should highlight it
1828        #[cfg(not(target_os = "windows"))]
1829        let screen_coord = cx.pixel_position(indoc! {"
1830            You can't go to a file that does_not_exist.txt.
1831            Go to file2.rs if you want.
1832            Or go to ../dir/file2.rs if you want.
1833            Or go to /root/dir/file2.rs if project is local.
1834            Or go to /root/diˇr/file2 if this is a Rust file.
1835        "});
1836        #[cfg(target_os = "windows")]
1837        let screen_coord = cx.pixel_position(indoc! {"
1838            You can't go to a file that does_not_exist.txt.
1839            Go to file2.rs if you want.
1840            Or go to ../dir/file2.rs if you want.
1841            Or go to C:/root/dir/file2.rs if project is local.
1842            Or go to C:/root/diˇr/file2 if this is a Rust file.
1843        "});
1844
1845        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1846        #[cfg(not(target_os = "windows"))]
1847        cx.assert_editor_text_highlights(
1848            HighlightKey::HoveredLinkState,
1849            indoc! {"
1850            You can't go to a file that does_not_exist.txt.
1851            Go to file2.rs if you want.
1852            Or go to ../dir/file2.rs if you want.
1853            Or go to /root/dir/file2.rs if project is local.
1854            Or go to «/root/dir/file2ˇ» if this is a Rust file.
1855        "},
1856        );
1857        #[cfg(target_os = "windows")]
1858        cx.assert_editor_text_highlights(
1859            HighlightKey::HoveredLinkState,
1860            indoc! {"
1861            You can't go to a file that does_not_exist.txt.
1862            Go to file2.rs if you want.
1863            Or go to ../dir/file2.rs if you want.
1864            Or go to C:/root/dir/file2.rs if project is local.
1865            Or go to «C:/root/dir/file2ˇ» if this is a Rust file.
1866        "},
1867        );
1868
1869        cx.simulate_click(screen_coord, Modifiers::secondary_key());
1870
1871        cx.update_workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
1872        cx.update_workspace(|workspace, _, cx| {
1873            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
1874
1875            let buffer = active_editor
1876                .read(cx)
1877                .buffer()
1878                .read(cx)
1879                .as_singleton()
1880                .unwrap();
1881
1882            let file = buffer.read(cx).file().unwrap();
1883            let file_path = file.as_local().unwrap().abs_path(cx);
1884
1885            assert_eq!(
1886                file_path,
1887                std::path::PathBuf::from(path!("/root/dir/file2.rs"))
1888            );
1889        });
1890    }
1891
1892    #[gpui::test]
1893    async fn test_hover_directories(cx: &mut gpui::TestAppContext) {
1894        init_test(cx, |_| {});
1895        let mut cx = EditorLspTestContext::new_rust(
1896            lsp::ServerCapabilities {
1897                ..Default::default()
1898            },
1899            cx,
1900        )
1901        .await;
1902
1903        // Insert a new file
1904        let fs = cx.update_workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
1905        fs.as_fake()
1906            .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
1907            .await;
1908
1909        cx.set_state(indoc! {"
1910            You can't open ../diˇr because it's a directory.
1911        "});
1912
1913        // File does not exist
1914        let screen_coord = cx.pixel_position(indoc! {"
1915            You can't open ../diˇr because it's a directory.
1916        "});
1917        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1918
1919        // No highlight
1920        cx.update_editor(|editor, window, cx| {
1921            assert!(
1922                editor
1923                    .snapshot(window, cx)
1924                    .text_highlight_ranges(HighlightKey::HoveredLinkState)
1925                    .unwrap_or_default()
1926                    .1
1927                    .is_empty()
1928            );
1929        });
1930
1931        // Does not open the directory
1932        cx.simulate_click(screen_coord, Modifiers::secondary_key());
1933        cx.update_workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
1934    }
1935
1936    #[gpui::test]
1937    async fn test_hover_unicode(cx: &mut gpui::TestAppContext) {
1938        init_test(cx, |_| {});
1939        let mut cx = EditorLspTestContext::new_rust(
1940            lsp::ServerCapabilities {
1941                ..Default::default()
1942            },
1943            cx,
1944        )
1945        .await;
1946
1947        cx.set_state(indoc! {"
1948            You can't open ˇ\"🤩\" because it's an emoji.
1949        "});
1950
1951        // File does not exist
1952        let screen_coord = cx.pixel_position(indoc! {"
1953            You can't open ˇ\"🤩\" because it's an emoji.
1954        "});
1955        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
1956
1957        // No highlight, does not panic...
1958        cx.update_editor(|editor, window, cx| {
1959            assert!(
1960                editor
1961                    .snapshot(window, cx)
1962                    .text_highlight_ranges(HighlightKey::HoveredLinkState)
1963                    .unwrap_or_default()
1964                    .1
1965                    .is_empty()
1966            );
1967        });
1968
1969        // Does not open the directory
1970        cx.simulate_click(screen_coord, Modifiers::secondary_key());
1971        cx.update_workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
1972    }
1973
1974    #[gpui::test]
1975    async fn test_pressure_links(cx: &mut gpui::TestAppContext) {
1976        init_test(cx, |_| {});
1977
1978        let mut cx = EditorLspTestContext::new_rust(
1979            lsp::ServerCapabilities {
1980                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
1981                definition_provider: Some(lsp::OneOf::Left(true)),
1982                ..Default::default()
1983            },
1984            cx,
1985        )
1986        .await;
1987
1988        cx.set_state(indoc! {"
1989                    fn ˇtest() { do_work(); }
1990                    fn do_work() { test(); }
1991                "});
1992
1993        // Position the mouse over a symbol that has a definition
1994        let hover_point = cx.pixel_position(indoc! {"
1995                    fn test() { do_wˇork(); }
1996                    fn do_work() { test(); }
1997                "});
1998        let symbol_range = cx.lsp_range(indoc! {"
1999                    fn test() { «do_work»(); }
2000                    fn do_work() { test(); }
2001                "});
2002        let target_range = cx.lsp_range(indoc! {"
2003                    fn test() { do_work(); }
2004                    fn «do_work»() { test(); }
2005                "});
2006
2007        let mut requests =
2008            cx.set_request_handler::<GotoDefinition, _, _>(move |url, _, _| async move {
2009                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
2010                    lsp::LocationLink {
2011                        origin_selection_range: Some(symbol_range),
2012                        target_uri: url.clone(),
2013                        target_range,
2014                        target_selection_range: target_range,
2015                    },
2016                ])))
2017            });
2018
2019        cx.simulate_mouse_move(hover_point, None, Modifiers::none());
2020
2021        // First simulate Normal pressure to set up the previous stage
2022        cx.simulate_event(MousePressureEvent {
2023            pressure: 0.5,
2024            stage: PressureStage::Normal,
2025            position: hover_point,
2026            modifiers: Modifiers::none(),
2027        });
2028        cx.background_executor.run_until_parked();
2029
2030        // Now simulate Force pressure to trigger the force click and go-to definition
2031        cx.simulate_event(MousePressureEvent {
2032            pressure: 1.0,
2033            stage: PressureStage::Force,
2034            position: hover_point,
2035            modifiers: Modifiers::none(),
2036        });
2037        requests.next().await;
2038        cx.background_executor.run_until_parked();
2039
2040        // Assert that we navigated to the definition
2041        cx.assert_editor_state(indoc! {"
2042                    fn test() { do_work(); }
2043                    fn «do_workˇ»() { test(); }
2044                "});
2045    }
2046}