diff --git a/Cargo.lock b/Cargo.lock index 4ded336..e50501e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -249,11 +249,34 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", +] + [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" dependencies = [ "cfg-if", ] @@ -428,6 +451,22 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indenter" version = "0.3.3" @@ -592,6 +631,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "miette" version = "5.10.0" @@ -1058,6 +1106,7 @@ dependencies = [ "anyhow", "clap", "directories", + "ignore", "line-index", "log", "lsp-server", diff --git a/crates/stef-lsp/Cargo.toml b/crates/stef-lsp/Cargo.toml index 451f905..bf28a37 100644 --- a/crates/stef-lsp/Cargo.toml +++ b/crates/stef-lsp/Cargo.toml @@ -13,6 +13,7 @@ license.workspace = true anyhow = "1.0.75" clap.workspace = true directories = "5.0.1" +ignore = "0.4.21" line-index = "0.1.1" log = { version = "0.4.20", features = ["kv_unstable_std", "std"] } lsp-server = "0.7.5" diff --git a/crates/stef-lsp/src/handlers/mod.rs b/crates/stef-lsp/src/handlers/mod.rs index 19bc739..b78751c 100644 --- a/crates/stef-lsp/src/handlers/mod.rs +++ b/crates/stef-lsp/src/handlers/mod.rs @@ -4,17 +4,24 @@ use anyhow::{Context, Result}; use line_index::{LineIndex, TextRange}; use log::{as_debug, as_display, debug, error, warn}; use lsp_types::{ - DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, - DidOpenTextDocumentParams, DocumentSymbolParams, DocumentSymbolResponse, Hover, HoverContents, - HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams, - MarkupContent, MarkupKind, OneOf, PositionEncodingKind, Registration, SemanticTokens, - SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams, - SemanticTokensResult, SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo, - TextDocumentSyncCapability, TextDocumentSyncKind, WorkDoneProgressOptions, + DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams, + DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentSymbolParams, + DocumentSymbolResponse, FileOperationFilter, FileOperationPattern, FileOperationPatternKind, + FileOperationRegistrationOptions, Hover, HoverContents, HoverParams, HoverProviderCapability, + InitializeParams, InitializeResult, InitializedParams, MarkupContent, MarkupKind, OneOf, + PositionEncodingKind, Registration, SemanticTokens, SemanticTokensFullOptions, + SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams, SemanticTokensResult, + SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo, + TextDocumentContentChangeEvent, TextDocumentSyncCapability, TextDocumentSyncKind, Url, + WorkDoneProgressOptions, WorkspaceFileOperationsServerCapabilities, + WorkspaceServerCapabilities, }; use ropey::Rope; -use crate::{state::FileBuilder, GlobalState}; +use crate::{ + state::{self, FileBuilder}, + GlobalState, +}; mod compile; mod document_symbols; @@ -22,9 +29,33 @@ mod hover; mod semantic_tokens; pub fn initialize( - _state: &mut GlobalState<'_>, - _params: InitializeParams, + state: &mut GlobalState<'_>, + params: InitializeParams, ) -> Result { + log::trace!("{params:#?}"); + + if let Some(root) = params.root_uri { + for path in ignore::Walk::new(root.path()) { + let Ok(path) = path else { continue }; + + if path.file_type().is_some_and(|ty| ty.is_file()) + && path.path().extension().is_some_and(|ext| ext == "stef") + { + let Ok(text) = std::fs::read_to_string(path.path()) else { + error!(path = as_debug!(path.path()); "failed reading file content"); + continue; + }; + + let Ok(uri) = Url::from_file_path(path.path()) else { + error!(path = as_debug!(path.path()); "failed parsing file path as URI"); + continue; + }; + + state.files.insert(uri.clone(), create_file(uri, text)); + } + } + } + Ok(InitializeResult { server_info: Some(ServerInfo { name: env!("CARGO_CRATE_NAME").to_owned(), @@ -37,6 +68,22 @@ pub fn initialize( )), hover_provider: Some(HoverProviderCapability::Simple(true)), document_symbol_provider: Some(OneOf::Left(true)), + workspace: Some(WorkspaceServerCapabilities { + workspace_folders: None, + file_operations: Some(WorkspaceFileOperationsServerCapabilities { + did_delete: Some(FileOperationRegistrationOptions { + filters: vec![FileOperationFilter { + scheme: Some("file".to_owned()), + pattern: FileOperationPattern { + glob: "**/*.stef".to_owned(), + matches: Some(FileOperationPatternKind::File), + options: None, + }, + }], + }), + ..WorkspaceFileOperationsServerCapabilities::default() + }), + }), semantic_tokens_provider: Some( SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions { work_done_progress_options: WorkDoneProgressOptions { @@ -76,18 +123,23 @@ pub fn did_open(state: &mut GlobalState<'_>, params: DidOpenTextDocumentParams) debug!(uri = as_display!(params.text_document.uri); "schema opened"); let text = params.text_document.text; - let file = FileBuilder { - rope: Rope::from_str(&text), - index: LineIndex::new(&text), - content: text, - schema_builder: |index, schema| { - compile::compile(params.text_document.uri.clone(), schema, index) - }, - } - .build(); + let file = if let Some(file) = state + .files + .get(¶ms.text_document.uri) + .filter(|file| file.borrow_content() == &text) + { + file + } else { + debug!("file missing from state"); + + let file = create_file(params.text_document.uri.clone(), text); + + state.files.insert(params.text_document.uri.clone(), file); + &state.files[¶ms.text_document.uri] + }; if let Err(e) = state.client.publish_diagnostics( - params.text_document.uri.clone(), + params.text_document.uri, file.borrow_schema() .as_ref() .err() @@ -97,63 +149,44 @@ pub fn did_open(state: &mut GlobalState<'_>, params: DidOpenTextDocumentParams) ) { error!(error = as_debug!(e); "failed publishing diagnostics"); } - - state.files.insert(params.text_document.uri, file); } pub fn did_change(state: &mut GlobalState<'_>, mut params: DidChangeTextDocumentParams) { - debug!(uri = as_display!(params.text_document.uri); "schema changed"); + fn is_full(changes: &[TextDocumentContentChangeEvent]) -> bool { + changes.len() == 1 && changes.first().is_some_and(|change| change.range.is_none()) + } - let file = if params.content_changes.len() == 1 - && params - .content_changes - .first() - .is_some_and(|change| change.range.is_none()) - { + debug!( + uri = as_display!(params.text_document.uri), + full = as_display!(is_full(¶ms.content_changes)); + "schema changed", + ); + + let file = if is_full(¶ms.content_changes) { let text = params.content_changes.remove(0).text; - FileBuilder { - rope: Rope::from_str(&text), - index: LineIndex::new(&text), - content: text, - schema_builder: |index, schema| { - compile::compile(params.text_document.uri.clone(), schema, index) - }, - } - .build() + create_file(params.text_document.uri.clone(), text) } else { let Some(file) = state.files.remove(¶ms.text_document.uri) else { warn!("missing state for changed file"); return; }; - let mut heads = file.into_heads(); - - for change in params.content_changes { - let range = match convert_range(&heads.index, change.range) { - Ok(range) => range, - Err(e) => { - error!(error = as_debug!(e); "invalid change"); - continue; - } - }; - - let start = heads.rope.byte_to_char(range.start().into()); - let end = heads.rope.byte_to_char(range.end().into()); - heads.rope.remove(start..end); - heads.rope.insert(start, &change.text); - } - - let text = String::from(&heads.rope); - - FileBuilder { - rope: heads.rope, - index: LineIndex::new(&text), - content: text, - schema_builder: |index, schema| { - compile::compile(params.text_document.uri.clone(), schema, index) - }, - } - .build() + update_file(params.text_document.uri.clone(), file, |rope, index| { + for change in params.content_changes { + let range = match convert_range(index, change.range) { + Ok(range) => range, + Err(e) => { + error!(error = as_debug!(e); "invalid change"); + continue; + } + }; + + let start = rope.byte_to_char(range.start().into()); + let end = rope.byte_to_char(range.end().into()); + rope.remove(start..end); + rope.insert(start, &change.text); + } + }) }; if let Err(e) = state.client.publish_diagnostics( @@ -171,9 +204,15 @@ pub fn did_change(state: &mut GlobalState<'_>, mut params: DidChangeTextDocument state.files.insert(params.text_document.uri, file); } -pub fn did_close(state: &mut GlobalState<'_>, params: DidCloseTextDocumentParams) { +pub fn did_close(_state: &mut GlobalState<'_>, params: DidCloseTextDocumentParams) { debug!(uri = as_display!(params.text_document.uri); "schema closed"); - state.files.remove(¶ms.text_document.uri); +} + +pub fn did_delete(state: &mut GlobalState<'_>, params: DeleteFilesParams) { + debug!(files = as_debug!(params.files); "files deleted"); + state + .files + .retain(|uri, _| !params.files.iter().any(|file| file.uri == uri.as_str())); } pub fn hover(state: &mut GlobalState<'_>, params: HoverParams) -> Result> { @@ -259,7 +298,7 @@ pub fn did_change_configuration( } } -pub fn convert_range(index: &LineIndex, range: Option) -> Result { +fn convert_range(index: &LineIndex, range: Option) -> Result { let range = range.context("incremental change misses range")?; let start = index @@ -292,3 +331,32 @@ pub fn convert_range(index: &LineIndex, range: Option) -> Resu Ok(TextRange::new(start, end)) } + +fn create_file(uri: Url, text: String) -> state::File { + FileBuilder { + rope: Rope::from_str(&text), + index: LineIndex::new(&text), + content: text, + schema_builder: |index, schema| compile::compile(uri, schema, index), + } + .build() +} + +fn update_file( + uri: Url, + file: state::File, + update: impl FnOnce(&mut Rope, &LineIndex), +) -> state::File { + let mut heads = file.into_heads(); + + update(&mut heads.rope, &heads.index); + let text = String::from(&heads.rope); + + FileBuilder { + rope: heads.rope, + index: LineIndex::new(&text), + content: text, + schema_builder: |index, schema| compile::compile(uri, schema, index), + } + .build() +} diff --git a/crates/stef-lsp/src/main.rs b/crates/stef-lsp/src/main.rs index b0abc56..ec877a1 100644 --- a/crates/stef-lsp/src/main.rs +++ b/crates/stef-lsp/src/main.rs @@ -8,8 +8,8 @@ use log::{as_debug, debug, error, info, warn}; use lsp_server::{Connection, ErrorCode, ExtractError, Notification, Request, RequestId, Response}; use lsp_types::{ notification::{ - DidChangeConfiguration, DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, - Initialized, Notification as LspNotification, + DidChangeConfiguration, DidChangeTextDocument, DidCloseTextDocument, DidDeleteFiles, + DidOpenTextDocument, Initialized, Notification as LspNotification, }, request::{ DocumentSymbolRequest, HoverRequest, Request as LspRequest, SemanticTokensFullRequest, @@ -138,6 +138,9 @@ fn main_loop(conn: &Connection, mut state: GlobalState<'_>) -> Result<()> { DidCloseTextDocument::METHOD => { handle_notify::(&mut state, notif, handlers::did_close); } + DidDeleteFiles::METHOD => { + handle_notify::(&mut state, notif, handlers::did_delete); + } DidChangeConfiguration::METHOD => { handle_notify::( &mut state, diff --git a/crates/stef-lsp/src/state.rs b/crates/stef-lsp/src/state.rs index 9bb0e64..ba3c25b 100644 --- a/crates/stef-lsp/src/state.rs +++ b/crates/stef-lsp/src/state.rs @@ -22,7 +22,7 @@ pub struct GlobalState<'a> { pub struct File { rope: Rope, pub index: LineIndex, - content: String, + pub content: String, #[borrows(index, content)] #[covariant] pub schema: Result, Diagnostic>,