hover_links.rs

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