feat: preconfigure Python with Astral ty and Ruff
This commit is contained in:
+211
-72
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user