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