use std::{ collections::{BTreeMap, HashMap, HashSet, VecDeque}, io::{Read, Write}, path::{Component, Path, PathBuf}, process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, Stdio}, sync::{ Arc, atomic::{AtomicBool, Ordering}, mpsc::{self, Receiver, SyncSender, TryRecvError, TrySendError}, }, thread, time::{Duration, Instant}, }; use serde_json::{Value, json}; use unicode_segmentation::UnicodeSegmentation; use crate::{ editor::Context, plugin::{ CompletionEdit, CompletionItem, Diagnostic, DiagnosticSeverity, DocumentEvent, DocumentEventKind, DocumentSnapshot, PluginUpdate, SemanticToken, SemanticTokenKind, SemanticTokenModifier, TextPosition, UpdateEffect, }, snippet, }; const MAX_BODY: usize = 4 * 1024 * 1024; const MAX_HEADER: usize = 16 * 1024; const QUEUE_SIZE: usize = 128; const MAX_SERVERS: usize = 16; const MAX_VERSION: u64 = i32::MAX as u64; const TICK: Duration = Duration::from_millis(5); #[derive(Clone, Debug)] pub struct ServerConfig { pub language_id: String, pub command: Vec, pub extensions: Vec, pub initialization_options: Value, pub settings: Value, } #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] struct ServerKey { config: usize, root: PathBuf, } struct CachedDocument { snapshot: Arc, servers: Vec, diagnostics: HashMap, semantic_tokens: Option>, } struct Contribution { items: Vec, bytes: usize, } enum Output { Update(PluginUpdate), Diagnostics { snapshot: Arc, items: Vec, }, SemanticTokens { snapshot: Arc, tokens: Option>, }, } struct Worker { input: SyncSender, output: Receiver, failure: Receiver, cancelled: Arc, retired: bool, thread: Option>, } enum Input { Document { snapshot: Arc, event: DocumentEventKind, served: bool, primary: bool, }, Position(PositionKind, Source), Diagnostics(usize), } #[derive(Clone, Copy)] enum PositionKind { Definition, Hover, Completion, } impl PositionKind { fn name(self) -> &'static str { match self { Self::Definition => "definition", Self::Hover => "hover", Self::Completion => "completion", } } } struct Source { buffer_id: usize, revision: u64, row: usize, column: usize, request_id: u64, } /// Callbacks only enqueue snapshots. Each lazy server owns its process and I/O on /// one worker; neither spawning, disk reads, pipe I/O nor reaping runs in poll. pub struct Lsp { configs: Vec>, cwd: PathBuf, documents: HashMap, servers: BTreeMap, updates: Vec, changed_diagnostics: HashSet, changed_semantics: HashSet, } impl Lsp { pub fn new(configs: Vec) -> Self { Self { configs: configs.into_iter().map(Arc::new).collect(), cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")), documents: HashMap::new(), servers: BTreeMap::new(), updates: Vec::new(), changed_diagnostics: HashSet::new(), changed_semantics: HashSet::new(), } } pub fn is_enabled(&self) -> bool { !self.configs.is_empty() } pub fn notify_document(&mut self, event: &DocumentEvent) -> Result<(), String> { if !self.is_enabled() { return Ok(()); } let snapshot = self .documents .get(&event.document.id) .filter(|cached| { matches!(event.event, DocumentEventKind::Close) || (cached.snapshot.revision == event.document.revision && cached.snapshot.path == event.document.path && cached.snapshot.root == event.document.root) }) .map(|cached| Arc::clone(&cached.snapshot)) .unwrap_or_else(|| Arc::new(event.document.clone())); let mut startup_error = None; if matches!(event.event, DocumentEventKind::Close) { self.documents.remove(&snapshot.id); self.changed_diagnostics.remove(&snapshot.id); self.changed_semantics.remove(&snapshot.id); } else if self.documents.get(&snapshot.id).is_none_or(|cached| { cached.snapshot.path != snapshot.path || cached.snapshot.root != snapshot.root }) { let keys = snapshot .path .as_ref() .and_then(|path| { let extension = Path::new(path).extension()?.to_str()?; let matches = |config: &ServerConfig| { config .extensions .iter() .any(|candidate| candidate == extension) }; let language = &self .configs .iter() .find(|config| matches(config))? .language_id; let root = absolute(Path::new(&snapshot.root), &self.cwd); Some( self.configs .iter() .enumerate() .filter(|(_, config)| { config.language_id == *language && matches(config) }) .map(|(config, _)| ServerKey { config, root: root.clone(), }) .collect::>(), ) }) .unwrap_or_default(); self.documents.insert( snapshot.id, CachedDocument { snapshot: Arc::clone(&snapshot), servers: keys, diagnostics: HashMap::new(), semantic_tokens: None, }, ); self.changed_diagnostics.insert(snapshot.id); self.changed_semantics.insert(snapshot.id); for index in 0..self.documents[&snapshot.id].servers.len() { let key = self.documents[&snapshot.id].servers[index].clone(); if let Err(error) = self.start(&key) { startup_error.get_or_insert(error); } } } else { let cached = self.documents.get_mut(&snapshot.id).unwrap(); if !Arc::ptr_eq(&cached.snapshot, &snapshot) { cached.snapshot = Arc::clone(&snapshot); cached.diagnostics.clear(); cached.semantic_tokens = None; self.changed_semantics.insert(snapshot.id); self.changed_diagnostics.insert(snapshot.id); } } let mut failures = Vec::new(); for (server_key, worker) in &self.servers { if worker.retired { continue; } if worker .input .try_send(Input::Document { snapshot: Arc::clone(&snapshot), event: event.event, served: self .documents .get(&snapshot.id) .is_some_and(|doc| doc.servers.contains(server_key)), primary: self .documents .get(&snapshot.id) .is_some_and(|doc| doc.servers.first() == Some(server_key)), }) .is_err() { failures.push(server_key.clone()); } } for key in failures { self.retire( &key, "LSP server stopped: document/command queue is full or unavailable".into(), ); } startup_error.map_or(Ok(()), Err) } pub fn definition(&mut self, context: &Context, request_id: u64) -> Result<(), String> { self.position(context, request_id, PositionKind::Definition) } pub fn hover(&mut self, context: &Context, request_id: u64) -> Result<(), String> { self.position(context, request_id, PositionKind::Hover) } pub fn completion(&mut self, context: &Context, request_id: u64) -> Result<(), String> { self.position(context, request_id, PositionKind::Completion) } fn position( &mut self, context: &Context, request_id: u64, kind: PositionKind, ) -> Result<(), String> { let source = Source { buffer_id: context.buffer_id, revision: context.revision, row: context .row .checked_sub(1) .ok_or_else(|| format!("LSP {} row is invalid", kind.name()))?, column: context .column .checked_sub(1) .ok_or_else(|| format!("LSP {} column is invalid", kind.name()))?, request_id, }; self.command( context.buffer_id, Input::Position(kind, source), Some(request_id), ) } pub fn diagnostics(&mut self, buffer_id: usize) -> Result<(), String> { self.command(buffer_id, Input::Diagnostics(buffer_id), None) } pub fn poll(&mut self) -> Vec { let failures: Vec<_> = self .servers .iter() .filter_map(|(key, worker)| { if worker.retired { return None; } match worker.failure.try_recv() { Ok(reason) => Some((key.clone(), reason)), Err(TryRecvError::Disconnected) => Some(( key.clone(), "LSP server stopped: worker exited unexpectedly".into(), )), Err(TryRecvError::Empty) => None, } }) .collect(); for (key, reason) in failures { self.retire(&key, reason); } // Ordered storage keeps attribution deterministic without per-poll sorting. for (key, worker) in &mut self.servers { if worker.retired { continue; } let first_update = self.updates.len(); for _ in 0..QUEUE_SIZE { let Ok(output) = worker.output.try_recv() else { break; }; match output { Output::Update(update) => self.updates.push(update), Output::Diagnostics { snapshot, items } => { let Some(doc) = self.documents.get_mut(&snapshot.id).filter(|doc| { doc.servers.contains(key) && Arc::ptr_eq(&doc.snapshot, &snapshot) }) else { continue; }; let bytes = items.iter().map(|item| item.message.len()).sum::(); let siblings = doc .diagnostics .iter() .filter(|(config, _)| **config != key.config) .map(|(_, contribution)| contribution.bytes) .sum::(); if bytes > MAX_BODY.saturating_sub(siblings) { self.updates.truncate(first_update); Self::retire_worker( key, worker, &mut self.documents, &mut self.changed_diagnostics, &mut self.changed_semantics, &mut self.updates, "LSP server stopped: combined diagnostic messages exceed 4 MiB" .into(), ); break; } if items.is_empty() { doc.diagnostics.remove(&key.config); } else { doc.diagnostics .insert(key.config, Contribution { items, bytes }); } self.changed_diagnostics.insert(snapshot.id); } Output::SemanticTokens { snapshot, tokens } => { let Some(doc) = self.documents.get_mut(&snapshot.id).filter(|doc| { doc.servers.first() == Some(key) && Arc::ptr_eq(&doc.snapshot, &snapshot) }) else { continue; }; doc.semantic_tokens = tokens; self.changed_semantics.insert(snapshot.id); } } } } let mut effects: Vec<_> = self .changed_diagnostics .drain() .filter_map(|id| { let doc = self.documents.get(&id)?; let count = doc .diagnostics .values() .map(|value| value.items.len()) .sum(); let mut items = Vec::with_capacity(count); for key in &doc.servers { if let Some(contribution) = doc.diagnostics.get(&key.config) { items.extend(contribution.items.iter().cloned()); } } Some(UpdateEffect::Diagnostics { buffer_id: id, revision: doc.snapshot.revision, items, }) }) .collect(); effects.extend(self.changed_semantics.drain().filter_map(|id| { let doc = self.documents.get(&id)?; Some(UpdateEffect::SemanticTokens { buffer_id: id, revision: doc.snapshot.revision, tokens: doc.semantic_tokens.clone(), }) })); if !effects.is_empty() { self.updates.push(PluginUpdate { request_id: None, effects, }); } std::mem::take(&mut self.updates) } fn start(&mut self, key: &ServerKey) -> Result<(), String> { if self.servers.contains_key(key) { return Ok(()); } if self.servers.len() >= MAX_SERVERS { return Err("LSP server/project limit of 16 reached".into()); } let (input, receiver) = mpsc::sync_channel(QUEUE_SIZE); let (sender, output) = mpsc::sync_channel(QUEUE_SIZE); let (failed, failure) = mpsc::sync_channel(1); let cancelled = Arc::new(AtomicBool::new(false)); let stop = Arc::clone(&cancelled); let config = Arc::clone(&self.configs[key.config]); let root = key.root.clone(); let initial: Vec<_> = self .documents .values() .map(|doc| { ( Arc::clone(&doc.snapshot), doc.servers.contains(key), doc.servers.first() == Some(key), ) }) .collect(); let started = thread::Builder::new() .name(format!("lsp-{}", config.language_id)) .spawn(move || { let mut runtime = match Runtime::new(Arc::clone(&config), root, receiver, sender, stop) { Ok(runtime) => runtime, Err(reason) => { let _ = failed.try_send(format!( "LSP: {} server failed to start: {}", config.language_id, clean(&reason) )); return; } }; let result = initial .into_iter() .try_for_each(|(snapshot, served, primary)| { runtime.document(snapshot, DocumentEventKind::Open, served, primary) }) .and_then(|()| runtime.run()); if let Err(reason) = result { let _ = failed.try_send(format!("LSP server stopped: {}", clean(&reason))); } }); let (thread, startup_error) = match started { Ok(thread) => (Some(thread), None), Err(error) => (None, Some(error)), }; self.servers.insert( key.clone(), Worker { input, output, failure, cancelled, retired: false, thread, }, ); if let Some(error) = startup_error { self.retire(key, format!("LSP: server failed to start: {error}")); } Ok(()) } fn command( &mut self, buffer_id: usize, input: Input, request_id: Option, ) -> Result<(), String> { let key = self .documents .get(&buffer_id) .and_then(|doc| doc.servers.first().cloned()); if let Some(key) = key && let Some(worker) = self.servers.get(&key) && !worker.retired { if worker.input.try_send(input).is_ok() { return Ok(()); } self.retire( &key, "LSP server stopped: document/command queue is full or unavailable".into(), ); } if self.updates.len() >= QUEUE_SIZE { return Err("LSP update queue is full".into()); } self.updates .push(message("no running server for this document", request_id)); Ok(()) } fn retire(&mut self, key: &ServerKey, reason: String) { let Some(worker) = self.servers.get_mut(key) else { return; }; Self::retire_worker( key, worker, &mut self.documents, &mut self.changed_diagnostics, &mut self.changed_semantics, &mut self.updates, reason, ); } fn retire_worker( key: &ServerKey, worker: &mut Worker, documents: &mut HashMap, changed: &mut HashSet, changed_semantics: &mut HashSet, updates: &mut Vec, reason: String, ) { if worker.retired { return; } worker.retired = true; worker.cancelled.store(true, Ordering::Release); for (id, doc) in documents { if doc.servers.first() == Some(key) { doc.semantic_tokens = None; changed_semantics.insert(*id); } if doc.servers.contains(key) { doc.diagnostics.remove(&key.config); changed.insert(*id); } } // Retirement cleanup remains independent of the command-message queue. updates.push(PluginUpdate { request_id: None, effects: vec![UpdateEffect::Message { text: reason }], }); } } impl Drop for Lsp { fn drop(&mut self) { for worker in self.servers.values() { worker.cancelled.store(true, Ordering::Release); } // Application exit must not race detached workers' process cleanup. for worker in self.servers.values_mut() { if let Some(thread) = worker.thread.take() { let _ = thread.join(); } } } } struct Document { snapshot: Arc, uri: String, version: u64, opened: bool, saved: bool, primary: bool, semantic_dirty: bool, } #[derive(Default)] struct SyncOptions { open_close: bool, change: u64, save: bool, include_text: bool, } enum Request { Initialize, Definition(Source), Hover(Source), Completion(Source, Arc), SemanticTokens(Arc), Shutdown, } struct Runtime { config: Arc, root: PathBuf, input: Receiver, output: SyncSender, cancelled: Arc, transport: Transport, snapshots: HashMap>, documents: HashMap, requests: HashMap, pending: VecDeque<(PositionKind, Source)>, semantic_inflight: HashSet, next_id: u64, ready: bool, definition: bool, hover: bool, completion: bool, semantic: Option, sync: SyncOptions, } impl Runtime { fn new( config: Arc, root: PathBuf, input: Receiver, output: SyncSender, cancelled: Arc, ) -> Result { let transport = Transport::spawn(&config.command, &root)?; let mut runtime = Self { config, root, input, output, cancelled, transport, snapshots: HashMap::new(), documents: HashMap::new(), requests: HashMap::new(), pending: VecDeque::new(), semantic_inflight: HashSet::new(), next_id: 1, ready: false, definition: false, hover: false, completion: false, semantic: None, sync: SyncOptions::default(), }; runtime.request( "initialize", Some(json!({ "processId": std::process::id(), "clientInfo": {"name": "vamp"}, "rootUri": file_uri(&runtime.root)?, "workspaceFolders": runtime.workspace_folders()?, "initializationOptions": runtime.config.initialization_options, "capabilities": { "general": {"positionEncodings": ["utf-16"]}, "workspace": {"configuration": true, "workspaceFolders": true, "semanticTokens": {"refreshSupport": true}}, "textDocument": { "synchronization": {"dynamicRegistration": false, "willSave": false, "willSaveWaitUntil": false, "didSave": true}, "definition": {"dynamicRegistration": false, "linkSupport": true}, "hover": {"dynamicRegistration": false, "contentFormat": ["plaintext", "markdown"]}, "completion": { "dynamicRegistration": false, "contextSupport": true, "insertTextMode": 1, "completionItem": { "snippetSupport": true, "commitCharactersSupport": false, "insertReplaceSupport": true, "insertTextModeSupport": {"valueSet": [1]}, "resolveSupport": {"properties": []} }, "completionList": { "itemDefaults": ["editRange", "insertTextFormat", "insertTextMode"] } }, "publishDiagnostics": {"relatedInformation": false, "versionSupport": true}, "semanticTokens": { "dynamicRegistration": false, "requests": {"range": true, "full": true}, "tokenTypes": SEMANTIC_TYPES.iter().map(|(name, _)| *name).collect::>(), "tokenModifiers": SEMANTIC_MODIFIERS.iter().map(|(name, _)| *name).collect::>(), "formats": ["relative"], "overlappingTokenSupport": false, "multilineTokenSupport": false, "serverCancelSupport": false, "augmentsSyntaxTokens": true } } } })), Request::Initialize, )?; Ok(runtime) } fn run(&mut self) -> Result<(), String> { while !self.cancelled.load(Ordering::Acquire) { for _ in 0..QUEUE_SIZE { match self.input.try_recv() { Ok(Input::Document { snapshot, event, served, primary, }) => self.document(snapshot, event, served, primary)?, Ok(Input::Position(kind, source)) => self.position(kind, source)?, Ok(Input::Diagnostics(id)) => { let text = if !self.documents.contains_key(&id) { "no running server for this document".to_owned() } else if !self.ready { "server initializing".to_owned() } else { format!( "diagnostics update automatically from {}", self.config.language_id ) }; self.emit(message(&text, None))?; } Err(TryRecvError::Empty) => break, Err(TryRecvError::Disconnected) => { self.cancelled.store(true, Ordering::Release); break; } } } self.transport.flush()?; self.transport.drain_stderr()?; self.transport.read()?; for _ in 0..QUEUE_SIZE { let Some(frame) = self.transport.frame()? else { break; }; self.incoming(frame)?; } if self.ready && self.semantic.is_some() && self.requests.len() < QUEUE_SIZE { let pending: Vec<_> = self .documents .iter() .filter(|(id, doc)| { doc.primary && doc.semantic_dirty && !self.semantic_inflight.contains(*id) }) .map(|(id, _)| *id) .collect(); for id in pending { self.semantic_request(id)?; } } self.transport.check_alive()?; thread::sleep(TICK); } self.shutdown(); Ok(()) } fn emit(&self, update: PluginUpdate) -> Result<(), String> { self.send(Output::Update(update)) } fn emit_diagnostics( &self, snapshot: &Arc, items: Vec, ) -> Result<(), String> { self.send(Output::Diagnostics { snapshot: Arc::clone(snapshot), items, }) } fn emit_semantics( &self, snapshot: &Arc, tokens: Option>, ) -> Result<(), String> { self.send(Output::SemanticTokens { snapshot: Arc::clone(snapshot), tokens, }) } fn send(&self, output: Output) -> Result<(), String> { self.output.try_send(output).map_err(|error| match error { TrySendError::Full(_) => "LSP incoming update queue is full".into(), TrySendError::Disconnected(_) => "LSP client disconnected".into(), }) } fn document( &mut self, snapshot: Arc, event: DocumentEventKind, served: bool, primary: bool, ) -> Result<(), String> { let id = snapshot.id; let old = self.documents.remove(&id); if matches!(event, DocumentEventKind::Close) { self.snapshots.remove(&id); if let Some(old) = old { self.close(&old)?; } return Ok(()); } self.snapshots.insert(id, Arc::clone(&snapshot)); if !served { if let Some(old) = old { self.close(&old)?; self.emit_diagnostics(&snapshot, Vec::new())?; } return Ok(()); } let path = snapshot.path.as_ref().ok_or("LSP document has no path")?; let uri = file_uri(&absolute(Path::new(path), &self.root))?; let same_uri = old.as_ref().is_some_and(|old| old.uri == uri); if let Some(old) = &old && !same_uri { self.close(old)?; } let changed = old .as_ref() .is_some_and(|old| old.snapshot.revision != snapshot.revision); let version = if same_uri { old.as_ref().unwrap().version + u64::from(changed) } else { 1 }; if version > MAX_VERSION { return Err("LSP document version exhausted".into()); } let mut doc = Document { snapshot: Arc::clone(&snapshot), uri, version, opened: same_uri && old.as_ref().is_some_and(|old| old.opened), saved: matches!(event, DocumentEventKind::Save), primary, semantic_dirty: primary && old.as_ref().is_none_or(|old| { old.semantic_dirty || !old.primary || !Arc::ptr_eq(&old.snapshot, &snapshot) }), }; if changed { self.emit_diagnostics(&doc.snapshot, Vec::new())?; } if self.ready { if !doc.opened { self.open(&mut doc)?; } else if changed { self.change(old.as_ref().unwrap(), &doc)?; } if doc.saved { self.save(&mut doc)?; } } self.documents.insert(id, doc); self.semantic_request(id)?; Ok(()) } fn open(&mut self, doc: &mut Document) -> Result<(), String> { doc.opened = true; if self.sync.open_close { self.notify( "textDocument/didOpen", Some(json!({"textDocument": { "uri": doc.uri, "languageId": self.config.language_id, "version": doc.version, "text": doc.snapshot.text }})), )?; } Ok(()) } fn change(&mut self, old: &Document, doc: &Document) -> Result<(), String> { if self.sync.change == 0 { return Ok(()); } let mut change = json!({"text": doc.snapshot.text}); if self.sync.change == 2 { let text = normalized(&old.snapshot.text); let mut lines = text.split('\n'); let mut row = 0; let mut last = lines.next().unwrap_or(""); for line in lines { row += 1; last = line; } change["range"] = json!({"start": {"line": 0, "character": 0}, "end": {"line": row, "character": last.encode_utf16().count()}}); } self.notify( "textDocument/didChange", Some(json!({ "textDocument": {"uri": doc.uri, "version": doc.version}, "contentChanges": [change] })), ) } fn save(&mut self, doc: &mut Document) -> Result<(), String> { if self.sync.save { let mut params = json!({"textDocument": {"uri": doc.uri}}); if self.sync.include_text { params["text"] = json!(doc.snapshot.text); } self.notify("textDocument/didSave", Some(params))?; } doc.saved = false; Ok(()) } fn close(&mut self, doc: &Document) -> Result<(), String> { if self.ready && doc.opened && self.sync.open_close { self.notify( "textDocument/didClose", Some(json!({"textDocument": {"uri": doc.uri}})), )?; } Ok(()) } fn position(&mut self, kind: PositionKind, source: Source) -> Result<(), String> { let Some(doc) = self.documents.get(&source.buffer_id) else { return Ok(()); }; if doc.snapshot.revision != source.revision { return Ok(()); } if !self.ready { if self.pending.len() == QUEUE_SIZE { return Err("LSP pending command queue is full".into()); } self.pending.push_back((kind, source)); return Ok(()); } let (supported, method, unsupported) = match kind { PositionKind::Definition => ( self.definition, "textDocument/definition", "server does not support go-to-definition", ), PositionKind::Hover => ( self.hover, "textDocument/hover", "server does not support hover", ), PositionKind::Completion => ( self.completion, "textDocument/completion", "server does not support completion", ), }; if !supported { return self.emit(message(unsupported, Some(source.request_id))); } let position = outbound_position(&doc.snapshot.text, source.row, source.column)?; let mut params = json!({ "textDocument": {"uri": doc.uri}, "position": position }); if matches!(kind, PositionKind::Completion) { params["context"] = json!({"triggerKind": 1}); } let request = match kind { PositionKind::Definition => Request::Definition(source), PositionKind::Hover => Request::Hover(source), PositionKind::Completion => Request::Completion(source, Arc::clone(&doc.snapshot)), }; self.request(method, Some(params), request) } fn semantic_request(&mut self, id: usize) -> Result<(), String> { let Some(options) = &self.semantic else { return Ok(()); }; let Some(doc) = self.documents.get(&id).filter(|doc| { self.ready && doc.primary && doc.semantic_dirty && !self.semantic_inflight.contains(&id) }) else { return Ok(()); }; if self.requests.len() == QUEUE_SIZE { return Ok(()); } let snapshot = Arc::clone(&doc.snapshot); let mut params = json!({"textDocument": {"uri": doc.uri}}); let method = if options.full { "textDocument/semanticTokens/full" } else { let text = normalized(&snapshot.text); let (row, line) = text.split('\n').enumerate().last().unwrap(); params["range"] = json!({"start": {"line": 0, "character": 0}, "end": {"line": row, "character": line.encode_utf16().count()}}); "textDocument/semanticTokens/range" }; self.request(method, Some(params), Request::SemanticTokens(snapshot))?; self.semantic_inflight.insert(id); self.documents.get_mut(&id).unwrap().semantic_dirty = false; Ok(()) } fn completion_result( &self, source: Source, snapshot: Arc, result: Result<&Value, String>, ) -> Result<(), String> { if !self .documents .get(&source.buffer_id) .is_some_and(|doc| doc.primary && Arc::ptr_eq(&doc.snapshot, &snapshot)) { return Ok(()); } match result.and_then(|value| { decode_completion(&snapshot.text, value, &source, snapshot.path.as_deref()) }) { Ok(items) => self.emit(PluginUpdate { request_id: Some(source.request_id), effects: vec![UpdateEffect::Completion { buffer_id: source.buffer_id, revision: source.revision, items, }], }), Err(reason) => self.emit(message( &format!("completion failed: {reason}"), Some(source.request_id), )), } } fn semantic_result( &mut self, snapshot: Arc, result: Result<&Value, String>, ) -> Result<(), String> { self.semantic_inflight.remove(&snapshot.id); let fresh = self.documents.get(&snapshot.id).is_some_and(|doc| { doc.primary && !doc.semantic_dirty && Arc::ptr_eq(&doc.snapshot, &snapshot) }); // Stale responses are discarded before inspecting their token payload. if fresh { let decoded = result.and_then(|result| { decode_semantic_tokens(&snapshot.text, result, self.semantic.as_ref().unwrap()) }); match decoded { Ok(tokens) => self.emit_semantics(&snapshot, tokens)?, Err(reason) => { self.emit_semantics(&snapshot, None)?; self.emit(message(&format!("semantic tokens failed: {reason}"), None))?; } } } self.semantic_request(snapshot.id) } fn request( &mut self, method: &str, params: Option, context: Request, ) -> Result<(), String> { if self.next_id > MAX_VERSION { return Err("LSP request IDs exhausted".into()); } if self.requests.len() == QUEUE_SIZE { return Err("LSP pending request queue is full".into()); } let id = self.next_id; self.next_id += 1; let mut frame = json!({"jsonrpc": "2.0", "id": id, "method": method}); if let Some(params) = params { frame["params"] = params; } self.transport.send(frame)?; self.requests.insert(id, context); Ok(()) } fn notify(&mut self, method: &str, params: Option) -> Result<(), String> { let mut frame = json!({"jsonrpc": "2.0", "method": method}); if let Some(params) = params { frame["params"] = params; } self.transport.send(frame) } fn incoming(&mut self, frame: Value) -> Result<(), String> { let object = frame .as_object() .filter(|object| object.get("jsonrpc") == Some(&json!("2.0"))) .ok_or("invalid LSP JSON-RPC frame")?; if let Some(method) = object.get("method") { let method = method.as_str().ok_or("invalid LSP method")?; if let Some(id) = object.get("id") { return self.server_request( id, method, object.get("params").unwrap_or(&Value::Null), ); } match method { "textDocument/publishDiagnostics" => self.publish_diagnostics(&frame["params"]), "window/showMessage" => { let text = frame["params"]["message"] .as_str() .ok_or("invalid LSP showMessage")?; self.emit(message(text, None)) } _ => Ok(()), } } else { let id = frame["id"].as_u64().ok_or("unexpected LSP response")?; let context = self.requests.remove(&id).ok_or("unexpected LSP response")?; if object.contains_key("result") == object.contains_key("error") { return match context { Request::Hover(source) => { self.hover_result(source, Err("unexpected LSP response".into())) } Request::Completion(source, snapshot) => self.completion_result( source, snapshot, Err("unexpected LSP response".into()), ), Request::SemanticTokens(snapshot) => { self.semantic_result(snapshot, Err("unexpected LSP response".into())) } _ => Err("unexpected LSP response".into()), }; } if let Some(error) = object.get("error") { let text = error["message"] .as_str() .filter(|_| error["code"].as_i64().is_some()); let Some(text) = text else { return match context { Request::Hover(source) => { self.hover_result(source, Err("invalid LSP error response".into())) } Request::Completion(source, snapshot) => self.completion_result( source, snapshot, Err("invalid LSP error response".into()), ), Request::SemanticTokens(snapshot) => { self.semantic_result(snapshot, Err("invalid LSP error response".into())) } _ => Err("invalid LSP error response".into()), }; }; match context { Request::Initialize => Err(format!("initialization failed: {text}")), Request::Definition(source) => self.emit(message( &format!("definition failed: {text}"), Some(source.request_id), )), Request::Hover(source) => self.hover_result(source, Err(text.to_owned())), Request::Completion(source, snapshot) => { self.completion_result(source, snapshot, Err(text.to_owned())) } Request::SemanticTokens(snapshot) => { self.semantic_result(snapshot, Err(text.to_owned())) } Request::Shutdown => Ok(()), } } else { match context { Request::Initialize => self.initialized(&frame["result"]), Request::Definition(source) => self.definition_result(source, &frame["result"]), Request::Hover(source) => self.hover_result(source, Ok(&frame["result"])), Request::Completion(source, snapshot) => { self.completion_result(source, snapshot, Ok(&frame["result"])) } Request::SemanticTokens(snapshot) => { self.semantic_result(snapshot, Ok(&frame["result"])) } Request::Shutdown => self.notify("exit", None), } } } } fn initialized(&mut self, result: &Value) -> Result<(), String> { let capabilities = result["capabilities"] .as_object() .ok_or("invalid LSP initialize result")?; if capabilities .get("positionEncoding") .is_some_and(|encoding| encoding != "utf-16") { return Err("server selected unsupported position encoding".into()); } self.sync = sync_options(capabilities.get("textDocumentSync").unwrap_or(&json!(0)))?; self.definition = capabilities .get("definitionProvider") .is_some_and(|value| value == true || value.is_object()); self.hover = capabilities .get("hoverProvider") .is_some_and(|value| value == true || value.is_object()); self.completion = capabilities .get("completionProvider") .is_some_and(Value::is_object); match semantic_options(capabilities.get("semanticTokensProvider")) { Ok(options) => self.semantic = options, Err(reason) => { self.emit(message(&format!("semantic tokens failed: {reason}"), None))? } } self.notify("initialized", Some(json!({})))?; self.notify( "workspace/didChangeConfiguration", Some(json!({"settings": self.config.settings})), )?; self.ready = true; let documents = std::mem::take(&mut self.documents); for (id, mut doc) in documents { self.open(&mut doc)?; if doc.saved { self.save(&mut doc)?; } self.documents.insert(id, doc); self.semantic_request(id)?; } while let Some((kind, source)) = self.pending.pop_front() { self.position(kind, source)?; } Ok(()) } fn workspace_folders(&self) -> Result { Ok( json!([{"uri": file_uri(&self.root)?, "name": self.root.file_name().and_then(|name| name.to_str()).unwrap_or("/")}]), ) } fn server_request(&mut self, id: &Value, method: &str, params: &Value) -> Result<(), String> { if !(id.is_string() || id.as_i64().is_some() || id.as_u64().is_some()) { return Err("invalid LSP server request ID".into()); } let mut response = json!({"jsonrpc": "2.0", "id": id}); response["result"] = match method { "workspace/configuration" => { let items = params["items"] .as_array() .ok_or("invalid LSP configuration request")?; let mut values = Vec::with_capacity(items.len()); for item in items { if !item.is_object() { return Err("invalid LSP configuration item".into()); } let value = match item.get("section") { None | Some(Value::Null) => &self.config.settings, Some(Value::String(section)) if section.is_empty() => &self.config.settings, Some(Value::String(section)) => { self.config.settings.get(section).unwrap_or_else(|| { section .split('.') .try_fold(&self.config.settings, |value, part| value.get(part)) .unwrap_or(&Value::Null) }) } _ => return Err("invalid LSP configuration section".into()), }; values.push(value.clone()); } Value::Array(values) } "workspace/workspaceFolders" => self.workspace_folders()?, "window/showMessageRequest" => Value::Null, "workspace/semanticTokens/refresh" => { self.transport .send(json!({"jsonrpc": "2.0", "id": id, "result": null}))?; if self.semantic.is_some() { let ids: Vec<_> = self .documents .iter() .filter(|(_, doc)| doc.primary) .map(|(id, _)| *id) .collect(); for id in ids { let doc = self.documents.get_mut(&id).unwrap(); doc.semantic_dirty = true; self.semantic_request(id)?; } } return Ok(()); } _ => { response.as_object_mut().unwrap().remove("result"); response["error"] = json!({"code": -32601, "message": "Method not supported by vamp"}); return self.transport.send(response); } }; self.transport.send(response) } fn publish_diagnostics(&self, params: &Value) -> Result<(), String> { let uri = params["uri"] .as_str() .ok_or("invalid LSP publishDiagnostics")?; let items = params["diagnostics"] .as_array() .ok_or("invalid LSP publishDiagnostics")?; let Some(doc) = self.documents.values().find(|doc| doc.uri == uri) else { return Ok(()); }; if let Some(version) = params.get("version").filter(|value| !value.is_null()) { let version = version .as_i64() .filter(|value| i32::try_from(*value).is_ok()) .ok_or("invalid LSP diagnostic version")?; if version < doc.version as i64 { return Ok(()); } if version > doc.version as i64 { return Err("LSP diagnostics refer to an unknown future version".into()); } } // Without a version only the revision at arrival is knowable. let text = normalized(&doc.snapshot.text); let mut diagnostics = Vec::with_capacity(items.len()); for item in items { let (start, end) = range(&text, &item["range"])?; let severity = match item.get("severity").unwrap_or(&json!(1)).as_u64() { Some(1) => DiagnosticSeverity::Error, Some(2) => DiagnosticSeverity::Warning, Some(3) => DiagnosticSeverity::Information, Some(4) => DiagnosticSeverity::Hint, _ => return Err("invalid LSP diagnostic severity".into()), }; diagnostics.push(Diagnostic { start, end, severity, message: clean( item["message"] .as_str() .ok_or("invalid LSP diagnostic message")?, ), }); } self.emit_diagnostics(&doc.snapshot, diagnostics) } fn definition_result(&self, source: Source, result: &Value) -> Result<(), String> { if self .documents .get(&source.buffer_id) .is_none_or(|doc| doc.snapshot.revision != source.revision) { return Ok(()); } let result = self.definition_effects(&source, result); match result { Ok(effects) => self.emit(PluginUpdate { request_id: Some(source.request_id), effects, }), Err(reason) => self.emit(message( &format!("definition failed: {reason}"), Some(source.request_id), )), } } fn hover_result(&self, source: Source, result: Result<&Value, String>) -> Result<(), String> { let Some(doc) = self .documents .get(&source.buffer_id) .filter(|doc| doc.snapshot.revision == source.revision) else { return Ok(()); }; match result.and_then(|result| hover_text(result, &doc.snapshot.text)) { Ok(Some(text)) => self.emit(PluginUpdate { request_id: Some(source.request_id), effects: vec![UpdateEffect::Hover { buffer_id: source.buffer_id, revision: source.revision, text, }], }), Ok(None) => self.emit(message("no hover information", Some(source.request_id))), Err(reason) => self.emit(message( &format!("hover failed: {reason}"), Some(source.request_id), )), } } fn definition_effects( &self, source: &Source, result: &Value, ) -> Result, String> { let (location, count) = match result { Value::Null => return Ok(message("no definition found", None).effects), Value::Array(locations) if locations.is_empty() => { return Ok(message("no definition found", None).effects); } Value::Array(locations) => (&locations[0], locations.len()), location => (location, 1), }; if !location.is_object() { return Err("invalid LSP definition location".into()); } let link = location.get("targetUri").is_some(); let path = uri_path( location[if link { "targetUri" } else { "uri" }] .as_str() .ok_or("invalid definition URI")?, )?; let identity = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone()); let cached = self.snapshots.values().find(|doc| { doc.path.as_ref().is_some_and(|candidate| { let candidate = absolute(Path::new(candidate), &self.root); std::fs::canonicalize(&candidate).unwrap_or(candidate) == identity }) }); let disk; let text = if let Some(doc) = cached { &doc.text } else { if !std::fs::metadata(&path) .map_err(|error| error.to_string())? .is_file() { return Err("definition target must be a regular file".into()); } let mut bytes = Vec::new(); std::fs::File::open(&path) .map_err(|error| error.to_string())? .take((MAX_BODY + 1) as u64) .read_to_end(&mut bytes) .map_err(|error| error.to_string())?; if bytes.len() > MAX_BODY { return Err("definition target exceeds 4 MiB".into()); } disk = String::from_utf8(bytes).map_err(|_| "definition target is not UTF-8")?; &disk }; let text = normalized(text); if link { range(&text, &location["targetRange"])?; } let (start, _) = range( &text, &location[if link { "targetSelectionRange" } else { "range" }], )?; let mut effects = vec![UpdateEffect::Navigate { buffer_id: source.buffer_id, revision: source.revision, path, row: start.row, column: start.column, }]; if count > 1 { effects.push(UpdateEffect::Message { text: format!("LSP: selected first of {count} definitions"), }); } Ok(effects) } fn shutdown(&mut self) { if !self.ready { return; } if self.request("shutdown", None, Request::Shutdown).is_err() { return; } let deadline = Instant::now() + Duration::from_millis(200); let mut exiting = false; while Instant::now() < deadline { if self.transport.flush().is_err() || self.transport.read().is_err() { break; } // A flushed exit notification is not proof the peer has consumed it. if exiting && self.transport.flushed() && self.transport.child.try_wait().ok().flatten().is_some() { break; } match self.transport.frame() { Ok(Some(frame)) => { let shutdown = frame["id"].as_u64().is_some_and(|id| { matches!(self.requests.get(&id), Some(Request::Shutdown)) }); if shutdown { self.notify("exit", None).ok(); exiting = true; } } Ok(None) => thread::sleep(TICK), Err(_) => break, } let _ = self.transport.drain_stderr(); } } } const SEMANTIC_TYPES: &[(&str, SemanticTokenKind)] = &[ ("namespace", SemanticTokenKind::Namespace), ("type", SemanticTokenKind::Type), ("class", SemanticTokenKind::Class), ("enum", SemanticTokenKind::Enum), ("interface", SemanticTokenKind::Interface), ("struct", SemanticTokenKind::Struct), ("typeParameter", SemanticTokenKind::TypeParameter), ("parameter", SemanticTokenKind::Parameter), ("variable", SemanticTokenKind::Variable), ("property", SemanticTokenKind::Property), ("enumMember", SemanticTokenKind::EnumMember), ("event", SemanticTokenKind::Event), ("function", SemanticTokenKind::Function), ("method", SemanticTokenKind::Method), ("macro", SemanticTokenKind::Macro), ("keyword", SemanticTokenKind::Keyword), ("modifier", SemanticTokenKind::Modifier), ("comment", SemanticTokenKind::Comment), ("string", SemanticTokenKind::String), ("number", SemanticTokenKind::Number), ("regexp", SemanticTokenKind::Regexp), ("operator", SemanticTokenKind::Operator), ("decorator", SemanticTokenKind::Decorator), ]; const SEMANTIC_MODIFIERS: &[(&str, SemanticTokenModifier)] = &[ ("declaration", SemanticTokenModifier::Declaration), ("definition", SemanticTokenModifier::Definition), ("readonly", SemanticTokenModifier::Readonly), ("static", SemanticTokenModifier::Static), ("deprecated", SemanticTokenModifier::Deprecated), ("abstract", SemanticTokenModifier::Abstract), ("async", SemanticTokenModifier::Async), ("modification", SemanticTokenModifier::Modification), ("documentation", SemanticTokenModifier::Documentation), ("defaultLibrary", SemanticTokenModifier::DefaultLibrary), ]; struct SemanticOptions { full: bool, types: Vec>, modifiers: [u16; 31], modifier_mask: u32, } fn semantic_options(value: Option<&Value>) -> Result, String> { let Some(value) = value.filter(|value| !value.is_null() && *value != false) else { return Ok(None); }; let object = value .as_object() .ok_or("invalid semantic tokens capability")?; let supported = |name| -> Result { match object.get(name) { None | Some(Value::Bool(false)) => Ok(false), Some(Value::Bool(true) | Value::Object(_)) => Ok(true), _ => Err("invalid semantic tokens request capability".into()), } }; let full = supported("full")?; if !full && !supported("range")? { return Ok(None); } let types = value["legend"]["tokenTypes"] .as_array() .filter(|items| items.len() <= 65536) .ok_or("invalid semantic token type legend")?; let names = value["legend"]["tokenModifiers"] .as_array() .ok_or("invalid semantic token modifier legend")?; let types = types .iter() .map(|value| { let name = value.as_str().ok_or("invalid semantic token type legend")?; Ok(SEMANTIC_TYPES .iter() .find(|(candidate, _)| *candidate == name) .map(|(_, kind)| *kind)) }) .collect::, String>>()?; let mut modifiers = [0; 31]; for (index, value) in names.iter().enumerate() { let name = value .as_str() .ok_or("invalid semantic token modifier legend")?; if let Some(slot) = modifiers.get_mut(index) { *slot = SEMANTIC_MODIFIERS .iter() .find(|(candidate, _)| *candidate == name) .map_or(0, |(_, modifier)| *modifier as u16); } } Ok(Some(SemanticOptions { full, types, modifiers, modifier_mask: (1u32 << names.len().min(31)) - 1, })) } // Both cursors only advance: strict codepoint boundaries and outward grapheme // rounding cost O(document + tokens), including many tokens in one grapheme. struct SemanticLine<'a> { chars: std::str::Chars<'a>, graphemes: unicode_segmentation::Graphemes<'a>, units: usize, bytes: usize, column: usize, grapheme_start: usize, grapheme_end: usize, } impl<'a> SemanticLine<'a> { fn new(line: &'a str) -> Self { let mut graphemes = line.graphemes(true); let grapheme_end = graphemes.next().map_or(0, str::len); Self { chars: line.chars(), graphemes, units: 0, bytes: 0, column: 0, grapheme_start: 0, grapheme_end, } } fn column(&mut self, target: usize, end: bool) -> Result { while self.units < target { let Some(character) = self.chars.next() else { if end { break; // LSP: an overlong end truncates at EOL, never wraps. } return Err("semantic token start is beyond its line".into()); }; let next = self.units + character.len_utf16(); if next > target { return Err("semantic token splits a UTF-16 surrogate pair".into()); } self.units = next; self.bytes += character.len_utf8(); } while self.grapheme_end > self.grapheme_start && self.bytes >= self.grapheme_end { self.grapheme_start = self.grapheme_end; self.column += 1; self.grapheme_end += self.graphemes.next().map_or(0, str::len); } Ok(self.column + usize::from(end && self.bytes > self.grapheme_start)) } } fn decode_semantic_tokens( text: &str, result: &Value, options: &SemanticOptions, ) -> Result>, String> { if result.is_null() { return Ok(None); } if result .get("resultId") .is_some_and(|value| !value.is_string()) { return Err("invalid semantic token result ID".into()); } let data = result["data"] .as_array() .ok_or("invalid semantic token data")?; if !data.len().is_multiple_of(5) { return Err("incomplete semantic token data".into()); } let count = data.len() / 5; if count > MAX_BODY / std::mem::size_of::() { return Err("decoded semantic tokens exceed 4 MiB".into()); } if count == 0 { return Ok(None); } let text = normalized(text); let mut lines = text.split('\n'); let mut line = SemanticLine::new(lines.next().unwrap()); let mut row = 0usize; let mut start = 0usize; let mut previous_end = 0usize; let mut tokens = Vec::with_capacity(count); for chunk in data.chunks_exact(5) { let integer = |index: usize| -> Result { chunk[index] .as_u64() .filter(|value| *value <= MAX_VERSION) .map(|value| value as usize) .ok_or_else(|| "invalid semantic token integer".into()) }; let delta_row = integer(0)?; let delta_start = integer(1)?; let length = integer(2)?; let kind = integer(3)?; let mask = integer(4)? as u32; let next_row = row .checked_add(delta_row) .filter(|value| *value <= MAX_VERSION as usize) .ok_or("semantic token row overflow")?; if delta_row != 0 { for _ in row..next_row { line = SemanticLine::new(lines.next().ok_or("semantic token row is out of bounds")?); } row = next_row; start = delta_start; previous_end = 0; } else { start = start .checked_add(delta_start) .filter(|value| *value <= MAX_VERSION as usize) .ok_or("semantic token column overflow")?; } let end = start .checked_add(length) .ok_or("semantic token length overflow")?; let token_kind = options .types .get(kind) .filter(|_| kind < 65536) .ok_or("semantic token type is outside its legend")?; if mask & !options.modifier_mask != 0 { return Err("semantic token modifier is outside its legend".into()); } if start < previous_end { return Err("overlapping semantic tokens are unsupported".into()); } let start_column = line.column(start, false)?; let end_column = line.column(end, true)?; previous_end = end.min(line.units); if length == 0 || start_column >= end_column { continue; } if let Some(kind) = token_kind { let mut modifiers = 0; let mut remaining = mask; while remaining != 0 { modifiers |= options.modifiers[remaining.trailing_zeros() as usize]; remaining &= remaining - 1; } tokens.push(SemanticToken { start: TextPosition { row, column: start_column, }, end: TextPosition { row, column: end_column, }, kind: *kind, modifiers, }); } } Ok((!tokens.is_empty()).then(|| tokens.into())) } fn sync_options(value: &Value) -> Result { if let Some(kind) = value.as_u64().filter(|kind| *kind <= 2) { return Ok(SyncOptions { open_close: kind != 0, change: kind, ..SyncOptions::default() }); } let object = value .as_object() .ok_or("invalid LSP textDocumentSync capability")?; let open_close = object .get("openClose") .unwrap_or(&Value::Bool(false)) .as_bool() .ok_or("invalid LSP synchronization options")?; let change = object .get("change") .unwrap_or(&json!(0)) .as_u64() .filter(|kind| *kind <= 2) .ok_or("invalid LSP synchronization options")?; let (save, include_text) = match object.get("save") { None | Some(Value::Bool(false)) => (false, false), Some(Value::Bool(true)) => (true, false), Some(Value::Object(save)) => ( true, save.get("includeText") .unwrap_or(&Value::Bool(false)) .as_bool() .ok_or("invalid LSP synchronization options")?, ), _ => return Err("invalid LSP synchronization options".into()), }; Ok(SyncOptions { open_close, change, save, include_text, }) } struct Transport { child: Child, #[cfg(windows)] job: WindowsJob, #[cfg(unix)] stdin: ChildStdin, #[cfg(unix)] stdout: ChildStdout, #[cfg(unix)] stderr: ChildStderr, #[cfg(not(unix))] portable: PortablePipes, incoming: Vec, consumed: usize, writes: VecDeque>, #[cfg(unix)] written: usize, } impl Transport { fn spawn(argv: &[String], root: &Path) -> Result { let program = argv .first() .filter(|program| !program.is_empty()) .ok_or("LSP command argv is empty")?; let mut command = Command::new(program); command .args(&argv[1..]) .current_dir(root) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); #[cfg(unix)] { use std::os::unix::process::CommandExt; command.process_group(0); } let mut child = command.spawn().map_err(|error| error.to_string())?; #[cfg(windows)] let job = match WindowsJob::new(&child) { Ok(job) => job, Err(error) => { let _ = child.kill(); let _ = child.wait(); return Err(error); } }; let stdin = child.stdin.take().expect("piped LSP stdin"); let stdout = child.stdout.take().expect("piped LSP stdout"); let stderr = child.stderr.take().expect("piped LSP stderr"); let transport = Self { child, #[cfg(windows)] job, #[cfg(unix)] stdin, #[cfg(unix)] stdout, #[cfg(unix)] stderr, #[cfg(not(unix))] portable: PortablePipes::new(stdin, stdout, stderr), incoming: Vec::new(), consumed: 0, writes: VecDeque::new(), #[cfg(unix)] written: 0, }; #[cfg(unix)] { nonblocking(&transport.stdin)?; nonblocking(&transport.stdout)?; nonblocking(&transport.stderr)?; } Ok(transport) } fn send(&mut self, frame: Value) -> Result<(), String> { if self.writes.len() == QUEUE_SIZE { return Err("LSP outgoing queue is full".into()); } let body = serde_json::to_vec(&frame).map_err(|error| error.to_string())?; if body.len() > MAX_BODY { return Err("LSP outgoing frame exceeds 4 MiB".into()); } let mut bytes = format!("Content-Length: {}\r\n\r\n", body.len()).into_bytes(); bytes.extend_from_slice(&body); self.writes.push_back(bytes); Ok(()) } #[cfg(unix)] fn flush(&mut self) -> Result<(), String> { for _ in 0..QUEUE_SIZE { let Some(bytes) = self.writes.front() else { break; }; let result = self.stdin.write(&bytes[self.written..]); match result { Ok(0) => return Err("LSP server closed stdin".into()), Ok(count) => { self.written += count; if self.written == bytes.len() { self.writes.pop_front(); self.written = 0; } } Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break, Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, Err(error) => return Err(error.to_string()), } } Ok(()) } #[cfg(not(unix))] fn flush(&mut self) -> Result<(), String> { for _ in 0..QUEUE_SIZE { let Some(bytes) = self.writes.pop_front() else { break; }; self.portable.pending.fetch_add(1, Ordering::AcqRel); match self .portable .writes .as_ref() .ok_or("LSP writer unavailable")? .try_send(bytes) { Ok(()) => {} Err(TrySendError::Full(bytes)) => { self.portable.pending.fetch_sub(1, Ordering::Release); self.writes.push_front(bytes); break; } Err(TrySendError::Disconnected(_)) => { self.portable.pending.fetch_sub(1, Ordering::Release); return Err("LSP server closed stdin".into()); } } } Ok(()) } fn flushed(&self) -> bool { #[cfg(unix)] { self.writes.is_empty() } #[cfg(not(unix))] { self.writes.is_empty() && self.portable.pending.load(Ordering::Acquire) == 0 } } fn read(&mut self) -> Result<(), String> { if self.consumed != 0 { self.incoming.drain(..self.consumed); self.consumed = 0; } let mut bytes = [0; 8192]; for _ in 0..8 { if self.incoming.len() >= MAX_BODY + MAX_HEADER { break; } let remaining = (MAX_BODY + MAX_HEADER - self.incoming.len()).min(bytes.len()); #[cfg(unix)] let result = self.stdout.read(&mut bytes[..remaining]); #[cfg(not(unix))] let result = self.portable.read(&mut bytes[..remaining]); match result { Ok(0) => return Err("LSP server exited or truncated a frame".into()), Ok(count) => self.incoming.extend_from_slice(&bytes[..count]), Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break, Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, Err(error) => return Err(error.to_string()), } } Ok(()) } fn frame(&mut self) -> Result, String> { let bytes = &self.incoming[self.consumed..]; let Some(end) = bytes.windows(4).position(|bytes| bytes == b"\r\n\r\n") else { if bytes.len() > MAX_HEADER { return Err("LSP headers exceed 16 KiB".into()); } return Ok(None); }; if end + 4 > MAX_HEADER { return Err("LSP headers exceed 16 KiB".into()); } let header = &bytes[..end]; if !header.is_ascii() { return Err("LSP headers must be ASCII".into()); } let header = std::str::from_utf8(header).map_err(|_| "invalid LSP header")?; let mut length = None; for line in header.split("\r\n") { let (name, value) = line.split_once(':').ok_or("invalid LSP header")?; if name.is_empty() || line.contains(['\r', '\n']) { return Err("invalid LSP header newline".into()); } let value = value.trim(); if name.eq_ignore_ascii_case("content-length") { if length.is_some() || value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) { return Err("invalid LSP Content-Length".into()); } length = Some( value .parse::() .map_err(|_| "invalid LSP Content-Length")?, ); } else if name.eq_ignore_ascii_case("content-type") { for parameter in value.split(';').skip(1) { if let Some((name, encoding)) = parameter.trim().split_once('=') && name.trim().eq_ignore_ascii_case("charset") && !["utf8", "utf-8"] .iter() .any(|supported| encoding.trim().eq_ignore_ascii_case(supported)) { return Err("unsupported LSP content encoding".into()); } } } } let length = length .filter(|length| (1..=MAX_BODY).contains(length)) .ok_or("invalid LSP body length")?; let body_start = end + 4; if bytes.len() < body_start + length { return Ok(None); } let frame = serde_json::from_slice(&bytes[body_start..body_start + length]) .map_err(|error| format!("invalid LSP JSON: {error}"))?; self.consumed += body_start + length; Ok(Some(frame)) } #[cfg(unix)] fn drain_stderr(&mut self) -> Result<(), String> { let mut bytes = [0; 8192]; for _ in 0..8 { match self.stderr.read(&mut bytes) { Ok(0) => break, Ok(_) => {} Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break, Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, Err(error) => return Err(error.to_string()), } } Ok(()) } #[cfg(not(unix))] fn drain_stderr(&mut self) -> Result<(), String> { match self.portable.failures.try_recv() { Ok(reason) => Err(reason), Err(_) => Ok(()), } } fn check_alive(&mut self) -> Result<(), String> { if let Some(status) = self.child.try_wait().map_err(|error| error.to_string())? { return Err(format!("LSP server exited: {status}")); } Ok(()) } } impl Drop for Transport { fn drop(&mut self) { // This runs only on the already bounded server worker, never on input or // poll. Kill the entire group even if the direct child has already exited. #[cfg(unix)] unsafe { kill(-(self.child.id() as i32), 9); } #[cfg(windows)] self.job.terminate(); let _ = self.child.kill(); let _ = self.child.wait(); #[cfg(not(unix))] self.portable.stop(); } } // Anonymous Windows pipes cannot be made nonblocking through std. Three bounded // pipe workers keep the same framed transport off the editor and server workers. #[cfg(not(unix))] struct PortablePipes { writes: Option>>, reads: Option>>, failures: Receiver, chunk: Vec, consumed: usize, pending: Arc, workers: Vec>, } #[cfg(not(unix))] impl PortablePipes { fn new(mut stdin: ChildStdin, mut stdout: ChildStdout, mut stderr: ChildStderr) -> Self { let (writes, outgoing) = mpsc::sync_channel::>(QUEUE_SIZE); let (incoming, reads) = mpsc::sync_channel(QUEUE_SIZE); let (failed, failures) = mpsc::sync_channel(1); let pending = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let written = Arc::clone(&pending); let write_failed = failed.clone(); let writer = thread::spawn(move || { while let Ok(bytes) = outgoing.recv() { let result = stdin.write_all(&bytes).and_then(|()| stdin.flush()); written.fetch_sub(1, Ordering::Release); if let Err(error) = result { let _ = write_failed.try_send(error.to_string()); break; } } }); let reader = thread::spawn(move || { loop { let mut bytes = vec![0; 8192]; match stdout.read(&mut bytes) { Ok(0) => { let _ = failed.try_send("LSP server exited or truncated a frame".into()); break; } Ok(count) => { bytes.truncate(count); if incoming.send(bytes).is_err() { break; } } Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, Err(error) => { let _ = failed.try_send(error.to_string()); break; } } } }); let drain = thread::spawn(move || { let mut bytes = [0; 8192]; loop { match stderr.read(&mut bytes) { Ok(0) => break, Ok(_) => {} Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, Err(_) => break, } } }); Self { writes: Some(writes), reads: Some(reads), failures, chunk: Vec::new(), consumed: 0, pending, workers: vec![writer, reader, drain], } } fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { if self.consumed == self.chunk.len() { self.chunk = match self .reads .as_ref() .ok_or(std::io::ErrorKind::BrokenPipe)? .try_recv() { Ok(bytes) => bytes, Err(TryRecvError::Empty) => return Err(std::io::ErrorKind::WouldBlock.into()), Err(TryRecvError::Disconnected) => return Ok(0), }; self.consumed = 0; } let count = buffer.len().min(self.chunk.len() - self.consumed); buffer[..count].copy_from_slice(&self.chunk[self.consumed..self.consumed + count]); self.consumed += count; Ok(count) } fn stop(&mut self) { self.writes.take(); self.reads.take(); for worker in self.workers.drain(..) { let _ = worker.join(); } } } #[cfg(windows)] struct WindowsJob(*mut std::ffi::c_void); #[cfg(windows)] impl WindowsJob { fn new(child: &Child) -> Result { use std::os::windows::io::AsRawHandle; let handle = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) }; if handle.is_null() { return Err(std::io::Error::last_os_error().to_string()); } let job = Self(handle); let mut limits: JobLimits = unsafe { std::mem::zeroed() }; limits.basic.flags = 0x2000; // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. if unsafe { SetInformationJobObject( handle, 9, &limits as *const _ as *const std::ffi::c_void, std::mem::size_of::() as u32, ) == 0 || AssignProcessToJobObject(handle, child.as_raw_handle()) == 0 } { return Err(std::io::Error::last_os_error().to_string()); } Ok(job) } fn terminate(&self) { unsafe { TerminateJobObject(self.0, 1); } } } #[cfg(windows)] impl Drop for WindowsJob { fn drop(&mut self) { unsafe { CloseHandle(self.0); } } } #[cfg(windows)] #[repr(C)] struct BasicJobLimits { process_time: i64, job_time: i64, flags: u32, min_working_set: usize, max_working_set: usize, active_processes: u32, affinity: usize, priority: u32, scheduling: u32, } #[cfg(windows)] #[repr(C)] struct JobLimits { basic: BasicJobLimits, io: [u64; 6], process_memory: usize, job_memory: usize, peak_process_memory: usize, peak_job_memory: usize, } #[cfg(windows)] #[link(name = "kernel32")] unsafe extern "system" { fn CreateJobObjectW( attributes: *const std::ffi::c_void, name: *const u16, ) -> *mut std::ffi::c_void; fn SetInformationJobObject( job: *mut std::ffi::c_void, class: u32, information: *const std::ffi::c_void, length: u32, ) -> i32; fn AssignProcessToJobObject(job: *mut std::ffi::c_void, process: *mut std::ffi::c_void) -> i32; fn TerminateJobObject(job: *mut std::ffi::c_void, code: u32) -> i32; fn CloseHandle(handle: *mut std::ffi::c_void) -> i32; } #[cfg(unix)] fn nonblocking(pipe: &impl std::os::fd::AsRawFd) -> Result<(), String> { #[cfg(any(target_os = "linux", target_os = "android"))] const O_NONBLOCK: i32 = 0x800; #[cfg(not(any(target_os = "linux", target_os = "android")))] const O_NONBLOCK: i32 = 4; let fd = pipe.as_raw_fd(); unsafe { let flags = fcntl(fd, 3); if flags == -1 || fcntl(fd, 4, flags | O_NONBLOCK) == -1 { return Err(std::io::Error::last_os_error().to_string()); } } Ok(()) } #[cfg(unix)] unsafe extern "C" { fn fcntl(fd: i32, command: i32, ...) -> i32; fn kill(pid: i32, signal: i32) -> i32; } fn message(text: &str, request_id: Option) -> PluginUpdate { PluginUpdate { request_id, effects: vec![UpdateEffect::Message { text: format!("LSP: {}", clean(text)), }], } } fn hover_text(result: &Value, source: &str) -> Result, String> { if result.is_null() { return Ok(None); } let object = result.as_object().ok_or("invalid LSP hover result")?; if let Some(value) = object.get("range") { range(&normalized(source), value)?; } let contents = object.get("contents").ok_or("missing LSP hover contents")?; let mut text = String::new(); match contents { Value::Array(items) => { for item in items { append_hover(&mut text, marked_string(item)?)?; } } Value::Object(object) if object.contains_key("kind") => { match object.get("kind").and_then(Value::as_str) { Some("plaintext" | "markdown") => {} _ => return Err("invalid LSP hover markup kind".into()), } append_hover( &mut text, object .get("value") .and_then(Value::as_str) .ok_or("invalid LSP hover markup value")?, )?; } value => append_hover(&mut text, marked_string(value)?)?, } Ok((!text.trim().is_empty()).then_some(text)) } fn marked_string(value: &Value) -> Result<&str, String> { match value { Value::String(text) => Ok(text), Value::Object(object) if object.get("language").and_then(Value::as_str).is_some() => object .get("value") .and_then(Value::as_str) .ok_or_else(|| "invalid LSP marked string value".into()), _ => Err("invalid LSP marked string".into()), } } fn append_hover(text: &mut String, value: &str) -> Result<(), String> { if value.is_empty() { return Ok(()); } if !text.is_empty() { if text.len() + 2 > MAX_BODY { return Err("LSP hover contents exceed 4 MiB".into()); } text.push_str("\n\n"); } let mut characters = value.chars().peekable(); while let Some(character) = characters.next() { let character = match character { '\r' => { if characters.peek() == Some(&'\n') { characters.next(); } '\n' } '\n' | '\t' => character, character if character.is_control() => ' ', character => character, }; let length = if character == '\t' { 4 } else { character.len_utf8() }; if text.len() + length > MAX_BODY { return Err("LSP hover contents exceed 4 MiB".into()); } if character == '\t' { text.push_str(" "); } else { text.push(character); } } Ok(()) } fn clean(text: &str) -> String { text.chars() .map(|character| { if character.is_control() { ' ' } else { character } }) .collect() } fn normalized(text: &str) -> std::borrow::Cow<'_, str> { if text.contains('\r') { std::borrow::Cow::Owned(text.replace("\r\n", "\n").replace('\r', "\n")) } else { std::borrow::Cow::Borrowed(text) } } fn completion_string<'a>(value: &'a Value, field: &str) -> Result, String> { value .get(field) .map(|value| { value .as_str() .ok_or_else(|| format!("invalid completion {field}")) }) .transpose() } fn completion_text(text: &str, multiline: bool) -> Result<(), String> { if text .chars() .any(|ch| ch.is_control() && !(multiline && matches!(ch, '\n' | '\r' | '\t'))) { return Err("unsupported control character in completion".into()); } Ok(()) } fn completion_position(lines: &[&str], value: &Value) -> Result { let (row, units) = coordinates(value)?; let line = lines .get(row) .ok_or("completion line is outside the document")?; let mut offset = 0; let mut column = 0; for cluster in line.graphemes(true) { if units == offset { return Ok(TextPosition { row, column }); } offset += cluster.encode_utf16().count(); if units < offset { return Err("completion position splits a grapheme".into()); } column += 1; } if units != offset { return Err("completion position is outside the line".into()); } Ok(TextPosition { row, column }) } fn completion_range(lines: &[&str], value: &Value) -> Result<(TextPosition, TextPosition), String> { let start = completion_position(lines, &value["start"])?; let end = completion_position(lines, &value["end"])?; if (start.row, start.column) > (end.row, end.column) { return Err("reversed completion range".into()); } Ok((start, end)) } fn completion_primary_range( lines: &[&str], value: &Value, source: &Source, ) -> Result<(TextPosition, TextPosition), String> { let (start, end) = completion_range(lines, value)?; if start.row != source.row || end.row != source.row || start.column > source.column || end.column < source.column { return Err( "completion primary range must be single-line and contain the request position".into(), ); } Ok((start, end)) } fn completion_replacement( lines: &[&str], value: &Value, source: &Source, ) -> Result<(TextPosition, TextPosition), String> { if value.get("insert").is_some() || value.get("replace").is_some() { if value.get("range").is_some() || value.get("start").is_some() { return Err("ambiguous completion edit range".into()); } let (start, end) = completion_primary_range(lines, &value["replace"], source)?; let (insert_start, insert_end) = completion_primary_range(lines, &value["insert"], source)?; if insert_start != start || insert_end.column > end.column { return Err("completion insert range is not a prefix of replace range".into()); } Ok((start, end)) } else { completion_primary_range(lines, value.get("range").unwrap_or(value), source) } } fn decode_completion( text: &str, result: &Value, source: &Source, path: Option<&str>, ) -> Result, String> { if result.is_null() { return Ok(Vec::new()); } let (items, defaults) = if let Some(items) = result.as_array() { (items, None) } else { if !result["isIncomplete"].is_boolean() { return Err("invalid completion list isIncomplete".into()); } let defaults = result.get("itemDefaults"); if defaults.is_some_and(|value| !value.is_object()) { return Err("invalid completion itemDefaults".into()); } if let Some(defaults) = defaults { for key in defaults.as_object().unwrap().keys() { if !matches!( key.as_str(), "editRange" | "insertTextFormat" | "insertTextMode" ) { return Err("unsupported completion item default".into()); } } } ( result["items"] .as_array() .ok_or("invalid completion items")?, defaults, ) }; let text = normalized(text); let lines: Vec<_> = text.split('\n').collect(); let line = lines .get(source.row) .ok_or("completion request line is outside the document")?; let mut prefix_start = 0; let mut prefix_byte = 0; let mut caret_byte = line.len(); let mut columns = 0; for (column, (byte, cluster)) in line.grapheme_indices(true).enumerate() { if column == source.column { caret_byte = byte; break; } if !cluster .chars() .next() .is_some_and(|ch| ch.is_alphanumeric() || ch == '_') { prefix_start = column + 1; prefix_byte = byte + cluster.len(); } columns = column + 1; } if source.column > columns { return Err("completion request column is outside the line".into()); } let prefix = &line[prefix_byte..caret_byte]; // The prefix is the only guessed range; explicit server edits remain authoritative. if let Some(defaults) = defaults { if let Some(range) = defaults.get("editRange") { completion_replacement(&lines, range, source)?; } for name in ["insertTextFormat", "insertTextMode"] { if defaults .get(name) .is_some_and(|value| !matches!(value.as_u64(), Some(1 | 2))) { return Err(format!("invalid completion default {name}")); } } } let word_end = caret_byte + line[caret_byte..] .graphemes(true) .take_while(|cluster| { cluster .chars() .next() .is_some_and(|ch| ch.is_alphanumeric() || ch == '_') }) .map(str::len) .sum::(); let mut decoded = Vec::with_capacity(items.len()); let mut bytes = 0usize; for item in items { if !item.is_object() { return Err("invalid completion item".into()); } let label = completion_string(item, "label")?.ok_or("missing completion label")?; completion_text(label, false)?; let detail = completion_string(item, "detail")?; let sort = completion_string(item, "sortText")?.unwrap_or(label); let filter = completion_string(item, "filterText")?; let insert = completion_string(item, "insertText")?; let edit_text = completion_string(item, "textEditText")?; if let Some(detail) = detail { completion_text(detail, true)?; } completion_text(sort, false)?; if let Some(filter) = filter { completion_text(filter, false)?; } let property = |name: &str| { item.get(name) .or_else(|| defaults.and_then(|value| value.get(name))) }; let format = property("insertTextFormat") .map(|value| { value .as_u64() .filter(|value| matches!(value, 1 | 2)) .ok_or("invalid completion insertTextFormat") }) .transpose()? .unwrap_or(1); let mode = property("insertTextMode") .map(|value| { value .as_u64() .filter(|value| matches!(value, 1 | 2)) .ok_or("invalid completion insertTextMode") }) .transpose()? .unwrap_or(1); // No indentation adjustment, server commands or lazy edit resolution. if mode == 2 || item.get("command").is_some() { continue; } let explicit = item.get("textEdit"); let default_range = defaults.and_then(|value| value.get("editRange")); let (start, end, new_text) = if let Some(edit) = explicit { if !edit.is_object() { return Err("invalid completion textEdit".into()); } let (start, end) = completion_replacement(&lines, edit, source)?; let new_text = completion_string(edit, "newText")?.ok_or("missing completion newText")?; (start, end, new_text) } else if let Some(range) = default_range { let (start, end) = completion_replacement(&lines, range, source)?; (start, end, edit_text.or(insert).unwrap_or(label)) } else { ( TextPosition { row: source.row, column: prefix_start, }, TextPosition { row: source.row, column: source.column, }, insert.unwrap_or(label), ) }; let filter_prefix = if explicit.is_some() || default_range.is_some() { let byte = line .grapheme_indices(true) .nth(start.column) .map_or(line.len(), |(byte, _)| byte); &line[byte..caret_byte] } else { prefix }; completion_text(new_text, true)?; let (new_text, snippet) = if format == 2 { match snippet::expand(new_text, |name| { completion_variable(name, line, &line[prefix_byte..word_end], source.row, path) }) { Ok((text, snippet)) => (text, Some(snippet)), Err(snippet::Error::Unsupported) => continue, Err(snippet::Error::Invalid) => return Err("invalid completion snippet".into()), Err(snippet::Error::Limit) => { return Err("completion snippet exceeds expansion limits".into()); } } } else { (normalized(new_text).into_owned(), None) }; completion_text(&new_text, true)?; let mut edits = vec![CompletionEdit { start, end, text: new_text, }]; if let Some(additional) = item.get("additionalTextEdits") { for edit in additional .as_array() .ok_or("invalid completion additionalTextEdits")? { let (start, end) = completion_range(&lines, &edit["range"])?; let new_text = completion_string(edit, "newText")?.ok_or("missing additional edit newText")?; completion_text(new_text, true)?; edits.push(CompletionEdit { start, end, text: normalized(new_text).into_owned(), }); } } edits.sort_unstable_by_key(|edit| { ( (edit.start.row, edit.start.column), (edit.end.row, edit.end.column), ) }); if edits.windows(2).any(|pair| { (pair[0].end.row, pair[0].end.column) > (pair[1].start.row, pair[1].start.column) || pair[0].start == pair[1].start }) { return Err("overlapping completion edits".into()); } let size = label.len() + detail.map_or(0, str::len) + edits.iter().map(|edit| edit.text.len()).sum::(); bytes = bytes .checked_add(size) .filter(|bytes| *bytes <= MAX_BODY) .ok_or("completion text exceeds 4 MiB")?; if (filter.is_some() || (explicit.is_none() && default_range.is_none())) && !filter.unwrap_or(label).starts_with(filter_prefix) { continue; } let primary_edit = edits .iter() .position(|edit| edit.start == start && edit.end == end) .unwrap(); decoded.push(( sort, CompletionItem { label: label.to_owned(), detail: detail.map(str::to_owned), primary_edit, edits, snippet, }, )); } decoded.sort_by(|a, b| a.0.cmp(b.0)); Ok(decoded.into_iter().map(|(_, item)| item).collect()) } fn completion_variable( name: &str, line: &str, word: &str, row: usize, path: Option<&str>, ) -> Option { let value = match name { "TM_SELECTED_TEXT" => "", "TM_CURRENT_LINE" => line, "TM_CURRENT_WORD" => word, "TM_LINE_INDEX" => return Some(row.to_string()), "TM_LINE_NUMBER" => return Some((row + 1).to_string()), "TM_FILEPATH" => path.unwrap_or(""), "TM_FILENAME" => path .and_then(|path| Path::new(path).file_name()) .and_then(|name| name.to_str()) .unwrap_or(""), "TM_FILENAME_BASE" => path .and_then(|path| Path::new(path).file_stem()) .and_then(|name| name.to_str()) .unwrap_or(""), "TM_DIRECTORY" => path .and_then(|path| Path::new(path).parent()) .and_then(|name| name.to_str()) .unwrap_or(""), _ => return None, }; // Context values have the same per-expansion bound as template text. (value.len() <= 256 * 1024).then(|| value.to_owned()) } fn coordinates(value: &Value) -> Result<(usize, usize), String> { let line = value["line"] .as_u64() .filter(|number| *number <= MAX_VERSION) .ok_or("invalid LSP line")?; let character = value["character"] .as_u64() .filter(|number| *number <= MAX_VERSION) .ok_or("invalid LSP character")?; Ok((line as usize, character as usize)) } fn position(text: &str, value: &Value, round_up: bool) -> Result { let (row, units) = coordinates(value)?; let line = text .split('\n') .nth(row) .ok_or("LSP line is outside the document")?; let mut offset = 0; let mut column = 0; for cluster in line.graphemes(true) { if units == offset { return Ok(TextPosition { row, column }); } let width = cluster.encode_utf16().count(); if units < offset + width { return Ok(TextPosition { row, column: column + usize::from(round_up), }); } offset += width; column += 1; } Ok(TextPosition { row, column }) } fn range(text: &str, value: &Value) -> Result<(TextPosition, TextPosition), String> { if coordinates(&value["start"])? > coordinates(&value["end"])? { return Err("reversed LSP range".into()); } Ok(( position(text, &value["start"], false)?, position(text, &value["end"], true)?, )) } fn outbound_position(text: &str, row: usize, column: usize) -> Result { let text = normalized(text); let line = text .split('\n') .nth(row) .ok_or("LSP source row is outside the document")?; let mut units = 0; let mut clusters = line.graphemes(true); for _ in 0..column { units += clusters .next() .ok_or("LSP source column is outside the line")? .encode_utf16() .count(); } Ok(json!({"line": row, "character": units})) } fn absolute(path: &Path, cwd: &Path) -> PathBuf { let joined; let path = if path.is_absolute() { path } else { joined = cwd.join(path); &joined }; let mut result = PathBuf::new(); for component in path.components() { match component { Component::CurDir => {} Component::ParentDir => { result.pop(); } component => result.push(component.as_os_str()), } } result } fn file_uri(path: &Path) -> Result { use std::fmt::Write as _; let path = path.to_str().ok_or("LSP path is not UTF-8")?; #[cfg(windows)] let windows_path = path.replace('\\', "/"); #[cfg(windows)] let path = windows_path.as_str(); let mut uri = String::from("file://"); #[cfg(windows)] if path.as_bytes().get(1) == Some(&b':') { uri.push('/'); } for byte in path.bytes() { if byte.is_ascii_alphanumeric() || b"-._~/".contains(&byte) { uri.push(byte as char); } else { let _ = write!(uri, "%{byte:02X}"); } } Ok(uri) } fn uri_path(uri: &str) -> Result { let unsupported = || format!("unsupported local file URI: {}", clean(uri)); let mut path = uri.strip_prefix("file:").ok_or_else(unsupported)?; if let Some(rest) = path.strip_prefix("//") { let (host, _) = rest.split_once('/').ok_or_else(unsupported)?; if !host.is_empty() && host != "localhost" { return Err(unsupported()); } path = &rest[host.len()..]; } if !path.starts_with('/') || path.contains(['?', '#']) { return Err(unsupported()); } let mut decoded = Vec::with_capacity(path.len()); let bytes = path.as_bytes(); let mut index = 0; while index < bytes.len() { if bytes[index] == b'%' { let pair = bytes.get(index + 1..index + 3).ok_or_else(unsupported)?; let hex = std::str::from_utf8(pair).map_err(|_| unsupported())?; decoded.push(u8::from_str_radix(hex, 16).map_err(|_| unsupported())?); index += 3; } else { decoded.push(bytes[index]); index += 1; } } let path = String::from_utf8(decoded).map_err(|_| "invalid UTF-8 file URI")?; if path.chars().any(char::is_control) { return Err("invalid control character in file URI".into()); } #[cfg(windows)] let path = if path.as_bytes().get(2) == Some(&b':') && path.starts_with('/') { path[1..].to_owned() } else { path }; Ok(absolute(Path::new(&path), Path::new("/"))) } #[cfg(test)] mod tests { use super::*; #[test] fn semantic_decoder_validates_raw_coordinates_before_grapheme_rounding() { let options = semantic_options(Some(&json!({ "full": true, "legend": {"tokenTypes": ["custom", "function"], "tokenModifiers": ["custom", "readonly", "declaration", "async"]} }))) .unwrap() .unwrap(); let tokens = decode_semantic_tokens( "A😀e\u{301}Z\r\n\r\nx", &json!({"data": [ 0, 1, 2, 1, 2, 0, 2, 1, 1, 12, 0, 1, 1, 1, 0, 0, 1, 999, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 100, 1, 0 ]}), &options, ) .unwrap() .unwrap(); assert_eq!(tokens.len(), 4); assert_eq!(tokens[0].start, TextPosition { row: 0, column: 1 }); assert_eq!(tokens[0].end, TextPosition { row: 0, column: 2 }); assert_eq!(tokens[0].modifiers, SemanticTokenModifier::Readonly as u16); assert_eq!(tokens[1].start, TextPosition { row: 0, column: 2 }); assert_eq!(tokens[1].end, TextPosition { row: 0, column: 3 }); assert_eq!( tokens[1].modifiers, SemanticTokenModifier::Declaration as u16 | SemanticTokenModifier::Async as u16 ); assert_eq!(tokens[2].start, tokens[1].start); assert_eq!(tokens[3].end, TextPosition { row: 2, column: 1 }); for data in [ json!([0, 0, 1, 1]), json!([-1, 0, 1, 1, 0]), json!([0, 0.5, 1, 1, 0]), json!([0, 0, 1, 1, 2147483648u64]), json!([0, 0, 1, 65536, 0]), json!([0, 0, 1, 2, 0]), json!([0, 0, 1, 1, 16]), json!([0, 2, 1, 0, 0]), json!([0, 1, 1, 1, 0]), json!([0, 7, 1, 1, 0]), json!([2, 0, 1, 1, 0]), json!([0, 1, 2, 1, 0, 0, 1, 1, 1, 0]), ] { assert!( decode_semantic_tokens("A😀e\u{301}Z\n", &json!({"data": data}), &options).is_err(), "{data}" ); } let count = MAX_BODY / std::mem::size_of::() + 1; assert!( decode_semantic_tokens("x", &json!({"data": vec![0; count * 5]}), &options).is_err() ); } #[test] fn unicode_positions_and_local_uri_boundaries() { let text = "A😀e\u{301}Z\n"; let (start, end) = range( text, &json!({"start": {"line": 0, "character": 2}, "end": {"line": 0, "character": 4}}), ) .unwrap(); assert_eq!(start, TextPosition { row: 0, column: 1 }); assert_eq!(end, TextPosition { row: 0, column: 3 }); assert_eq!( outbound_position(text, 0, 3).unwrap(), json!({"line": 0, "character": 5}) ); assert_eq!( position(text, &json!({"line": 1, "character": 999}), false).unwrap(), TextPosition { row: 1, column: 0 } ); assert!( range( text, &json!({"start": {"line": 0, "character": 3}, "end": {"line": 0, "character": 2}}) ) .is_err() ); let path = Path::new("/tmp/source space λ😀.rs"); assert_eq!(uri_path(&file_uri(path).unwrap()).unwrap(), path); assert_eq!( uri_path("file://localhost/tmp/a%23b").unwrap(), Path::new("/tmp/a#b") ); for uri in [ "https://example.invalid/a", "file://remote/tmp/a", "file:///tmp/%", "file:///tmp/%00", "file:///tmp/%FF", "file:///tmp/a?query", ] { assert!(uri_path(uri).is_err(), "{uri}"); } } #[test] fn hover_contents_keep_lines_and_reject_malformed_values() { let source = "A😀e\u{301}Z\r\n"; for kind in ["plaintext", "markdown"] { assert_eq!( hover_text( &json!({ "contents": {"kind": kind, "value": "😀e\u{301}\r\n\tcode\rnext\u{1b}\u{85}"}, "range": {"start": {"line": 0, "character": 2}, "end": {"line": 0, "character": 4}} }), source, ) .unwrap(), Some("😀e\u{301}\n code\nnext ".into()), ); } for contents in [ json!("line\ncode"), json!({"language": "rust", "value": "line\ncode"}), ] { assert_eq!( hover_text(&json!({"contents": contents}), source).unwrap(), Some("line\ncode".into()), ); } assert_eq!( hover_text( &json!({"contents": ["first", {"language": "rust", "value": "fn main() {\n}"}]}), source, ) .unwrap(), Some("first\n\nfn main() {\n}".into()), ); for value in [ Value::Null, json!({"contents": ""}), json!({"contents": []}), ] { assert_eq!(hover_text(&value, source).unwrap(), None); } for value in [ json!({}), json!({"contents": {"kind": "html", "value": "x"}}), json!({"contents": {"language": "rust", "value": false}}), json!({"contents": [{"kind": "markdown", "value": "x"}]}), json!({"contents": "x", "range": {"start": {"line": 1, "character": 0}, "end": {"line": 0, "character": 0}}}), json!({"contents": "\t".repeat(MAX_BODY / 4 + 1)}), ] { assert!(hover_text(&value, source).is_err()); } } #[test] fn completion_text_limit_counts_the_atomic_batch_not_sort_metadata() { let source = Source { buffer_id: 1, revision: 0, row: 0, column: 0, request_id: 1, }; let at_limit = json!([{"label": "x", "insertText": "x".repeat(MAX_BODY - 1)}]); assert_eq!( decode_completion("", &at_limit, &source, None).unwrap()[0].edits[0] .text .len(), MAX_BODY - 1 ); let over_limit = json!([ {"label": "x", "insertText": "x".repeat(MAX_BODY - 1)}, {"label": "y"} ]); assert!( decode_completion("", &over_limit, &source, None) .unwrap_err() .contains("4 MiB") ); } }