omena-lsp-server 0.3.0

Rust LSP server boundary scaffold for Omena CSS Modules
Documentation
use std::{collections::BTreeSet, fs, path::Path};

use omena_query::load_omena_query_workspace_style_resolution_inputs;
use omena_sif::compute_omena_sif_leaf_hash_v1;
use serde_json::{Value, json};

use crate::{
    external_sif_loader::refresh_external_sifs_for_state,
    protocol::{file_uri_to_path, normalize_path},
    state::{LspQueryReadView, LspShellState},
};

pub(crate) fn initialize_workspace_folders(state: &mut LspShellState, params: Option<&Value>) {
    state.workspace_runtime_registry.clear();
    state.client_supports_work_done_progress = params
        .and_then(|value| value.pointer("/capabilities/window/workDoneProgress"))
        .and_then(Value::as_bool)
        .unwrap_or(false);
    if let Some(folders) = params
        .and_then(|value| value.get("workspaceFolders"))
        .and_then(Value::as_array)
    {
        for folder in folders {
            insert_workspace_folder(state, folder);
        }
        refresh_workspace_resolution_inputs(state);
        refresh_external_sifs_for_state(state);
        return;
    }

    if let Some(root_uri) = params
        .and_then(|value| value.get("rootUri"))
        .and_then(Value::as_str)
    {
        state
            .workspace_runtime_registry
            .insert(root_uri.to_string(), root_uri.to_string());
    }
    refresh_workspace_resolution_inputs(state);
    refresh_external_sifs_for_state(state);
}

pub(crate) fn refresh_workspace_resolution_inputs(state: &mut LspShellState) {
    let configured_package_manifests = state.resolution.package_manifests.clone();
    let workspace_uris = state
        .workspace_runtime_registry
        .folder_snapshots()
        .into_iter()
        .map(|folder| folder.uri)
        .collect::<BTreeSet<_>>();
    state
        .resolution
        .workspace_style_resolution_inputs
        .retain(|workspace_uri, _| workspace_uris.contains(workspace_uri));
    for workspace_uri in workspace_uris {
        let inputs = load_lsp_workspace_style_resolution_inputs(
            Some(workspace_uri.as_str()),
            configured_package_manifests.as_slice(),
        );
        state
            .resolution
            .workspace_style_resolution_inputs
            .insert(workspace_uri, inputs);
    }
}

pub(crate) fn refresh_workspace_resolution_inputs_for_uri(state: &mut LspShellState, uri: &str) {
    let Some(workspace_uri) = resolve_workspace_folder_uri(state, uri) else {
        return;
    };
    let inputs = load_lsp_workspace_style_resolution_inputs(
        Some(workspace_uri.as_str()),
        state.resolution.package_manifests.as_slice(),
    );
    state
        .resolution
        .workspace_style_resolution_inputs
        .insert(workspace_uri, inputs);
}

pub(crate) fn load_lsp_workspace_style_resolution_inputs(
    workspace_folder_uri: Option<&str>,
    configured_package_manifests: &[omena_query::OmenaQueryStylePackageManifestV0],
) -> omena_query::OmenaQueryStyleResolutionInputsV0 {
    let mut inputs = load_omena_query_workspace_style_resolution_inputs(
        workspace_folder_uri,
        configured_package_manifests,
    );
    inputs.external_sif_cache_fingerprint =
        workspace_folder_uri.and_then(external_sif_cache_fingerprint_for_workspace_uri);
    inputs
}

fn external_sif_cache_fingerprint_for_workspace_uri(workspace_folder_uri: &str) -> Option<String> {
    const METADATA_SCAN_LIMIT: usize = 2048;
    let root = file_uri_to_path(workspace_folder_uri)?;
    let root = normalize_path(root);
    let mut identities = Vec::new();
    for relative in [
        "omena.lock",
        "pnpm-lock.yaml",
        "package-lock.json",
        "yarn.lock",
        "bun.lock",
        "bun.lockb",
        "node_modules/.modules.yaml",
    ] {
        push_file_identity(&mut identities, root.join(relative).as_path());
    }
    collect_node_modules_package_link_identities(
        root.join("node_modules").as_path(),
        &mut identities,
        METADATA_SCAN_LIMIT,
    );
    if identities.is_empty() {
        return None;
    }
    let value = json!({
        "schemaVersion": "0",
        "product": "omena-lsp.external-sif-cache-freshness",
        "workspaceRoot": root.to_string_lossy(),
        "identities": identities,
    });
    let bytes = serde_json::to_vec(&value).ok()?;
    Some(
        compute_omena_sif_leaf_hash_v1(bytes.as_slice())
            .as_str()
            .to_string(),
    )
}

fn push_file_identity(output: &mut Vec<String>, path: &Path) {
    let Ok(metadata) = fs::symlink_metadata(path) else {
        return;
    };
    let modified = metadata
        .modified()
        .ok()
        .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|duration| format!("{}.{:09}", duration.as_secs(), duration.subsec_nanos()))
        .unwrap_or_else(|| "unknownMtime".to_string());
    let file_type = if metadata.file_type().is_symlink() {
        "symlink"
    } else if metadata.is_dir() {
        "dir"
    } else if metadata.is_file() {
        "file"
    } else {
        "other"
    };
    let target = if metadata.file_type().is_symlink() {
        fs::read_link(path)
            .ok()
            .map(|target| target.to_string_lossy().to_string())
            .unwrap_or_else(|| "unknownTarget".to_string())
    } else {
        String::new()
    };
    output.push(format!(
        "{}|{file_type}|len{}|mtime{modified}|target{target}",
        normalize_path(path.to_path_buf()).to_string_lossy(),
        metadata.len()
    ));
}

fn collect_node_modules_package_link_identities(
    node_modules: &Path,
    output: &mut Vec<String>,
    limit: usize,
) {
    let Ok(entries) = fs::read_dir(node_modules) else {
        return;
    };
    let mut seen = 0usize;
    for entry in entries.flatten() {
        if seen >= limit {
            break;
        }
        let path = entry.path();
        let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
            continue;
        };
        if name.starts_with('.') {
            continue;
        }
        if name.starts_with('@') && path.is_dir() {
            let Ok(scoped_entries) = fs::read_dir(path.as_path()) else {
                continue;
            };
            for scoped_entry in scoped_entries.flatten() {
                if seen >= limit {
                    break;
                }
                push_file_identity(output, scoped_entry.path().as_path());
                seen = seen.saturating_add(1);
            }
            continue;
        }
        push_file_identity(output, path.as_path());
        seen = seen.saturating_add(1);
    }
}

pub(crate) fn resolution_inputs_for_workspace_uri(
    state: &dyn LspQueryReadView,
    workspace_folder_uri: Option<&str>,
) -> omena_query::OmenaQueryStyleResolutionInputsV0 {
    workspace_folder_uri
        .and_then(|workspace_uri| {
            state
                .query_resolution()
                .workspace_style_resolution_inputs
                .get(workspace_uri)
        })
        .cloned()
        .unwrap_or_else(|| {
            load_lsp_workspace_style_resolution_inputs(
                workspace_folder_uri,
                state.query_resolution().package_manifests.as_slice(),
            )
        })
}

pub(crate) fn insert_workspace_folder(state: &mut LspShellState, folder: &Value) {
    let Some(uri) = folder.get("uri").and_then(Value::as_str) else {
        return;
    };
    state.workspace_runtime_registry.insert(
        uri.to_string(),
        folder
            .get("name")
            .and_then(Value::as_str)
            .unwrap_or(uri)
            .to_string(),
    );
}

pub(crate) fn refresh_document_workspace_owners(state: &mut LspShellState) {
    let workspace_runtime_registry = state.workspace_runtime_registry.clone();
    for document in state.documents.values_mut() {
        let document = std::sync::Arc::make_mut(document);
        document.workspace_folder_uri =
            workspace_runtime_registry.resolve_owner_uri(document.uri.as_str());
    }
}

pub(crate) fn resolve_workspace_folder_uri(
    state: &dyn LspQueryReadView,
    document_uri: &str,
) -> Option<String> {
    state
        .query_workspace_runtime_registry()
        .resolve_owner_uri(document_uri)
}