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;
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_workspace_edit,
};
pub use root::discover_root;
pub use types::{
CodeAction, CompletionItem, Diagnostic, Hover, Location, LspEvent, Position, Range, Severity,
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,
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"
]
}
}
}
},
"window": {
"workDoneProgress": true
}
},
"clientInfo": { "name": "vorto" },
});
write_framed(&stdin, &request(init_id, "initialize", init_params))?;
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);
}
break;
}
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: child,
stdin,
next_id: 2,
docs: HashMap::new(),
language_id,
blocking_pending,
})
}
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")
}
}
}
}