use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;
use std::thread;
use std::time::Duration;
use anyhow::{Context, Result};
use serde_json::Value;
use crate::editor::Cursor;
use crate::event::AppEvent;
use crate::lsp::{
self, CodeAction, CompletionItem, Diagnostic, Hover, Location, LspClient, LspEvent,
SignatureHelp, TextEdit, WorkspaceEdit,
};
mod group;
mod requests;
mod sync;
use group::{Group, GroupAccum, accumulate, diagnostic_to_json, finalize, signature_help_to_json};
pub fn client_key(lang: &str, server: &str) -> String {
format!("{}::{}", lang, server)
}
#[derive(Debug, Clone, Copy)]
pub enum LspRequestKind {
Jump,
References,
Rename,
CodeAction,
CodeActionResolve,
Hover,
Completion,
CompletionResolve,
SignatureHelp,
}
pub enum LspEventOutcome {
Nothing,
InfoMessage(String),
ErrorMessage(String),
Jump {
label: &'static str,
locations: Vec<Location>,
},
References(Vec<Location>),
Rename {
new_name: String,
edit: Option<WorkspaceEdit>,
},
CodeActions(Vec<CodeAction>),
CodeActionResolved(Option<CodeAction>),
Hover(Option<Hover>),
Completion {
prefix_start: Cursor,
items: Vec<CompletionItem>,
},
CompletionResolved {
uri: String,
item_index: Option<usize>,
item: Option<CompletionItem>,
},
SignatureHelp {
anchor_row: usize,
help: Option<SignatureHelp>,
},
}
pub struct RunningLspInfo {
pub client_key: String,
pub pid: u32,
pub root_uri: String,
pub language_id: String,
pub open_count: usize,
}
pub struct WorkspaceEditResult {
pub current_buffer_edits: Vec<TextEdit>,
pub files_touched: usize,
pub total_edits: usize,
}
pub(super) struct Pending {
pub(super) group: u64,
pub(super) kind: LspRequestKind,
}
pub struct LspCoordinator {
clients: HashMap<String, LspClient>,
open_uris: HashMap<String, Vec<String>>,
diagnostics: HashMap<String, HashMap<String, Vec<Diagnostic>>>,
pending: HashMap<(String, u64), Pending>,
groups: HashMap<u64, Group>,
next_group_id: u64,
current_uri: Option<String>,
current_language: Option<String>,
current_clients: Vec<String>,
last_synced_version: u64,
event_tx: Sender<AppEvent>,
startup_cwd: PathBuf,
}
impl LspCoordinator {
pub fn new(event_tx: Sender<AppEvent>, startup_cwd: PathBuf) -> Self {
Self {
clients: HashMap::new(),
open_uris: HashMap::new(),
diagnostics: HashMap::new(),
pending: HashMap::new(),
groups: HashMap::new(),
next_group_id: 0,
current_uri: None,
current_language: None,
current_clients: Vec::new(),
last_synced_version: 0,
event_tx,
startup_cwd,
}
}
pub fn last_synced_version(&self) -> u64 {
self.last_synced_version
}
pub fn set_last_synced_version(&mut self, v: u64) {
self.last_synced_version = v;
}
pub fn has_lsp(&self) -> bool {
self.current_uri.is_some() && !self.current_clients.is_empty()
}
pub fn is_completion_trigger_char(&self, c: char) -> bool {
let mut buf = [0u8; 4];
let needle = c.encode_utf8(&mut buf);
self.current_clients.iter().any(|key| {
self.clients
.get(key)
.map(|client| {
client
.completion_trigger_characters()
.iter()
.any(|t| t == needle)
})
.unwrap_or(false)
})
}
pub fn is_signature_help_trigger_char(&self, c: char) -> bool {
let mut buf = [0u8; 4];
let needle = c.encode_utf8(&mut buf);
self.current_clients.iter().any(|key| {
self.clients
.get(key)
.map(|client| {
client
.signature_help_trigger_characters()
.iter()
.any(|t| t == needle)
})
.unwrap_or(false)
})
}
pub fn all_diagnostics(&self) -> Vec<(String, Vec<Diagnostic>)> {
let mut out: Vec<(String, Vec<Diagnostic>)> = self
.diagnostics
.iter()
.filter_map(|(uri, per_client)| {
let mut merged: Vec<Diagnostic> = per_client
.values()
.flat_map(|v| v.iter().cloned())
.collect();
if merged.is_empty() {
return None;
}
merged.sort_by_key(|d| (d.range.start.line, d.range.start.character));
Some((uri.clone(), merged))
})
.collect();
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
pub fn current_diagnostics(&self) -> Option<Vec<Diagnostic>> {
let uri = self.current_uri.as_ref()?;
let per_client = self.diagnostics.get(uri)?;
let mut out: Vec<Diagnostic> = per_client
.values()
.flat_map(|v| v.iter().cloned())
.collect();
if out.is_empty() {
return None;
}
out.sort_by_key(|d| (d.range.start.line, d.range.start.character));
Some(out)
}
pub fn detach_current(&mut self) {
self.current_uri = None;
self.current_clients.clear();
self.current_language = None;
}
pub fn close_uri(&mut self, uri: &str) {
let client_keys = self.open_uris.remove(uri).unwrap_or_default();
for key in &client_keys {
if let Some(client) = self.clients.get_mut(key) {
let _ = client.did_close(uri);
}
}
self.diagnostics.remove(uri);
if self.current_uri.as_deref() == Some(uri) {
self.detach_current();
}
for key in &client_keys {
let still_holding = self
.open_uris
.values()
.any(|keys| keys.iter().any(|k| k == key));
if !still_holding {
self.drop_client(key);
}
}
}
pub fn has_client(&self, client_key: &str) -> bool {
self.clients.contains_key(client_key)
}
pub fn running_clients(&self) -> Vec<RunningLspInfo> {
self.clients
.iter()
.map(|(key, client)| {
let open_count = self
.open_uris
.values()
.filter(|keys| keys.iter().any(|k| k == key))
.count();
RunningLspInfo {
client_key: key.clone(),
pid: client.pid(),
root_uri: client.root_uri().to_string(),
language_id: client.language_id().to_string(),
open_count,
}
})
.collect()
}
pub fn attach_client(&mut self, client_key: &str, client: LspClient) -> bool {
if self.clients.contains_key(client_key) {
return false;
}
self.clients.insert(client_key.to_string(), client);
true
}
pub fn add_current_client(&mut self, client_key: &str) {
if !self.current_clients.iter().any(|k| k == client_key) {
self.current_clients.push(client_key.to_string());
}
}
pub fn make_emit(&self) -> Box<dyn Fn(LspEvent) + Send + 'static> {
let tx = self.event_tx.clone();
Box::new(move |ev| {
let _ = tx.send(AppEvent::Lsp(ev));
})
}
pub fn startup_cwd(&self) -> &Path {
&self.startup_cwd
}
pub fn format_first_client(
&mut self,
options: Value,
timeout: Duration,
) -> Result<Option<Vec<lsp::TextEdit>>> {
let Some(uri) = self.current_uri.clone() else {
return Ok(None);
};
let Some(key) = self.current_clients.first().cloned() else {
return Ok(None);
};
let Some(client) = self.clients.get_mut(&key) else {
return Ok(None);
};
let edits = client.formatting(&uri, options, timeout)?;
Ok(Some(edits))
}
pub fn format_with_servers(
&mut self,
servers: &[String],
options: Value,
timeout: Duration,
) -> Result<Option<Vec<lsp::TextEdit>>> {
let Some(uri) = self.current_uri.clone() else {
return Ok(None);
};
let mut last_err = None;
for name in servers {
let key = self
.current_clients
.iter()
.find(|k| k.rsplit_once("::").map(|(_, s)| s) == Some(name.as_str()))
.cloned();
let Some(key) = key else { continue };
let Some(client) = self.clients.get_mut(&key) else {
continue;
};
match client.formatting(&uri, options.clone(), timeout) {
Ok(edits) => return Ok(Some(edits)),
Err(e) => last_err = Some(e),
}
}
match last_err {
Some(e) => Err(e),
None => Ok(None),
}
}
pub fn current_uri(&self) -> Option<&str> {
self.current_uri.as_deref()
}
pub fn handle_event(&mut self, ev: LspEvent) -> LspEventOutcome {
match ev {
LspEvent::Diagnostics { client, uri, items } => {
let entry = self.diagnostics.entry(uri.clone()).or_default();
if items.is_empty() {
entry.remove(&client);
if entry.is_empty() {
self.diagnostics.remove(&uri);
}
} else {
entry.insert(client, items);
}
LspEventOutcome::Nothing
}
LspEvent::Message { level, text } => {
if level == 1 {
LspEventOutcome::ErrorMessage(text)
} else {
LspEventOutcome::InfoMessage(text)
}
}
LspEvent::Error { client, message } => {
if !self.clients.contains_key(&client) {
return LspEventOutcome::Nothing;
}
self.drop_client(&client);
LspEventOutcome::ErrorMessage(format!("lsp: {}", message))
}
LspEvent::Response {
client,
id,
result,
error,
} => self.handle_response(client, id, result, error),
}
}
fn handle_response(
&mut self,
client: String,
id: u64,
result: Option<Value>,
error: Option<String>,
) -> LspEventOutcome {
let Some(pending) = self.pending.remove(&(client.clone(), id)) else {
return LspEventOutcome::Nothing;
};
let group_id = pending.group;
let result = result.unwrap_or(Value::Null);
let had_error = error.is_some();
if let Some(group) = self.groups.get_mut(&group_id) {
if !had_error {
accumulate(&mut group.accum, &client, &result, &pending.kind);
}
group.remaining = group.remaining.saturating_sub(1);
if group.remaining == 0 {
let group = self.groups.remove(&group_id).unwrap();
return finalize(group.accum);
}
}
LspEventOutcome::Nothing
}
fn drop_client(&mut self, client_key: &str) {
let removed = self.clients.remove(client_key);
self.current_clients.retain(|k| k != client_key);
let dead_keys: Vec<(String, u64)> = self
.pending
.keys()
.filter(|(k, _)| k == client_key)
.cloned()
.collect();
for k in dead_keys {
if let Some(pending) = self.pending.remove(&k)
&& let Some(group) = self.groups.get_mut(&pending.group)
{
group.remaining = group.remaining.saturating_sub(1);
if group.remaining == 0 {
self.groups.remove(&pending.group);
}
}
}
for slices in self.diagnostics.values_mut() {
slices.remove(client_key);
}
self.diagnostics.retain(|_, slices| !slices.is_empty());
for keys in self.open_uris.values_mut() {
keys.retain(|k| k != client_key);
}
self.open_uris.retain(|_, keys| !keys.is_empty());
if let Some(client) = removed {
thread::spawn(move || drop(client));
}
}
pub fn apply_workspace_edit(&self, edit: WorkspaceEdit) -> Result<WorkspaceEditResult> {
let mut current_buffer_edits = Vec::new();
let files_touched = edit.changes.len();
let mut total_edits = 0usize;
let current_uri = self.current_uri.clone();
for (uri, edits) in edit.changes {
total_edits += edits.len();
if Some(&uri) == current_uri.as_ref() {
current_buffer_edits = edits;
continue;
}
let Some(path) = lsp::uri_to_path(&uri) else {
continue;
};
let text = std::fs::read_to_string(&path)
.with_context(|| format!("reading {}", path.display()))?;
let mut lines: Vec<String> = text.split('\n').map(|s| s.to_string()).collect();
if lines.is_empty() {
lines.push(String::new());
}
lsp::apply_text_edits(&mut lines, edits);
std::fs::write(&path, lines.join("\n"))
.with_context(|| format!("writing {}", path.display()))?;
}
Ok(WorkspaceEditResult {
current_buffer_edits,
files_touched,
total_edits,
})
}
}