use crate::{
    editor_settings::MultiCursorModifier,
    hover_popover::{self, InlayHover},
    scroll::ScrollAmount,
    Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
    GoToTypeDefinition, GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase,
};
use gpui::{px, App, AsyncWindowContext, Context, Entity, Modifiers, Task, Window};
use language::{Bias, ToOffset};
use linkify::{LinkFinder, LinkKind};
use lsp::LanguageServerId;
use project::{
    HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project,
    ResolveState, ResolvedPath,
};
use settings::Settings;
use std::ops::Range;
use theme::ActiveTheme as _;
use util::{maybe, ResultExt, TryFutureExt as _};

#[derive(Debug)]
pub struct HoveredLinkState {
    pub last_trigger_point: TriggerPoint,
    pub preferred_kind: GotoDefinitionKind,
    pub symbol_range: Option<RangeInEditor>,
    pub links: Vec<HoverLink>,
    pub task: Option<Task<Option<()>>>,
}

#[derive(Debug, Eq, PartialEq, Clone)]
pub enum RangeInEditor {
    Text(Range<Anchor>),
    Inlay(InlayHighlight),
}

impl RangeInEditor {
    pub fn as_text_range(&self) -> Option<Range<Anchor>> {
        match self {
            Self::Text(range) => Some(range.clone()),
            Self::Inlay(_) => None,
        }
    }

    pub fn point_within_range(
        &self,
        trigger_point: &TriggerPoint,
        snapshot: &EditorSnapshot,
    ) -> bool {
        match (self, trigger_point) {
            (Self::Text(range), TriggerPoint::Text(point)) => {
                let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
                point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge()
            }
            (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => {
                highlight.inlay == point.inlay
                    && highlight.range.contains(&point.range.start)
                    && highlight.range.contains(&point.range.end)
            }
            (Self::Inlay(_), TriggerPoint::Text(_))
            | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
        }
    }
}

#[derive(Debug, Clone)]
pub enum HoverLink {
    Url(String),
    File(ResolvedPath),
    Text(LocationLink),
    InlayHint(lsp::Location, LanguageServerId),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InlayHighlight {
    pub inlay: InlayId,
    pub inlay_position: Anchor,
    pub range: Range<usize>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum TriggerPoint {
    Text(Anchor),
    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
}

impl TriggerPoint {
    fn anchor(&self) -> &Anchor {
        match self {
            TriggerPoint::Text(anchor) => anchor,
            TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position,
        }
    }
}

pub fn exclude_link_to_position(
    buffer: &Entity<language::Buffer>,
    current_position: &text::Anchor,
    location: &LocationLink,
    cx: &App,
) -> bool {
    // Exclude definition links that points back to cursor position.
    // (i.e., currently cursor upon definition).
    let snapshot = buffer.read(cx).snapshot();
    !(buffer == &location.target.buffer
        && current_position
            .bias_right(&snapshot)
            .cmp(&location.target.range.start, &snapshot)
            .is_ge()
        && current_position
            .cmp(&location.target.range.end, &snapshot)
            .is_le())
}

impl Editor {
    pub(crate) fn update_hovered_link(
        &mut self,
        point_for_position: PointForPosition,
        snapshot: &EditorSnapshot,
        modifiers: Modifiers,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
        let hovered_link_modifier = match multi_cursor_setting {
            MultiCursorModifier::Alt => modifiers.secondary(),
            MultiCursorModifier::CmdOrCtrl => modifiers.alt,
        };
        if !hovered_link_modifier || self.has_pending_selection() {
            self.hide_hovered_link(cx);
            return;
        }

        match point_for_position.as_valid() {
            Some(point) => {
                let trigger_point = TriggerPoint::Text(
                    snapshot
                        .buffer_snapshot
                        .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)),
                );

                show_link_definition(modifiers.shift, self, trigger_point, snapshot, window, cx);
            }
            None => {
                update_inlay_link_and_hover_points(
                    snapshot,
                    point_for_position,
                    self,
                    hovered_link_modifier,
                    modifiers.shift,
                    window,
                    cx,
                );
            }
        }
    }

    pub(crate) fn hide_hovered_link(&mut self, cx: &mut Context<Self>) {
        self.hovered_link_state.take();
        self.clear_highlights::<HoveredLinkState>(cx);
    }

    pub(crate) fn handle_click_hovered_link(
        &mut self,
        point: PointForPosition,
        modifiers: Modifiers,
        window: &mut Window,
        cx: &mut Context<Editor>,
    ) {
        let reveal_task = self.cmd_click_reveal_task(point, modifiers, window, cx);
        cx.spawn_in(window, |editor, mut cx| async move {
            let definition_revealed = reveal_task.await.log_err().unwrap_or(Navigated::No);
            let find_references = editor
                .update_in(&mut cx, |editor, window, cx| {
                    if definition_revealed == Navigated::Yes {
                        return None;
                    }
                    editor.find_all_references(&FindAllReferences, window, cx)
                })
                .ok()
                .flatten();
            if let Some(find_references) = find_references {
                find_references.await.log_err();
            }
        })
        .detach();
    }

    pub fn scroll_hover(
        &mut self,
        amount: &ScrollAmount,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> bool {
        let selection = self.selections.newest_anchor().head();
        let snapshot = self.snapshot(window, cx);

        let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
            popover
                .symbol_range
                .point_within_range(&TriggerPoint::Text(selection), &snapshot)
        }) else {
            return false;
        };
        popover.scroll(amount, window, cx);
        true
    }

    fn cmd_click_reveal_task(
        &mut self,
        point: PointForPosition,
        modifiers: Modifiers,
        window: &mut Window,
        cx: &mut Context<Editor>,
    ) -> Task<anyhow::Result<Navigated>> {
        if let Some(hovered_link_state) = self.hovered_link_state.take() {
            self.hide_hovered_link(cx);
            if !hovered_link_state.links.is_empty() {
                if !self.focus_handle.is_focused(window) {
                    window.focus(&self.focus_handle);
                }

                // exclude links pointing back to the current anchor
                let current_position = point
                    .next_valid
                    .to_point(&self.snapshot(window, cx).display_snapshot);
                let Some((buffer, anchor)) = self
                    .buffer()
                    .read(cx)
                    .text_anchor_for_position(current_position, cx)
                else {
                    return Task::ready(Ok(Navigated::No));
                };
                let links = hovered_link_state
                    .links
                    .into_iter()
                    .filter(|link| {
                        if let HoverLink::Text(location) = link {
                            exclude_link_to_position(&buffer, &anchor, location, cx)
                        } else {
                            true
                        }
                    })
                    .collect();

                return self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
            }
        }

        // We don't have the correct kind of link cached, set the selection on
        // click and immediately trigger GoToDefinition.
        self.select(
            SelectPhase::Begin {
                position: point.next_valid,
                add: false,
                click_count: 1,
            },
            window,
            cx,
        );

        if point.as_valid().is_some() {
            if modifiers.shift {
                self.go_to_type_definition(&GoToTypeDefinition, window, cx)
            } else {
                self.go_to_definition(&GoToDefinition, window, cx)
            }
        } else {
            Task::ready(Ok(Navigated::No))
        }
    }
}

pub fn update_inlay_link_and_hover_points(
    snapshot: &EditorSnapshot,
    point_for_position: PointForPosition,
    editor: &mut Editor,
    secondary_held: bool,
    shift_held: bool,
    window: &mut Window,
    cx: &mut Context<Editor>,
) {
    let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
        Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
    } else {
        None
    };
    let mut go_to_definition_updated = false;
    let mut hover_updated = false;
    if let Some(hovered_offset) = hovered_offset {
        let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
        let previous_valid_anchor = buffer_snapshot.anchor_at(
            point_for_position.previous_valid.to_point(snapshot),
            Bias::Left,
        );
        let next_valid_anchor = buffer_snapshot.anchor_at(
            point_for_position.next_valid.to_point(snapshot),
            Bias::Right,
        );
        if let Some(hovered_hint) = editor
            .visible_inlay_hints(cx)
            .into_iter()
            .skip_while(|hint| {
                hint.position
                    .cmp(&previous_valid_anchor, &buffer_snapshot)
                    .is_lt()
            })
            .take_while(|hint| {
                hint.position
                    .cmp(&next_valid_anchor, &buffer_snapshot)
                    .is_le()
            })
            .max_by_key(|hint| hint.id)
        {
            let inlay_hint_cache = editor.inlay_hint_cache();
            let excerpt_id = previous_valid_anchor.excerpt_id;
            if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
                match cached_hint.resolve_state {
                    ResolveState::CanResolve(_, _) => {
                        if let Some(buffer_id) = previous_valid_anchor.buffer_id {
                            inlay_hint_cache.spawn_hint_resolve(
                                buffer_id,
                                excerpt_id,
                                hovered_hint.id,
                                window,
                                cx,
                            );
                        }
                    }
                    ResolveState::Resolved => {
                        let mut extra_shift_left = 0;
                        let mut extra_shift_right = 0;
                        if cached_hint.padding_left {
                            extra_shift_left += 1;
                            extra_shift_right += 1;
                        }
                        if cached_hint.padding_right {
                            extra_shift_right += 1;
                        }
                        match cached_hint.label {
                            project::InlayHintLabel::String(_) => {
                                if let Some(tooltip) = cached_hint.tooltip {
                                    hover_popover::hover_at_inlay(
                                        editor,
                                        InlayHover {
                                            tooltip: match tooltip {
                                                InlayHintTooltip::String(text) => HoverBlock {
                                                    text,
                                                    kind: HoverBlockKind::PlainText,
                                                },
                                                InlayHintTooltip::MarkupContent(content) => {
                                                    HoverBlock {
                                                        text: content.value,
                                                        kind: content.kind,
                                                    }
                                                }
                                            },
                                            range: InlayHighlight {
                                                inlay: hovered_hint.id,
                                                inlay_position: hovered_hint.position,
                                                range: extra_shift_left
                                                    ..hovered_hint.text.len() + extra_shift_right,
                                            },
                                        },
                                        window,
                                        cx,
                                    );
                                    hover_updated = true;
                                }
                            }
                            project::InlayHintLabel::LabelParts(label_parts) => {
                                let hint_start =
                                    snapshot.anchor_to_inlay_offset(hovered_hint.position);
                                if let Some((hovered_hint_part, part_range)) =
                                    hover_popover::find_hovered_hint_part(
                                        label_parts,
                                        hint_start,
                                        hovered_offset,
                                    )
                                {
                                    let highlight_start =
                                        (part_range.start - hint_start).0 + extra_shift_left;
                                    let highlight_end =
                                        (part_range.end - hint_start).0 + extra_shift_right;
                                    let highlight = InlayHighlight {
                                        inlay: hovered_hint.id,
                                        inlay_position: hovered_hint.position,
                                        range: highlight_start..highlight_end,
                                    };
                                    if let Some(tooltip) = hovered_hint_part.tooltip {
                                        hover_popover::hover_at_inlay(
                                            editor,
                                            InlayHover {
                                                tooltip: match tooltip {
                                                    InlayHintLabelPartTooltip::String(text) => {
                                                        HoverBlock {
                                                            text,
                                                            kind: HoverBlockKind::PlainText,
                                                        }
                                                    }
                                                    InlayHintLabelPartTooltip::MarkupContent(
                                                        content,
                                                    ) => HoverBlock {
                                                        text: content.value,
                                                        kind: content.kind,
                                                    },
                                                },
                                                range: highlight.clone(),
                                            },
                                            window,
                                            cx,
                                        );
                                        hover_updated = true;
                                    }
                                    if let Some((language_server_id, location)) =
                                        hovered_hint_part.location
                                    {
                                        if secondary_held
                                            && !editor.has_pending_nonempty_selection()
                                        {
                                            go_to_definition_updated = true;
                                            show_link_definition(
                                                shift_held,
                                                editor,
                                                TriggerPoint::InlayHint(
                                                    highlight,
                                                    location,
                                                    language_server_id,
                                                ),
                                                snapshot,
                                                window,
                                                cx,
                                            );
                                        }
                                    }
                                }
                            }
                        };
                    }
                    ResolveState::Resolving => {}
                }
            }
        }
    }

    if !go_to_definition_updated {
        editor.hide_hovered_link(cx)
    }
    if !hover_updated {
        hover_popover::hover_at(editor, None, window, cx);
    }
}

pub fn show_link_definition(
    shift_held: bool,
    editor: &mut Editor,
    trigger_point: TriggerPoint,
    snapshot: &EditorSnapshot,
    window: &mut Window,
    cx: &mut Context<Editor>,
) {
    let preferred_kind = match trigger_point {
        TriggerPoint::Text(_) if !shift_held => GotoDefinitionKind::Symbol,
        _ => GotoDefinitionKind::Type,
    };

    let (mut hovered_link_state, is_cached) =
        if let Some(existing) = editor.hovered_link_state.take() {
            (existing, true)
        } else {
            (
                HoveredLinkState {
                    last_trigger_point: trigger_point.clone(),
                    symbol_range: None,
                    preferred_kind,
                    links: vec![],
                    task: None,
                },
                false,
            )
        };

    if editor.pending_rename.is_some() {
        return;
    }

    let trigger_anchor = trigger_point.anchor();
    let Some((buffer, buffer_position)) = editor
        .buffer
        .read(cx)
        .text_anchor_for_position(*trigger_anchor, cx)
    else {
        return;
    };

    let Some((excerpt_id, _, _)) = editor
        .buffer()
        .read(cx)
        .excerpt_containing(*trigger_anchor, cx)
    else {
        return;
    };

    let same_kind = hovered_link_state.preferred_kind == preferred_kind
        || hovered_link_state
            .links
            .first()
            .is_some_and(|d| matches!(d, HoverLink::Url(_)));

    if same_kind {
        if is_cached && (hovered_link_state.last_trigger_point == trigger_point)
            || hovered_link_state
                .symbol_range
                .as_ref()
                .is_some_and(|symbol_range| {
                    symbol_range.point_within_range(&trigger_point, snapshot)
                })
        {
            editor.hovered_link_state = Some(hovered_link_state);
            return;
        }
    } else {
        editor.hide_hovered_link(cx)
    }
    let project = editor.project.clone();
    let provider = editor.semantics_provider.clone();

    let snapshot = snapshot.buffer_snapshot.clone();
    hovered_link_state.task = Some(cx.spawn_in(window, |this, mut cx| {
        async move {
            let result = match &trigger_point {
                TriggerPoint::Text(_) => {
                    if let Some((url_range, url)) = find_url(&buffer, buffer_position, cx.clone()) {
                        this.update(&mut cx, |_, _| {
                            let range = maybe!({
                                let start =
                                    snapshot.anchor_in_excerpt(excerpt_id, url_range.start)?;
                                let end = snapshot.anchor_in_excerpt(excerpt_id, url_range.end)?;
                                Some(RangeInEditor::Text(start..end))
                            });
                            (range, vec![HoverLink::Url(url)])
                        })
                        .ok()
                    } else if let Some((filename_range, filename)) =
                        find_file(&buffer, project.clone(), buffer_position, &mut cx).await
                    {
                        let range = maybe!({
                            let start =
                                snapshot.anchor_in_excerpt(excerpt_id, filename_range.start)?;
                            let end = snapshot.anchor_in_excerpt(excerpt_id, filename_range.end)?;
                            Some(RangeInEditor::Text(start..end))
                        });

                        Some((range, vec![HoverLink::File(filename)]))
                    } else if let Some(provider) = provider {
                        let task = cx.update(|_, cx| {
                            provider.definitions(&buffer, buffer_position, preferred_kind, cx)
                        })?;
                        if let Some(task) = task {
                            task.await.ok().map(|definition_result| {
                                (
                                    definition_result.iter().find_map(|link| {
                                        link.origin.as_ref().and_then(|origin| {
                                            let start = snapshot.anchor_in_excerpt(
                                                excerpt_id,
                                                origin.range.start,
                                            )?;
                                            let end = snapshot
                                                .anchor_in_excerpt(excerpt_id, origin.range.end)?;
                                            Some(RangeInEditor::Text(start..end))
                                        })
                                    }),
                                    definition_result.into_iter().map(HoverLink::Text).collect(),
                                )
                            })
                        } else {
                            None
                        }
                    } else {
                        None
                    }
                }
                TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
                    Some(RangeInEditor::Inlay(highlight.clone())),
                    vec![HoverLink::InlayHint(lsp_location.clone(), *server_id)],
                )),
            };

            this.update(&mut cx, |editor, cx| {
                // Clear any existing highlights
                editor.clear_highlights::<HoveredLinkState>(cx);
                let Some(hovered_link_state) = editor.hovered_link_state.as_mut() else {
                    editor.hide_hovered_link(cx);
                    return;
                };
                hovered_link_state.preferred_kind = preferred_kind;
                hovered_link_state.symbol_range = result
                    .as_ref()
                    .and_then(|(symbol_range, _)| symbol_range.clone());

                if let Some((symbol_range, definitions)) = result {
                    hovered_link_state.links = definitions;

                    let underline_hovered_link = !hovered_link_state.links.is_empty()
                        || hovered_link_state.symbol_range.is_some();

                    if underline_hovered_link {
                        let style = gpui::HighlightStyle {
                            underline: Some(gpui::UnderlineStyle {
                                thickness: px(1.),
                                ..Default::default()
                            }),
                            color: Some(cx.theme().colors().link_text_hover),
                            ..Default::default()
                        };
                        let highlight_range =
                            symbol_range.unwrap_or_else(|| match &trigger_point {
                                TriggerPoint::Text(trigger_anchor) => {
                                    // If no symbol range returned from language server, use the surrounding word.
                                    let (offset_range, _) =
                                        snapshot.surrounding_word(*trigger_anchor, false);
                                    RangeInEditor::Text(
                                        snapshot.anchor_before(offset_range.start)
                                            ..snapshot.anchor_after(offset_range.end),
                                    )
                                }
                                TriggerPoint::InlayHint(highlight, _, _) => {
                                    RangeInEditor::Inlay(highlight.clone())
                                }
                            });

                        match highlight_range {
                            RangeInEditor::Text(text_range) => editor
                                .highlight_text::<HoveredLinkState>(vec![text_range], style, cx),
                            RangeInEditor::Inlay(highlight) => editor
                                .highlight_inlays::<HoveredLinkState>(vec![highlight], style, cx),
                        }
                    }
                } else {
                    editor.hide_hovered_link(cx);
                }
            })?;

            Ok::<_, anyhow::Error>(())
        }
        .log_err()
    }));

    editor.hovered_link_state = Some(hovered_link_state);
}

pub(crate) fn find_url(
    buffer: &Entity<language::Buffer>,
    position: text::Anchor,
    mut cx: AsyncWindowContext,
) -> Option<(Range<text::Anchor>, String)> {
    const LIMIT: usize = 2048;

    let Ok(snapshot) = buffer.update(&mut cx, |buffer, _| buffer.snapshot()) else {
        return None;
    };

    let offset = position.to_offset(&snapshot);
    let mut token_start = offset;
    let mut token_end = offset;
    let mut found_start = false;
    let mut found_end = false;

    for ch in snapshot.reversed_chars_at(offset).take(LIMIT) {
        if ch.is_whitespace() {
            found_start = true;
            break;
        }
        token_start -= ch.len_utf8();
    }
    // Check if we didn't find the starting whitespace or if we didn't reach the start of the buffer
    if !found_start && token_start != 0 {
        return None;
    }

    for ch in snapshot
        .chars_at(offset)
        .take(LIMIT - (offset - token_start))
    {
        if ch.is_whitespace() {
            found_end = true;
            break;
        }
        token_end += ch.len_utf8();
    }
    // Check if we didn't find the ending whitespace or if we read more or equal than LIMIT
    // which at this point would happen only if we reached the end of buffer
    if !found_end && (token_end - token_start >= LIMIT) {
        return None;
    }

    let mut finder = LinkFinder::new();
    finder.kinds(&[LinkKind::Url]);
    let input = snapshot
        .text_for_range(token_start..token_end)
        .collect::<String>();

    let relative_offset = offset - token_start;
    for link in finder.links(&input) {
        if link.start() <= relative_offset && link.end() >= relative_offset {
            let range = snapshot.anchor_before(token_start + link.start())
                ..snapshot.anchor_after(token_start + link.end());
            return Some((range, link.as_str().to_string()));
        }
    }
    None
}

pub(crate) fn find_url_from_range(
    buffer: &Entity<language::Buffer>,
    range: Range<text::Anchor>,
    mut cx: AsyncWindowContext,
) -> Option<String> {
    const LIMIT: usize = 2048;

    let Ok(snapshot) = buffer.update(&mut cx, |buffer, _| buffer.snapshot()) else {
        return None;
    };

    let start_offset = range.start.to_offset(&snapshot);
    let end_offset = range.end.to_offset(&snapshot);

    let mut token_start = start_offset.min(end_offset);
    let mut token_end = start_offset.max(end_offset);

    let range_len = token_end - token_start;

    if range_len >= LIMIT {
        return None;
    }

    // Skip leading whitespace
    for ch in snapshot.chars_at(token_start).take(range_len) {
        if !ch.is_whitespace() {
            break;
        }
        token_start += ch.len_utf8();
    }

    // Skip trailing whitespace
    for ch in snapshot.reversed_chars_at(token_end).take(range_len) {
        if !ch.is_whitespace() {
            break;
        }
        token_end -= ch.len_utf8();
    }

    if token_start >= token_end {
        return None;
    }

    let text = snapshot
        .text_for_range(token_start..token_end)
        .collect::<String>();

    let mut finder = LinkFinder::new();
    finder.kinds(&[LinkKind::Url]);

    if let Some(link) = finder.links(&text).next() {
        if link.start() == 0 && link.end() == text.len() {
            return Some(link.as_str().to_string());
        }
    }

    None
}

pub(crate) async fn find_file(
    buffer: &Entity<language::Buffer>,
    project: Option<Entity<Project>>,
    position: text::Anchor,
    cx: &mut AsyncWindowContext,
) -> Option<(Range<text::Anchor>, ResolvedPath)> {
    let project = project?;
    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()).ok()?;
    let scope = snapshot.language_scope_at(position);
    let (range, candidate_file_path) = surrounding_filename(snapshot, position)?;

    async fn check_path(
        candidate_file_path: &str,
        project: &Entity<Project>,
        buffer: &Entity<language::Buffer>,
        cx: &mut AsyncWindowContext,
    ) -> Option<ResolvedPath> {
        project
            .update(cx, |project, cx| {
                project.resolve_path_in_buffer(&candidate_file_path, buffer, cx)
            })
            .ok()?
            .await
            .filter(|s| s.is_file())
    }

    if let Some(existing_path) = check_path(&candidate_file_path, &project, buffer, cx).await {
        return Some((range, existing_path));
    }

    if let Some(scope) = scope {
        for suffix in scope.path_suffixes() {
            if candidate_file_path.ends_with(format!(".{suffix}").as_str()) {
                continue;
            }

            let suffixed_candidate = format!("{candidate_file_path}.{suffix}");
            if let Some(existing_path) = check_path(&suffixed_candidate, &project, buffer, cx).await
            {
                return Some((range, existing_path));
            }
        }
    }

    None
}

fn surrounding_filename(
    snapshot: language::BufferSnapshot,
    position: text::Anchor,
) -> Option<(Range<text::Anchor>, String)> {
    const LIMIT: usize = 2048;

    let offset = position.to_offset(&snapshot);
    let mut token_start = offset;
    let mut token_end = offset;
    let mut found_start = false;
    let mut found_end = false;
    let mut inside_quotes = false;

    let mut filename = String::new();

    let mut backwards = snapshot.reversed_chars_at(offset).take(LIMIT).peekable();
    while let Some(ch) = backwards.next() {
        // Escaped whitespace
        if ch.is_whitespace() && backwards.peek() == Some(&'\\') {
            filename.push(ch);
            token_start -= ch.len_utf8();
            backwards.next();
            token_start -= '\\'.len_utf8();
            continue;
        }
        if ch.is_whitespace() {
            found_start = true;
            break;
        }
        if (ch == '"' || ch == '\'') && !inside_quotes {
            found_start = true;
            inside_quotes = true;
            break;
        }

        filename.push(ch);
        token_start -= ch.len_utf8();
    }
    if !found_start && token_start != 0 {
        return None;
    }

    filename = filename.chars().rev().collect();

    let mut forwards = snapshot
        .chars_at(offset)
        .take(LIMIT - (offset - token_start))
        .peekable();
    while let Some(ch) = forwards.next() {
        // Skip escaped whitespace
        if ch == '\\' && forwards.peek().map_or(false, |ch| ch.is_whitespace()) {
            token_end += ch.len_utf8();
            let whitespace = forwards.next().unwrap();
            token_end += whitespace.len_utf8();
            filename.push(whitespace);
            continue;
        }

        if ch.is_whitespace() {
            found_end = true;
            break;
        }
        if ch == '"' || ch == '\'' {
            // If we're inside quotes, we stop when we come across the next quote
            if inside_quotes {
                found_end = true;
                break;
            } else {
                // Otherwise, we skip the quote
                inside_quotes = true;
                continue;
            }
        }
        filename.push(ch);
        token_end += ch.len_utf8();
    }

    if !found_end && (token_end - token_start >= LIMIT) {
        return None;
    }

    if filename.is_empty() {
        return None;
    }

    let range = snapshot.anchor_before(token_start)..snapshot.anchor_after(token_end);

    Some((range, filename))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        display_map::ToDisplayPoint,
        editor_tests::init_test,
        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
        test::editor_lsp_test_context::EditorLspTestContext,
        DisplayPoint,
    };
    use futures::StreamExt;
    use gpui::Modifiers;
    use indoc::indoc;
    use language::language_settings::InlayHintSettings;
    use lsp::request::{GotoDefinition, GotoTypeDefinition};
    use util::{assert_set_eq, path};
    use workspace::item::Item;

    #[gpui::test]
    async fn test_hover_type_links(cx: &mut gpui::TestAppContext) {
        init_test(cx, |_| {});

        let mut cx = EditorLspTestContext::new_rust(
            lsp::ServerCapabilities {
                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
                type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
                ..Default::default()
            },
            cx,
        )
        .await;

        cx.set_state(indoc! {"
            struct A;
            let vˇariable = A;
        "});
        let screen_coord = cx.editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));

        // Basic hold cmd+shift, expect highlight in region if response contains type definition
        let symbol_range = cx.lsp_range(indoc! {"
            struct A;
            let «variable» = A;
        "});
        let target_range = cx.lsp_range(indoc! {"
            struct «A»;
            let variable = A;
        "});

        cx.run_until_parked();

        let mut requests =
            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
                    lsp::LocationLink {
                        origin_selection_range: Some(symbol_range),
                        target_uri: url.clone(),
                        target_range,
                        target_selection_range: target_range,
                    },
                ])))
            });

        let modifiers = if cfg!(target_os = "macos") {
            Modifiers::command_shift()
        } else {
            Modifiers::control_shift()
        };

        cx.simulate_mouse_move(screen_coord.unwrap(), None, modifiers);

        requests.next().await;
        cx.run_until_parked();
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            struct A;
            let «variable» = A;
        "});

        cx.simulate_modifiers_change(Modifiers::secondary_key());
        cx.run_until_parked();
        // Assert no link highlights
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            struct A;
            let variable = A;
        "});

        cx.simulate_click(screen_coord.unwrap(), modifiers);

        cx.assert_editor_state(indoc! {"
            struct «Aˇ»;
            let variable = A;
        "});
    }

    #[gpui::test]
    async fn test_hover_links(cx: &mut gpui::TestAppContext) {
        init_test(cx, |_| {});

        let mut cx = EditorLspTestContext::new_rust(
            lsp::ServerCapabilities {
                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
                definition_provider: Some(lsp::OneOf::Left(true)),
                ..Default::default()
            },
            cx,
        )
        .await;

        cx.set_state(indoc! {"
                fn ˇtest() { do_work(); }
                fn do_work() { test(); }
            "});

        // Basic hold cmd, expect highlight in region if response contains definition
        let hover_point = cx.pixel_position(indoc! {"
                fn test() { do_wˇork(); }
                fn do_work() { test(); }
            "});
        let symbol_range = cx.lsp_range(indoc! {"
                fn test() { «do_work»(); }
                fn do_work() { test(); }
            "});
        let target_range = cx.lsp_range(indoc! {"
                fn test() { do_work(); }
                fn «do_work»() { test(); }
            "});

        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
                lsp::LocationLink {
                    origin_selection_range: Some(symbol_range),
                    target_uri: url.clone(),
                    target_range,
                    target_selection_range: target_range,
                },
            ])))
        });

        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
        requests.next().await;
        cx.background_executor.run_until_parked();
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { «do_work»(); }
                fn do_work() { test(); }
            "});

        // Unpress cmd causes highlight to go away
        cx.simulate_modifiers_change(Modifiers::none());
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { do_work(); }
                fn do_work() { test(); }
            "});

        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
                lsp::LocationLink {
                    origin_selection_range: Some(symbol_range),
                    target_uri: url.clone(),
                    target_range,
                    target_selection_range: target_range,
                },
            ])))
        });

        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
        requests.next().await;
        cx.background_executor.run_until_parked();
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { «do_work»(); }
                fn do_work() { test(); }
            "});

        // Moving mouse to location with no response dismisses highlight
        let hover_point = cx.pixel_position(indoc! {"
                fˇn test() { do_work(); }
                fn do_work() { test(); }
            "});
        let mut requests = cx
            .lsp
            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
                // No definitions returned
                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
            });
        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());

        requests.next().await;
        cx.background_executor.run_until_parked();

        // Assert no link highlights
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { do_work(); }
                fn do_work() { test(); }
            "});

        // // Move mouse without cmd and then pressing cmd triggers highlight
        let hover_point = cx.pixel_position(indoc! {"
                fn test() { do_work(); }
                fn do_work() { teˇst(); }
            "});
        cx.simulate_mouse_move(hover_point, None, Modifiers::none());

        // Assert no link highlights
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { do_work(); }
                fn do_work() { test(); }
            "});

        let symbol_range = cx.lsp_range(indoc! {"
                fn test() { do_work(); }
                fn do_work() { «test»(); }
            "});
        let target_range = cx.lsp_range(indoc! {"
                fn «test»() { do_work(); }
                fn do_work() { test(); }
            "});

        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
                lsp::LocationLink {
                    origin_selection_range: Some(symbol_range),
                    target_uri: url,
                    target_range,
                    target_selection_range: target_range,
                },
            ])))
        });

        cx.simulate_modifiers_change(Modifiers::secondary_key());

        requests.next().await;
        cx.background_executor.run_until_parked();

        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { do_work(); }
                fn do_work() { «test»(); }
            "});

        cx.deactivate_window();
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { do_work(); }
                fn do_work() { test(); }
            "});

        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
        cx.background_executor.run_until_parked();
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { do_work(); }
                fn do_work() { «test»(); }
            "});

        // Moving again within the same symbol range doesn't re-request
        let hover_point = cx.pixel_position(indoc! {"
                fn test() { do_work(); }
                fn do_work() { tesˇt(); }
            "});
        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
        cx.background_executor.run_until_parked();
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { do_work(); }
                fn do_work() { «test»(); }
            "});

        // Cmd click with existing definition doesn't re-request and dismisses highlight
        cx.simulate_click(hover_point, Modifiers::secondary_key());
        cx.lsp
            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
                // Empty definition response to make sure we aren't hitting the lsp and using
                // the cached location instead
                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
            });
        cx.background_executor.run_until_parked();
        cx.assert_editor_state(indoc! {"
                fn «testˇ»() { do_work(); }
                fn do_work() { test(); }
            "});

        // Assert no link highlights after jump
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { do_work(); }
                fn do_work() { test(); }
            "});

        // Cmd click without existing definition requests and jumps
        let hover_point = cx.pixel_position(indoc! {"
                fn test() { do_wˇork(); }
                fn do_work() { test(); }
            "});
        let target_range = cx.lsp_range(indoc! {"
                fn test() { do_work(); }
                fn «do_work»() { test(); }
            "});

        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
                lsp::LocationLink {
                    origin_selection_range: None,
                    target_uri: url,
                    target_range,
                    target_selection_range: target_range,
                },
            ])))
        });
        cx.simulate_click(hover_point, Modifiers::secondary_key());
        requests.next().await;
        cx.background_executor.run_until_parked();
        cx.assert_editor_state(indoc! {"
                fn test() { do_work(); }
                fn «do_workˇ»() { test(); }
            "});

        // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
        // 2. Selection is completed, hovering
        let hover_point = cx.pixel_position(indoc! {"
                fn test() { do_wˇork(); }
                fn do_work() { test(); }
            "});
        let target_range = cx.lsp_range(indoc! {"
                fn test() { do_work(); }
                fn «do_work»() { test(); }
            "});
        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
                lsp::LocationLink {
                    origin_selection_range: None,
                    target_uri: url,
                    target_range,
                    target_selection_range: target_range,
                },
            ])))
        });

        // create a pending selection
        let selection_range = cx.ranges(indoc! {"
                fn «test() { do_w»ork(); }
                fn do_work() { test(); }
            "})[0]
            .clone();
        cx.update_editor(|editor, window, cx| {
            let snapshot = editor.buffer().read(cx).snapshot(cx);
            let anchor_range = snapshot.anchor_before(selection_range.start)
                ..snapshot.anchor_after(selection_range.end);
            editor.change_selections(Some(crate::Autoscroll::fit()), window, cx, |s| {
                s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
            });
        });
        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
        cx.background_executor.run_until_parked();
        assert!(requests.try_next().is_err());
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                fn test() { do_work(); }
                fn do_work() { test(); }
            "});
        cx.background_executor.run_until_parked();
    }

    #[gpui::test]
    async fn test_inlay_hover_links(cx: &mut gpui::TestAppContext) {
        init_test(cx, |settings| {
            settings.defaults.inlay_hints = Some(InlayHintSettings {
                enabled: true,
                edit_debounce_ms: 0,
                scroll_debounce_ms: 0,
                show_type_hints: true,
                show_parameter_hints: true,
                show_other_hints: true,
                show_background: false,
            })
        });

        let mut cx = EditorLspTestContext::new_rust(
            lsp::ServerCapabilities {
                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
                ..Default::default()
            },
            cx,
        )
        .await;
        cx.set_state(indoc! {"
                struct TestStruct;

                fn main() {
                    let variableˇ = TestStruct;
                }
            "});
        let hint_start_offset = cx.ranges(indoc! {"
                struct TestStruct;

                fn main() {
                    let variableˇ = TestStruct;
                }
            "})[0]
            .start;
        let hint_position = cx.to_lsp(hint_start_offset);
        let target_range = cx.lsp_range(indoc! {"
                struct «TestStruct»;

                fn main() {
                    let variable = TestStruct;
                }
            "});

        let expected_uri = cx.buffer_lsp_url.clone();
        let hint_label = ": TestStruct";
        cx.lsp
            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
                let expected_uri = expected_uri.clone();
                async move {
                    assert_eq!(params.text_document.uri, expected_uri);
                    Ok(Some(vec![lsp::InlayHint {
                        position: hint_position,
                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
                            value: hint_label.to_string(),
                            location: Some(lsp::Location {
                                uri: params.text_document.uri,
                                range: target_range,
                            }),
                            ..Default::default()
                        }]),
                        kind: Some(lsp::InlayHintKind::TYPE),
                        text_edits: None,
                        tooltip: None,
                        padding_left: Some(false),
                        padding_right: Some(false),
                        data: None,
                    }]))
                }
            })
            .next()
            .await;
        cx.background_executor.run_until_parked();
        cx.update_editor(|editor, _window, cx| {
            let expected_layers = vec![hint_label.to_string()];
            assert_eq!(expected_layers, cached_hint_labels(editor));
            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
        });

        let inlay_range = cx
            .ranges(indoc! {"
                struct TestStruct;

                fn main() {
                    let variable« »= TestStruct;
                }
            "})
            .first()
            .cloned()
            .unwrap();
        let midpoint = cx.update_editor(|editor, window, cx| {
            let snapshot = editor.snapshot(window, cx);
            let previous_valid = inlay_range.start.to_display_point(&snapshot);
            let next_valid = inlay_range.end.to_display_point(&snapshot);
            assert_eq!(previous_valid.row(), next_valid.row());
            assert!(previous_valid.column() < next_valid.column());
            DisplayPoint::new(
                previous_valid.row(),
                previous_valid.column() + (hint_label.len() / 2) as u32,
            )
        });
        // Press cmd to trigger highlight
        let hover_point = cx.pixel_position_for(midpoint);
        cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
        cx.background_executor.run_until_parked();
        cx.update_editor(|editor, window, cx| {
            let snapshot = editor.snapshot(window, cx);
            let actual_highlights = snapshot
                .inlay_highlights::<HoveredLinkState>()
                .into_iter()
                .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
                .collect::<Vec<_>>();

            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
            let expected_highlight = InlayHighlight {
                inlay: InlayId::Hint(0),
                inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
                range: 0..hint_label.len(),
            };
            assert_set_eq!(actual_highlights, vec![&expected_highlight]);
        });

        cx.simulate_mouse_move(hover_point, None, Modifiers::none());
        // Assert no link highlights
        cx.update_editor(|editor, window, cx| {
                let snapshot = editor.snapshot(window, cx);
                let actual_ranges = snapshot
                    .text_highlight_ranges::<HoveredLinkState>()
                    .map(|ranges| ranges.as_ref().clone().1)
                    .unwrap_or_default();

                assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
            });

        cx.simulate_modifiers_change(Modifiers::secondary_key());
        cx.background_executor.run_until_parked();
        cx.simulate_click(hover_point, Modifiers::secondary_key());
        cx.background_executor.run_until_parked();
        cx.assert_editor_state(indoc! {"
                struct «TestStructˇ»;

                fn main() {
                    let variable = TestStruct;
                }
            "});
    }

    #[gpui::test]
    async fn test_urls(cx: &mut gpui::TestAppContext) {
        init_test(cx, |_| {});
        let mut cx = EditorLspTestContext::new_rust(
            lsp::ServerCapabilities {
                ..Default::default()
            },
            cx,
        )
        .await;

        cx.set_state(indoc! {"
            Let's test a [complex](https://zed.dev/channel/had-(oops)) caseˇ.
        "});

        let screen_coord = cx.pixel_position(indoc! {"
            Let's test a [complex](https://zed.dev/channel/had-(ˇoops)) case.
            "});

        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            Let's test a [complex](«https://zed.dev/channel/had-(oops)ˇ») case.
        "});

        cx.simulate_click(screen_coord, Modifiers::secondary_key());
        assert_eq!(
            cx.opened_url(),
            Some("https://zed.dev/channel/had-(oops)".into())
        );
    }

    #[gpui::test]
    async fn test_urls_at_beginning_of_buffer(cx: &mut gpui::TestAppContext) {
        init_test(cx, |_| {});
        let mut cx = EditorLspTestContext::new_rust(
            lsp::ServerCapabilities {
                ..Default::default()
            },
            cx,
        )
        .await;

        cx.set_state(indoc! {"https://zed.dev/releases is a cool ˇwebpage."});

        let screen_coord =
            cx.pixel_position(indoc! {"https://zed.dev/relˇeases is a cool webpage."});

        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
        cx.assert_editor_text_highlights::<HoveredLinkState>(
            indoc! {"«https://zed.dev/releasesˇ» is a cool webpage."},
        );

        cx.simulate_click(screen_coord, Modifiers::secondary_key());
        assert_eq!(cx.opened_url(), Some("https://zed.dev/releases".into()));
    }

    #[gpui::test]
    async fn test_urls_at_end_of_buffer(cx: &mut gpui::TestAppContext) {
        init_test(cx, |_| {});
        let mut cx = EditorLspTestContext::new_rust(
            lsp::ServerCapabilities {
                ..Default::default()
            },
            cx,
        )
        .await;

        cx.set_state(indoc! {"A cool ˇwebpage is https://zed.dev/releases"});

        let screen_coord =
            cx.pixel_position(indoc! {"A cool webpage is https://zed.dev/releˇases"});

        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
        cx.assert_editor_text_highlights::<HoveredLinkState>(
            indoc! {"A cool webpage is «https://zed.dev/releasesˇ»"},
        );

        cx.simulate_click(screen_coord, Modifiers::secondary_key());
        assert_eq!(cx.opened_url(), Some("https://zed.dev/releases".into()));
    }

    #[gpui::test]
    async fn test_surrounding_filename(cx: &mut gpui::TestAppContext) {
        init_test(cx, |_| {});
        let mut cx = EditorLspTestContext::new_rust(
            lsp::ServerCapabilities {
                ..Default::default()
            },
            cx,
        )
        .await;

        let test_cases = [
            ("file ˇ name", None),
            ("ˇfile name", Some("file")),
            ("file ˇname", Some("name")),
            ("fiˇle name", Some("file")),
            ("filenˇame", Some("filename")),
            // Absolute path
            ("foobar ˇ/home/user/f.txt", Some("/home/user/f.txt")),
            ("foobar /home/useˇr/f.txt", Some("/home/user/f.txt")),
            // Windows
            ("C:\\Useˇrs\\user\\f.txt", Some("C:\\Users\\user\\f.txt")),
            // Whitespace
            ("ˇfile\\ -\\ name.txt", Some("file - name.txt")),
            ("file\\ -\\ naˇme.txt", Some("file - name.txt")),
            // Tilde
            ("ˇ~/file.txt", Some("~/file.txt")),
            ("~/fiˇle.txt", Some("~/file.txt")),
            // Double quotes
            ("\"fˇile.txt\"", Some("file.txt")),
            ("ˇ\"file.txt\"", Some("file.txt")),
            ("ˇ\"fi\\ le.txt\"", Some("fi le.txt")),
            // Single quotes
            ("'fˇile.txt'", Some("file.txt")),
            ("ˇ'file.txt'", Some("file.txt")),
            ("ˇ'fi\\ le.txt'", Some("fi le.txt")),
        ];

        for (input, expected) in test_cases {
            cx.set_state(input);

            let (position, snapshot) = cx.editor(|editor, _, cx| {
                let positions = editor.selections.newest_anchor().head().text_anchor;
                let snapshot = editor
                    .buffer()
                    .clone()
                    .read(cx)
                    .as_singleton()
                    .unwrap()
                    .read(cx)
                    .snapshot();
                (positions, snapshot)
            });

            let result = surrounding_filename(snapshot, position);

            if let Some(expected) = expected {
                assert!(result.is_some(), "Failed to find file path: {}", input);
                let (_, path) = result.unwrap();
                assert_eq!(&path, expected, "Incorrect file path for input: {}", input);
            } else {
                assert!(
                    result.is_none(),
                    "Expected no result, but got one: {:?}",
                    result
                );
            }
        }
    }

    #[gpui::test]
    async fn test_hover_filenames(cx: &mut gpui::TestAppContext) {
        init_test(cx, |_| {});
        let mut cx = EditorLspTestContext::new_rust(
            lsp::ServerCapabilities {
                ..Default::default()
            },
            cx,
        )
        .await;

        // Insert a new file
        let fs = cx.update_workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
        fs.as_fake()
            .insert_file(
                path!("/root/dir/file2.rs"),
                "This is file2.rs".as_bytes().to_vec(),
            )
            .await;

        #[cfg(not(target_os = "windows"))]
        cx.set_state(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to /root/dir/file2.rs if project is local.
            Or go to /root/dir/file2 if this is a Rust file.ˇ
            "});
        #[cfg(target_os = "windows")]
        cx.set_state(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to C:/root/dir/file2.rs if project is local.
            Or go to C:/root/dir/file2 if this is a Rust file.ˇ
        "});

        // File does not exist
        #[cfg(not(target_os = "windows"))]
        let screen_coord = cx.pixel_position(indoc! {"
            You can't go to a file that dˇoes_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to /root/dir/file2.rs if project is local.
            Or go to /root/dir/file2 if this is a Rust file.
        "});
        #[cfg(target_os = "windows")]
        let screen_coord = cx.pixel_position(indoc! {"
            You can't go to a file that dˇoes_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to C:/root/dir/file2.rs if project is local.
            Or go to C:/root/dir/file2 if this is a Rust file.
        "});
        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
        // No highlight
        cx.update_editor(|editor, window, cx| {
            assert!(editor
                .snapshot(window, cx)
                .text_highlight_ranges::<HoveredLinkState>()
                .unwrap_or_default()
                .1
                .is_empty());
        });

        // Moving the mouse over a file that does exist should highlight it.
        #[cfg(not(target_os = "windows"))]
        let screen_coord = cx.pixel_position(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to fˇile2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to /root/dir/file2.rs if project is local.
            Or go to /root/dir/file2 if this is a Rust file.
        "});
        #[cfg(target_os = "windows")]
        let screen_coord = cx.pixel_position(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to fˇile2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to C:/root/dir/file2.rs if project is local.
            Or go to C:/root/dir/file2 if this is a Rust file.
        "});

        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
        #[cfg(not(target_os = "windows"))]
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to «file2.rsˇ» if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to /root/dir/file2.rs if project is local.
            Or go to /root/dir/file2 if this is a Rust file.
        "});
        #[cfg(target_os = "windows")]
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to «file2.rsˇ» if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to C:/root/dir/file2.rs if project is local.
            Or go to C:/root/dir/file2 if this is a Rust file.
        "});

        // Moving the mouse over a relative path that does exist should highlight it
        #[cfg(not(target_os = "windows"))]
        let screen_coord = cx.pixel_position(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/fˇile2.rs if you want.
            Or go to /root/dir/file2.rs if project is local.
            Or go to /root/dir/file2 if this is a Rust file.
        "});
        #[cfg(target_os = "windows")]
        let screen_coord = cx.pixel_position(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/fˇile2.rs if you want.
            Or go to C:/root/dir/file2.rs if project is local.
            Or go to C:/root/dir/file2 if this is a Rust file.
        "});

        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
        #[cfg(not(target_os = "windows"))]
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to «../dir/file2.rsˇ» if you want.
            Or go to /root/dir/file2.rs if project is local.
            Or go to /root/dir/file2 if this is a Rust file.
        "});
        #[cfg(target_os = "windows")]
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to «../dir/file2.rsˇ» if you want.
            Or go to C:/root/dir/file2.rs if project is local.
            Or go to C:/root/dir/file2 if this is a Rust file.
        "});

        // Moving the mouse over an absolute path that does exist should highlight it
        #[cfg(not(target_os = "windows"))]
        let screen_coord = cx.pixel_position(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to /root/diˇr/file2.rs if project is local.
            Or go to /root/dir/file2 if this is a Rust file.
        "});

        #[cfg(target_os = "windows")]
        let screen_coord = cx.pixel_position(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to C:/root/diˇr/file2.rs if project is local.
            Or go to C:/root/dir/file2 if this is a Rust file.
        "});

        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
        #[cfg(not(target_os = "windows"))]
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to «/root/dir/file2.rsˇ» if project is local.
            Or go to /root/dir/file2 if this is a Rust file.
        "});
        #[cfg(target_os = "windows")]
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to «C:/root/dir/file2.rsˇ» if project is local.
            Or go to C:/root/dir/file2 if this is a Rust file.
        "});

        // Moving the mouse over a path that exists, if we add the language-specific suffix, it should highlight it
        #[cfg(not(target_os = "windows"))]
        let screen_coord = cx.pixel_position(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to /root/dir/file2.rs if project is local.
            Or go to /root/diˇr/file2 if this is a Rust file.
        "});
        #[cfg(target_os = "windows")]
        let screen_coord = cx.pixel_position(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to C:/root/dir/file2.rs if project is local.
            Or go to C:/root/diˇr/file2 if this is a Rust file.
        "});

        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
        #[cfg(not(target_os = "windows"))]
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to /root/dir/file2.rs if project is local.
            Or go to «/root/dir/file2ˇ» if this is a Rust file.
        "});
        #[cfg(target_os = "windows")]
        cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
            You can't go to a file that does_not_exist.txt.
            Go to file2.rs if you want.
            Or go to ../dir/file2.rs if you want.
            Or go to C:/root/dir/file2.rs if project is local.
            Or go to «C:/root/dir/file2ˇ» if this is a Rust file.
        "});

        cx.simulate_click(screen_coord, Modifiers::secondary_key());

        cx.update_workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
        cx.update_workspace(|workspace, _, cx| {
            let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();

            let buffer = active_editor
                .read(cx)
                .buffer()
                .read(cx)
                .as_singleton()
                .unwrap();

            let file = buffer.read(cx).file().unwrap();
            let file_path = file.as_local().unwrap().abs_path(cx);

            assert_eq!(
                file_path,
                std::path::PathBuf::from(path!("/root/dir/file2.rs"))
            );
        });
    }

    #[gpui::test]
    async fn test_hover_directories(cx: &mut gpui::TestAppContext) {
        init_test(cx, |_| {});
        let mut cx = EditorLspTestContext::new_rust(
            lsp::ServerCapabilities {
                ..Default::default()
            },
            cx,
        )
        .await;

        // Insert a new file
        let fs = cx.update_workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
        fs.as_fake()
            .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
            .await;

        cx.set_state(indoc! {"
            You can't open ../diˇr because it's a directory.
        "});

        // File does not exist
        let screen_coord = cx.pixel_position(indoc! {"
            You can't open ../diˇr because it's a directory.
        "});
        cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());

        // No highlight
        cx.update_editor(|editor, window, cx| {
            assert!(editor
                .snapshot(window, cx)
                .text_highlight_ranges::<HoveredLinkState>()
                .unwrap_or_default()
                .1
                .is_empty());
        });

        // Does not open the directory
        cx.simulate_click(screen_coord, Modifiers::secondary_key());
        cx.update_workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1));
    }
}
