omena-lsp-server 0.3.0

Rust LSP server boundary scaffold for Omena CSS Modules
Documentation
use crate::{LspStyleHoverCandidate, protocol::file_label_from_uri};
use omena_query::{
    AbstractPropertyValueNarrowingV0, AbstractPropertyValueV0, OmenaQueryStyleHoverRenderPartsV0,
    is_omena_query_sass_symbol_candidate_kind, is_omena_query_sass_symbol_declaration_kind,
    omena_query_sass_symbol_kind_from_candidate_kind,
};

pub(crate) fn render_style_hover_candidate_markdown_from_parts(
    document_uri: &str,
    candidate: &LspStyleHoverCandidate,
    render_parts: &OmenaQueryStyleHoverRenderPartsV0,
) -> String {
    let location = format!(
        "{}:{}",
        file_label_from_uri(document_uri),
        candidate.range.start.line + 1
    );
    match candidate.kind {
        "selector" => {
            let narrowing_markdown =
                render_property_value_narrowings_markdown(&render_parts.property_value_narrowings);
            format!(
                "**`.{}`** - _{}_\n\n```scss\n{}\n```{}",
                candidate.name, location, render_parts.snippet, narrowing_markdown
            )
        }
        "customPropertyReference" => {
            format!(
                "**`var({})`** - _{}_\n\n```scss\n{}\n```",
                candidate.name, location, render_parts.snippet
            )
        }
        "customPropertyDeclaration" => {
            format!(
                "**`{}`** - _{}_\n\n```scss\n{}\n```",
                candidate.name, location, render_parts.snippet
            )
        }
        kind if is_omena_query_sass_symbol_candidate_kind(kind) => {
            render_sass_symbol_hover_markdown(candidate, location.as_str(), render_parts)
        }
        _ => candidate.name.clone(),
    }
}

fn render_property_value_narrowings_markdown(
    narrowings: &[AbstractPropertyValueNarrowingV0],
) -> String {
    if narrowings.is_empty() {
        return String::new();
    }
    let lines = narrowings
        .iter()
        .take(6)
        .map(|narrowing| {
            format!(
                "- `{}`: {}{}",
                narrowing.property_name,
                render_property_value_narrowing_value(narrowing),
                render_property_value_narrowing_context(narrowing)
            )
        })
        .collect::<Vec<_>>()
        .join("\n");
    format!("\n\nCascade narrowed values:\n{lines}")
}

fn render_property_value_narrowing_value(narrowing: &AbstractPropertyValueNarrowingV0) -> String {
    if let Some(display_value) = narrowing.display_value.as_deref() {
        return format!("`{display_value}`");
    }
    if !narrowing.display_values.is_empty() {
        return narrowing
            .display_values
            .iter()
            .map(|value| format!("`{value}`"))
            .collect::<Vec<_>>()
            .join(" | ");
    }
    render_abstract_property_value(&narrowing.value)
}

fn render_abstract_property_value(value: &AbstractPropertyValueV0) -> String {
    match value {
        AbstractPropertyValueV0::Bottom { .. } => "`<bottom>`".to_string(),
        AbstractPropertyValueV0::Exact { value, .. } => format!("`{value}`"),
        AbstractPropertyValueV0::FiniteSet { values, .. } => values
            .iter()
            .map(|value| format!("`{value}`"))
            .collect::<Vec<_>>()
            .join(" | "),
        AbstractPropertyValueV0::CustomPropertyReference {
            custom_property_name,
            ..
        } => {
            format!("`var({custom_property_name})`")
        }
        AbstractPropertyValueV0::Top { .. } => "`<top>`".to_string(),
    }
}

fn render_property_value_narrowing_context(narrowing: &AbstractPropertyValueNarrowingV0) -> String {
    let mut context = Vec::new();
    if !narrowing.requested_condition_context.is_empty() {
        context.push(narrowing.requested_condition_context.join(" / "));
    }
    if let Some(layer_name) = narrowing.requested_layer_name.as_deref() {
        context.push(format!("@layer {layer_name}"));
    } else if narrowing.requested_layer_scope == "exactLayer" {
        context.push("unlayered".to_string());
    }
    if context.is_empty() {
        String::new()
    } else {
        format!(" ({})", context.join(", "))
    }
}

fn render_sass_symbol_hover_markdown(
    candidate: &LspStyleHoverCandidate,
    location: &str,
    render_parts: &OmenaQueryStyleHoverRenderPartsV0,
) -> String {
    let label = render_sass_symbol_label(candidate);
    match omena_query_sass_symbol_kind_from_candidate_kind(candidate.kind) {
        Some("variable") if is_omena_query_sass_symbol_declaration_kind(candidate.kind) => {
            if let Some(value) = render_parts.value.as_deref() {
                return format!(
                    "**`{}`** - _{}_\n\nValue: `{}`\n\n```scss\n{}\n```",
                    label, location, value, render_parts.snippet
                );
            }
            format!(
                "**`{}`** - _{}_\n\n```scss\n{}\n```",
                label, location, render_parts.snippet
            )
        }
        Some("mixin" | "function")
            if is_omena_query_sass_symbol_declaration_kind(candidate.kind) =>
        {
            let rendered_label = render_parts.signature.as_deref().unwrap_or(label.as_str());
            format!(
                "**`{}`** - _{}_\n\n```scss\n{}\n```",
                rendered_label, location, render_parts.snippet
            )
        }
        _ => {
            format!(
                "**`{}`** - _{}_\n\n```scss\n{}\n```",
                label, location, render_parts.snippet
            )
        }
    }
}

fn render_sass_symbol_label(candidate: &LspStyleHoverCandidate) -> String {
    let namespace_prefix = candidate
        .namespace
        .as_deref()
        .map(|namespace| format!("{namespace}."))
        .unwrap_or_default();
    match omena_query_sass_symbol_kind_from_candidate_kind(candidate.kind) {
        Some("variable") => format!("{namespace_prefix}${}", candidate.name),
        Some("mixin") if is_omena_query_sass_symbol_declaration_kind(candidate.kind) => {
            format!("@mixin {}", candidate.name)
        }
        Some("mixin") => format!("@include {namespace_prefix}{}", candidate.name),
        Some("function") => format!("{namespace_prefix}{}()", candidate.name),
        _ => candidate.name.clone(),
    }
}