omena-lsp-server 0.5.0

Rust LSP server boundary scaffold for Omena CSS Modules
Documentation
use omena_query::{
    OmenaQuerySourceSelectorOccurrenceV0, OmenaWorkspaceOccurrenceFamilyV0,
    OmenaWorkspaceOccurrenceKindV0, OmenaWorkspaceOccurrenceRoleV0,
    OmenaWorkspaceOccurrenceSurfaceV0, OmenaWorkspaceOccurrenceV0,
};

use crate::{
    file_uri_equivalent,
    state::{
        LspDocumentOrigin, LspStyleHoverCandidate, LspStyleSymbolOccurrenceV0, LspTextDocumentState,
    },
};

pub(super) fn workspace_occurrence_from_source_selector_occurrence_for_lsp(
    occurrence: &OmenaQuerySourceSelectorOccurrenceV0,
) -> OmenaWorkspaceOccurrenceV0 {
    OmenaWorkspaceOccurrenceV0 {
        moniker: occurrence.moniker.clone(),
        uri: occurrence.uri.clone(),
        name: occurrence.selector_name.clone(),
        range: occurrence.range,
        kind: occurrence.kind,
        role: occurrence.role,
        surface: occurrence.source,
        family: Some(OmenaWorkspaceOccurrenceFamilyV0::CssModuleSelector),
        namespace: None,
        target_style_uri: occurrence.target_style_uri.clone(),
        rename_target: occurrence.rename_target,
    }
}

pub(super) fn source_selector_occurrence_from_workspace_occurrence_for_lsp(
    occurrence: OmenaWorkspaceOccurrenceV0,
) -> Option<OmenaQuerySourceSelectorOccurrenceV0> {
    (occurrence.family == Some(OmenaWorkspaceOccurrenceFamilyV0::CssModuleSelector)).then_some(
        OmenaQuerySourceSelectorOccurrenceV0 {
            moniker: occurrence.moniker,
            uri: occurrence.uri,
            selector_name: occurrence.name,
            range: occurrence.range,
            kind: occurrence.kind,
            role: occurrence.role,
            source: occurrence.surface,
            target_style_uri: occurrence.target_style_uri,
            rename_target: occurrence.rename_target,
        },
    )
}

pub(super) fn workspace_occurrence_kind_from_source_reference_kind_for_lsp(
    kind: &str,
) -> OmenaWorkspaceOccurrenceKindV0 {
    match kind {
        "sourceSelectorPrefixReference" => {
            OmenaWorkspaceOccurrenceKindV0::SourceSelectorPrefixReference
        }
        _ => OmenaWorkspaceOccurrenceKindV0::SourceSelectorReference,
    }
}

pub(super) fn workspace_occurrence_matches_target_style(
    occurrence: &OmenaWorkspaceOccurrenceV0,
    target_style_uri: Option<&str>,
) -> bool {
    target_style_uri.is_none_or(|target_uri| {
        occurrence
            .target_style_uri
            .as_deref()
            .is_none_or(|candidate_target_uri| {
                file_uri_equivalent(candidate_target_uri, target_uri)
            })
    })
}

pub(super) fn workspace_occurrence_from_style_symbol_occurrence(
    document: &LspTextDocumentState,
    occurrence: &LspStyleSymbolOccurrenceV0,
) -> OmenaWorkspaceOccurrenceV0 {
    OmenaWorkspaceOccurrenceV0 {
        moniker: occurrence.moniker.clone(),
        uri: occurrence.uri.clone(),
        name: occurrence.name.clone(),
        range: occurrence.range,
        kind: occurrence.kind,
        role: occurrence.role,
        surface: OmenaWorkspaceOccurrenceSurfaceV0::OmenaLspStyleIndex,
        family: Some(occurrence.family),
        namespace: occurrence.namespace.clone(),
        target_style_uri: None,
        rename_target: document.origin == LspDocumentOrigin::Local,
    }
}

#[allow(
    clippy::expect_used,
    reason = "the producer filter and exhaustive mapping test make this a fail-closed invariant"
)]
pub(super) fn style_symbol_occurrence_for_candidate(
    moniker: String,
    uri: &str,
    candidate: &LspStyleHoverCandidate,
    family: &'static str,
    role: &'static str,
) -> LspStyleSymbolOccurrenceV0 {
    let mut name = String::new();
    let _ = omena_syntax::ident::render_authored(&candidate.name, &mut name);
    let kind = workspace_occurrence_kind_from_style_symbol_kind(candidate.kind)
        .expect("filtered style symbol occurrence kind must be mapped");
    let family = workspace_occurrence_family_from_style_symbol_family(family)
        .expect("filtered style symbol occurrence family must be mapped");
    assert_eq!(
        kind.family(),
        family,
        "style symbol occurrence kind and family must name the same identity domain"
    );
    LspStyleSymbolOccurrenceV0 {
        moniker,
        uri: uri.to_string(),
        kind,
        family,
        name,
        range: candidate.range,
        role: workspace_occurrence_role_from_style_symbol_role(role),
        namespace: candidate.namespace.clone(),
    }
}

fn workspace_occurrence_kind_from_style_symbol_kind(
    kind: &str,
) -> Option<OmenaWorkspaceOccurrenceKindV0> {
    match kind {
        "customPropertyDeclaration" => {
            Some(OmenaWorkspaceOccurrenceKindV0::CustomPropertyDeclaration)
        }
        "customPropertyReference" => Some(OmenaWorkspaceOccurrenceKindV0::CustomPropertyReference),
        "sassVariableDeclaration" => Some(OmenaWorkspaceOccurrenceKindV0::SassVariableDeclaration),
        "sassVariableReference" => Some(OmenaWorkspaceOccurrenceKindV0::SassVariableReference),
        "sassMixinDeclaration" => Some(OmenaWorkspaceOccurrenceKindV0::SassMixinDeclaration),
        "sassMixinInclude" => Some(OmenaWorkspaceOccurrenceKindV0::SassMixinInclude),
        "sassMixinReference" => Some(OmenaWorkspaceOccurrenceKindV0::SassMixinReference),
        "sassFunctionDeclaration" => Some(OmenaWorkspaceOccurrenceKindV0::SassFunctionDeclaration),
        "sassFunctionCall" => Some(OmenaWorkspaceOccurrenceKindV0::SassFunctionCall),
        "sassFunctionReference" => Some(OmenaWorkspaceOccurrenceKindV0::SassFunctionReference),
        "sassSymbolDeclaration" => Some(OmenaWorkspaceOccurrenceKindV0::SassSymbolDeclaration),
        "sassSymbolReference" => Some(OmenaWorkspaceOccurrenceKindV0::SassSymbolReference),
        _ => None,
    }
}

fn workspace_occurrence_role_from_style_symbol_role(role: &str) -> OmenaWorkspaceOccurrenceRoleV0 {
    if role == "definition" {
        OmenaWorkspaceOccurrenceRoleV0::Definition
    } else {
        OmenaWorkspaceOccurrenceRoleV0::Reference
    }
}

fn workspace_occurrence_family_from_style_symbol_family(
    family: &str,
) -> Option<OmenaWorkspaceOccurrenceFamilyV0> {
    match family {
        "customProperty" => Some(OmenaWorkspaceOccurrenceFamilyV0::CustomProperty),
        "variable" => Some(OmenaWorkspaceOccurrenceFamilyV0::Variable),
        "mixin" => Some(OmenaWorkspaceOccurrenceFamilyV0::Mixin),
        "function" => Some(OmenaWorkspaceOccurrenceFamilyV0::Function),
        "symbol" => Some(OmenaWorkspaceOccurrenceFamilyV0::Symbol),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use omena_query::{
        OmenaWorkspaceOccurrenceFamilyV0, OmenaWorkspaceOccurrenceSurfaceV0,
        OmenaWorkspaceOccurrenceV0, ParserPositionV0, ParserRangeV0,
        is_omena_query_sass_symbol_candidate_kind,
        omena_query_sass_symbol_kind_from_candidate_kind,
    };
    use omena_syntax::ident::{AuthoredPropertyTextV0, PropertyNameV0};

    use super::{
        style_symbol_occurrence_for_candidate, workspace_occurrence_kind_from_style_symbol_kind,
    };
    use crate::state::{LspStyleHoverCandidate, LspStyleSymbolOccurrenceV0};

    fn custom_property_style_occurrence(name: &str) -> LspStyleSymbolOccurrenceV0 {
        let candidate = LspStyleHoverCandidate {
            kind: "customPropertyDeclaration",
            name: AuthoredPropertyTextV0::new(name),
            selector_key: None,
            property_key: Some(PropertyNameV0::canonical_custom_key(name)),
            range: ParserRangeV0 {
                start: ParserPositionV0 {
                    line: 0,
                    character: 0,
                },
                end: ParserPositionV0 {
                    line: 0,
                    character: 7,
                },
            },
            source: "fixture",
            target_style_uri: None,
            namespace: None,
        };
        style_symbol_occurrence_for_candidate(
            "custom-property:file:///workspace/app.css#--token".to_string(),
            "file:///workspace/app.css",
            &candidate,
            "customProperty",
            "definition",
        )
    }

    fn sass_mixin_reference_style_occurrence(name: &str) -> LspStyleSymbolOccurrenceV0 {
        let candidate = LspStyleHoverCandidate {
            kind: "sassMixinReference",
            name: AuthoredPropertyTextV0::new(name),
            selector_key: None,
            property_key: None,
            range: ParserRangeV0 {
                start: ParserPositionV0 {
                    line: 0,
                    character: 0,
                },
                end: ParserPositionV0 {
                    line: 0,
                    character: 5,
                },
            },
            source: "fixture",
            target_style_uri: None,
            namespace: None,
        };
        style_symbol_occurrence_for_candidate(
            "sass-mixin:file:///workspace/app.scss#mixin".to_string(),
            "file:///workspace/app.scss",
            &candidate,
            "mixin",
            "reference",
        )
    }

    #[test]
    fn style_symbol_occurrence_identity_uses_custom_property_keys_without_changing_wire_name() {
        let escaped = custom_property_style_occurrence(r"--to\6b en");
        let plain = custom_property_style_occurrence("--token");

        assert_eq!(escaped, plain);
        let mut occurrences = vec![escaped.clone(), plain];
        occurrences.sort();
        occurrences.dedup();
        assert_eq!(occurrences.len(), 1);
        let serialized = serde_json::to_value(&escaped).unwrap_or_default();
        assert_eq!(serialized["name"], r"--to\6b en");
    }

    #[test]
    fn sass_mixin_reference_identity_preserves_escape_distinct_authored_names() {
        let escaped = sass_mixin_reference_style_occurrence(r"m\69 xin");
        let plain = sass_mixin_reference_style_occurrence("mixin");
        assert_eq!(escaped.kind.as_str(), "sassMixinReference");
        assert_ne!(escaped, plain);

        let to_workspace_occurrence =
            |occurrence: LspStyleSymbolOccurrenceV0| OmenaWorkspaceOccurrenceV0 {
                moniker: occurrence.moniker,
                uri: occurrence.uri,
                name: occurrence.name,
                range: occurrence.range,
                kind: occurrence.kind,
                role: occurrence.role,
                surface: OmenaWorkspaceOccurrenceSurfaceV0::OmenaLspStyleIndex,
                family: Some(occurrence.family),
                namespace: occurrence.namespace,
                target_style_uri: None,
                rename_target: true,
            };
        let mut occurrences = vec![
            to_workspace_occurrence(escaped),
            to_workspace_occurrence(plain),
        ];
        occurrences.sort();
        occurrences.dedup();
        assert_eq!(occurrences.len(), 2);
    }

    #[test]
    #[allow(
        clippy::expect_used,
        reason = "the assertion below proves every producer-accepted kind is mapped"
    )]
    fn every_accepted_sass_candidate_kind_has_a_lossless_occurrence_mapping() {
        let rows = [
            (
                "sassVariableDeclaration",
                "variable",
                OmenaWorkspaceOccurrenceFamilyV0::Variable,
            ),
            (
                "sassVariableReference",
                "variable",
                OmenaWorkspaceOccurrenceFamilyV0::Variable,
            ),
            (
                "sassMixinDeclaration",
                "mixin",
                OmenaWorkspaceOccurrenceFamilyV0::Mixin,
            ),
            (
                "sassMixinInclude",
                "mixin",
                OmenaWorkspaceOccurrenceFamilyV0::Mixin,
            ),
            (
                "sassMixinReference",
                "mixin",
                OmenaWorkspaceOccurrenceFamilyV0::Mixin,
            ),
            (
                "sassFunctionDeclaration",
                "function",
                OmenaWorkspaceOccurrenceFamilyV0::Function,
            ),
            (
                "sassFunctionCall",
                "function",
                OmenaWorkspaceOccurrenceFamilyV0::Function,
            ),
            (
                "sassFunctionReference",
                "function",
                OmenaWorkspaceOccurrenceFamilyV0::Function,
            ),
            (
                "sassSymbolDeclaration",
                "symbol",
                OmenaWorkspaceOccurrenceFamilyV0::Symbol,
            ),
            (
                "sassSymbolReference",
                "symbol",
                OmenaWorkspaceOccurrenceFamilyV0::Symbol,
            ),
        ];

        assert_eq!(rows.len(), 10);
        for (candidate_kind, expected_query_family, expected_occurrence_family) in rows {
            assert!(is_omena_query_sass_symbol_candidate_kind(candidate_kind));
            assert_eq!(
                omena_query_sass_symbol_kind_from_candidate_kind(candidate_kind),
                Some(expected_query_family)
            );
            let occurrence_kind = workspace_occurrence_kind_from_style_symbol_kind(candidate_kind)
                .expect("every accepted Sass candidate kind must map without a fallback");
            assert_eq!(occurrence_kind.as_str(), candidate_kind);
            assert_eq!(occurrence_kind.family(), expected_occurrence_family);
        }
    }
}