feat: preconfigure Python with Astral ty and Ruff

This commit is contained in:
2026-10-09 21:11:07 +02:00
parent 2c3c73e5b9
commit 548711980c
9 changed files with 1251 additions and 127 deletions
+211 -72
View File
@@ -1,5 +1,5 @@
use std::{
collections::{HashMap, VecDeque},
collections::{BTreeMap, HashMap, HashSet, VecDeque},
io::{Read, Write},
path::{Component, Path, PathBuf},
process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, Stdio},
@@ -39,7 +39,7 @@ pub struct ServerConfig {
pub settings: Value,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct ServerKey {
config: usize,
root: PathBuf,
@@ -47,12 +47,26 @@ struct ServerKey {
struct CachedDocument {
snapshot: Arc<DocumentSnapshot>,
server: Option<ServerKey>,
servers: Vec<ServerKey>,
diagnostics: HashMap<usize, Contribution>,
}
struct Contribution {
items: Vec<Diagnostic>,
bytes: usize,
}
enum Output {
Update(PluginUpdate),
Diagnostics {
snapshot: Arc<DocumentSnapshot>,
items: Vec<Diagnostic>,
},
}
struct Worker {
input: SyncSender<Input>,
output: Receiver<PluginUpdate>,
output: Receiver<Output>,
failure: Receiver<String>,
cancelled: Arc<AtomicBool>,
retired: bool,
@@ -98,8 +112,9 @@ pub struct Lsp {
configs: Vec<Arc<ServerConfig>>,
cwd: PathBuf,
documents: HashMap<usize, CachedDocument>,
servers: HashMap<ServerKey, Worker>,
servers: BTreeMap<ServerKey, Worker>,
updates: Vec<PluginUpdate>,
changed_diagnostics: HashSet<usize>,
}
impl Lsp {
@@ -108,8 +123,9 @@ impl Lsp {
configs: configs.into_iter().map(Arc::new).collect(),
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
documents: HashMap::new(),
servers: HashMap::new(),
servers: BTreeMap::new(),
updates: Vec::new(),
changed_diagnostics: HashSet::new(),
}
}
@@ -133,31 +149,65 @@ impl Lsp {
.map(|cached| Arc::clone(&cached.snapshot))
.unwrap_or_else(|| Arc::new(event.document.clone()));
let mut startup_error = None;
let key = snapshot.path.as_ref().and_then(|path| {
let extension = Path::new(path).extension()?.to_str()?;
let config = self.configs.iter().position(|config| {
config
.extensions
.iter()
.any(|candidate| candidate == extension)
})?;
Some(ServerKey {
config,
root: absolute(Path::new(&snapshot.root), &self.cwd),
})
});
if matches!(event.event, DocumentEventKind::Close) {
self.documents.remove(&snapshot.id);
} else {
self.changed_diagnostics.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),
server: key.clone(),
servers: keys,
diagnostics: HashMap::new(),
},
);
if let Some(key) = &key {
startup_error = self.start(key).err();
self.changed_diagnostics.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();
self.changed_diagnostics.insert(snapshot.id);
}
}
let mut failures = Vec::new();
@@ -170,7 +220,10 @@ impl Lsp {
.try_send(Input::Document {
snapshot: Arc::clone(&snapshot),
event: event.event,
served: key.as_ref() == Some(server_key),
served: self
.documents
.get(&snapshot.id)
.is_some_and(|doc| doc.servers.contains(server_key)),
})
.is_err()
{
@@ -245,13 +298,85 @@ impl Lsp {
for (key, reason) in failures {
self.retire(&key, reason);
}
let mut updates = std::mem::take(&mut self.updates);
for worker in self.servers.values() {
if !worker.retired {
updates.extend(worker.output.try_iter().take(QUEUE_SIZE));
// 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.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);
}
}
}
}
updates
let 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();
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> {
@@ -271,7 +396,7 @@ impl Lsp {
let initial: Vec<_> = self
.documents
.values()
.map(|doc| (Arc::clone(&doc.snapshot), doc.server.as_ref() == Some(key)))
.map(|doc| (Arc::clone(&doc.snapshot), doc.servers.contains(key)))
.collect();
let started = thread::Builder::new()
.name(format!("lsp-{}", config.language_id))
@@ -328,7 +453,7 @@ impl Lsp {
let key = self
.documents
.get(&buffer_id)
.and_then(|doc| doc.server.clone());
.and_then(|doc| doc.servers.first().cloned());
if let Some(key) = key
&& let Some(worker) = self.servers.get(&key)
&& !worker.retired
@@ -353,23 +478,39 @@ impl Lsp {
let Some(worker) = self.servers.get_mut(key) else {
return;
};
Self::retire_worker(
key,
worker,
&mut self.documents,
&mut self.changed_diagnostics,
&mut self.updates,
reason,
);
}
fn retire_worker(
key: &ServerKey,
worker: &mut Worker,
documents: &mut HashMap<usize, CachedDocument>,
changed: &mut HashSet<usize>,
updates: &mut Vec<PluginUpdate>,
reason: String,
) {
if worker.retired {
return;
}
worker.retired = true;
worker.cancelled.store(true, Ordering::Release);
let mut effects: Vec<_> = self
.documents
.values()
.filter(|doc| doc.server.as_ref() == Some(key))
.map(|doc| clear(&doc.snapshot))
.collect();
effects.push(UpdateEffect::Message { text: reason });
// At most one retirement update per bounded worker, independently of the
// command-message queue, so a full queue cannot hide diagnostic cleanup.
self.updates.push(PluginUpdate {
for (id, doc) in documents {
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,
effects: vec![UpdateEffect::Message { text: reason }],
});
}
}
@@ -415,7 +556,7 @@ struct Runtime {
config: Arc<ServerConfig>,
root: PathBuf,
input: Receiver<Input>,
output: SyncSender<PluginUpdate>,
output: SyncSender<Output>,
cancelled: Arc<AtomicBool>,
transport: Transport,
snapshots: HashMap<usize, Arc<DocumentSnapshot>>,
@@ -434,7 +575,7 @@ impl Runtime {
config: Arc<ServerConfig>,
root: PathBuf,
input: Receiver<Input>,
output: SyncSender<PluginUpdate>,
output: SyncSender<Output>,
cancelled: Arc<AtomicBool>,
) -> Result<Self, String> {
let transport = Transport::spawn(&config.command, &root)?;
@@ -528,7 +669,22 @@ impl Runtime {
}
fn emit(&self, update: PluginUpdate) -> Result<(), String> {
self.output.try_send(update).map_err(|error| match error {
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 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(),
})
@@ -553,10 +709,7 @@ impl Runtime {
if !served {
if let Some(old) = old {
self.close(&old)?;
self.emit(PluginUpdate {
request_id: None,
effects: vec![clear(&snapshot)],
})?;
self.emit_diagnostics(&snapshot, Vec::new())?;
}
return Ok(());
}
@@ -587,10 +740,7 @@ impl Runtime {
saved: matches!(event, DocumentEventKind::Save),
};
if changed {
self.emit(PluginUpdate {
request_id: None,
effects: vec![clear(&doc.snapshot)],
})?;
self.emit_diagnostics(&doc.snapshot, Vec::new())?;
}
if self.ready {
if !doc.opened {
@@ -937,21 +1087,14 @@ impl Runtime {
),
});
}
self.emit(PluginUpdate {
request_id: None,
effects: vec![UpdateEffect::Diagnostics {
buffer_id: doc.snapshot.id,
revision: doc.snapshot.revision,
items: diagnostics,
}],
})
self.emit_diagnostics(&doc.snapshot, diagnostics)
}
fn definition_result(&self, source: Source, result: &Value) -> Result<(), String> {
if !self
if self
.documents
.get(&source.buffer_id)
.is_some_and(|doc| doc.snapshot.revision == source.revision)
.is_none_or(|doc| doc.snapshot.revision != source.revision)
{
return Ok(());
}
@@ -1084,7 +1227,11 @@ impl Runtime {
if self.transport.flush().is_err() || self.transport.read().is_err() {
break;
}
if exiting && self.transport.flushed() {
// 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() {
@@ -1651,14 +1798,6 @@ unsafe extern "C" {
fn kill(pid: i32, signal: i32) -> i32;
}
fn clear(snapshot: &DocumentSnapshot) -> UpdateEffect {
UpdateEffect::Diagnostics {
buffer_id: snapshot.id,
revision: snapshot.revision,
items: Vec::new(),
}
}
fn message(text: &str, request_id: Option<u64>) -> PluginUpdate {
PluginUpdate {
request_id,