Files
vamp/src/lsp.rs
T
2026-10-16 18:52:50 +02:00

3242 lines
113 KiB
Rust

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, Utf16Range,
},
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<String>,
pub extensions: Vec<String>,
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<DocumentSnapshot>,
servers: Vec<ServerKey>,
diagnostics: HashMap<usize, Contribution>,
semantic_tokens: Option<Arc<[SemanticToken]>>,
}
struct Contribution {
items: Vec<Diagnostic>,
bytes: usize,
}
enum Output {
Ready(String),
Update(PluginUpdate),
Diagnostics {
snapshot: Arc<DocumentSnapshot>,
items: Vec<Diagnostic>,
},
SemanticTokens {
snapshot: Arc<DocumentSnapshot>,
tokens: Option<Arc<[SemanticToken]>>,
},
}
struct Worker {
input: SyncSender<Input>,
output: Receiver<Output>,
failure: Receiver<String>,
cancelled: Arc<AtomicBool>,
retired: bool,
name: Option<String>,
thread: Option<thread::JoinHandle<()>>,
}
enum Input {
Document {
snapshot: Arc<DocumentSnapshot>,
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<Arc<ServerConfig>>,
cwd: PathBuf,
documents: HashMap<usize, CachedDocument>,
servers: BTreeMap<ServerKey, Worker>,
updates: Vec<PluginUpdate>,
changed_diagnostics: HashSet<usize>,
changed_semantics: HashSet<usize>,
}
impl Lsp {
pub fn new(configs: Vec<ServerConfig>) -> 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()
}
/// Ready servers assigned to this document, in configured stack order.
pub fn active_server_names(&self, buffer_id: usize) -> impl Iterator<Item = &str> {
self.documents
.get(&buffer_id)
.into_iter()
.flat_map(|doc| &doc.servers)
.filter_map(|key| {
self.servers
.get(key)
.filter(|worker| !worker.retired)
.and_then(|worker| worker.name.as_deref())
})
}
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::<Vec<_>>(),
)
})
.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();
if cached.semantic_tokens.take().is_some() {
// Discard queued old coordinates, not explicit clears such as retirement.
// The editor keeps remapped colors until a server reply replaces them.
self.changed_semantics.remove(&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<PluginUpdate> {
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::Ready(name) => worker.name = Some(name),
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::<usize>();
let siblings = doc
.diagnostics
.iter()
.filter(|(config, _)| **config != key.config)
.map(|(_, contribution)| contribution.bytes)
.sum::<usize>();
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,
name: None,
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<u64>,
) -> 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<usize, CachedDocument>,
changed: &mut HashSet<usize>,
changed_semantics: &mut HashSet<usize>,
updates: &mut Vec<PluginUpdate>,
reason: String,
) {
if worker.retired {
return;
}
worker.retired = true;
worker.name = None;
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<DocumentSnapshot>,
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, Arc<DocumentSnapshot>),
Hover(Source, Arc<DocumentSnapshot>),
Completion(Source, Arc<DocumentSnapshot>),
SemanticTokens(Arc<DocumentSnapshot>),
Shutdown,
}
struct Runtime {
config: Arc<ServerConfig>,
root: PathBuf,
input: Receiver<Input>,
output: SyncSender<Output>,
cancelled: Arc<AtomicBool>,
transport: Transport,
snapshots: HashMap<usize, Arc<DocumentSnapshot>>,
documents: HashMap<usize, Document>,
requests: HashMap<u64, Request>,
pending: VecDeque<(PositionKind, Source)>,
semantic_inflight: HashSet<usize>,
next_id: u64,
ready: bool,
definition: bool,
hover: bool,
completion: bool,
semantic: Option<SemanticOptions>,
sync: SyncOptions,
}
impl Runtime {
fn new(
config: Arc<ServerConfig>,
root: PathBuf,
input: Receiver<Input>,
output: SyncSender<Output>,
cancelled: Arc<AtomicBool>,
) -> Result<Self, String> {
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::<Vec<_>>(),
"tokenModifiers": SEMANTIC_MODIFIERS.iter().map(|(name, _)| *name).collect::<Vec<_>>(),
"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<DocumentSnapshot>,
items: Vec<Diagnostic>,
) -> Result<(), String> {
self.send(Output::Diagnostics {
snapshot: Arc::clone(snapshot),
items,
})
}
fn emit_semantics(
&self,
snapshot: &Arc<DocumentSnapshot>,
tokens: Option<Arc<[SemanticToken]>>,
) -> 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<DocumentSnapshot>,
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, Arc::clone(&doc.snapshot)),
PositionKind::Hover => Request::Hover(source, Arc::clone(&doc.snapshot)),
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<DocumentSnapshot>,
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<DocumentSnapshot>,
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<Value>,
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<Value>) -> 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, snapshot) => {
self.hover_result(source, snapshot, 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, snapshot) => self.hover_result(
source,
snapshot,
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, snapshot) => {
self.definition_result(source, snapshot, Err(text.to_owned()))
}
Request::Hover(source, snapshot) => {
self.hover_result(source, snapshot, 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, snapshot) => {
self.definition_result(source, snapshot, Ok(&frame["result"]))
}
Request::Hover(source, snapshot) => {
self.hover_result(source, snapshot, 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 name = result["serverInfo"]["name"]
.as_str()
.filter(|name| !name.chars().any(char::is_control))
.map(str::trim)
.filter(|name| !name.is_empty())
.unwrap_or_else(|| {
Path::new(&self.config.command[0])
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(&self.config.command[0])
});
self.send(Output::Ready(clean(name)))?;
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<Value, String> {
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,
snapshot: Arc<DocumentSnapshot>,
result: Result<&Value, String>,
) -> Result<(), String> {
if self
.documents
.get(&source.buffer_id)
.is_none_or(|doc| !Arc::ptr_eq(&doc.snapshot, &snapshot))
{
return Ok(());
}
match result.and_then(|result| Self::definition_effects(&source, 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,
snapshot: Arc<DocumentSnapshot>,
result: Result<&Value, String>,
) -> Result<(), String> {
let Some(doc) = self
.documents
.get(&source.buffer_id)
.filter(|doc| Arc::ptr_eq(&doc.snapshot, &snapshot))
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(source: &Source, result: &Value) -> Result<Vec<UpdateEffect>, 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")?,
)?;
// Keep the server's document identity and UTF-16 coordinates together.
// The editor resolves historical/working text, including lazily loaded
// comparison files, before converting the target position.
let target_range = link
.then(|| range_coordinates(&location["targetRange"]))
.transpose()?;
let range = range_coordinates(
&location[if link {
"targetSelectionRange"
} else {
"range"
}],
)?;
let mut effects = vec![UpdateEffect::NavigateUtf16 {
buffer_id: source.buffer_id,
revision: source.revision,
path,
range,
target_range,
}];
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<Option<SemanticTokenKind>>,
modifiers: [u16; 31],
modifier_mask: u32,
}
fn semantic_options(value: Option<&Value>) -> Result<Option<SemanticOptions>, 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<bool, String> {
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::<Result<Vec<_>, 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<usize, String> {
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<Option<Arc<[SemanticToken]>>, 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::<SemanticToken>() {
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<usize, String> {
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<SyncOptions, String> {
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<u8>,
consumed: usize,
writes: VecDeque<Vec<u8>>,
#[cfg(unix)]
written: usize,
}
impl Transport {
fn spawn(argv: &[String], root: &Path) -> Result<Self, String> {
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<Option<Value>, 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::<usize>()
.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<SyncSender<Vec<u8>>>,
reads: Option<Receiver<Vec<u8>>>,
failures: Receiver<String>,
chunk: Vec<u8>,
consumed: usize,
pending: Arc<std::sync::atomic::AtomicUsize>,
workers: Vec<thread::JoinHandle<()>>,
}
#[cfg(not(unix))]
impl PortablePipes {
fn new(mut stdin: ChildStdin, mut stdout: ChildStdout, mut stderr: ChildStderr) -> Self {
let (writes, outgoing) = mpsc::sync_channel::<Vec<u8>>(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<usize> {
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<Self, String> {
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::<JobLimits>() 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<u64>) -> PluginUpdate {
PluginUpdate {
request_id,
effects: vec![UpdateEffect::Message {
text: format!("LSP: {}", clean(text)),
}],
}
}
fn hover_text(result: &Value, source: &str) -> Result<Option<String>, 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<Option<&'a str>, 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<TextPosition, String> {
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<Vec<CompletionItem>, 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::<usize>();
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::<usize>();
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<String> {
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<TextPosition, String> {
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_coordinates(value: &Value) -> Result<Utf16Range, String> {
let start = coordinates(&value["start"])?;
let end = coordinates(&value["end"])?;
if start > end {
return Err("reversed LSP range".into());
}
Ok(Utf16Range { start, end })
}
fn range(text: &str, value: &Value) -> Result<(TextPosition, TextPosition), String> {
range_coordinates(value)?;
Ok((
position(text, &value["start"], false)?,
position(text, &value["end"], true)?,
))
}
fn outbound_position(text: &str, row: usize, column: usize) -> Result<Value, String> {
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<String, String> {
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<PathBuf, String> {
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 native_definitions_preserve_snapshot_identity_and_utf16_until_navigation() {
let source = Source {
buffer_id: 7,
revision: 3,
row: 0,
column: 0,
request_id: 11,
};
let path = Path::new("/vamp-immutable-snapshot/Before/renamed/λ.rs");
let span = json!({
"start": {"line": 4, "character": 5},
"end": {"line": 4, "character": 8}
});
for location in [
json!({"uri": file_uri(path).unwrap(), "range": span}),
json!({"targetUri": file_uri(path).unwrap(),
"targetRange": span, "targetSelectionRange": span}),
] {
let effects = Runtime::definition_effects(&source, &location).unwrap();
let expected_target_range = location.get("targetUri").map(|_| Utf16Range {
start: (4, 5),
end: (4, 8),
});
assert!(matches!(
effects.as_slice(),
[UpdateEffect::NavigateUtf16 {
buffer_id: 7, revision: 3, path: target,
range: Utf16Range { start: (4, 5), end: (4, 8) }, target_range,
}] if target == path && *target_range == expected_target_range
));
}
for invalid in [
json!({"start": {"line": 4, "character": 8},
"end": {"line": 4, "character": 5}}),
json!({"start": {"line": -1, "character": 0},
"end": {"line": 0, "character": 0}}),
] {
for location in [
json!({"uri": file_uri(path).unwrap(), "range": invalid}),
json!({"targetUri": file_uri(path).unwrap(),
"targetRange": invalid, "targetSelectionRange": span}),
json!({"targetUri": file_uri(path).unwrap(),
"targetRange": span, "targetSelectionRange": invalid}),
] {
assert!(Runtime::definition_effects(&source, &location).is_err());
}
}
assert!(
serde_json::from_value::<UpdateEffect>(json!({
"type": "navigate_utf16", "buffer_id": 7, "revision": 3,
"path": path, "range": {"start": [4,5], "end": [4,8]},
"target_range": null
}))
.is_err()
);
}
#[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::<SemanticToken>() + 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")
);
}
}