diff --git a/Cargo.lock b/Cargo.lock index 7177eeb6..1614710a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2302,6 +2302,7 @@ dependencies = [ "crossbeam-channel", "directories", "git2", + "globset", "grep-matcher", "grep-regex", "grep-searcher", diff --git a/lapce-proxy/Cargo.toml b/lapce-proxy/Cargo.toml index d22c9028..2d1acef4 100644 --- a/lapce-proxy/Cargo.toml +++ b/lapce-proxy/Cargo.toml @@ -11,6 +11,7 @@ grep-searcher = "0.1.8" grep-matcher = "0.1.5" grep-regex = "0.1.9" ignore = "0.4.18" +globset = "0.4.9" reqwest = { version = "0.11", features = ["blocking", "json", "socks"] } wasmer = "2.1.1" wasmer-wasi = "2.1.1" diff --git a/lapce-proxy/src/dispatch.rs b/lapce-proxy/src/dispatch.rs index f107cc2e..5a383f7c 100644 --- a/lapce-proxy/src/dispatch.rs +++ b/lapce-proxy/src/dispatch.rs @@ -629,11 +629,13 @@ fn handle_request(&self, id: RequestId, rpc: ProxyRequest) { } } Save { rev, buffer_id } => { - let mut buffers = self.buffers.lock(); - let buffer = buffers.get_mut(&buffer_id).unwrap(); - let resp = buffer.save(rev).map(|_r| json!({})); - self.lsp.lock().save_buffer(buffer); - self.respond(id, resp); + if let Some(workspace) = self.workspace.lock().as_ref() { + let mut buffers = self.buffers.lock(); + let buffer = buffers.get_mut(&buffer_id).unwrap(); + let resp = buffer.save(rev).map(|_r| json!({})); + self.lsp.lock().save_buffer(buffer, workspace); + self.respond(id, resp); + } } SaveBufferAs { buffer_id, diff --git a/lapce-proxy/src/lsp.rs b/lapce-proxy/src/lsp.rs index f3683c1d..88851ab2 100644 --- a/lapce-proxy/src/lsp.rs +++ b/lapce-proxy/src/lsp.rs @@ -2,6 +2,7 @@ collections::HashMap, io::BufRead, io::{BufReader, BufWriter, Write}, + path::Path, process::{self, Child, ChildStdout, Command, Stdio}, sync::{mpsc::channel, Arc}, thread, @@ -52,6 +53,37 @@ pub struct LspState { pub server_capabilities: Option, pub opened_documents: HashMap, pub is_initialized: bool, + pub did_save_capabilities: Vec, +} + +pub struct DocumentFilter { + /// The document must have this language id, if it exists + pub language_id: Option, + /// The document's path must match this glob, if it exists + pub pattern: Option, + // TODO: URI Scheme from lsp-types document filter +} +impl DocumentFilter { + /// Constructs a document filter from the LSP version + /// This ignores any fields that are badly constructed + fn from_lsp_filter_loose(filter: lsp_types::DocumentFilter) -> DocumentFilter { + DocumentFilter { + language_id: filter.language, + // TODO: clean this up + pattern: filter + .pattern + .as_deref() + .map(globset::Glob::new) + .and_then(Result::ok) + .map(|x| globset::Glob::compile_matcher(&x)), + } + } +} +pub struct DidSaveCapability { + /// A filter on what documents this applies to + filter: DocumentFilter, + /// Whether we are supposed to include the text when sending a didSave event + include_text: bool, } #[derive(Clone)] @@ -135,10 +167,53 @@ pub fn new_buffer( } } - pub fn save_buffer(&self, buffer: &Buffer) { - if let Some(client) = self.clients.get(&buffer.language_id) { - let uri = client.get_uri(buffer); - client.send_did_save(uri); + pub fn save_buffer(&self, buffer: &Buffer, workspace_path: &Path) { + for (client_language_id, client) in self.clients.iter() { + // Get rid of the workspace path prefix so that it can be used with the filters + let buffer_path = buffer + .path + .strip_prefix(workspace_path) + .unwrap_or(&buffer.path); + + let mut passed_filter = client_language_id == &buffer.language_id; + let mut include_text = false; + if !passed_filter { + let lsp_state = client.state.lock(); + + // TODO: Should we iterate in reverse order so that later capabilities + // can overwrite old ones? + // Find the first capability that wants this file, if any. + for cap in &lsp_state.did_save_capabilities { + if let Some(language_id) = &cap.filter.language_id { + if language_id != &buffer.language_id { + continue; + } + } + + if let Some(pattern) = &cap.filter.pattern { + if !pattern.is_match(buffer_path) { + continue; + } + } + + passed_filter = true; + include_text = cap.include_text; + break; + } + + // Get rid of the mutex guard + drop(lsp_state); + } + + if passed_filter { + let uri = client.get_uri(buffer); + let text = if include_text { + Some(buffer.get_document()) + } else { + None + }; + client.send_did_save(uri, text); + } } } @@ -457,6 +532,7 @@ pub fn new( server_capabilities: None, opened_documents: HashMap::new(), is_initialized: false, + did_save_capabilities: Vec::new(), })), }); @@ -577,7 +653,7 @@ pub fn handle_message(&self, message: &str) { } } - pub fn handle_request(&self, method: &str, id: Id, _params: Params) { + pub fn handle_request(&self, method: &str, id: Id, params: Params) { match method { "window/workDoneProgress/create" => { // Token is ignored as the workProgress Widget is always working @@ -585,6 +661,41 @@ pub fn handle_request(&self, method: &str, id: Id, _params: Params) { // probably store the token self.send_success_response(id, &json!({})); } + "client/registerCapability" => { + if let Ok(registrations) = + serde_json::from_value::(json!(params)) + { + for registration in registrations.registrations { + match registration.method.as_str() { + "textDocument/didSave" => { + if let Some(options) = registration.register_options { + if let Ok(options) = serde_json::from_value::(options) { + if let Some(selectors) = options.text_document_registration_options.document_selector { + // TODO: is false a reasonable default? + let include_text = options.include_text.unwrap_or(false); + + let mut lsp_state = self.state.lock(); + + // Add each selector our did save filtering + for selector in selectors { + let filter = DocumentFilter::from_lsp_filter_loose(selector); + let cap = DidSaveCapability { + filter, + include_text, + }; + + lsp_state.did_save_capabilities.push(cap); + } + } + } + } + // TODO: report error? + } + _ => println!("Received unhandled client/registerCapability request {}", registration.method), + } + } + } + } method => { println!("Received unhandled request {method}"); } @@ -740,10 +851,10 @@ pub fn send_did_open( self.send_notification("textDocument/didOpen", params); } - pub fn send_did_save(&self, uri: Url) { + pub fn send_did_save(&self, uri: Url, text: Option) { let params = DidSaveTextDocumentParams { text_document: TextDocumentIdentifier { uri }, - text: None, + text, }; let params = Params::from(serde_json::to_value(params).unwrap()); self.send_notification("textDocument/didSave", params); @@ -759,6 +870,11 @@ pub fn send_initialize(&self, root_uri: Option, on_init: CB) { let client_capabilities = ClientCapabilities { text_document: Some(TextDocumentClientCapabilities { + synchronization: Some(TextDocumentSyncClientCapabilities { + did_save: Some(true), + dynamic_registration: Some(true), + ..Default::default() + }), completion: Some(CompletionClientCapabilities { completion_item: Some(CompletionItemCapability { snippet_support: Some(true),