omena-lsp-server 0.3.0

Rust LSP server boundary scaffold for Omena CSS Modules
Documentation
use serde_json::Value;

use omena_query::invalidate_omena_resolver_style_identity_cache;

use crate::{
    LspShellState, LspTextDocumentState, LspWatchedFileChangeState,
    StyleExternalDependencySnapshot, admit_foreign_style_dependencies_for_style_uri,
    byte_offset_for_parser_position, index_workspace_style_files, insert_workspace_folder,
    invalidate_file_uri_identity_cache, is_resolution_config_document_uri, is_style_document_uri,
    lsp_range_from_value, lsp_text_document_state, refresh_document_reusable_indexes,
    refresh_external_sifs_for_state, refresh_source_indexes_for_resolution_config_change,
    refresh_source_indexes_for_style_document_change,
    refresh_source_type_fact_candidates_for_document,
    refresh_style_external_inputs_after_document_removal,
    refresh_style_external_inputs_for_document_event, refresh_workspace_resolution_inputs,
    reload_indexed_source_document_from_disk, reload_indexed_style_document_from_disk,
    resolution_inputs_for_workspace_uri, resolve_workspace_folder_uri,
    style_external_dependency_snapshot,
};

pub(crate) fn did_open_text_document(state: &mut LspShellState, params: Option<&Value>) {
    let Some(document) = params.and_then(|value| value.get("textDocument")) else {
        return;
    };
    let Some(uri) = document.get("uri").and_then(Value::as_str) else {
        return;
    };

    state.insert_open_document_uri(uri);
    let workspace_folder_uri = resolve_workspace_folder_uri(state, uri);
    let resolution_inputs =
        resolution_inputs_for_workspace_uri(state, workspace_folder_uri.as_deref());
    state.insert_document(
        uri,
        lsp_text_document_state(
            uri.to_string(),
            workspace_folder_uri,
            document
                .get("languageId")
                .and_then(Value::as_str)
                .unwrap_or("unknown")
                .to_string(),
            document.get("version").and_then(Value::as_i64).unwrap_or(0),
            document
                .get("text")
                .and_then(Value::as_str)
                .unwrap_or_default()
                .to_string(),
            &resolution_inputs,
        ),
    );
    if is_style_document_uri(uri) {
        #[cfg(feature = "salsa-style-diagnostics")]
        state.mark_style_workspace_snapshot_changed();
        let started = std::time::Instant::now();
        refresh_style_external_inputs_for_document_event(state, uri, None);
        let external_ms = started.elapsed().as_millis();
        let started = std::time::Instant::now();
        refresh_source_indexes_for_style_document_change(state, uri);
        crate::loop_trace!(
            "did-open style uri={} external_ms={} source_index_ms={}",
            uri,
            external_ms,
            started.elapsed().as_millis()
        );
    } else {
        let started = std::time::Instant::now();
        refresh_source_type_fact_candidates_for_document(state, uri);
        crate::loop_trace!(
            "did-open source uri={} type_fact_ms={}",
            uri,
            started.elapsed().as_millis()
        );
    }
}

pub(crate) fn did_change_text_document(state: &mut LspShellState, params: Option<&Value>) {
    let Some(text_document) = params.and_then(|value| value.get("textDocument")) else {
        return;
    };
    let Some(uri) = text_document.get("uri").and_then(Value::as_str) else {
        return;
    };
    let resolution_inputs = state
        .document(uri)
        .map(|document| {
            resolution_inputs_for_workspace_uri(state, document.workspace_folder_uri.as_deref())
        })
        .unwrap_or_else(|| resolution_inputs_for_workspace_uri(state, None));
    let previous_external_inputs = if is_style_document_uri(uri) {
        style_external_dependency_snapshot(state, uri)
    } else {
        StyleExternalDependencySnapshot::default()
    };
    let Some(existing) = state.document_mut(uri) else {
        return;
    };

    if let Some(version) = text_document.get("version").and_then(Value::as_i64) {
        existing.version = version;
    }
    let Some(changes) = params
        .and_then(|value| value.get("contentChanges"))
        .and_then(Value::as_array)
    else {
        return;
    };

    let mut text_changed = false;
    for change in changes {
        if apply_text_document_content_change(existing, change) {
            text_changed = true;
        }
    }
    if text_changed {
        refresh_document_reusable_indexes(existing, &resolution_inputs);
    }
    if text_changed {
        if is_style_document_uri(uri) {
            #[cfg(feature = "salsa-style-diagnostics")]
            state.mark_style_workspace_snapshot_changed();
            refresh_style_external_inputs_for_document_event(
                state,
                uri,
                Some(previous_external_inputs),
            );
            refresh_source_indexes_for_style_document_change(state, uri);
        } else {
            refresh_source_type_fact_candidates_for_document(state, uri);
        }
    }
}

fn apply_text_document_content_change(document: &mut LspTextDocumentState, change: &Value) -> bool {
    let Some(next_text) = change.get("text").and_then(Value::as_str) else {
        return false;
    };
    let Some(range) = change.get("range").and_then(lsp_range_from_value) else {
        document.text = next_text.to_string();
        return true;
    };
    let Some(start_offset) = byte_offset_for_parser_position(document.text.as_str(), range.start)
    else {
        return false;
    };
    let Some(end_offset) = byte_offset_for_parser_position(document.text.as_str(), range.end)
    else {
        return false;
    };
    if start_offset > end_offset {
        return false;
    }
    document
        .text
        .replace_range(start_offset..end_offset, next_text);
    true
}

pub(crate) fn did_close_text_document(state: &mut LspShellState, params: Option<&Value>) {
    let Some(uri) = params
        .and_then(|value| value.get("textDocument"))
        .and_then(|value| value.get("uri"))
        .and_then(Value::as_str)
    else {
        return;
    };
    invalidate_file_uri_identity_cache();
    invalidate_omena_resolver_style_identity_cache();
    #[cfg(feature = "parallel-style-diagnostics")]
    {
        *state.resolver_identity_index_memo_lock() = None;
    }
    state.remove_open_document_uri(uri);
    let previous_external_inputs = if is_style_document_uri(uri) {
        style_external_dependency_snapshot(state, uri)
    } else {
        StyleExternalDependencySnapshot::default()
    };
    if is_style_document_uri(uri) && reload_indexed_style_document_from_disk(state, uri) {
        refresh_style_external_inputs_for_document_event(
            state,
            uri,
            Some(previous_external_inputs),
        );
        refresh_source_indexes_for_style_document_change(state, uri);
        return;
    }
    state.remove_document_uri(uri);
    if is_style_document_uri(uri) {
        refresh_style_external_inputs_after_document_removal(state, previous_external_inputs);
        refresh_source_indexes_for_style_document_change(state, uri);
    }
}

pub(crate) fn did_change_workspace_folders(
    state: &mut LspShellState,
    params: Option<&Value>,
    index_added_workspace_folders: bool,
) -> bool {
    let event = params.and_then(|value| value.get("event"));
    let mut removed_workspace_uris = Vec::new();
    if let Some(removed) = event
        .and_then(|value| value.get("removed"))
        .and_then(Value::as_array)
    {
        for folder in removed {
            if let Some(uri) = folder.get("uri").and_then(Value::as_str) {
                state.workspace_runtime_registry.remove(uri);
                removed_workspace_uris.push(uri.to_string());
            }
        }
    }
    let mut added_workspace_folder = false;
    if let Some(added) = event
        .and_then(|value| value.get("added"))
        .and_then(Value::as_array)
    {
        for folder in added {
            insert_workspace_folder(state, folder);
            added_workspace_folder = true;
        }
    }
    reconcile_documents_after_workspace_folder_changes(state, removed_workspace_uris.as_slice());
    refresh_workspace_resolution_inputs(state);
    refresh_external_sifs_for_state(state);
    if added_workspace_folder && index_added_workspace_folders {
        index_workspace_style_files(state);
    }
    added_workspace_folder
}

fn reconcile_documents_after_workspace_folder_changes(
    state: &mut LspShellState,
    removed_workspace_uris: &[String],
) {
    remove_unowned_indexed_documents_for_removed_workspaces(state, removed_workspace_uris);
    crate::refresh_document_workspace_owners(state);
}

fn remove_unowned_indexed_documents_for_removed_workspaces(
    state: &mut LspShellState,
    removed_workspace_uris: &[String],
) {
    if removed_workspace_uris.is_empty() {
        return;
    }
    let open_document_uris = state.open_document_uris.clone();
    let workspace_runtime_registry = state.workspace_runtime_registry.clone();
    let file_identity = state.file_identity.clone();
    state.documents.retain(|file_id, document| {
        if open_document_uris.contains(file_id) {
            return true;
        }
        let uri = file_identity
            .storage_uri_for_file_id(*file_id)
            .unwrap_or(document.uri.as_str());
        let owned_by_removed_workspace =
            document
                .workspace_folder_uri
                .as_deref()
                .is_some_and(|workspace_uri| {
                    removed_workspace_uris
                        .iter()
                        .any(|removed_uri| removed_uri == workspace_uri)
                });
        !owned_by_removed_workspace || workspace_runtime_registry.resolve_owner_uri(uri).is_some()
    });
}

pub(crate) fn did_change_watched_files(state: &mut LspShellState, params: Option<&Value>) {
    let Some(changes) = params
        .and_then(|value| value.get("changes"))
        .and_then(Value::as_array)
    else {
        return;
    };
    invalidate_file_uri_identity_cache();
    invalidate_omena_resolver_style_identity_cache();
    #[cfg(feature = "parallel-style-diagnostics")]
    {
        *state.resolver_identity_index_memo_lock() = None;
    }
    for change in changes {
        let Some(uri) = change.get("uri").and_then(Value::as_str) else {
            continue;
        };
        let change_type = change.get("type").and_then(Value::as_u64).unwrap_or(0);
        state.watched_file_changes.push(LspWatchedFileChangeState {
            uri: uri.to_string(),
            change_type,
        });
        apply_watched_file_change_to_index(state, uri, change_type);
    }
}

fn apply_watched_file_change_to_index(state: &mut LspShellState, uri: &str, change_type: u64) {
    if !is_style_document_uri(uri) {
        if is_resolution_config_document_uri(uri) {
            refresh_source_indexes_for_resolution_config_change(state, uri);
            return;
        }
        if state.has_open_document_uri(uri) {
            return;
        }
        if change_type == 3 {
            state.remove_document_uri(uri);
            return;
        }
        let _ = reload_indexed_source_document_from_disk(state, uri);
        return;
    }
    if state.has_open_document_uri(uri) {
        return;
    }
    if change_type == 3 {
        state.remove_document_uri(uri);
        refresh_source_indexes_for_style_document_change(state, uri);
        return;
    }

    if reload_indexed_style_document_from_disk(state, uri) {
        admit_foreign_style_dependencies_for_style_uri(state, uri);
        refresh_external_sifs_for_state(state);
        refresh_source_indexes_for_style_document_change(state, uri);
    }
}