3159 lines
110 KiB
Rust
3159 lines
110 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,
|
|
},
|
|
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 {
|
|
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,
|
|
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()
|
|
}
|
|
|
|
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();
|
|
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<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::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,
|
|
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.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),
|
|
Hover(Source),
|
|
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),
|
|
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<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) => {
|
|
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<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, 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<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")?,
|
|
)?;
|
|
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<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(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<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 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")
|
|
);
|
|
}
|
|
}
|