use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;
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, TextEdit,
WorkspaceEdit,
};
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,
}
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,
edits: Vec<TextEdit>,
},
}
pub struct WorkspaceEditResult {
pub current_buffer_edits: Vec<TextEdit>,
pub files_touched: usize,
pub total_edits: usize,
}
struct Group {
remaining: usize,
accum: GroupAccum,
}
enum GroupAccum {
Jump {
label: &'static str,
locations: Vec<Location>,
},
References(Vec<Location>),
Rename {
new_name: String,
edit: Option<WorkspaceEdit>,
},
CodeAction(Vec<CodeAction>),
Hover(Vec<String>),
Completion {
prefix_start: Cursor,
items: Vec<CompletionItem>,
},
CompletionResolve {
uri: String,
edits: Vec<TextEdit>,
},
CodeActionResolve {
action: Option<CodeAction>,
},
}
struct Pending {
group: u64,
kind: LspRequestKind,
}
pub struct LspCoordinator {
clients: HashMap<String, LspClient>,
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(),
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 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) {
let Some(uri) = self.current_uri.take() else {
return;
};
let clients = std::mem::take(&mut self.current_clients);
for key in &clients {
if let Some(client) = self.clients.get_mut(key) {
let _ = client.did_close(&uri);
}
}
self.current_language = None;
}
pub fn has_client(&self, client_key: &str) -> bool {
self.clients.contains_key(client_key)
}
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 did_open(
&mut self,
client_key: &str,
lang_name: &str,
path: &Path,
text: &str,
) -> Result<()> {
let uri = lsp::path_to_uri(path);
if let Some(client) = self.clients.get_mut(client_key) {
client.did_open(&uri, text)?;
}
self.current_uri = Some(uri);
self.current_language = Some(lang_name.to_string());
self.add_current_client(client_key);
Ok(())
}
pub fn did_change(&mut self, text: &str) -> Result<()> {
let Some(uri) = self.current_uri.clone() else {
return Ok(());
};
let keys = self.current_clients.clone();
for key in &keys {
if let Some(client) = self.clients.get_mut(key) {
client.did_change(&uri, text)?;
}
}
Ok(())
}
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 did_save(&mut self, text: &str) -> Result<()> {
let Some(uri) = self.current_uri.clone() else {
return Ok(());
};
let keys = self.current_clients.clone();
for key in &keys {
if let Some(client) = self.clients.get_mut(key) {
client.did_save(&uri, text)?;
}
}
Ok(())
}
pub fn request_jump(
&mut self,
method: &str,
label: &'static str,
cursor: Cursor,
) -> Result<()> {
let params = self.text_document_position_params(cursor);
self.fan_out_request(
method,
params,
LspRequestKind::Jump,
GroupAccum::Jump {
label,
locations: Vec::new(),
},
)
}
pub fn request_references(&mut self, cursor: Cursor) -> Result<()> {
let mut params = self.text_document_position_params(cursor);
if let Some(obj) = params.as_object_mut() {
obj.insert(
"context".to_string(),
serde_json::json!({ "includeDeclaration": true }),
);
}
self.fan_out_request(
"textDocument/references",
params,
LspRequestKind::References,
GroupAccum::References(Vec::new()),
)
}
pub fn request_code_action(
&mut self,
cursor: Cursor,
diagnostics: &[Diagnostic],
) -> Result<()> {
let uri = self.current_uri.clone().unwrap_or_default();
let line = cursor.row as u64;
let character = cursor.col as u64;
let diagnostics_json = Value::Array(
diagnostics
.iter()
.filter(|d| {
d.range.start.line <= cursor.row as u32
&& cursor.row as u32 <= d.range.end.line
})
.map(diagnostic_to_json)
.collect(),
);
let params = serde_json::json!({
"textDocument": { "uri": uri },
"range": {
"start": { "line": line, "character": character },
"end": { "line": line, "character": character },
},
"context": { "diagnostics": diagnostics_json },
});
self.fan_out_request(
"textDocument/codeAction",
params,
LspRequestKind::CodeAction,
GroupAccum::CodeAction(Vec::new()),
)
}
pub fn request_hover(&mut self, cursor: Cursor) -> Result<()> {
let params = self.text_document_position_params(cursor);
self.fan_out_request(
"textDocument/hover",
params,
LspRequestKind::Hover,
GroupAccum::Hover(Vec::new()),
)
}
pub fn request_completion(&mut self, cursor: Cursor, prefix_start: Cursor) -> Result<()> {
let params = self.text_document_position_params(cursor);
self.fan_out_request(
"textDocument/completion",
params,
LspRequestKind::Completion,
GroupAccum::Completion {
prefix_start,
items: Vec::new(),
},
)
}
pub fn request_completion_resolve(&mut self, raw: Value, source: &str) -> Result<()> {
let uri = self.current_uri.clone().unwrap_or_default();
self.send_single(
source,
"completionItem/resolve",
raw,
LspRequestKind::CompletionResolve,
GroupAccum::CompletionResolve {
uri,
edits: Vec::new(),
},
)
}
pub fn current_uri(&self) -> Option<&str> {
self.current_uri.as_deref()
}
pub fn request_code_action_resolve(&mut self, action: Value, source: &str) -> Result<()> {
self.send_single(
source,
"codeAction/resolve",
action,
LspRequestKind::CodeActionResolve,
GroupAccum::CodeActionResolve { action: None },
)
}
pub fn request_rename(&mut self, new_name: String, cursor: Cursor) -> Result<()> {
let mut params = self.text_document_position_params(cursor);
if let Some(obj) = params.as_object_mut() {
obj.insert("newName".to_string(), Value::String(new_name.clone()));
}
let kind_new_name = new_name.clone();
self.fan_out_request(
"textDocument/rename",
params,
LspRequestKind::Rename,
GroupAccum::Rename {
new_name: kind_new_name,
edit: None,
},
)
}
fn text_document_position_params(&self, cursor: Cursor) -> Value {
let uri = self.current_uri.clone().unwrap_or_default();
serde_json::json!({
"textDocument": { "uri": uri },
"position": {
"line": cursor.row as u64,
"character": cursor.col as u64,
}
})
}
fn fan_out_request(
&mut self,
method: &str,
params: Value,
kind: LspRequestKind,
accum: GroupAccum,
) -> Result<()> {
let keys = self.current_clients.clone();
if keys.is_empty() {
return Ok(());
}
let group_id = self.alloc_group();
let mut sent = 0usize;
for key in &keys {
if let Some(client) = self.clients.get_mut(key) {
match client.request(method, params.clone()) {
Ok(id) => {
self.pending.insert(
(key.clone(), id),
Pending {
group: group_id,
kind,
},
);
sent += 1;
}
Err(_) => {
}
}
}
}
if sent == 0 {
return Ok(());
}
self.groups.insert(
group_id,
Group {
remaining: sent,
accum,
},
);
Ok(())
}
fn send_single(
&mut self,
source: &str,
method: &str,
params: Value,
kind: LspRequestKind,
accum: GroupAccum,
) -> Result<()> {
let key = if self.clients.contains_key(source) {
source.to_string()
} else if let Some(first) = self.current_clients.first().cloned() {
first
} else {
return Ok(());
};
let Some(client) = self.clients.get_mut(&key) else {
return Ok(());
};
let id = client.request(method, params)?;
let group_id = self.alloc_group();
self.pending.insert(
(key, id),
Pending {
group: group_id,
kind,
},
);
self.groups.insert(
group_id,
Group {
remaining: 1,
accum,
},
);
Ok(())
}
fn alloc_group(&mut self) -> u64 {
let id = self.next_group_id;
self.next_group_id = self.next_group_id.wrapping_add(1);
id
}
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 } => {
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) {
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());
}
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,
})
}
}
fn accumulate(accum: &mut GroupAccum, source: &str, result: &Value, kind: &LspRequestKind) {
match (accum, kind) {
(GroupAccum::Jump { locations, .. }, LspRequestKind::Jump) => {
locations.extend(lsp::parse_locations(result));
}
(GroupAccum::References(locations), LspRequestKind::References) => {
locations.extend(lsp::parse_locations(result));
}
(GroupAccum::Rename { edit, .. }, LspRequestKind::Rename) if edit.is_none() => {
*edit = lsp::parse_workspace_edit(result);
}
(GroupAccum::CodeAction(actions), LspRequestKind::CodeAction) => {
let mut parsed = lsp::parse_code_actions(result);
for a in &mut parsed {
a.source = source.to_string();
}
actions.extend(parsed);
}
(GroupAccum::Hover(parts), LspRequestKind::Hover) => {
if let Some(h) = lsp::parse_hover(result) {
parts.push(h.contents);
}
}
(GroupAccum::Completion { items, .. }, LspRequestKind::Completion) => {
let mut parsed = lsp::parse_completion(result);
for it in &mut parsed {
it.source = source.to_string();
}
items.extend(parsed);
}
(GroupAccum::CompletionResolve { edits, .. }, LspRequestKind::CompletionResolve) => {
if let Some(item) = lsp::parse_completion_resolve(result) {
*edits = item.additional_text_edits;
}
}
(GroupAccum::CodeActionResolve { action }, LspRequestKind::CodeActionResolve) => {
let mut parsed = lsp::parse_code_action(result);
if let Some(a) = parsed.as_mut() {
a.source = source.to_string();
}
*action = parsed;
}
_ => {}
}
}
fn finalize(accum: GroupAccum) -> LspEventOutcome {
match accum {
GroupAccum::Jump { label, locations } => LspEventOutcome::Jump { label, locations },
GroupAccum::References(locations) => LspEventOutcome::References(locations),
GroupAccum::Rename { new_name, edit } => LspEventOutcome::Rename { new_name, edit },
GroupAccum::CodeAction(actions) => LspEventOutcome::CodeActions(actions),
GroupAccum::Hover(parts) => {
if parts.is_empty() {
LspEventOutcome::Hover(None)
} else {
LspEventOutcome::Hover(Some(Hover {
contents: parts.join("\n\n---\n\n"),
}))
}
}
GroupAccum::Completion {
prefix_start,
items,
} => {
let items = dedup_completion(items);
LspEventOutcome::Completion {
prefix_start,
items,
}
}
GroupAccum::CompletionResolve { uri, edits } => {
LspEventOutcome::CompletionResolved { uri, edits }
}
GroupAccum::CodeActionResolve { action } => LspEventOutcome::CodeActionResolved(action),
}
}
fn dedup_completion(items: Vec<CompletionItem>) -> Vec<CompletionItem> {
use std::collections::HashSet;
let mut seen: HashSet<(String, u8, String)> = HashSet::new();
let mut out = Vec::with_capacity(items.len());
for it in items {
let text_key = it
.text_edit
.as_ref()
.map(|te| te.new_text.clone())
.or_else(|| it.insert_text.clone())
.unwrap_or_else(|| it.label.clone());
let key = (it.label.clone(), it.kind, text_key);
if seen.insert(key) {
out.push(it);
}
}
out
}
fn diagnostic_to_json(d: &Diagnostic) -> Value {
serde_json::json!({
"range": {
"start": { "line": d.range.start.line, "character": d.range.start.character },
"end": { "line": d.range.end.line, "character": d.range.end.character },
},
"severity": d.severity as u8 + 1,
"message": d.message,
"source": d.source,
})
}