use std::collections::HashMap;
use std::io::BufReader;
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use anyhow::{Context, Result, anyhow, bail};
use serde_json::{Value, json};
use crate::config::LspConfig;
use crate::vlog;
pub(crate) mod codec;
mod edits;
mod parse;
mod root;
mod types;
mod uri;
pub use edits::apply_text_edits;
pub use parse::{
parse_code_action, parse_code_actions, parse_completion, parse_completion_resolve, parse_hover,
parse_locations, parse_signature_help, parse_workspace_edit,
};
pub use root::discover_root;
pub use types::{
CodeAction, CompletionItem, Diagnostic, Hover, Location, LspEvent, ParameterLabel, Position,
Range, Severity, SignatureHelp, SignatureInformation, TextEdit, WorkspaceEdit,
};
pub use uri::{path_to_uri, uri_to_path};
use codec::{notification, read_message, reader_loop, request, write_framed};
pub struct LspClient {
child: Child,
stdin: Arc<Mutex<ChildStdin>>,
next_id: u64,
docs: HashMap<String, i32>,
language_id: String,
root_uri: String,
completion_trigger_characters: Vec<String>,
signature_help_trigger_characters: Vec<String>,
blocking_pending: Arc<Mutex<HashMap<u64, mpsc::Sender<BlockingReply>>>>,
}
pub(crate) enum BlockingReply {
Ok(Value),
Err(String),
}
pub(crate) type BlockingPending = Arc<Mutex<HashMap<u64, mpsc::Sender<BlockingReply>>>>;
impl LspClient {
pub fn spawn(
client_key: &str,
lang_name: &str,
spec: &LspConfig,
root_uri: &str,
emit: Box<dyn Fn(LspEvent) + Send + 'static>,
) -> Result<Self> {
let mut child = Command::new(&spec.command)
.args(&spec.args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.with_context(|| format!("spawning LSP server `{}`", spec.command))?;
let stdin_raw = child.stdin.take().ok_or_else(|| anyhow!("no stdin"))?;
let stdout = child.stdout.take().ok_or_else(|| anyhow!("no stdout"))?;
let mut reader = BufReader::new(stdout);
let stdin = Arc::new(Mutex::new(stdin_raw));
let workspace_name = root_uri
.rsplit('/')
.find(|s| !s.is_empty())
.unwrap_or("workspace")
.to_string();
let init_id: u64 = 1;
let init_params = json!({
"processId": std::process::id(),
"rootUri": root_uri,
"workspaceFolders": [{ "uri": root_uri, "name": workspace_name }],
"capabilities": {
"workspace": {
"configuration": false,
"workspaceFolders": true,
"didChangeConfiguration": { "dynamicRegistration": false },
"didChangeWatchedFiles": { "dynamicRegistration": true }
},
"textDocument": {
"synchronization": {
"dynamicRegistration": false,
"didSave": true
},
"publishDiagnostics": { "relatedInformation": false },
"codeAction": {
"dynamicRegistration": false,
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"", "quickfix", "refactor",
"refactor.extract", "refactor.inline",
"refactor.rewrite", "source",
"source.organizeImports"
]
}
},
"resolveSupport": { "properties": ["edit"] },
"dataSupport": true
},
"completion": {
"dynamicRegistration": false,
"completionItem": {
"snippetSupport": false,
"insertReplaceSupport": true,
"labelDetailsSupport": true,
"resolveSupport": {
"properties": [
"additionalTextEdits",
"detail",
"documentation"
]
}
}
},
"signatureHelp": {
"dynamicRegistration": false,
"signatureInformation": {
"parameterInformation": {
"labelOffsetSupport": true
},
"activeParameterSupport": true
}
}
},
"window": {
"workDoneProgress": true
}
},
"clientInfo": { "name": "vorto" },
});
write_framed(&stdin, &request(init_id, "initialize", init_params))?;
let (completion_trigger_characters, signature_help_trigger_characters): (
Vec<String>,
Vec<String>,
) = loop {
let msg = read_message(&mut reader).with_context(|| "reading initialize response")?;
let is_init_response = msg.get("id").and_then(|v| v.as_u64()) == Some(init_id)
&& msg.get("method").is_none();
if is_init_response {
if let Some(err) = msg.get("error") {
bail!("LSP initialize error: {}", err);
}
let str_array = |ptr: &str| {
msg.pointer(ptr)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default()
};
break (
str_array("/result/capabilities/completionProvider/triggerCharacters"),
str_array("/result/capabilities/signatureHelpProvider/triggerCharacters"),
);
}
codec::handle_server_request(&stdin, &msg);
};
write_framed(&stdin, ¬ification("initialized", json!({})))?;
let language_id = spec
.language_id
.clone()
.unwrap_or_else(|| lang_name.to_string());
let stdin_reader = Arc::clone(&stdin);
let key_for_reader = client_key.to_string();
let blocking_pending: BlockingPending = Arc::new(Mutex::new(HashMap::new()));
let blocking_for_reader = Arc::clone(&blocking_pending);
thread::spawn(move || {
reader_loop(
reader,
emit,
stdin_reader,
key_for_reader,
blocking_for_reader,
)
});
Ok(Self {
child,
stdin,
next_id: 2,
docs: HashMap::new(),
language_id,
root_uri: root_uri.to_string(),
completion_trigger_characters,
signature_help_trigger_characters,
blocking_pending,
})
}
pub fn pid(&self) -> u32 {
self.child.id()
}
pub fn root_uri(&self) -> &str {
&self.root_uri
}
pub fn language_id(&self) -> &str {
&self.language_id
}
pub fn completion_trigger_characters(&self) -> &[String] {
&self.completion_trigger_characters
}
pub fn signature_help_trigger_characters(&self) -> &[String] {
&self.signature_help_trigger_characters
}
pub fn request(&mut self, method: &str, params: Value) -> Result<u64> {
let id = self.next_id;
self.next_id += 1;
write_framed(&self.stdin, &request(id, method, params))?;
Ok(id)
}
pub fn did_open(&mut self, uri: &str, text: &str) -> Result<()> {
if self.docs.contains_key(uri) {
return Ok(());
}
self.docs.insert(uri.to_string(), 1);
let params = json!({
"textDocument": {
"uri": uri,
"languageId": self.language_id,
"version": 1,
"text": text,
}
});
write_framed(&self.stdin, ¬ification("textDocument/didOpen", params))
}
pub fn did_change(&mut self, uri: &str, text: &str) -> Result<()> {
let v = match self.docs.get_mut(uri) {
Some(v) => {
*v += 1;
*v
}
None => return Ok(()),
};
let params = json!({
"textDocument": { "uri": uri, "version": v },
"contentChanges": [ { "text": text } ],
});
write_framed(&self.stdin, ¬ification("textDocument/didChange", params))
}
pub fn did_save(&mut self, uri: &str, text: &str) -> Result<()> {
if !self.docs.contains_key(uri) {
return Ok(());
}
let params = json!({
"textDocument": { "uri": uri },
"text": text,
});
write_framed(&self.stdin, ¬ification("textDocument/didSave", params))
}
pub fn did_close(&mut self, uri: &str) -> Result<()> {
if self.docs.remove(uri).is_none() {
return Ok(());
}
let params = json!({ "textDocument": { "uri": uri } });
write_framed(&self.stdin, ¬ification("textDocument/didClose", params))
}
pub fn formatting(
&mut self,
uri: &str,
options: Value,
timeout: Duration,
) -> Result<Vec<TextEdit>> {
let id = self.next_id;
self.next_id += 1;
let (tx, rx) = mpsc::channel();
{
let mut guard = self
.blocking_pending
.lock()
.map_err(|_| anyhow!("lsp blocking pending poisoned"))?;
guard.insert(id, tx);
}
let params = json!({
"textDocument": { "uri": uri },
"options": options,
});
if let Err(e) = write_framed(&self.stdin, &request(id, "textDocument/formatting", params)) {
self.blocking_pending
.lock()
.ok()
.and_then(|mut g| g.remove(&id));
return Err(e);
}
let reply = rx.recv_timeout(timeout);
self.blocking_pending
.lock()
.ok()
.and_then(|mut g| g.remove(&id));
match reply {
Ok(BlockingReply::Ok(v)) => Ok(parse::parse_text_edits(&v)),
Ok(BlockingReply::Err(msg)) => bail!("textDocument/formatting: {}", msg),
Err(mpsc::RecvTimeoutError::Timeout) => {
bail!("textDocument/formatting timed out after {:?}", timeout)
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
bail!("textDocument/formatting: lsp reader gone")
}
}
}
}
impl Drop for LspClient {
fn drop(&mut self) {
let shutdown_wait = Duration::from_millis(500);
let exit_wait = Duration::from_millis(300);
vlog!(
"lsp shutdown begin lang={} pid={}",
self.language_id,
self.child.id()
);
let shutdown_id = self.next_id;
self.next_id += 1;
let (tx, rx) = mpsc::channel();
if let Ok(mut guard) = self.blocking_pending.lock() {
guard.insert(shutdown_id, tx);
}
let shutdown_sent =
write_framed(&self.stdin, &request(shutdown_id, "shutdown", Value::Null)).is_ok();
if shutdown_sent {
let _ = rx.recv_timeout(shutdown_wait);
}
if let Ok(mut guard) = self.blocking_pending.lock() {
guard.remove(&shutdown_id);
}
if shutdown_sent {
let _ = write_framed(&self.stdin, ¬ification("exit", Value::Null));
}
let deadline = std::time::Instant::now() + exit_wait;
loop {
match self.child.try_wait() {
Ok(Some(status)) => {
vlog!(
"lsp shutdown clean lang={} status={status}",
self.language_id
);
return;
}
Ok(None) if std::time::Instant::now() >= deadline => break,
Ok(None) => thread::sleep(Duration::from_millis(20)),
Err(_) => break,
}
}
vlog!("lsp shutdown kill lang={}", self.language_id);
let _ = self.child.kill();
let _ = self.child.wait();
}
}