omena-lsp-server 0.2.0

Rust LSP server boundary scaffold for Omena CSS Modules
Documentation
use crate::{
    LspStyleDocumentSummary, LspStyleHoverCandidate, LspTextDocumentState,
    build_source_syntax_index, collect_style_hover_candidates,
    protocol::{
        byte_offset_for_parser_position, is_style_document_uri, parser_range_for_byte_span,
    },
    source_selector_candidates_from_index, summarize_style_document,
};
use omena_query::OmenaQueryStyleResolutionInputsV0;
use omena_query::ParserByteSpanV0;
use serde::Serialize;

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RustQueryReuseBoundaryV0 {
    pub product: &'static str,
    pub owner: &'static str,
    pub reuse_model: &'static str,
    pub cached_surfaces: Vec<&'static str>,
    pub invalidation_policy: Vec<&'static str>,
    pub request_path_policy: Vec<&'static str>,
}

pub fn rust_query_reuse_contract() -> RustQueryReuseBoundaryV0 {
    RustQueryReuseBoundaryV0 {
        product: "omena-lsp-server.query-reuse",
        owner: "omena-lsp-server/documentQueryReuse",
        reuse_model: "documentRevisionOwnedReusableIndexes",
        cached_surfaces: vec![
            "workspaceStyleResolutionInputs",
            "styleDocumentSummary",
            "styleHoverCandidates",
            "sourceSyntaxIndex",
            "sourceSelectorCandidates",
        ],
        invalidation_policy: vec![
            "refreshOnDocumentOpen",
            "refreshOnDocumentContentChange",
            "refreshOnWorkspaceFileReload",
            "refreshOnResolutionConfigChange",
            "refreshOnResolutionSettingsChange",
        ],
        request_path_policy: vec![
            "noPackageManifestOrConfigReadOnProviderRequest",
            "noRawSourceRescanOnProviderRequest",
            "noStyleSelectorRescanOnProviderRequest",
            "providerRequestsConsumeDocumentIndexes",
        ],
    }
}

pub(crate) fn refresh_document_reusable_indexes(
    document: &mut LspTextDocumentState,
    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
) {
    if is_style_document_uri(document.uri.as_str()) {
        document.style_summary =
            summarize_style_document(document.uri.as_str(), Some(document.text.as_str()));
        document.style_candidates =
            collect_style_hover_candidates(document.uri.as_str(), document.text.as_str())
                .map(|(_, candidates)| candidates)
                .unwrap_or_default();
    } else if let Some((summary, candidates)) = collect_vue_embedded_module_style_indexes(document)
    {
        document.style_summary = Some(summary);
        document.style_candidates = candidates;
    } else {
        document.style_summary = None;
        document.style_candidates = Vec::new();
    }
    let source_syntax_index = build_source_syntax_index(document, resolution_inputs);
    document.source_selector_candidates =
        source_selector_candidates_from_index(document, &source_syntax_index);
    document.source_syntax_index = source_syntax_index;
}

fn collect_vue_embedded_module_style_indexes(
    document: &LspTextDocumentState,
) -> Option<(LspStyleDocumentSummary, Vec<LspStyleHoverCandidate>)> {
    let embedded = embedded_vue_module_style(document)?;
    let summary =
        summarize_style_document(embedded.virtual_uri.as_str(), Some(embedded.style_source))?;
    let (_, candidates) =
        collect_style_hover_candidates(embedded.virtual_uri.as_str(), embedded.style_source)?;
    let candidates = candidates
        .into_iter()
        .filter_map(|mut candidate| {
            candidate.range = embedded_range_to_document_range(
                document.text.as_str(),
                embedded.style_source,
                embedded.content_start,
                candidate.range,
            )?;
            Some(candidate)
        })
        .collect();
    Some((summary, candidates))
}

struct EmbeddedVueModuleStyle<'a> {
    virtual_uri: String,
    style_source: &'a str,
    content_start: usize,
}

fn embedded_vue_module_style(
    document: &LspTextDocumentState,
) -> Option<EmbeddedVueModuleStyle<'_>> {
    if document.language_id != "vue" && !document.uri.ends_with(".vue") {
        return None;
    }

    let lower = document.text.to_ascii_lowercase();
    let mut cursor = 0usize;
    while let Some(relative_start) = lower[cursor..].find("<style") {
        let tag_start = cursor + relative_start;
        let relative_tag_end = lower[tag_start..].find('>')?;
        let tag_end = tag_start + relative_tag_end + 1;
        let tag = &lower[tag_start..tag_end];
        let close_start = lower[tag_end..].find("</style>")? + tag_end;
        let content_start = tag_end;
        let content_end = close_start;
        if tag.contains("module") {
            return Some(EmbeddedVueModuleStyle {
                virtual_uri: format!(
                    "{}{}",
                    document.uri,
                    vue_embedded_style_virtual_extension(tag)
                ),
                style_source: &document.text[content_start..content_end],
                content_start,
            });
        }
        cursor = close_start + "</style>".len();
    }
    None
}

fn vue_embedded_style_virtual_extension(tag: &str) -> &'static str {
    if tag.contains("lang=\"scss\"")
        || tag.contains("lang='scss'")
        || tag.contains("lang=scss")
        || tag.contains("lang=\"sass\"")
        || tag.contains("lang='sass'")
        || tag.contains("lang=sass")
    {
        ".module.scss"
    } else if tag.contains("lang=\"less\"")
        || tag.contains("lang='less'")
        || tag.contains("lang=less")
    {
        ".module.less"
    } else {
        ".module.css"
    }
}

fn embedded_range_to_document_range(
    document_source: &str,
    embedded_source: &str,
    content_start: usize,
    range: omena_query::ParserRangeV0,
) -> Option<omena_query::ParserRangeV0> {
    let start = content_start + byte_offset_for_parser_position(embedded_source, range.start)?;
    let end = content_start + byte_offset_for_parser_position(embedded_source, range.end)?;
    Some(parser_range_for_byte_span(
        document_source,
        ParserByteSpanV0 { start, end },
    ))
}