//! Conversion of rust-analyzer specific types to lsp_types equivalents.
use std::{
    iter::once,
    mem,
    ops::Not as _,
    sync::atomic::{AtomicU32, Ordering},
};

use base64::{Engine, prelude::BASE64_STANDARD};
use ide::{
    Annotation, AnnotationKind, Assist, AssistKind, Cancellable, CompletionFieldsToResolve,
    CompletionItem, CompletionItemKind, CompletionRelevance, Documentation, FileId, FileRange,
    FileSystemEdit, Fold, FoldKind, Highlight, HlMod, HlOperator, HlPunct, HlRange, HlTag, Indel,
    InlayFieldsToResolve, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayKind, LazyProperty,
    Markup, NavigationTarget, ReferenceCategory, RenameError, Runnable, Severity, SignatureHelp,
    SnippetEdit, SourceChange, StructureNodeKind, SymbolKind, TextEdit, TextRange, TextSize,
    UpdateTest,
};
use ide_db::{
    FxHasher, MiniCore, assists, rust_doc::format_docs, source_change::ChangeAnnotationId,
};
use itertools::Itertools;
use paths::{Utf8Component, Utf8Prefix};
use semver::VersionReq;
use serde_json::to_value;
use vfs::AbsPath;

use crate::{
    config::{CallInfoConfig, ClientCommandsConfig, Config},
    global_state::GlobalStateSnapshot,
    line_index::{LineEndings, LineIndex, PositionEncoding},
    lsp::{
        LspError, completion_item_hash,
        ext::ShellRunnableArgs,
        semantic_tokens::{self, standard_fallback_type},
        utils::invalid_params_error,
    },
    lsp_ext::{self, SnippetTextEdit},
    target_spec::{CargoTargetSpec, TargetSpec},
};

pub(crate) fn position(line_index: &LineIndex, offset: TextSize) -> lsp_types::Position {
    let line_col = line_index.index.line_col(offset);
    match line_index.encoding {
        PositionEncoding::Utf8 => lsp_types::Position::new(line_col.line, line_col.col),
        PositionEncoding::Wide(enc) => {
            let line_col = line_index.index.to_wide(enc, line_col).unwrap();
            lsp_types::Position::new(line_col.line, line_col.col)
        }
    }
}

pub(crate) fn range(line_index: &LineIndex, range: TextRange) -> lsp_types::Range {
    let start = position(line_index, range.start());
    let end = position(line_index, range.end());
    lsp_types::Range::new(start, end)
}

pub(crate) fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind {
    match symbol_kind {
        SymbolKind::Function => lsp_types::SymbolKind::Function,
        SymbolKind::Method => lsp_types::SymbolKind::Method,
        SymbolKind::Struct => lsp_types::SymbolKind::Struct,
        SymbolKind::Enum => lsp_types::SymbolKind::Enum,
        SymbolKind::Variant => lsp_types::SymbolKind::EnumMember,
        SymbolKind::Trait => lsp_types::SymbolKind::Interface,
        SymbolKind::Macro
        | SymbolKind::ProcMacro
        | SymbolKind::BuiltinAttr
        | SymbolKind::Attribute
        | SymbolKind::Derive
        | SymbolKind::DeriveHelper => lsp_types::SymbolKind::Function,
        SymbolKind::CrateRoot => lsp_types::SymbolKind::Package,
        SymbolKind::Module | SymbolKind::ToolModule => lsp_types::SymbolKind::Module,
        SymbolKind::TypeAlias | SymbolKind::TypeParam | SymbolKind::SelfType => {
            lsp_types::SymbolKind::TypeParameter
        }
        SymbolKind::Field => lsp_types::SymbolKind::Field,
        SymbolKind::Static => lsp_types::SymbolKind::Constant,
        SymbolKind::Const => lsp_types::SymbolKind::Constant,
        SymbolKind::ConstParam => lsp_types::SymbolKind::Constant,
        SymbolKind::Impl => lsp_types::SymbolKind::Object,
        SymbolKind::Local
        | SymbolKind::SelfParam
        | SymbolKind::LifetimeParam
        | SymbolKind::ValueParam
        | SymbolKind::Label => lsp_types::SymbolKind::Variable,
        SymbolKind::Union => lsp_types::SymbolKind::Struct,
        SymbolKind::InlineAsmRegOrRegClass => lsp_types::SymbolKind::Variable,
    }
}

pub(crate) fn structure_node_kind(kind: StructureNodeKind) -> lsp_types::SymbolKind {
    match kind {
        StructureNodeKind::SymbolKind(symbol) => symbol_kind(symbol),
        StructureNodeKind::Region => lsp_types::SymbolKind::Namespace,
        StructureNodeKind::ExternBlock => lsp_types::SymbolKind::Namespace,
    }
}

pub(crate) fn document_highlight_kind(
    category: ReferenceCategory,
) -> Option<lsp_types::DocumentHighlightKind> {
    if category.contains(ReferenceCategory::WRITE) {
        return Some(lsp_types::DocumentHighlightKind::Write);
    }
    if category.contains(ReferenceCategory::READ) {
        return Some(lsp_types::DocumentHighlightKind::Read);
    }
    None
}

pub(crate) fn diagnostic_severity(severity: Severity) -> lsp_types::DiagnosticSeverity {
    match severity {
        Severity::Error => lsp_types::DiagnosticSeverity::Error,
        Severity::Warning => lsp_types::DiagnosticSeverity::Warning,
        Severity::WeakWarning => lsp_types::DiagnosticSeverity::Hint,
        // unreachable
        Severity::Allow => lsp_types::DiagnosticSeverity::Information,
    }
}

pub(crate) fn documentation(documentation: Documentation<'_>) -> lsp_types::Documentation {
    let value = format_docs(&documentation);
    let markup_content = lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, value };
    lsp_types::Documentation::MarkupContent(markup_content)
}

pub(crate) fn completion_item_kind(
    completion_item_kind: CompletionItemKind,
) -> lsp_types::CompletionItemKind {
    match completion_item_kind {
        CompletionItemKind::Binding => lsp_types::CompletionItemKind::Variable,
        CompletionItemKind::BuiltinType => lsp_types::CompletionItemKind::Struct,
        CompletionItemKind::InferredType => lsp_types::CompletionItemKind::Snippet,
        CompletionItemKind::Keyword => lsp_types::CompletionItemKind::Keyword,
        CompletionItemKind::Snippet => lsp_types::CompletionItemKind::Snippet,
        CompletionItemKind::UnresolvedReference => lsp_types::CompletionItemKind::Reference,
        CompletionItemKind::Expression => lsp_types::CompletionItemKind::Snippet,
        CompletionItemKind::SymbolKind(symbol) => match symbol {
            SymbolKind::Attribute => lsp_types::CompletionItemKind::Function,
            SymbolKind::Method => lsp_types::CompletionItemKind::Method,
            SymbolKind::Const => lsp_types::CompletionItemKind::Constant,
            SymbolKind::ConstParam => lsp_types::CompletionItemKind::TypeParameter,
            SymbolKind::CrateRoot => lsp_types::CompletionItemKind::Module,
            SymbolKind::Derive => lsp_types::CompletionItemKind::Function,
            SymbolKind::DeriveHelper => lsp_types::CompletionItemKind::Function,
            SymbolKind::Enum => lsp_types::CompletionItemKind::Enum,
            SymbolKind::Field => lsp_types::CompletionItemKind::Field,
            SymbolKind::Function => lsp_types::CompletionItemKind::Function,
            SymbolKind::Impl => lsp_types::CompletionItemKind::Text,
            SymbolKind::Label => lsp_types::CompletionItemKind::Variable,
            SymbolKind::LifetimeParam => lsp_types::CompletionItemKind::TypeParameter,
            SymbolKind::Local => lsp_types::CompletionItemKind::Variable,
            SymbolKind::Macro => lsp_types::CompletionItemKind::Function,
            SymbolKind::ProcMacro => lsp_types::CompletionItemKind::Function,
            SymbolKind::Module => lsp_types::CompletionItemKind::Module,
            SymbolKind::SelfParam => lsp_types::CompletionItemKind::Value,
            SymbolKind::SelfType => lsp_types::CompletionItemKind::TypeParameter,
            SymbolKind::Static => lsp_types::CompletionItemKind::Value,
            SymbolKind::Struct => lsp_types::CompletionItemKind::Struct,
            SymbolKind::Trait => lsp_types::CompletionItemKind::Interface,
            SymbolKind::TypeAlias => lsp_types::CompletionItemKind::Struct,
            SymbolKind::TypeParam => lsp_types::CompletionItemKind::TypeParameter,
            SymbolKind::Union => lsp_types::CompletionItemKind::Struct,
            SymbolKind::ValueParam => lsp_types::CompletionItemKind::Value,
            SymbolKind::Variant => lsp_types::CompletionItemKind::EnumMember,
            SymbolKind::BuiltinAttr => lsp_types::CompletionItemKind::Function,
            SymbolKind::ToolModule => lsp_types::CompletionItemKind::Module,
            SymbolKind::InlineAsmRegOrRegClass => lsp_types::CompletionItemKind::Keyword,
        },
    }
}

pub(crate) fn text_edit(line_index: &LineIndex, indel: Indel) -> lsp_types::TextEdit {
    let range = range(line_index, indel.delete);
    let new_text = match line_index.endings {
        LineEndings::Unix => indel.insert,
        LineEndings::Dos => indel.insert.replace('\n', "\r\n"),
    };
    lsp_types::TextEdit { range, new_text }
}

pub(crate) fn completion_text_edit(
    line_index: &LineIndex,
    cursor_position: lsp_types::Position,
    insert_replace_support: bool,
    indel: Indel,
) -> lsp_types::CompletionItemTextEdit {
    let text_edit = text_edit(line_index, indel);
    let insert = lsp_types::Range { start: text_edit.range.start, end: cursor_position };
    if insert_replace_support {
        lsp_types::InsertReplaceEdit {
            new_text: text_edit.new_text,
            insert,
            replace: text_edit.range,
        }
        .into()
    } else {
        lsp_types::TextEdit { range: insert, new_text: text_edit.new_text }.into()
    }
}

pub(crate) fn snippet_text_edit(
    line_index: &LineIndex,
    is_snippet: bool,
    indel: Indel,
    annotation: Option<ChangeAnnotationId>,
    client_supports_annotations: bool,
) -> lsp_ext::SnippetTextEdit {
    let annotation_id = annotation.filter(|_| client_supports_annotations).map(|it| it.to_string());
    let text_edit = text_edit(line_index, indel);
    let insert_text_format =
        if is_snippet { Some(lsp_types::InsertTextFormat::Snippet) } else { None };
    lsp_ext::SnippetTextEdit {
        range: text_edit.range,
        new_text: text_edit.new_text,
        insert_text_format,
        annotation_id,
    }
}

pub(crate) fn text_edit_vec(
    line_index: &LineIndex,
    text_edit: TextEdit,
) -> Vec<lsp_types::TextEdit> {
    text_edit.into_iter().map(|indel| self::text_edit(line_index, indel)).collect()
}

pub(crate) fn snippet_text_edit_vec(
    line_index: &LineIndex,
    is_snippet: bool,
    text_edit: TextEdit,
    clients_support_annotations: bool,
) -> Vec<lsp_ext::SnippetTextEdit> {
    let annotation = text_edit.change_annotation();
    text_edit
        .into_iter()
        .map(|indel| {
            self::snippet_text_edit(
                line_index,
                is_snippet,
                indel,
                annotation,
                clients_support_annotations,
            )
        })
        .collect()
}

pub(crate) fn completion_items(
    config: &Config,
    fields_to_resolve: &CompletionFieldsToResolve,
    line_index: &LineIndex,
    version: Option<i32>,
    tdpp: &lsp_types::TextDocumentPositionParams,
    completion_trigger_character: Option<char>,
    mut items: Vec<CompletionItem>,
) -> Vec<lsp_types::CompletionItem> {
    if config.completion_hide_deprecated() {
        items.retain(|item| !item.deprecated);
    }

    let max_relevance = items.iter().map(|it| it.relevance.score()).max().unwrap_or_default();
    let mut res = Vec::with_capacity(items.len());
    let client_commands = config.client_commands();
    for item in items {
        completion_item(
            &mut res,
            config,
            &client_commands,
            fields_to_resolve,
            line_index,
            version,
            tdpp,
            max_relevance,
            completion_trigger_character,
            item,
        );
    }

    if let Some(limit) = config.completion(None, MiniCore::default()).limit {
        res.sort_by(|item1, item2| item1.sort_text.cmp(&item2.sort_text));
        res.truncate(limit);
    }

    res
}

fn completion_item(
    acc: &mut Vec<lsp_types::CompletionItem>,
    config: &Config,
    client_commands: &ClientCommandsConfig,
    fields_to_resolve: &CompletionFieldsToResolve,
    line_index: &LineIndex,
    version: Option<i32>,
    tdpp: &lsp_types::TextDocumentPositionParams,
    max_relevance: u32,
    completion_trigger_character: Option<char>,
    item: CompletionItem,
) {
    let insert_replace_support = config.insert_replace_support();
    let ref_match = item.ref_match();

    let mut additional_text_edits = Vec::new();
    let mut something_to_resolve = false;

    let filter_text = if fields_to_resolve.resolve_filter_text {
        something_to_resolve |= !item.lookup().is_empty();
        None
    } else {
        Some(item.lookup().to_owned())
    };

    let text_edit = if fields_to_resolve.resolve_text_edit {
        something_to_resolve |= true;
        None
    } else {
        // LSP does not allow arbitrary edits in completion, so we have to do a
        // non-trivial mapping here.
        let mut text_edit = None;
        let source_range = item.source_range;
        for indel in &item.text_edit {
            if indel.delete.contains_range(source_range) {
                // Extract this indel as the main edit
                text_edit = Some(if indel.delete == source_range {
                    self::completion_text_edit(
                        line_index,
                        tdpp.position,
                        insert_replace_support,
                        indel.clone(),
                    )
                } else {
                    assert!(source_range.end() == indel.delete.end());
                    let range1 = TextRange::new(indel.delete.start(), source_range.start());
                    let range2 = source_range;
                    let indel1 = Indel::delete(range1);
                    let indel2 = Indel::replace(range2, indel.insert.clone());
                    additional_text_edits.push(self::text_edit(line_index, indel1));
                    self::completion_text_edit(
                        line_index,
                        tdpp.position,
                        insert_replace_support,
                        indel2,
                    )
                })
            } else {
                assert!(source_range.intersect(indel.delete).is_none());
                let text_edit = self::text_edit(line_index, indel.clone());
                additional_text_edits.push(text_edit);
            }
        }
        Some(text_edit.unwrap())
    };

    let insert_text_format = item.is_snippet.then_some(lsp_types::InsertTextFormat::Snippet);
    let tags = if fields_to_resolve.resolve_tags {
        something_to_resolve |= item.deprecated;
        None
    } else {
        item.deprecated.then(|| vec![lsp_types::CompletionItemTag::Deprecated])
    };
    let command = if item.trigger_call_info && client_commands.trigger_parameter_hints {
        if fields_to_resolve.resolve_command {
            something_to_resolve |= true;
            None
        } else {
            Some(command::trigger_parameter_hints())
        }
    } else {
        None
    };

    let detail = if fields_to_resolve.resolve_detail {
        something_to_resolve |= item.detail.is_some();
        None
    } else {
        item.detail.clone()
    };

    let documentation = if fields_to_resolve.resolve_documentation {
        something_to_resolve |= item.documentation.is_some();
        None
    } else {
        item.documentation.clone().map(documentation)
    };

    let mut lsp_item = lsp_types::CompletionItem {
        label: item.label.primary.to_string(),
        detail,
        filter_text,
        kind: Some(completion_item_kind(item.kind)),
        text_edit,
        additional_text_edits: additional_text_edits
            .is_empty()
            .not()
            .then_some(additional_text_edits),
        documentation,
        #[expect(deprecated, reason = "we do use tags")]
        deprecated: item.deprecated.then_some(item.deprecated),
        tags,
        command,
        insert_text_format,
        ..Default::default()
    };

    if config.completion_label_details_support() {
        let has_label_details =
            item.label.detail_left.is_some() || item.label.detail_right.is_some();
        if fields_to_resolve.resolve_label_details {
            something_to_resolve |= has_label_details;
        } else if has_label_details {
            lsp_item.label_details = Some(lsp_types::CompletionItemLabelDetails {
                detail: item.label.detail_left.clone(),
                description: item.label.detail_right.clone(),
            });
        }
    } else if let Some(label_detail) = &item.label.detail_left {
        lsp_item.label.push_str(label_detail.as_str());
    }

    set_score(&mut lsp_item, max_relevance, item.relevance);

    let imports = if config.completion(None, MiniCore::default()).enable_imports_on_the_fly
        && !item.import_to_add.is_empty()
    {
        item.import_to_add
            .clone()
            .into_iter()
            .map(|import| lsp_ext::CompletionImport {
                full_import_path: import.path,
                as_underscore: import.as_underscore,
            })
            .collect()
    } else {
        Vec::new()
    };
    let (ref_resolve_data, resolve_data) = if something_to_resolve || !imports.is_empty() {
        let ref_resolve_data = if ref_match.is_some() {
            let ref_resolve_data = lsp_ext::CompletionResolveData {
                position: tdpp.clone(),
                imports: Vec::new(),
                version,
                trigger_character: completion_trigger_character,
                for_ref: true,
                hash: BASE64_STANDARD.encode(completion_item_hash(&item, true)),
            };
            Some(to_value(ref_resolve_data).unwrap())
        } else {
            None
        };
        let resolve_data = lsp_ext::CompletionResolveData {
            position: tdpp.clone(),
            imports,
            version,
            trigger_character: completion_trigger_character,
            for_ref: false,
            hash: BASE64_STANDARD.encode(completion_item_hash(&item, false)),
        };
        (ref_resolve_data, Some(to_value(resolve_data).unwrap()))
    } else {
        (None, None)
    };

    if let Some((label, indel, relevance)) = ref_match {
        let mut lsp_item_with_ref =
            lsp_types::CompletionItem { label, data: ref_resolve_data, ..lsp_item.clone() };
        lsp_item_with_ref
            .additional_text_edits
            .get_or_insert_with(Default::default)
            .push(self::text_edit(line_index, indel));
        set_score(&mut lsp_item_with_ref, max_relevance, relevance);
        acc.push(lsp_item_with_ref);
    };

    lsp_item.data = resolve_data;
    acc.push(lsp_item);

    fn set_score(
        res: &mut lsp_types::CompletionItem,
        max_relevance: u32,
        relevance: CompletionRelevance,
    ) {
        if relevance.is_relevant() && relevance.score() == max_relevance {
            res.preselect = Some(true);
        }
        // The relevance needs to be inverted to come up with a sort score
        // because the client will sort ascending.
        let sort_score = relevance.score() ^ 0xFF_FF_FF_FF;
        // Zero pad the string to ensure values can be properly sorted
        // by the client. Hex format is used because it is easier to
        // visually compare very large values, which the sort text
        // tends to be since it is the opposite of the score.
        res.sort_text = Some(format!("{sort_score:08x}"));
    }
}

pub(crate) fn signature_help(
    call_info: SignatureHelp,
    config: CallInfoConfig,
    label_offsets: bool,
) -> lsp_types::SignatureHelp {
    let (label, parameters) = match (config.params_only, label_offsets) {
        (concise, false) => {
            let params = call_info
                .parameter_labels()
                .map(|label| lsp_types::ParameterInformation {
                    label: lsp_types::ParameterInformationLabel::String(label.to_owned()),
                    documentation: None,
                })
                .collect::<Vec<_>>();
            let label =
                if concise { call_info.parameter_labels().join(", ") } else { call_info.signature };
            (label, params)
        }
        (false, true) => {
            let params = call_info
                .parameter_ranges()
                .iter()
                .map(|it| {
                    let start = call_info.signature[..it.start().into()]
                        .chars()
                        .map(|c| c.len_utf16())
                        .sum::<usize>() as u32;
                    let end = start
                        + call_info.signature[it.start().into()..it.end().into()]
                            .chars()
                            .map(|c| c.len_utf16())
                            .sum::<usize>() as u32;
                    (start, end)
                })
                .map(|label_offsets| lsp_types::ParameterInformation {
                    label: lsp_types::ParameterInformationLabel::Tuple(label_offsets),
                    documentation: None,
                })
                .collect::<Vec<_>>();
            (call_info.signature, params)
        }
        (true, true) => {
            let mut params = Vec::new();
            let mut label = String::new();
            let mut first = true;
            for param in call_info.parameter_labels() {
                if !first {
                    label.push_str(", ");
                }
                first = false;
                let start = label.len() as u32;
                label.push_str(param);
                let end = label.len() as u32;
                params.push(lsp_types::ParameterInformation {
                    label: lsp_types::ParameterInformationLabel::Tuple((start, end)),
                    documentation: None,
                });
            }

            (label, params)
        }
    };

    let documentation = call_info.doc.filter(|_| config.docs).map(|doc| {
        lsp_types::Documentation::MarkupContent(lsp_types::MarkupContent {
            kind: lsp_types::MarkupKind::Markdown,
            value: format_docs(&doc),
        })
    });

    let active_parameter =
        call_info.active_parameter.map(|it| lsp_types::ActiveParameter::Int(it as u32));

    let signature = lsp_types::SignatureInformation {
        label,
        documentation,
        parameters: Some(parameters),
        active_parameter,
    };
    lsp_types::SignatureHelp {
        signatures: vec![signature],
        active_signature: Some(0),
        active_parameter,
    }
}

pub(crate) fn inlay_hint(
    snap: &GlobalStateSnapshot,
    fields_to_resolve: &InlayFieldsToResolve,
    line_index: &LineIndex,
    file_id: FileId,
    mut inlay_hint: InlayHint,
) -> Cancellable<lsp_types::InlayHint> {
    let hint_needs_resolve = |hint: &InlayHint| -> Option<TextRange> {
        hint.resolve_parent.filter(|_| {
            hint.text_edit.as_ref().is_some_and(LazyProperty::is_lazy)
                || hint.label.parts.iter().any(|part| {
                    part.linked_location.as_ref().is_some_and(LazyProperty::is_lazy)
                        || part.tooltip.as_ref().is_some_and(LazyProperty::is_lazy)
                })
        })
    };

    let resolve_range_and_hash = hint_needs_resolve(&inlay_hint).map(|range| {
        (
            range,
            std::hash::BuildHasher::hash_one(
                &std::hash::BuildHasherDefault::<FxHasher>::default(),
                &inlay_hint,
            ),
        )
    });

    let mut something_to_resolve = false;
    let text_edits = inlay_hint
        .text_edit
        .take()
        .and_then(|it| match it {
            LazyProperty::Computed(it) => Some(it),
            LazyProperty::Lazy => {
                something_to_resolve |=
                    snap.config.visual_studio_code_version().is_none_or(|version| {
                        VersionReq::parse(">=1.86.0").unwrap().matches(version)
                    }) && resolve_range_and_hash.is_some()
                        && fields_to_resolve.resolve_text_edits;
                None
            }
        })
        .map(|it| text_edit_vec(line_index, it));
    let (label, tooltip) = inlay_hint_label(
        snap,
        fields_to_resolve,
        &mut something_to_resolve,
        resolve_range_and_hash.is_some(),
        inlay_hint.label,
    )?;

    let data = match resolve_range_and_hash {
        Some((resolve_range, hash)) if something_to_resolve => Some(
            to_value(lsp_ext::InlayHintResolveData {
                file_id: file_id.index(),
                hash: hash.to_string(),
                version: snap.file_version(file_id),
                resolve_range: range(line_index, resolve_range),
            })
            .unwrap(),
        ),
        _ => None,
    };

    Ok(lsp_types::InlayHint {
        position: match inlay_hint.position {
            ide::InlayHintPosition::Before => position(line_index, inlay_hint.range.start()),
            ide::InlayHintPosition::After => position(line_index, inlay_hint.range.end()),
        },
        padding_left: Some(inlay_hint.pad_left),
        padding_right: Some(inlay_hint.pad_right),
        kind: match inlay_hint.kind {
            InlayKind::Parameter | InlayKind::GenericParameter => {
                Some(lsp_types::InlayHintKind::Parameter)
            }
            InlayKind::Type | InlayKind::Chaining => Some(lsp_types::InlayHintKind::Type),
            _ => None,
        },
        text_edits,
        data,
        tooltip,
        label,
    })
}

fn inlay_hint_label(
    snap: &GlobalStateSnapshot,
    fields_to_resolve: &InlayFieldsToResolve,
    something_to_resolve: &mut bool,
    needs_resolve: bool,
    mut label: InlayHintLabel,
) -> Cancellable<(lsp_types::Label, Option<lsp_types::Tooltip>)> {
    let (label, tooltip) = match &*label.parts {
        [InlayHintLabelPart { linked_location: None, .. }] => {
            let InlayHintLabelPart { text, tooltip, .. } = label.parts.pop().unwrap();
            let tooltip = tooltip.and_then(|it| match it {
                LazyProperty::Computed(it) => Some(it),
                LazyProperty::Lazy => {
                    *something_to_resolve |=
                        needs_resolve && fields_to_resolve.resolve_hint_tooltip;
                    None
                }
            });
            let hint_tooltip = match tooltip {
                Some(ide::InlayTooltip::String(s)) => Some(lsp_types::Tooltip::String(s)),
                Some(ide::InlayTooltip::Markdown(s)) => {
                    Some(lsp_types::Tooltip::MarkupContent(lsp_types::MarkupContent {
                        kind: lsp_types::MarkupKind::Markdown,
                        value: s,
                    }))
                }
                None => None,
            };
            (lsp_types::Label::String(text), hint_tooltip)
        }
        _ => {
            let parts = label
                .parts
                .into_iter()
                .map(|part| {
                    let tooltip = part.tooltip.and_then(|it| match it {
                        LazyProperty::Computed(it) => Some(it),
                        LazyProperty::Lazy => {
                            *something_to_resolve |= fields_to_resolve.resolve_label_tooltip;
                            None
                        }
                    });
                    let tooltip = match tooltip {
                        Some(ide::InlayTooltip::String(s)) => Some(lsp_types::Tooltip::String(s)),
                        Some(ide::InlayTooltip::Markdown(s)) => {
                            Some(lsp_types::Tooltip::MarkupContent(lsp_types::MarkupContent {
                                kind: lsp_types::MarkupKind::Markdown,
                                value: s,
                            }))
                        }
                        None => None,
                    };
                    let location = part
                        .linked_location
                        .and_then(|it| match it {
                            LazyProperty::Computed(it) => Some(it),
                            LazyProperty::Lazy => {
                                *something_to_resolve |= fields_to_resolve.resolve_label_location;
                                None
                            }
                        })
                        .map(|range| location(snap, range))
                        .transpose()?;
                    Ok(lsp_types::InlayHintLabelPart {
                        value: part.text,
                        tooltip,
                        location,
                        command: None,
                    })
                })
                .collect::<Cancellable<_>>()?;
            (lsp_types::Label::InlayHintLabelPartList(parts), None)
        }
    };
    Ok((label, tooltip))
}

static TOKEN_RESULT_COUNTER: AtomicU32 = AtomicU32::new(1);

pub(crate) fn semantic_tokens(
    text: &str,
    line_index: &LineIndex,
    highlights: Vec<HlRange>,
    semantics_tokens_augments_syntax_tokens: bool,
    non_standard_tokens: bool,
) -> lsp_types::SemanticTokens {
    let id = TOKEN_RESULT_COUNTER.fetch_add(1, Ordering::SeqCst).to_string();
    let mut builder = semantic_tokens::SemanticTokensBuilder::new(id);

    for highlight_range in highlights {
        if highlight_range.highlight.is_empty() {
            continue;
        }

        if semantics_tokens_augments_syntax_tokens {
            match highlight_range.highlight.tag {
                HlTag::BoolLiteral
                | HlTag::ByteLiteral
                | HlTag::CharLiteral
                | HlTag::Comment
                | HlTag::Keyword
                | HlTag::NumericLiteral
                | HlTag::Operator(_)
                | HlTag::Punctuation(_)
                | HlTag::StringLiteral
                | HlTag::None
                    if highlight_range.highlight.mods.is_empty() =>
                {
                    continue;
                }
                _ => (),
            }
        }

        let (mut ty, mut mods) = semantic_token_type_and_modifiers(highlight_range.highlight);

        if !non_standard_tokens {
            ty = match standard_fallback_type(ty) {
                Some(fallback) => fallback,
                None => continue,
            };
            mods.standard_fallback();
        };
        let token_index = semantic_tokens::type_index(ty);
        let modifier_bitset = mods.0;

        for mut text_range in line_index.index.lines(highlight_range.range) {
            if text[text_range].ends_with('\n') {
                text_range =
                    TextRange::new(text_range.start(), text_range.end() - TextSize::of('\n'));
            }
            let range = range(line_index, text_range);
            builder.push(range, token_index, modifier_bitset);
        }
    }

    builder.build()
}

pub(crate) fn semantic_token_delta(
    previous: &lsp_types::SemanticTokens,
    current: &lsp_types::SemanticTokens,
) -> lsp_types::SemanticTokensDelta {
    let result_id = current.result_id.clone();
    let edits = semantic_tokens::diff_tokens(&previous.data, &current.data);
    lsp_types::SemanticTokensDelta { result_id, edits }
}

fn semantic_token_type_and_modifiers(
    highlight: Highlight,
) -> (semantic_tokens::SupportedType, semantic_tokens::ModifierSet) {
    use semantic_tokens::{SupportedModifiers, SupportedType};

    let mut mods = semantic_tokens::ModifierSet::default();
    let ty = match highlight.tag {
        HlTag::Symbol(symbol) => match symbol {
            SymbolKind::Attribute => SupportedType::Decorator,
            SymbolKind::Derive => SupportedType::Derive,
            SymbolKind::DeriveHelper => SupportedType::DeriveHelper,
            SymbolKind::CrateRoot => {
                mods |= SupportedModifiers::CrateRoot;
                SupportedType::Namespace
            }
            SymbolKind::Module => SupportedType::Namespace,
            SymbolKind::Impl => SupportedType::TypeAlias,
            SymbolKind::Field => SupportedType::Property,
            SymbolKind::TypeParam => SupportedType::TypeParameter,
            SymbolKind::ConstParam => SupportedType::ConstParameter,
            SymbolKind::LifetimeParam => SupportedType::Lifetime,
            SymbolKind::Label => SupportedType::Label,
            SymbolKind::ValueParam => SupportedType::Parameter,
            SymbolKind::SelfParam => SupportedType::SelfKeyword,
            SymbolKind::SelfType => SupportedType::SelfTypeKeyword,
            SymbolKind::Local => SupportedType::Variable,
            SymbolKind::Method => SupportedType::Method,
            SymbolKind::Function => SupportedType::Function,
            SymbolKind::Const => SupportedType::Const,
            SymbolKind::Static => SupportedType::Static,
            SymbolKind::Struct => SupportedType::Struct,
            SymbolKind::Enum => SupportedType::Enum,
            SymbolKind::Variant => SupportedType::EnumMember,
            SymbolKind::Union => SupportedType::Union,
            SymbolKind::TypeAlias => SupportedType::TypeAlias,
            SymbolKind::Trait => SupportedType::Interface,
            SymbolKind::Macro => SupportedType::Macro,
            SymbolKind::ProcMacro => SupportedType::ProcMacro,
            SymbolKind::BuiltinAttr => SupportedType::BuiltinAttribute,
            SymbolKind::ToolModule => SupportedType::ToolModule,
            SymbolKind::InlineAsmRegOrRegClass => SupportedType::Keyword,
        },
        HlTag::AttributeBracket => SupportedType::AttributeBracket,
        HlTag::BoolLiteral => SupportedType::Boolean,
        HlTag::BuiltinType => SupportedType::BuiltinType,
        HlTag::ByteLiteral | HlTag::NumericLiteral => SupportedType::Number,
        HlTag::CharLiteral => SupportedType::Char,
        HlTag::Comment => SupportedType::Comment,
        HlTag::EscapeSequence => SupportedType::EscapeSequence,
        HlTag::InvalidEscapeSequence => SupportedType::InvalidEscapeSequence,
        HlTag::FormatSpecifier => SupportedType::FormatSpecifier,
        HlTag::Keyword => SupportedType::Keyword,
        HlTag::None => SupportedType::Generic,
        HlTag::Operator(op) => match op {
            HlOperator::Bitwise => SupportedType::Bitwise,
            HlOperator::Arithmetic => SupportedType::Arithmetic,
            HlOperator::Logical => SupportedType::Logical,
            HlOperator::Negation => SupportedType::Negation,
            HlOperator::Comparison => SupportedType::Comparison,
            HlOperator::Other => SupportedType::Operator,
        },
        HlTag::StringLiteral => SupportedType::String,
        HlTag::UnresolvedReference => SupportedType::UnresolvedReference,
        HlTag::Punctuation(punct) => match punct {
            HlPunct::Bracket => SupportedType::Bracket,
            HlPunct::Brace => SupportedType::Brace,
            HlPunct::Parenthesis => SupportedType::Parenthesis,
            HlPunct::Angle => SupportedType::Angle,
            HlPunct::Comma => SupportedType::Comma,
            HlPunct::Dot => SupportedType::Dot,
            HlPunct::Colon => SupportedType::Colon,
            HlPunct::Semi => SupportedType::Semicolon,
            HlPunct::Other => SupportedType::Punctuation,
            HlPunct::MacroBang => SupportedType::MacroBang,
        },
    };

    for modifier in highlight.mods.iter() {
        let modifier = match modifier {
            HlMod::Associated => SupportedModifiers::Associated,
            HlMod::Async => SupportedModifiers::Async,
            HlMod::Attribute => SupportedModifiers::AttributeModifier,
            HlMod::Callable => SupportedModifiers::Callable,
            HlMod::Const => SupportedModifiers::Constant,
            HlMod::Consuming => SupportedModifiers::Consuming,
            HlMod::ControlFlow => SupportedModifiers::ControlFlow,
            HlMod::CrateRoot => SupportedModifiers::CrateRoot,
            HlMod::DefaultLibrary => SupportedModifiers::DefaultLibrary,
            HlMod::Deprecated => SupportedModifiers::Deprecated,
            HlMod::Definition => SupportedModifiers::Declaration,
            HlMod::Documentation => SupportedModifiers::Documentation,
            HlMod::Injected => SupportedModifiers::Injected,
            HlMod::IntraDocLink => SupportedModifiers::IntraDocLink,
            HlMod::Library => SupportedModifiers::Library,
            HlMod::Macro => SupportedModifiers::MacroModifier,
            HlMod::ProcMacro => SupportedModifiers::ProcMacroModifier,
            HlMod::Mutable => SupportedModifiers::Mutable,
            HlMod::Public => SupportedModifiers::Public,
            HlMod::Reference => SupportedModifiers::Reference,
            HlMod::Static => SupportedModifiers::Static,
            HlMod::Trait => SupportedModifiers::TraitModifier,
            HlMod::Unsafe => SupportedModifiers::Unsafe,
        };
        mods |= modifier;
    }

    (ty, mods)
}

pub(crate) fn folding_range(
    text: &str,
    line_index: &LineIndex,
    line_folding_only: bool,
    Fold { range: text_range, kind, collapsed_text }: Fold,
) -> lsp_types::FoldingRange {
    let kind = match kind {
        FoldKind::Comment => Some(lsp_types::FoldingRangeKind::Comment),
        FoldKind::Imports => Some(lsp_types::FoldingRangeKind::Imports),
        FoldKind::Region => Some(lsp_types::FoldingRangeKind::Region),
        FoldKind::Modules
        | FoldKind::Block
        | FoldKind::ArgList
        | FoldKind::Consts
        | FoldKind::Statics
        | FoldKind::TypeAliases
        | FoldKind::WhereClause
        | FoldKind::ReturnType
        | FoldKind::Array
        | FoldKind::ExternCrates
        | FoldKind::MatchArm
        | FoldKind::Function
        | FoldKind::Stmt
        | FoldKind::TailExpr => None,
    };

    let range = range(line_index, text_range);

    if line_folding_only {
        // Clients with line_folding_only == true (such as VSCode) will fold the whole end line
        // even if it contains text not in the folding range. To prevent that we exclude
        // range.end.line from the folding region if there is more text after range.end
        // on the same line.
        let has_more_text_on_end_line = text[TextRange::new(text_range.end(), TextSize::of(text))]
            .chars()
            .take_while(|it| *it != '\n')
            .any(|it| !it.is_whitespace());

        let end_line = if has_more_text_on_end_line {
            range.end.line.saturating_sub(1)
        } else {
            range.end.line
        };

        let collapsed_text = collapsed_text.map(|collapsed_text| {
            let range_start = text_range.start();
            let line_start = range_start - TextSize::from(range.start.character);
            let text_before_range = &text[TextRange::new(line_start, range_start)];
            format!("{text_before_range}{collapsed_text}")
        });

        lsp_types::FoldingRange {
            start_line: range.start.line,
            start_character: None,
            end_line,
            end_character: None,
            kind,
            collapsed_text,
        }
    } else {
        lsp_types::FoldingRange {
            start_line: range.start.line,
            start_character: Some(range.start.character),
            end_line: range.end.line,
            end_character: Some(range.end.character),
            kind,
            collapsed_text,
        }
    }
}

pub(crate) fn url(snap: &GlobalStateSnapshot, file_id: FileId) -> lsp_types::Uri {
    snap.file_id_to_url(file_id)
}

/// Returns a `Uri` object from a given path, will lowercase drive letters if present.
/// This will only happen when processing windows paths.
///
/// When processing non-windows path, this is essentially the same as [`lsp_types::Uri::from_file_path`].
pub(crate) fn url_from_abs_path(path: &AbsPath) -> lsp_types::Uri {
    let url = lsp_types::Uri::from_file_path(path).unwrap();
    match path.components().next() {
        Some(Utf8Component::Prefix(prefix))
            if matches!(prefix.kind(), Utf8Prefix::Disk(_) | Utf8Prefix::VerbatimDisk(_)) =>
        {
            // Need to lowercase driver letter
        }
        _ => return url,
    }

    let driver_letter_range = {
        let (scheme, drive_letter, _rest) = match url.as_str().splitn(3, ':').collect_tuple() {
            Some(it) => it,
            None => return url,
        };
        let start = scheme.len() + ':'.len_utf8();
        start..(start + drive_letter.len())
    };

    // Note: lowercasing the `path` itself doesn't help, the `Url::parse`
    // machinery *also* canonicalizes the drive letter. So, just massage the
    // string in place.
    let mut url: String = url.into();
    url[driver_letter_range].make_ascii_lowercase();
    lsp_types::Uri::parse(&url).unwrap()
}

pub(crate) fn optional_versioned_text_document_identifier(
    snap: &GlobalStateSnapshot,
    file_id: FileId,
) -> lsp_types::OptionalVersionedTextDocumentIdentifier {
    let uri = url(snap, file_id);
    let version = snap.url_file_version(&uri);
    lsp_types::OptionalVersionedTextDocumentIdentifier {
        text_document_identifier: lsp_types::TextDocumentIdentifier { uri },
        version,
    }
}

pub(crate) fn location(
    snap: &GlobalStateSnapshot,
    frange: FileRange,
) -> Cancellable<lsp_types::Location> {
    let url = url(snap, frange.file_id);
    let line_index = snap.file_line_index(frange.file_id)?;
    let range = range(&line_index, frange.range);
    let loc = lsp_types::Location::new(url, range);
    Ok(loc)
}

/// Prefer using `location_link`, if the client has the cap.
pub(crate) fn location_from_nav(
    snap: &GlobalStateSnapshot,
    nav: &NavigationTarget,
) -> Cancellable<lsp_types::Location> {
    let url = url(snap, nav.file_id);
    let line_index = snap.file_line_index(nav.file_id)?;
    let range = range(&line_index, nav.focus_or_full_range());
    let loc = lsp_types::Location::new(url, range);
    Ok(loc)
}

pub(crate) fn location_link(
    snap: &GlobalStateSnapshot,
    src: Option<FileRange>,
    target: NavigationTarget,
) -> Cancellable<lsp_types::LocationLink> {
    let origin_selection_range = match src {
        Some(src) => {
            let line_index = snap.file_line_index(src.file_id)?;
            let range = range(&line_index, src.range);
            Some(range)
        }
        None => None,
    };
    let (target_uri, target_range, target_selection_range) = location_info(snap, target)?;
    let res = lsp_types::LocationLink {
        origin_selection_range,
        target_uri,
        target_range,
        target_selection_range,
    };
    Ok(res)
}

fn location_info(
    snap: &GlobalStateSnapshot,
    target: NavigationTarget,
) -> Cancellable<(lsp_types::Uri, lsp_types::Range, lsp_types::Range)> {
    let line_index = snap.file_line_index(target.file_id)?;

    let target_uri = url(snap, target.file_id);
    let target_range = range(&line_index, target.full_range);
    let target_selection_range =
        target.focus_range.map(|it| range(&line_index, it)).unwrap_or(target_range);
    Ok((target_uri, target_range, target_selection_range))
}

pub(crate) fn goto_definition_response(
    snap: &GlobalStateSnapshot,
    src: Option<FileRange>,
    targets: Vec<NavigationTarget>,
) -> Cancellable<lsp_types::DefinitionResponse> {
    if snap.config.location_link() {
        let links = targets_to_links(snap, src, targets)?;
        Ok(links.into())
    } else {
        let locations = targets_to_locations(snap, targets)?;
        Ok(lsp_types::DefinitionResponse::Definition(lsp_types::Definition::LocationList(
            locations,
        )))
    }
}

pub(crate) fn goto_declaration_response(
    snap: &GlobalStateSnapshot,
    src: Option<FileRange>,
    targets: Vec<NavigationTarget>,
) -> Cancellable<lsp_types::DeclarationResponse> {
    if snap.config.location_link() {
        let links = targets_to_links(snap, src, targets)?;
        Ok(links.into())
    } else {
        let locations = targets_to_locations(snap, targets)?;
        Ok(lsp_types::DeclarationResponse::Declaration(lsp_types::Declaration::LocationList(
            locations,
        )))
    }
}

pub(crate) fn goto_implementation_response(
    snap: &GlobalStateSnapshot,
    src: Option<FileRange>,
    targets: Vec<NavigationTarget>,
) -> Cancellable<lsp_types::ImplementationResponse> {
    if snap.config.location_link() {
        let links = targets_to_links(snap, src, targets)?;
        Ok(links.into())
    } else {
        let locations = targets_to_locations(snap, targets)?;
        Ok(lsp_types::ImplementationResponse::Definition(lsp_types::Definition::LocationList(
            locations,
        )))
    }
}

pub(crate) fn goto_type_definition_response(
    snap: &GlobalStateSnapshot,
    src: Option<FileRange>,
    targets: Vec<NavigationTarget>,
) -> Cancellable<lsp_types::TypeDefinitionResponse> {
    if snap.config.location_link() {
        let links = targets_to_links(snap, src, targets)?;
        Ok(links.into())
    } else {
        let locations = targets_to_locations(snap, targets)?;
        Ok(lsp_types::TypeDefinitionResponse::Definition(lsp_types::Definition::LocationList(
            locations,
        )))
    }
}

fn targets_to_links(
    snap: &GlobalStateSnapshot,
    src: Option<hir::FileRangeWrapper<FileId>>,
    targets: Vec<NavigationTarget>,
) -> Result<Vec<lsp_types::LocationLink>, ide_db::base_db::salsa::Cancelled> {
    targets
        .into_iter()
        .unique_by(|nav| (nav.file_id, nav.full_range, nav.focus_range))
        .map(|nav| location_link(snap, src, nav))
        .collect::<Cancellable<Vec<_>>>()
}

fn targets_to_locations(
    snap: &GlobalStateSnapshot,
    targets: Vec<NavigationTarget>,
) -> Result<Vec<lsp_types::Location>, ide_db::base_db::salsa::Cancelled> {
    targets
        .into_iter()
        .map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() })
        .unique()
        .map(|range| location(snap, range))
        .collect::<Cancellable<Vec<_>>>()
}

fn outside_workspace_annotation_id() -> String {
    String::from("OutsideWorkspace")
}

fn merge_text_and_snippet_edits(
    line_index: &LineIndex,
    edit: TextEdit,
    snippet_edit: SnippetEdit,
    client_supports_annotations: bool,
) -> Vec<SnippetTextEdit> {
    let mut edits: Vec<SnippetTextEdit> = vec![];
    let mut snippets = snippet_edit.into_edit_ranges().into_iter().peekable();
    let annotation = edit.change_annotation();
    let text_edits = edit.into_iter();
    // offset to go from the final source location to the original source location
    let mut source_text_offset = 0i32;

    let offset_range = |range: TextRange, offset: i32| -> TextRange {
        // map the snippet range from the target location into the original source location
        let start = u32::from(range.start()).checked_add_signed(offset).unwrap_or(0);
        let end = u32::from(range.end()).checked_add_signed(offset).unwrap_or(0);

        TextRange::new(start.into(), end.into())
    };

    for current_indel in text_edits {
        let new_range = {
            let insert_len =
                TextSize::try_from(current_indel.insert.len()).unwrap_or(TextSize::from(u32::MAX));
            TextRange::at(current_indel.delete.start(), insert_len)
        };

        // figure out how much this Indel will shift future ranges from the initial source
        let offset_adjustment =
            u32::from(current_indel.delete.len()) as i32 - u32::from(new_range.len()) as i32;

        // insert any snippets before the text edit
        for (snippet_index, snippet_range) in snippets.peeking_take_while(|(_, range)| {
            offset_range(*range, source_text_offset).end() < new_range.start()
        }) {
            // adjust the snippet range into the corresponding initial source location
            let snippet_range = offset_range(snippet_range, source_text_offset);

            let snippet_range = if !stdx::always!(
                snippet_range.is_empty(),
                "placeholder range {:?} is before current text edit range {:?}",
                snippet_range,
                new_range
            ) {
                // only possible for tabstops, so make sure it's an empty/insert range
                TextRange::empty(snippet_range.start())
            } else {
                snippet_range
            };

            edits.push(snippet_text_edit(
                line_index,
                true,
                Indel { insert: format!("${snippet_index}"), delete: snippet_range },
                annotation,
                client_supports_annotations,
            ))
        }

        if snippets.peek().is_some_and(|(_, range)| {
            new_range.intersect(offset_range(*range, source_text_offset)).is_some()
        }) {
            // at least one snippet edit intersects this text edit,
            // so gather all of the edits that intersect this text edit
            let mut all_snippets = snippets
                .peeking_take_while(|(_, range)| {
                    new_range.intersect(offset_range(*range, source_text_offset)).is_some()
                })
                .map(|(tabstop, range)| (tabstop, offset_range(range, source_text_offset)))
                .collect_vec();

            // ensure all of the ranges are wholly contained inside of the new range
            all_snippets.retain(|(_, range)| {
                    stdx::always!(
                        new_range.contains_range(*range),
                        "found placeholder range {:?} which wasn't fully inside of text edit's new range {:?}", range, new_range
                    )
                });

            let mut new_text = current_indel.insert;

            // find which snippet bits need to be escaped
            let escape_places =
                new_text.rmatch_indices(['\\', '$', '}']).map(|(insert, _)| insert).collect_vec();
            let mut escape_places = escape_places.into_iter().peekable();
            let mut escape_prior_bits = |new_text: &mut String, up_to: usize| {
                for before in escape_places.peeking_take_while(|insert| *insert >= up_to) {
                    new_text.insert(before, '\\');
                }
            };

            // insert snippets, and escaping any needed bits along the way
            for (index, range) in all_snippets.iter().rev() {
                let text_range = range - new_range.start();
                let (start, end) = (text_range.start().into(), text_range.end().into());

                if range.is_empty() {
                    escape_prior_bits(&mut new_text, start);
                    new_text.insert_str(start, &format!("${index}"));
                } else {
                    escape_prior_bits(&mut new_text, end);
                    new_text.insert(end, '}');
                    escape_prior_bits(&mut new_text, start);
                    new_text.insert_str(start, &format!("${{{index}:"));
                }
            }

            // escape any remaining bits
            escape_prior_bits(&mut new_text, 0);

            edits.push(snippet_text_edit(
                line_index,
                true,
                Indel { insert: new_text, delete: current_indel.delete },
                annotation,
                client_supports_annotations,
            ))
        } else {
            // snippet edit was beyond the current one
            // since it wasn't consumed, it's available for the next pass
            edits.push(snippet_text_edit(
                line_index,
                false,
                current_indel,
                annotation,
                client_supports_annotations,
            ));
        }

        // update the final source -> initial source mapping offset
        source_text_offset += offset_adjustment;
    }

    // insert any remaining tabstops
    edits.extend(snippets.map(|(snippet_index, snippet_range)| {
        // adjust the snippet range into the corresponding initial source location
        let snippet_range = offset_range(snippet_range, source_text_offset);

        let snippet_range = if !stdx::always!(
            snippet_range.is_empty(),
            "found placeholder snippet {:?} without a text edit",
            snippet_range
        ) {
            TextRange::empty(snippet_range.start())
        } else {
            snippet_range
        };

        snippet_text_edit(
            line_index,
            true,
            Indel { insert: format!("${snippet_index}"), delete: snippet_range },
            annotation,
            client_supports_annotations,
        )
    }));

    edits
}

pub(crate) fn snippet_text_document_edit(
    snap: &GlobalStateSnapshot,
    is_snippet: bool,
    file_id: FileId,
    edit: TextEdit,
    snippet_edit: Option<SnippetEdit>,
) -> Cancellable<lsp_ext::SnippetTextDocumentEdit> {
    let text_document = optional_versioned_text_document_identifier(snap, file_id);
    let line_index = snap.file_line_index(file_id)?;
    let client_supports_annotations = snap.config.change_annotation_support();
    let mut edits = if let Some(snippet_edit) = snippet_edit {
        merge_text_and_snippet_edits(&line_index, edit, snippet_edit, client_supports_annotations)
    } else {
        let annotation = edit.change_annotation();
        edit.into_iter()
            .map(|it| {
                snippet_text_edit(
                    &line_index,
                    is_snippet,
                    it,
                    annotation,
                    client_supports_annotations,
                )
            })
            .collect()
    };

    if snap.analysis.is_library_file(file_id)? && snap.config.change_annotation_support() {
        for edit in &mut edits {
            edit.annotation_id = Some(outside_workspace_annotation_id())
        }
    }
    Ok(lsp_ext::SnippetTextDocumentEdit { text_document, edits })
}

pub(crate) fn snippet_text_document_ops(
    snap: &GlobalStateSnapshot,
    file_system_edit: FileSystemEdit,
) -> Cancellable<Vec<lsp_ext::SnippetDocumentChangeOperation>> {
    let mut ops = Vec::new();
    match file_system_edit {
        FileSystemEdit::CreateFile { dst, initial_contents } => {
            let uri = snap.anchored_path(&dst);
            let create_file = lsp_types::DocumentChange::CreateFile(lsp_types::CreateFile {
                uri: uri.clone(),
                options: None,
                annotation_id: None,
            });
            ops.push(lsp_ext::SnippetDocumentChangeOperation::Change(create_file));
            if !initial_contents.is_empty() {
                let text_document = lsp_types::OptionalVersionedTextDocumentIdentifier {
                    text_document_identifier: lsp_types::TextDocumentIdentifier { uri },
                    version: None,
                };
                let text_edit = lsp_ext::SnippetTextEdit {
                    range: lsp_types::Range::default(),
                    new_text: initial_contents,
                    insert_text_format: Some(lsp_types::InsertTextFormat::PlainText),
                    annotation_id: None,
                };
                let edit_file =
                    lsp_ext::SnippetTextDocumentEdit { text_document, edits: vec![text_edit] };
                ops.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit_file));
            }
        }
        FileSystemEdit::MoveFile { src, dst } => {
            let old_uri = snap.file_id_to_url(src);
            let new_uri = snap.anchored_path(&dst);
            let mut rename_file =
                lsp_types::RenameFile { old_uri, new_uri, options: None, annotation_id: None };
            if snap.analysis.is_library_file(src).ok() == Some(true)
                && snap.config.change_annotation_support()
            {
                rename_file.annotation_id = Some(outside_workspace_annotation_id())
            }
            ops.push(lsp_ext::SnippetDocumentChangeOperation::Change(
                lsp_types::DocumentChange::RenameFile(rename_file),
            ))
        }
        FileSystemEdit::MoveDir { src, src_id, dst } => {
            let old_uri = snap.anchored_path(&src);
            let new_uri = snap.anchored_path(&dst);
            let mut rename_file =
                lsp_types::RenameFile { old_uri, new_uri, options: None, annotation_id: None };
            if snap.analysis.is_library_file(src_id).ok() == Some(true)
                && snap.config.change_annotation_support()
            {
                rename_file.annotation_id = Some(outside_workspace_annotation_id())
            }
            ops.push(lsp_ext::SnippetDocumentChangeOperation::Change(
                lsp_types::DocumentChange::RenameFile(rename_file),
            ))
        }
    }
    Ok(ops)
}

pub(crate) fn snippet_workspace_edit(
    snap: &GlobalStateSnapshot,
    mut source_change: SourceChange,
) -> Cancellable<lsp_ext::SnippetWorkspaceEdit> {
    let mut document_changes: Vec<lsp_ext::SnippetDocumentChangeOperation> = Vec::new();

    for op in &mut source_change.file_system_edits {
        if let FileSystemEdit::CreateFile { dst, initial_contents } = op {
            // replace with a placeholder to avoid cloneing the edit
            let op = FileSystemEdit::CreateFile {
                dst: dst.clone(),
                initial_contents: mem::take(initial_contents),
            };
            let ops = snippet_text_document_ops(snap, op)?;
            document_changes.extend_from_slice(&ops);
        }
    }
    for (file_id, (edit, snippet_edit)) in source_change.source_file_edits {
        let edit = snippet_text_document_edit(
            snap,
            source_change.is_snippet,
            file_id,
            edit,
            snippet_edit,
        )?;
        document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit));
    }
    for op in source_change.file_system_edits {
        if !matches!(op, FileSystemEdit::CreateFile { .. }) {
            let ops = snippet_text_document_ops(snap, op)?;
            document_changes.extend_from_slice(&ops);
        }
    }
    let mut workspace_edit = lsp_ext::SnippetWorkspaceEdit {
        changes: None,
        document_changes: Some(document_changes),
        change_annotations: None,
    };
    if snap.config.change_annotation_support() {
        workspace_edit.change_annotations = Some(
            once((
                outside_workspace_annotation_id(),
                lsp_types::ChangeAnnotation {
                    label: String::from("Edit outside of the workspace"),
                    needs_confirmation: Some(true),
                    description: Some(String::from(
                        "This edit lies outside of the workspace and may affect dependencies",
                    )),
                },
            ))
            .chain(source_change.annotations.into_iter().map(|(id, annotation)| {
                (
                    id.to_string(),
                    lsp_types::ChangeAnnotation {
                        label: annotation.label,
                        description: annotation.description,
                        needs_confirmation: Some(annotation.needs_confirmation),
                    },
                )
            }))
            .collect(),
        )
    }
    Ok(workspace_edit)
}

pub(crate) fn workspace_edit(
    snap: &GlobalStateSnapshot,
    source_change: SourceChange,
) -> Cancellable<lsp_types::WorkspaceEdit> {
    assert!(!source_change.is_snippet);
    snippet_workspace_edit(snap, source_change).map(|it| it.into())
}

impl From<lsp_ext::SnippetWorkspaceEdit> for lsp_types::WorkspaceEdit {
    fn from(snippet_workspace_edit: lsp_ext::SnippetWorkspaceEdit) -> lsp_types::WorkspaceEdit {
        lsp_types::WorkspaceEdit {
            changes: None,
            document_changes: snippet_workspace_edit.document_changes.map(|changes| {
                changes
                    .into_iter()
                    .map(|change| match change {
                        lsp_ext::SnippetDocumentChangeOperation::Change(op) => op,
                        lsp_ext::SnippetDocumentChangeOperation::Edit(edit) => {
                            lsp_types::DocumentChange::TextDocumentEdit(
                                lsp_types::TextDocumentEdit {
                                    text_document: edit.text_document,
                                    edits: edit.edits.into_iter().map(From::from).collect(),
                                },
                            )
                        }
                    })
                    .collect()
            }),
            change_annotations: snippet_workspace_edit.change_annotations,
        }
    }
}

impl From<lsp_ext::SnippetTextEdit> for lsp_types::Edit {
    fn from(
        lsp_ext::SnippetTextEdit { annotation_id, insert_text_format:_, new_text, range }: lsp_ext::SnippetTextEdit,
    ) -> Self {
        match annotation_id {
            Some(annotation_id) => {
                lsp_types::Edit::AnnotatedTextEdit(lsp_types::AnnotatedTextEdit {
                    text_edit: lsp_types::TextEdit { range, new_text },
                    annotation_id,
                })
            }
            None => lsp_types::Edit::TextEdit(lsp_types::TextEdit { range, new_text }),
        }
    }
}

pub(crate) fn call_hierarchy_item(
    snap: &GlobalStateSnapshot,
    target: NavigationTarget,
) -> Cancellable<lsp_types::CallHierarchyItem> {
    let name = target.name.to_string();
    let detail = target.description.clone();
    let kind = target.kind.map(symbol_kind).unwrap_or(lsp_types::SymbolKind::Function);
    let (uri, range, selection_range) = location_info(snap, target)?;
    Ok(lsp_types::CallHierarchyItem {
        name,
        kind,
        tags: None,
        detail,
        uri,
        range,
        selection_range,
        data: None,
    })
}

pub(crate) fn code_action_kind(kind: AssistKind) -> lsp_types::CodeActionKind {
    match kind {
        AssistKind::Generate => lsp_types::CodeActionKind::Empty,
        AssistKind::QuickFix => lsp_types::CodeActionKind::QuickFix,
        AssistKind::Refactor => lsp_types::CodeActionKind::Refactor,
        AssistKind::RefactorExtract => lsp_types::CodeActionKind::RefactorExtract,
        AssistKind::RefactorInline => lsp_types::CodeActionKind::RefactorInline,
        AssistKind::RefactorRewrite => lsp_types::CodeActionKind::RefactorRewrite,
    }
}

pub(crate) fn code_action(
    snap: &GlobalStateSnapshot,
    commands: &ClientCommandsConfig,
    assist: Assist,
    resolve_data: Option<(usize, lsp_types::CodeActionParams, Option<i32>)>,
) -> Cancellable<lsp_ext::CodeAction> {
    let mut res = lsp_ext::CodeAction {
        title: assist.label.to_string(),
        group: assist.group.filter(|_| snap.config.code_action_group()).map(|gr| gr.0),
        kind: Some(code_action_kind(assist.id.1)),
        edit: None,
        is_preferred: None,
        data: None,
        command: None,
    };

    res.command = match assist.command {
        Some(assists::Command::TriggerParameterHints) if commands.trigger_parameter_hints => {
            Some(command::trigger_parameter_hints())
        }
        Some(assists::Command::Rename) if commands.rename => Some(command::rename()),
        _ => None,
    };

    match (assist.source_change, resolve_data) {
        (Some(it), _) => res.edit = Some(snippet_workspace_edit(snap, it)?),
        (None, Some((index, code_action_params, version))) => {
            res.data = Some(lsp_ext::CodeActionData {
                id: format!(
                    "{}:{}:{index}:{}",
                    assist.id.0,
                    assist.id.1.name(),
                    assist.id.2.map(|x| x.to_string()).unwrap_or("".to_owned())
                ),
                code_action_params,
                version,
            });
        }
        (None, None) => {
            stdx::never!("assist should always be resolved if client can't do lazy resolving")
        }
    };
    Ok(res)
}

pub(crate) fn runnable(
    snap: &GlobalStateSnapshot,
    runnable: Runnable,
) -> Cancellable<Option<lsp_ext::Runnable>> {
    let target_spec = TargetSpec::for_file(snap, runnable.nav.file_id)?;
    let source_root = snap.analysis.source_root_id(runnable.nav.file_id).ok();
    let config = snap.config.runnables(source_root);

    match target_spec {
        Some(TargetSpec::Cargo(spec)) => {
            let workspace_root = spec.workspace_root.clone();

            let target = spec.target.clone();

            let override_command =
                CargoTargetSpec::override_command(snap, Some(spec.clone()), &runnable.kind);

            let (cargo_args, executable_args) = CargoTargetSpec::runnable_args(
                snap,
                Some(spec.clone()),
                &runnable.kind,
                &runnable.cfg,
            );

            let cwd = match runnable.kind {
                ide::RunnableKind::Bin => workspace_root.clone(),
                _ => spec.cargo_toml.parent().to_owned(),
            };

            let label = runnable.label(Some(&target));
            let location = location_link(snap, None, runnable.nav)?;

            let environment = spec
                .sysroot_root
                .map(|root| ("RUSTC_TOOLCHAIN".to_owned(), root.to_string()))
                .into_iter()
                .collect();

            Ok(match override_command {
                Some(override_command) => match override_command.split_first() {
                    Some((program, args)) => Some(lsp_ext::Runnable {
                        label,
                        location: Some(location),
                        args: lsp_ext::RunnableArgs::Shell(lsp_ext::ShellRunnableArgs {
                            environment,
                            cwd: cwd.into(),
                            program: program.to_string(),
                            args: args.to_vec(),
                        }),
                    }),
                    _ => None,
                },
                None => Some(lsp_ext::Runnable {
                    label,
                    location: Some(location),
                    args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs {
                        workspace_root: Some(workspace_root.into()),
                        override_cargo: config.override_cargo,
                        cargo_args,
                        cwd: cwd.into(),
                        executable_args,
                        environment,
                    }),
                }),
            })
        }
        Some(TargetSpec::ProjectJson(spec)) => {
            let label = runnable.label(Some(&spec.label));
            let location = location_link(snap, None, runnable.nav)?;

            match spec.runnable_args(&runnable.kind) {
                Some(json_shell_runnable_args) => {
                    let runnable_args = ShellRunnableArgs {
                        program: json_shell_runnable_args.program,
                        args: json_shell_runnable_args.args,
                        cwd: json_shell_runnable_args.cwd,
                        environment: Default::default(),
                    };
                    Ok(Some(lsp_ext::Runnable {
                        label,
                        location: Some(location),
                        args: lsp_ext::RunnableArgs::Shell(runnable_args),
                    }))
                }
                None => Ok(None),
            }
        }
        None => {
            let Some(path) = snap.file_id_to_file_path(runnable.nav.file_id).parent() else {
                return Ok(None);
            };
            let (cargo_args, executable_args) =
                CargoTargetSpec::runnable_args(snap, None, &runnable.kind, &runnable.cfg);

            let label = runnable.label(None);
            let location = location_link(snap, None, runnable.nav)?;

            Ok(Some(lsp_ext::Runnable {
                label,
                location: Some(location),
                args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs {
                    workspace_root: None,
                    override_cargo: config.override_cargo,
                    cargo_args,
                    cwd: path.as_path().unwrap().to_path_buf().into(),
                    executable_args,
                    environment: Default::default(),
                }),
            }))
        }
    }
}

pub(crate) fn code_lens(
    acc: &mut Vec<lsp_types::CodeLens>,
    snap: &GlobalStateSnapshot,
    annotation: Annotation,
) -> Cancellable<()> {
    let client_commands_config = snap.config.client_commands();
    match annotation.kind {
        AnnotationKind::Runnable(run) => {
            let line_index = snap.file_line_index(run.nav.file_id)?;
            let annotation_range = range(&line_index, annotation.range);

            let update_test = run.update_test;
            let title = run.title();
            let can_debug = match run.kind {
                ide::RunnableKind::DocTest { .. } => false,
                ide::RunnableKind::TestMod { .. }
                | ide::RunnableKind::Test { .. }
                | ide::RunnableKind::Bench { .. }
                | ide::RunnableKind::Bin => true,
            };
            let r = runnable(snap, run)?;

            if let Some(r) = r {
                let has_root = match &r.args {
                    lsp_ext::RunnableArgs::Cargo(c) => c.workspace_root.is_some(),
                    lsp_ext::RunnableArgs::Shell(_) => true,
                };

                let lens_config = snap.config.lens();

                if has_root {
                    if lens_config.run && client_commands_config.run_single {
                        let command = command::run_single(&r, &title);
                        acc.push(lsp_types::CodeLens {
                            range: annotation_range,
                            command: Some(command),
                            data: None,
                        })
                    }
                    if lens_config.debug && can_debug && client_commands_config.debug_single {
                        let command = command::debug_single(&r);
                        acc.push(lsp_types::CodeLens {
                            range: annotation_range,
                            command: Some(command),
                            data: None,
                        })
                    }
                    if lens_config.update_test && client_commands_config.run_single {
                        let label = update_test.label();
                        if let Some(r) = make_update_runnable(&r, update_test) {
                            let command = command::run_single(&r, label.unwrap().as_str());
                            acc.push(lsp_types::CodeLens {
                                range: annotation_range,
                                command: Some(command),
                                data: None,
                            })
                        }
                    }
                }

                if lens_config.interpret {
                    let command = command::interpret_single(&r);
                    acc.push(lsp_types::CodeLens {
                        range: annotation_range,
                        command: Some(command),
                        data: None,
                    })
                }
            }
        }
        AnnotationKind::HasImpls { pos, data } => {
            if !client_commands_config.show_reference {
                return Ok(());
            }
            let line_index = snap.file_line_index(pos.file_id)?;
            let annotation_range = range(&line_index, annotation.range);
            let url = url(snap, pos.file_id);
            let pos = position(&line_index, pos.offset);

            let id = lsp_types::TextDocumentIdentifier { uri: url.clone() };

            let doc_pos = lsp_types::TextDocumentPositionParams::new(id, pos);

            let goto_params = lsp_types::ImplementationParams {
                text_document_position_params: doc_pos,
                work_done_progress_params: Default::default(),
                partial_result_params: Default::default(),
            };

            let command = data.map(|ranges| {
                let locations: Vec<lsp_types::Location> = ranges
                    .into_iter()
                    .filter_map(|target| {
                        location(
                            snap,
                            FileRange { file_id: target.file_id, range: target.full_range },
                        )
                        .ok()
                    })
                    .collect();

                command::show_references(
                    implementation_title(locations.len()),
                    &url,
                    pos,
                    locations,
                )
            });

            acc.push(lsp_types::CodeLens {
                range: annotation_range,
                command,
                data: (|| {
                    let version = snap.url_file_version(&url)?;
                    Some(
                        to_value(lsp_ext::CodeLensResolveData {
                            version,
                            kind: lsp_ext::CodeLensResolveDataKind::Impls(goto_params),
                        })
                        .unwrap(),
                    )
                })(),
            })
        }
        AnnotationKind::HasReferences { pos, data } => {
            if !client_commands_config.show_reference {
                return Ok(());
            }
            let line_index = snap.file_line_index(pos.file_id)?;
            let annotation_range = range(&line_index, annotation.range);
            let url = url(snap, pos.file_id);
            let pos = position(&line_index, pos.offset);

            let id = lsp_types::TextDocumentIdentifier { uri: url.clone() };

            let doc_pos = lsp_types::TextDocumentPositionParams::new(id, pos);

            let command = data.map(|ranges| {
                let locations: Vec<lsp_types::Location> =
                    ranges.into_iter().filter_map(|range| location(snap, range).ok()).collect();

                command::show_references(reference_title(locations.len()), &url, pos, locations)
            });

            acc.push(lsp_types::CodeLens {
                range: annotation_range,
                command,
                data: (|| {
                    let version = snap.url_file_version(&url)?;
                    Some(
                        to_value(lsp_ext::CodeLensResolveData {
                            version,
                            kind: lsp_ext::CodeLensResolveDataKind::References(doc_pos),
                        })
                        .unwrap(),
                    )
                })(),
            })
        }
    }
    Ok(())
}

pub(crate) fn test_item(
    snap: &GlobalStateSnapshot,
    test_item: ide::TestItem,
    line_index: Option<&LineIndex>,
) -> Option<lsp_ext::TestItem> {
    Some(lsp_ext::TestItem {
        id: test_item.id,
        label: test_item.label,
        kind: match test_item.kind {
            ide::TestItemKind::Crate(id) => match snap.target_spec_for_crate(id) {
                Some(target_spec) => match target_spec.target_kind() {
                    project_model::TargetKind::Bin
                    | project_model::TargetKind::Lib { .. }
                    | project_model::TargetKind::Example
                    | project_model::TargetKind::BuildScript
                    | project_model::TargetKind::Other => lsp_ext::TestItemKind::Package,
                    project_model::TargetKind::Test => lsp_ext::TestItemKind::Test,
                    // benches are not tests needed to be shown in the test explorer
                    project_model::TargetKind::Bench => return None,
                },
                None => lsp_ext::TestItemKind::Package,
            },
            ide::TestItemKind::Module => lsp_ext::TestItemKind::Module,
            ide::TestItemKind::Function => lsp_ext::TestItemKind::Test,
        },
        can_resolve_children: matches!(
            test_item.kind,
            ide::TestItemKind::Crate(_) | ide::TestItemKind::Module
        ),
        parent: test_item.parent,
        text_document: test_item
            .file
            .map(|f| lsp_types::TextDocumentIdentifier { uri: url(snap, f) }),
        range: line_index.and_then(|l| Some(range(l, test_item.text_range?))),
        runnable: test_item.runnable.and_then(|r| runnable(snap, r).ok()).flatten(),
    })
}

pub(crate) mod command {
    use ide::{FileRange, NavigationTarget};
    use serde_json::to_value;

    use crate::{
        global_state::GlobalStateSnapshot,
        lsp::to_proto::{location, location_link},
        lsp_ext,
    };

    pub(crate) fn show_references(
        title: String,
        uri: &lsp_types::Uri,
        position: lsp_types::Position,
        locations: Vec<lsp_types::Location>,
    ) -> lsp_types::Command {
        // We cannot use the 'editor.action.showReferences' command directly
        // because that command requires vscode types which we convert in the handler
        // on the client side.

        lsp_types::Command {
            title,
            command: "rust-analyzer.showReferences".into(),
            arguments: Some(vec![
                to_value(uri).unwrap(),
                to_value(position).unwrap(),
                to_value(locations).unwrap(),
            ]),
            tooltip: None,
        }
    }

    pub(crate) fn run_single(runnable: &lsp_ext::Runnable, title: &str) -> lsp_types::Command {
        lsp_types::Command {
            title: title.to_owned(),
            command: "rust-analyzer.runSingle".into(),
            arguments: Some(vec![to_value(runnable).unwrap()]),
            tooltip: None,
        }
    }

    pub(crate) fn debug_single(runnable: &lsp_ext::Runnable) -> lsp_types::Command {
        lsp_types::Command {
            title: "⚙\u{fe0e} Debug".into(),
            command: "rust-analyzer.debugSingle".into(),
            arguments: Some(vec![to_value(runnable).unwrap()]),
            tooltip: None,
        }
    }

    pub(crate) fn interpret_single(_runnable: &lsp_ext::Runnable) -> lsp_types::Command {
        lsp_types::Command {
            title: "Interpret".into(),
            command: "rust-analyzer.interpretFunction".into(),
            // FIXME: use the `_runnable` here.
            arguments: Some(vec![]),
            tooltip: None,
        }
    }

    pub(crate) fn goto_location(
        snap: &GlobalStateSnapshot,
        nav: &NavigationTarget,
    ) -> Option<lsp_types::Command> {
        let value = if snap.config.location_link() {
            let link = location_link(snap, None, nav.clone()).ok()?;
            to_value(link).ok()?
        } else {
            let range = FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() };
            let location = location(snap, range).ok()?;
            to_value(location).ok()?
        };

        Some(lsp_types::Command {
            title: nav.name.to_string(),
            command: "rust-analyzer.gotoLocation".into(),
            arguments: Some(vec![value]),
            tooltip: None,
        })
    }

    pub(crate) fn trigger_parameter_hints() -> lsp_types::Command {
        lsp_types::Command {
            title: "triggerParameterHints".into(),
            command: "rust-analyzer.triggerParameterHints".into(),
            arguments: None,
            tooltip: None,
        }
    }

    pub(crate) fn rename() -> lsp_types::Command {
        lsp_types::Command {
            title: "rename".into(),
            command: "rust-analyzer.rename".into(),
            arguments: None,
            tooltip: None,
        }
    }
}

pub(crate) fn make_update_runnable(
    runnable: &lsp_ext::Runnable,
    update_test: UpdateTest,
) -> Option<lsp_ext::Runnable> {
    let label = update_test.label()?;

    let mut runnable = runnable.clone();
    runnable.label = format!("{} + {}", runnable.label, label);

    let lsp_ext::RunnableArgs::Cargo(r) = &mut runnable.args else {
        return None;
    };

    r.environment.extend(update_test.env().iter().map(|(k, v)| (k.to_string(), v.to_string())));

    if update_test.insta {
        r.cargo_args.insert(0, "insta".to_owned());
    }

    Some(runnable)
}

pub(crate) fn implementation_title(count: usize) -> String {
    if count == 1 { "1 implementation".into() } else { format!("{count} implementations") }
}

pub(crate) fn reference_title(count: usize) -> String {
    if count == 1 { "1 reference".into() } else { format!("{count} references") }
}

pub(crate) fn markup_content(
    markup: Markup,
    kind: ide::HoverDocFormat,
) -> lsp_types::MarkupContent {
    let kind = match kind {
        ide::HoverDocFormat::Markdown => lsp_types::MarkupKind::Markdown,
        ide::HoverDocFormat::PlainText => lsp_types::MarkupKind::PlainText,
    };
    let value = format_docs(&Documentation::new_owned(markup.into()));
    lsp_types::MarkupContent { kind, value }
}

pub(crate) fn rename_error(err: RenameError) -> LspError {
    // This is wrong, but we don't have a better alternative I suppose?
    // https://github.com/microsoft/language-server-protocol/issues/1341
    invalid_params_error(err.to_string())
}

#[cfg(test)]
mod tests {
    use expect_test::{Expect, expect};
    use ide::{Analysis, FilePosition};
    use ide_db::base_db::AbsPathBuf;
    use ide_db::source_change::Snippet;
    use test_utils::extract_offset;
    use triomphe::Arc;

    use super::*;

    #[test]
    fn conv_fold_line_folding_only_fixup() {
        let text = r#"mod a;
mod b;
mod c;

fn main() {
    if cond {
        a::do_a();
    } else {
        b::do_b();
    }
}"#;

        let (analysis, file_id) = Analysis::from_single_file(
            text.to_owned(),
            Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
        );
        let folds = analysis.folding_ranges(file_id, true).unwrap();
        assert_eq!(folds.len(), 5);

        let line_index = LineIndex {
            index: Arc::new(ide::LineIndex::new(text)),
            endings: LineEndings::Unix,
            encoding: PositionEncoding::Utf8,
        };
        let converted: Vec<lsp_types::FoldingRange> =
            folds.into_iter().map(|it| folding_range(text, &line_index, true, it)).collect();

        let expected_lines = [(0, 2), (4, 10), (5, 9), (5, 6), (7, 9)];
        assert_eq!(converted.len(), expected_lines.len());
        for (folding_range, (start_line, end_line)) in converted.iter().zip(expected_lines.iter()) {
            assert_eq!(folding_range.start_line, *start_line);
            assert_eq!(folding_range.start_character, None);
            assert_eq!(folding_range.end_line, *end_line);
            assert_eq!(folding_range.end_character, None);
        }
    }

    #[test]
    fn calling_function_with_ignored_code_in_signature() {
        let text = r#"
fn foo() {
    bar($0);
}
/// ```
/// # use crate::bar;
/// bar(5);
/// ```
fn bar(_: usize) {}
"#;

        let (offset, text) = extract_offset(text);
        let (analysis, file_id) = Analysis::from_single_file(
            text,
            Arc::new(AbsPathBuf::assert_utf8(std::env::current_dir().unwrap())),
        );
        let help = signature_help(
            analysis.signature_help(FilePosition { file_id, offset }).unwrap().unwrap(),
            CallInfoConfig { params_only: false, docs: true },
            false,
        );
        let docs = match &help.signatures[help.active_signature.unwrap() as usize].documentation {
            Some(lsp_types::Documentation::MarkupContent(content)) => &content.value,
            _ => panic!("documentation contains markup"),
        };
        assert!(docs.contains("bar(5)"));
        assert!(!docs.contains("use crate::bar"));
    }

    #[track_caller]
    fn check_rendered_snippets(edit: TextEdit, snippets: SnippetEdit, expect: Expect) {
        check_rendered_snippets_in_source(
            r"/* place to put all ranges in */",
            edit,
            snippets,
            expect,
        );
    }

    #[track_caller]
    fn check_rendered_snippets_in_source(
        #[rust_analyzer::rust_fixture] ra_fixture: &str,
        edit: TextEdit,
        snippets: SnippetEdit,
        expect: Expect,
    ) {
        let source = stdx::trim_indent(ra_fixture);
        let endings = if source.contains('\r') { LineEndings::Dos } else { LineEndings::Unix };
        let line_index = LineIndex {
            index: Arc::new(ide::LineIndex::new(&source)),
            endings,
            encoding: PositionEncoding::Utf8,
        };

        let res = merge_text_and_snippet_edits(&line_index, edit, snippets, true);

        // Ensure that none of the ranges overlap
        {
            let mut sorted = res.clone();
            sorted.sort_by_key(|edit| (edit.range.start, edit.range.end));
            let disjoint_ranges = sorted
                .iter()
                .zip(sorted.iter().skip(1))
                .all(|(l, r)| l.range.end <= r.range.start || l == r);
            assert!(disjoint_ranges, "ranges overlap for {res:#?}");
        }

        expect.assert_debug_eq(&res);
    }

    #[test]
    fn snippet_rendering_only_tabstops() {
        let edit = TextEdit::builder().finish();
        let snippets = SnippetEdit::new(vec![
            Snippet::Tabstop(0.into()),
            Snippet::Tabstop(0.into()),
            Snippet::Tabstop(1.into()),
            Snippet::Tabstop(1.into()),
        ]);

        check_rendered_snippets(
            edit,
            snippets,
            expect![[r#"
            [
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 0,
                        },
                        end: Position {
                            line: 0,
                            character: 0,
                        },
                    },
                    new_text: "$1",
                    insert_text_format: Some(
                        Snippet,
                    ),
                    annotation_id: None,
                },
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 0,
                        },
                        end: Position {
                            line: 0,
                            character: 0,
                        },
                    },
                    new_text: "$2",
                    insert_text_format: Some(
                        Snippet,
                    ),
                    annotation_id: None,
                },
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 1,
                        },
                        end: Position {
                            line: 0,
                            character: 1,
                        },
                    },
                    new_text: "$3",
                    insert_text_format: Some(
                        Snippet,
                    ),
                    annotation_id: None,
                },
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 1,
                        },
                        end: Position {
                            line: 0,
                            character: 1,
                        },
                    },
                    new_text: "$0",
                    insert_text_format: Some(
                        Snippet,
                    ),
                    annotation_id: None,
                },
            ]
        "#]],
        );
    }

    #[test]
    fn snippet_rendering_only_text_edits() {
        let mut edit = TextEdit::builder();
        edit.insert(0.into(), "abc".to_owned());
        edit.insert(3.into(), "def".to_owned());
        let edit = edit.finish();
        let snippets = SnippetEdit::new(vec![]);

        check_rendered_snippets(
            edit,
            snippets,
            expect![[r#"
            [
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 0,
                        },
                        end: Position {
                            line: 0,
                            character: 0,
                        },
                    },
                    new_text: "abc",
                    insert_text_format: None,
                    annotation_id: None,
                },
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 3,
                        },
                        end: Position {
                            line: 0,
                            character: 3,
                        },
                    },
                    new_text: "def",
                    insert_text_format: None,
                    annotation_id: None,
                },
            ]
        "#]],
        );
    }

    #[test]
    fn snippet_rendering_tabstop_after_text_edit() {
        let mut edit = TextEdit::builder();
        edit.insert(0.into(), "abc".to_owned());
        let edit = edit.finish();
        // Note: tabstops are positioned in the source where all text edits have been applied
        let snippets = SnippetEdit::new(vec![Snippet::Tabstop(10.into())]);

        check_rendered_snippets(
            edit,
            snippets,
            expect![[r#"
            [
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 0,
                        },
                        end: Position {
                            line: 0,
                            character: 0,
                        },
                    },
                    new_text: "abc",
                    insert_text_format: None,
                    annotation_id: None,
                },
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 7,
                        },
                        end: Position {
                            line: 0,
                            character: 7,
                        },
                    },
                    new_text: "$0",
                    insert_text_format: Some(
                        Snippet,
                    ),
                    annotation_id: None,
                },
            ]
        "#]],
        );
    }

    #[test]
    fn snippet_rendering_tabstops_before_text_edit() {
        let mut edit = TextEdit::builder();
        edit.insert(2.into(), "abc".to_owned());
        let edit = edit.finish();
        let snippets =
            SnippetEdit::new(vec![Snippet::Tabstop(0.into()), Snippet::Tabstop(0.into())]);

        check_rendered_snippets(
            edit,
            snippets,
            expect![[r#"
                [
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 0,
                                character: 0,
                            },
                            end: Position {
                                line: 0,
                                character: 0,
                            },
                        },
                        new_text: "$1",
                        insert_text_format: Some(
                            Snippet,
                        ),
                        annotation_id: None,
                    },
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 0,
                                character: 0,
                            },
                            end: Position {
                                line: 0,
                                character: 0,
                            },
                        },
                        new_text: "$0",
                        insert_text_format: Some(
                            Snippet,
                        ),
                        annotation_id: None,
                    },
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 0,
                                character: 2,
                            },
                            end: Position {
                                line: 0,
                                character: 2,
                            },
                        },
                        new_text: "abc",
                        insert_text_format: None,
                        annotation_id: None,
                    },
                ]
            "#]],
        );
    }

    #[test]
    fn snippet_rendering_tabstops_between_text_edits() {
        let mut edit = TextEdit::builder();
        edit.insert(0.into(), "abc".to_owned());
        edit.insert(7.into(), "abc".to_owned());
        let edit = edit.finish();
        // Note: tabstops are positioned in the source where all text edits have been applied
        let snippets =
            SnippetEdit::new(vec![Snippet::Tabstop(7.into()), Snippet::Tabstop(7.into())]);

        check_rendered_snippets(
            edit,
            snippets,
            expect![[r#"
            [
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 0,
                        },
                        end: Position {
                            line: 0,
                            character: 0,
                        },
                    },
                    new_text: "abc",
                    insert_text_format: None,
                    annotation_id: None,
                },
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 4,
                        },
                        end: Position {
                            line: 0,
                            character: 4,
                        },
                    },
                    new_text: "$1",
                    insert_text_format: Some(
                        Snippet,
                    ),
                    annotation_id: None,
                },
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 4,
                        },
                        end: Position {
                            line: 0,
                            character: 4,
                        },
                    },
                    new_text: "$0",
                    insert_text_format: Some(
                        Snippet,
                    ),
                    annotation_id: None,
                },
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 7,
                        },
                        end: Position {
                            line: 0,
                            character: 7,
                        },
                    },
                    new_text: "abc",
                    insert_text_format: None,
                    annotation_id: None,
                },
            ]
        "#]],
        );
    }

    #[test]
    fn snippet_rendering_multiple_tabstops_in_text_edit() {
        let mut edit = TextEdit::builder();
        edit.insert(0.into(), "abcdefghijkl".to_owned());
        let edit = edit.finish();
        let snippets = SnippetEdit::new(vec![
            Snippet::Tabstop(0.into()),
            Snippet::Tabstop(5.into()),
            Snippet::Tabstop(12.into()),
        ]);

        check_rendered_snippets(
            edit,
            snippets,
            expect![[r#"
            [
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 0,
                        },
                        end: Position {
                            line: 0,
                            character: 0,
                        },
                    },
                    new_text: "$1abcde$2fghijkl$0",
                    insert_text_format: Some(
                        Snippet,
                    ),
                    annotation_id: None,
                },
            ]
        "#]],
        );
    }

    #[test]
    fn snippet_rendering_multiple_placeholders_in_text_edit() {
        let mut edit = TextEdit::builder();
        edit.insert(0.into(), "abcdefghijkl".to_owned());
        let edit = edit.finish();
        let snippets = SnippetEdit::new(vec![
            Snippet::Placeholder(TextRange::new(0.into(), 3.into())),
            Snippet::Placeholder(TextRange::new(5.into(), 7.into())),
            Snippet::Placeholder(TextRange::new(10.into(), 12.into())),
        ]);

        check_rendered_snippets(
            edit,
            snippets,
            expect![[r#"
            [
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 0,
                            character: 0,
                        },
                        end: Position {
                            line: 0,
                            character: 0,
                        },
                    },
                    new_text: "${1:abc}de${2:fg}hij${0:kl}",
                    insert_text_format: Some(
                        Snippet,
                    ),
                    annotation_id: None,
                },
            ]
        "#]],
        );
    }

    #[test]
    fn snippet_rendering_escape_snippet_bits() {
        // only needed for snippet formats
        let mut edit = TextEdit::builder();
        edit.insert(0.into(), r"$ab{}$c\def".to_owned());
        edit.insert(8.into(), r"ghi\jk<-check_insert_here$".to_owned());
        edit.insert(10.into(), r"a\\b\\c{}$".to_owned());
        let edit = edit.finish();
        let snippets = SnippetEdit::new(vec![
            Snippet::Placeholder(TextRange::new(1.into(), 9.into())),
            Snippet::Tabstop(25.into()),
        ]);

        check_rendered_snippets(
            edit,
            snippets,
            expect![[r#"
                [
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 0,
                                character: 0,
                            },
                            end: Position {
                                line: 0,
                                character: 0,
                            },
                        },
                        new_text: "\\$${1:ab{\\}\\$c\\\\d}ef",
                        insert_text_format: Some(
                            Snippet,
                        ),
                        annotation_id: None,
                    },
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 0,
                                character: 8,
                            },
                            end: Position {
                                line: 0,
                                character: 8,
                            },
                        },
                        new_text: "ghi\\\\jk$0<-check_insert_here\\$",
                        insert_text_format: Some(
                            Snippet,
                        ),
                        annotation_id: None,
                    },
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 0,
                                character: 10,
                            },
                            end: Position {
                                line: 0,
                                character: 10,
                            },
                        },
                        new_text: "a\\\\b\\\\c{}$",
                        insert_text_format: None,
                        annotation_id: None,
                    },
                ]
            "#]],
        );
    }

    #[test]
    fn snippet_rendering_tabstop_adjust_offset_deleted() {
        // negative offset from inserting a smaller range
        let mut edit = TextEdit::builder();
        edit.replace(TextRange::new(47.into(), 56.into()), "let".to_owned());
        edit.replace(
            TextRange::new(57.into(), 89.into()),
            "disabled = false;\n    ProcMacro {\n        disabled,\n    }".to_owned(),
        );
        let edit = edit.finish();
        let snippets = SnippetEdit::new(vec![Snippet::Tabstop(51.into())]);

        check_rendered_snippets_in_source(
            r"
fn expander_to_proc_macro() -> ProcMacro {
    ProcMacro {
        disabled: false,
    }
}

struct ProcMacro {
    disabled: bool,
}",
            edit,
            snippets,
            expect![[r#"
                [
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 1,
                                character: 4,
                            },
                            end: Position {
                                line: 1,
                                character: 13,
                            },
                        },
                        new_text: "let",
                        insert_text_format: None,
                        annotation_id: None,
                    },
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 1,
                                character: 14,
                            },
                            end: Position {
                                line: 3,
                                character: 5,
                            },
                        },
                        new_text: "$0disabled = false;\n    ProcMacro {\n        disabled,\n    \\}",
                        insert_text_format: Some(
                            Snippet,
                        ),
                        annotation_id: None,
                    },
                ]
            "#]],
        );
    }

    #[test]
    fn snippet_rendering_tabstop_adjust_offset_added() {
        // positive offset from inserting a larger range
        let mut edit = TextEdit::builder();
        edit.replace(TextRange::new(39.into(), 40.into()), "let".to_owned());
        edit.replace(
            TextRange::new(41.into(), 73.into()),
            "disabled = false;\n    ProcMacro {\n        disabled,\n    }".to_owned(),
        );
        let edit = edit.finish();
        let snippets = SnippetEdit::new(vec![Snippet::Tabstop(43.into())]);

        check_rendered_snippets_in_source(
            r"
fn expander_to_proc_macro() -> P {
    P {
        disabled: false,
    }
}

struct P {
    disabled: bool,
}",
            edit,
            snippets,
            expect![[r#"
                [
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 1,
                                character: 4,
                            },
                            end: Position {
                                line: 1,
                                character: 5,
                            },
                        },
                        new_text: "let",
                        insert_text_format: None,
                        annotation_id: None,
                    },
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 1,
                                character: 6,
                            },
                            end: Position {
                                line: 3,
                                character: 5,
                            },
                        },
                        new_text: "$0disabled = false;\n    ProcMacro {\n        disabled,\n    \\}",
                        insert_text_format: Some(
                            Snippet,
                        ),
                        annotation_id: None,
                    },
                ]
            "#]],
        );
    }

    #[test]
    fn snippet_rendering_placeholder_adjust_offset_deleted() {
        // negative offset from inserting a smaller range
        let mut edit = TextEdit::builder();
        edit.replace(TextRange::new(47.into(), 56.into()), "let".to_owned());
        edit.replace(
            TextRange::new(57.into(), 89.into()),
            "disabled = false;\n    ProcMacro {\n        disabled,\n    }".to_owned(),
        );
        let edit = edit.finish();
        let snippets =
            SnippetEdit::new(vec![Snippet::Placeholder(TextRange::new(51.into(), 59.into()))]);

        check_rendered_snippets_in_source(
            r"
fn expander_to_proc_macro() -> ProcMacro {
    ProcMacro {
        disabled: false,
    }
}

struct ProcMacro {
    disabled: bool,
}",
            edit,
            snippets,
            expect![[r#"
                [
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 1,
                                character: 4,
                            },
                            end: Position {
                                line: 1,
                                character: 13,
                            },
                        },
                        new_text: "let",
                        insert_text_format: None,
                        annotation_id: None,
                    },
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 1,
                                character: 14,
                            },
                            end: Position {
                                line: 3,
                                character: 5,
                            },
                        },
                        new_text: "${0:disabled} = false;\n    ProcMacro {\n        disabled,\n    \\}",
                        insert_text_format: Some(
                            Snippet,
                        ),
                        annotation_id: None,
                    },
                ]
            "#]],
        );
    }

    #[test]
    fn snippet_rendering_placeholder_adjust_offset_added() {
        // positive offset from inserting a larger range
        let mut edit = TextEdit::builder();
        edit.replace(TextRange::new(39.into(), 40.into()), "let".to_owned());
        edit.replace(
            TextRange::new(41.into(), 73.into()),
            "disabled = false;\n    ProcMacro {\n        disabled,\n    }".to_owned(),
        );
        let edit = edit.finish();
        let snippets =
            SnippetEdit::new(vec![Snippet::Placeholder(TextRange::new(43.into(), 51.into()))]);

        check_rendered_snippets_in_source(
            r"
fn expander_to_proc_macro() -> P {
    P {
        disabled: false,
    }
}

struct P {
    disabled: bool,
}",
            edit,
            snippets,
            expect![[r#"
                [
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 1,
                                character: 4,
                            },
                            end: Position {
                                line: 1,
                                character: 5,
                            },
                        },
                        new_text: "let",
                        insert_text_format: None,
                        annotation_id: None,
                    },
                    SnippetTextEdit {
                        range: Range {
                            start: Position {
                                line: 1,
                                character: 6,
                            },
                            end: Position {
                                line: 3,
                                character: 5,
                            },
                        },
                        new_text: "${0:disabled} = false;\n    ProcMacro {\n        disabled,\n    \\}",
                        insert_text_format: Some(
                            Snippet,
                        ),
                        annotation_id: None,
                    },
                ]
            "#]],
        );
    }

    #[test]
    fn snippet_rendering_tabstop_adjust_offset_between_text_edits() {
        // inserting between edits, tabstop should be at (1, 14)
        let mut edit = TextEdit::builder();
        edit.replace(TextRange::new(47.into(), 56.into()), "let".to_owned());
        edit.replace(
            TextRange::new(58.into(), 90.into()),
            "disabled = false;\n    ProcMacro {\n        disabled,\n    }".to_owned(),
        );
        let edit = edit.finish();
        let snippets = SnippetEdit::new(vec![Snippet::Tabstop(51.into())]);

        // add an extra space between `ProcMacro` and `{` to insert the tabstop at
        check_rendered_snippets_in_source(
            r"
fn expander_to_proc_macro() -> ProcMacro {
    ProcMacro  {
        disabled: false,
    }
}

struct ProcMacro {
    disabled: bool,
}",
            edit,
            snippets,
            expect![[r#"
    [
        SnippetTextEdit {
            range: Range {
                start: Position {
                    line: 1,
                    character: 4,
                },
                end: Position {
                    line: 1,
                    character: 13,
                },
            },
            new_text: "let",
            insert_text_format: None,
            annotation_id: None,
        },
        SnippetTextEdit {
            range: Range {
                start: Position {
                    line: 1,
                    character: 14,
                },
                end: Position {
                    line: 1,
                    character: 14,
                },
            },
            new_text: "$0",
            insert_text_format: Some(
                Snippet,
            ),
            annotation_id: None,
        },
        SnippetTextEdit {
            range: Range {
                start: Position {
                    line: 1,
                    character: 15,
                },
                end: Position {
                    line: 3,
                    character: 5,
                },
            },
            new_text: "disabled = false;\n    ProcMacro {\n        disabled,\n    }",
            insert_text_format: None,
            annotation_id: None,
        },
    ]
"#]],
        );
    }

    #[test]
    fn snippet_rendering_tabstop_adjust_offset_after_text_edits() {
        // inserting after edits, tabstop should be before the closing curly of the fn
        let mut edit = TextEdit::builder();
        edit.replace(TextRange::new(47.into(), 56.into()), "let".to_owned());
        edit.replace(
            TextRange::new(57.into(), 89.into()),
            "disabled = false;\n    ProcMacro {\n        disabled,\n    }".to_owned(),
        );
        let edit = edit.finish();
        let snippets = SnippetEdit::new(vec![Snippet::Tabstop(109.into())]);

        check_rendered_snippets_in_source(
            r"
fn expander_to_proc_macro() -> ProcMacro {
    ProcMacro {
        disabled: false,
    }
}

struct ProcMacro {
    disabled: bool,
}",
            edit,
            snippets,
            expect![[r#"
    [
        SnippetTextEdit {
            range: Range {
                start: Position {
                    line: 1,
                    character: 4,
                },
                end: Position {
                    line: 1,
                    character: 13,
                },
            },
            new_text: "let",
            insert_text_format: None,
            annotation_id: None,
        },
        SnippetTextEdit {
            range: Range {
                start: Position {
                    line: 1,
                    character: 14,
                },
                end: Position {
                    line: 3,
                    character: 5,
                },
            },
            new_text: "disabled = false;\n    ProcMacro {\n        disabled,\n    }",
            insert_text_format: None,
            annotation_id: None,
        },
        SnippetTextEdit {
            range: Range {
                start: Position {
                    line: 4,
                    character: 0,
                },
                end: Position {
                    line: 4,
                    character: 0,
                },
            },
            new_text: "$0",
            insert_text_format: Some(
                Snippet,
            ),
            annotation_id: None,
        },
    ]
"#]],
        );
    }

    #[test]
    fn snippet_rendering_handle_dos_line_endings() {
        // unix -> dos conversion should be handled after placing snippets
        let mut edit = TextEdit::builder();
        edit.insert(6.into(), "\n\n->".to_owned());

        let edit = edit.finish();
        let snippets = SnippetEdit::new(vec![Snippet::Tabstop(10.into())]);

        check_rendered_snippets_in_source(
            "yeah\r\n<-tabstop here",
            edit,
            snippets,
            expect![[r#"
            [
                SnippetTextEdit {
                    range: Range {
                        start: Position {
                            line: 1,
                            character: 0,
                        },
                        end: Position {
                            line: 1,
                            character: 0,
                        },
                    },
                    new_text: "\r\n\r\n->$0",
                    insert_text_format: Some(
                        Snippet,
                    ),
                    annotation_id: None,
                },
            ]
        "#]],
        )
    }

    // `Uri` is not able to parse windows paths on unix machines.
    #[test]
    #[cfg(target_os = "windows")]
    fn test_lowercase_drive_letter() {
        use paths::Utf8Path;

        let url = url_from_abs_path(Utf8Path::new("C:\\Test").try_into().unwrap());
        assert_eq!(url.to_string(), "file:///c:/Test");

        let url = url_from_abs_path(Utf8Path::new(r#"\\localhost\C$\my_dir"#).try_into().unwrap());
        assert_eq!(url.to_string(), "file://localhost/C$/my_dir");
    }
}
