1use std::io;
26use std::ops::ControlFlow;
27use std::path::{Path, PathBuf};
28use std::process::Stdio;
29use std::sync::Arc;
30use std::sync::atomic::{AtomicU64, Ordering};
31use std::time::Duration;
32
33use async_lsp::router::Router;
34use async_lsp::{LanguageServer, MainLoop, ServerSocket};
35use async_process::Command;
36use dashmap::DashMap;
37use futures::AsyncReadExt;
38use lsp_types::notification::{Notification, PublishDiagnostics};
39use lsp_types::{
40 self as types, ClientCapabilities, InitializeParams, ServerCapabilities, Url, WorkspaceFolder,
41};
42use parking_lot::Mutex;
43use thiserror::Error;
44use tokio::sync::Notify;
45use tokio::task::JoinHandle;
46
47pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
49
50#[derive(Debug, Error)]
52pub enum LspError {
53 #[error("failed to spawn LSP server `{server}`: {source}")]
54 SpawnFailed {
55 server: String,
56 #[source]
57 source: io::Error,
58 },
59 #[error("LSP server `{server}` failed to initialize: {message}")]
60 InitFailed { server: String, message: String },
61 #[error("LSP `{server}` request `{method}` timed out after {timeout:?}")]
62 Timeout {
63 server: String,
64 method: String,
65 timeout: Duration,
66 },
67 #[error("LSP `{server}` request `{method}` failed: {message}")]
68 RequestFailed {
69 server: String,
70 method: String,
71 message: String,
72 },
73 #[error("LSP `{server}` transport error in `{method}`: {source}")]
74 Transport {
75 server: String,
76 method: String,
77 #[source]
78 source: async_lsp::Error,
79 },
80 #[error("LSP `{server}` is shut down")]
81 ShutDown { server: String },
82}
83
84impl LspError {
85 fn shut_down(server: &str) -> Self {
86 LspError::ShutDown {
87 server: server.to_string(),
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct PublishedDiagnostics {
95 pub uri: String,
96 pub diagnostics: serde_json::Value,
97}
98
99#[derive(Debug, Clone)]
101pub struct LspClientConfig {
102 pub server_name: String,
103 pub command: String,
104 pub args: Vec<String>,
105 pub startup_timeout: Duration,
106 pub request_timeout: Duration,
107 pub shutdown_timeout: Duration,
108}
109
110impl LspClientConfig {
111 pub fn new(
112 server_name: impl Into<String>,
113 command: impl Into<String>,
114 args: Vec<String>,
115 ) -> Self {
116 Self {
117 server_name: server_name.into(),
118 command: command.into(),
119 args,
120 startup_timeout: Duration::from_secs(10),
121 request_timeout: REQUEST_TIMEOUT,
122 shutdown_timeout: Duration::from_secs(5),
123 }
124 }
125}
126
127pub struct LspClient {
129 server_name: String,
130 workspace_root: PathBuf,
131 lifecycle_id: Arc<AtomicU64>,
132 diagnostics: Arc<DashMap<String, PublishedDiagnostics>>,
133 diagnostics_ready: Arc<Notify>,
134 server: Option<ServerSocket>,
135 capabilities: parking_lot::RwLock<Option<types::ServerCapabilities>>,
136 main_loop: Option<JoinHandle<async_lsp::Result<()>>>,
137 child: Option<async_process::Child>,
138 #[allow(dead_code)]
139 request_timeout: Duration,
140 shutdown_timeout: Duration,
141}
142
143impl std::fmt::Debug for LspClient {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 f.debug_struct("LspClient")
146 .field("server_name", &self.server_name)
147 .field("workspace_root", &self.workspace_root)
148 .field("lifecycle_id", &self.lifecycle_id.load(Ordering::Acquire))
149 .field("child_running", &self.child.is_some())
150 .finish()
151 }
152}
153
154impl LspClient {
155 pub async fn start(
156 config: LspClientConfig,
157 workspace_root: PathBuf,
158 ) -> Result<Arc<Self>, LspError> {
159 let server_name = config.server_name.clone();
160
161 let mut command = Command::new(&config.command);
162 command
163 .args(&config.args)
164 .stdin(Stdio::piped())
165 .stdout(Stdio::piped())
166 .stderr(Stdio::piped())
167 .kill_on_drop(true);
168
169 let mut child = command.spawn().map_err(|source| LspError::SpawnFailed {
170 server: config.server_name.clone(),
171 source,
172 })?;
173
174 let stdout = child.stdout.take().ok_or_else(|| LspError::SpawnFailed {
176 server: config.server_name.clone(),
177 source: io::Error::other("child stdout unavailable"),
178 })?;
179 let stdin = child.stdin.take().ok_or_else(|| LspError::SpawnFailed {
180 server: config.server_name.clone(),
181 source: io::Error::other("child stdin unavailable"),
182 })?;
183 let stderr = child.stderr.take();
184
185 let diagnostics: Arc<DashMap<String, PublishedDiagnostics>> = Arc::new(DashMap::new());
186 let diagnostics_for_handler = diagnostics.clone();
187 let server_name_for_handler = config.server_name.clone();
188
189 let (mainloop, server) = MainLoop::new_client(move |_server| {
190 let mut router = Router::new(());
191 router.notification::<PublishDiagnostics>(move |_st, params| {
192 let uri = params.uri.to_string();
193 let payload = serde_json::to_value(¶ms.diagnostics)
194 .unwrap_or(serde_json::Value::Array(vec![]));
195 diagnostics_for_handler.insert(
196 uri.clone(),
197 PublishedDiagnostics {
198 uri,
199 diagnostics: payload,
200 },
201 );
202 tracing::debug!(
203 server = %server_name_for_handler,
204 "publishDiagnostics stored",
205 );
206 ControlFlow::Continue(())
207 });
208 router
209 });
210
211 let main_loop_handle =
212 tokio::spawn(async move { mainloop.run_buffered(stdout, stdin).await });
213
214 if let Some(stderr) = stderr {
216 let srv = config.server_name.clone();
217 tokio::spawn(async move {
218 let mut stderr = stderr;
219 let mut buf = Vec::new();
220 let mut tmp = [0u8; 1024];
221 loop {
222 match stderr.read(&mut tmp).await {
223 Ok(0) => break,
224 Ok(n) => {
225 buf.extend_from_slice(&tmp[..n]);
226 while let Some(idx) = buf.iter().position(|b| *b == b'\n') {
227 let line: Vec<u8> = buf.drain(..=idx).collect();
228 tracing::debug!(server = %srv, stderr = ?line);
229 }
230 }
231 Err(_) => break,
232 }
233 }
234 });
235 }
236
237 let client = Arc::new(Self {
238 server_name: server_name.clone(),
239 workspace_root,
240 lifecycle_id: Arc::new(AtomicU64::new(1)),
241 diagnostics,
242 diagnostics_ready: Arc::new(Notify::new()),
243 server: Some(server),
244 capabilities: parking_lot::RwLock::new(None),
245 main_loop: Some(main_loop_handle),
246 child: Some(child),
247 request_timeout: config.request_timeout,
248 shutdown_timeout: config.shutdown_timeout,
249 });
250
251 client.spawn_diagnostics_watcher();
252
253 client
254 .initialize_with_timeout(config.startup_timeout)
255 .await?;
256
257 Ok(client)
258 }
259
260 pub fn server_name(&self) -> &str {
261 &self.server_name
262 }
263
264 pub fn workspace_root(&self) -> &Path {
265 &self.workspace_root
266 }
267
268 pub fn lifecycle_id(&self) -> u64 {
269 self.lifecycle_id.load(Ordering::Acquire)
270 }
271
272 pub fn bump_lifecycle_id(&self) -> u64 {
273 self.lifecycle_id.fetch_add(1, Ordering::AcqRel) + 1
274 }
275
276 pub async fn initialize_with_timeout(
277 &self,
278 timeout: Duration,
279 ) -> Result<ServerCapabilities, LspError> {
280 let mut server = self
281 .server
282 .as_ref()
283 .ok_or_else(|| LspError::shut_down(&self.server_name))?
284 .clone();
285
286 let workspace_uri =
287 Url::from_file_path(&self.workspace_root).map_err(|_| LspError::InitFailed {
288 server: self.server_name.clone(),
289 message: "workspace root is not absolute or has no URI form".into(),
290 })?;
291
292 let params = InitializeParams {
293 workspace_folders: Some(vec![WorkspaceFolder {
294 uri: workspace_uri,
295 name: self.workspace_root.display().to_string(),
296 }]),
297 capabilities: ClientCapabilities::default(),
298 ..InitializeParams::default()
299 };
300
301 let init_fut = server.initialize(params);
302 let resp = match tokio::time::timeout(timeout, init_fut).await {
303 Err(_) => {
304 return Err(LspError::Timeout {
305 server: self.server_name.clone(),
306 method: "initialize".into(),
307 timeout,
308 });
309 }
310 Ok(Err(e)) => {
311 return Err(LspError::Transport {
312 server: self.server_name.clone(),
313 method: "initialize".into(),
314 source: e,
315 });
316 }
317 Ok(Ok(v)) => v,
318 };
319
320 server
321 .initialized(types::InitializedParams {})
322 .map_err(|e| LspError::Transport {
323 server: self.server_name.clone(),
324 method: "initialized".into(),
325 source: e,
326 })?;
327 *self.capabilities.write() = Some(resp.capabilities.clone());
328
329 Ok(resp.capabilities)
330 }
331
332 pub fn cached_capabilities(&self) -> Option<ServerCapabilities> {
334 self.capabilities.read().clone()
335 }
336
337 pub async fn request<R>(
338 &self,
339 params: R::Params,
340 timeout: Duration,
341 ) -> Result<R::Result, LspError>
342 where
343 R: types::request::Request,
344 R::Params: serde::Serialize,
345 {
346 let server = self
347 .server
348 .as_ref()
349 .ok_or_else(|| LspError::shut_down(&self.server_name))?
350 .clone();
351
352 let method = R::METHOD.to_string();
353 let fut = server.request::<R>(params);
354 let value = match tokio::time::timeout(timeout, fut).await {
355 Err(_) => {
356 return Err(LspError::Timeout {
357 server: self.server_name.clone(),
358 method: method.clone(),
359 timeout,
360 });
361 }
362 Ok(Err(e)) => {
363 return Err(LspError::Transport {
364 server: self.server_name.clone(),
365 method: method.clone(),
366 source: e,
367 });
368 }
369 Ok(Ok(v)) => v,
370 };
371 Ok(value)
372 }
373
374 pub fn notify<N>(&self, params: N::Params) -> Result<(), LspError>
375 where
376 N: Notification,
377 N::Params: serde::Serialize,
378 {
379 let server = self
380 .server
381 .as_ref()
382 .ok_or_else(|| LspError::shut_down(&self.server_name))?;
383 server.notify::<N>(params).map_err(|e| LspError::Transport {
384 server: self.server_name.clone(),
385 method: N::METHOD.into(),
386 source: e,
387 })
388 }
389
390 pub async fn drain_diagnostics(&self, timeout: Duration) -> Option<Vec<PublishedDiagnostics>> {
391 let notified = tokio::time::timeout(timeout, self.diagnostics_ready.notified()).await;
392 if notified.is_err() {
393 return None;
394 }
395 let entries: Vec<PublishedDiagnostics> = self
396 .diagnostics
397 .iter()
398 .map(|kv| kv.value().clone())
399 .collect();
400 if entries.is_empty() {
401 None
402 } else {
403 Some(entries)
404 }
405 }
406
407 pub fn read_diagnostics(&self, uris: &[String]) -> Vec<PublishedDiagnostics> {
408 uris.iter()
409 .filter_map(|u| self.diagnostics.get(u).map(|kv| kv.value().clone()))
410 .collect()
411 }
412
413 pub fn diagnostics_file_count(&self) -> usize {
414 self.diagnostics.len()
415 }
416
417 pub fn clear_diagnostics(&self) {
418 self.diagnostics.clear();
419 }
420
421 pub async fn shutdown(&mut self) -> Result<(), LspError> {
422 use types::notification::Exit;
423
424 if let Some(server) = self.server.as_ref() {
425 let mut server = server.clone();
426 let _ = tokio::time::timeout(self.shutdown_timeout, async {
427 let _ = server.shutdown(()).await;
428 let _ = server.notify::<Exit>(());
429 })
430 .await;
431 }
432 self.server = None;
433
434 if let Some(handle) = self.main_loop.as_ref() {
435 handle.abort();
436 self.main_loop = None;
437 }
438
439 let _ = self.child.take();
440
441 Ok(())
442 }
443
444 fn spawn_diagnostics_watcher(&self) {
445 let diag_map = self.diagnostics.clone();
446 let notify = self.diagnostics_ready.clone();
447 tokio::spawn(async move {
448 let mut last: std::collections::HashSet<String> =
449 diag_map.iter().map(|kv| kv.key().clone()).collect();
450 loop {
451 tokio::time::sleep(Duration::from_millis(50)).await;
452 let current: std::collections::HashSet<String> =
453 diag_map.iter().map(|kv| kv.key().clone()).collect();
454 if current != last {
455 last = current;
456 notify.notify_waiters();
457 }
458 }
459 });
460 }
461}
462
463pub fn uri_for(path: &Path) -> Option<Url> {
465 Url::from_file_path(path).ok()
466}
467
468#[derive(Debug, Default)]
470pub struct ReplayState {
471 inner: Mutex<std::collections::HashMap<PathBuf, String>>,
472}
473
474impl ReplayState {
475 pub fn new() -> Self {
476 Self::default()
477 }
478
479 pub fn record(&self, path: PathBuf, content: String) {
480 self.inner.lock().insert(path, content);
481 }
482
483 pub fn forget(&self, path: &Path) {
484 self.inner.lock().remove(path);
485 }
486
487 pub fn snapshot(&self) -> Vec<(PathBuf, String)> {
488 self.inner
489 .lock()
490 .iter()
491 .map(|(k, v)| (k.clone(), v.clone()))
492 .collect()
493 }
494
495 pub fn len(&self) -> usize {
496 self.inner.lock().len()
497 }
498
499 pub fn is_empty(&self) -> bool {
500 self.inner.lock().is_empty()
501 }
502}
503
504pub use async_lsp::Error as AsyncLspError;
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn request_timeout_constant_is_sane() {
512 assert_eq!(REQUEST_TIMEOUT, Duration::from_secs(15));
513 }
514
515 #[test]
516 fn uri_for_absolute_path() {
517 let uri = uri_for(Path::new("/tmp/foo.rs")).unwrap();
518 assert!(uri.to_string().starts_with("file:///"));
519 }
520
521 #[test]
522 fn uri_for_relative_path_is_none() {
523 assert!(uri_for(Path::new("foo.rs")).is_none());
524 }
525
526 #[test]
527 fn config_defaults_match_spec() {
528 let cfg = LspClientConfig::new("rust-analyzer", "rust-analyzer", vec![]);
529 assert_eq!(cfg.server_name, "rust-analyzer");
530 assert_eq!(cfg.startup_timeout, Duration::from_secs(10));
531 assert_eq!(cfg.request_timeout, REQUEST_TIMEOUT);
532 assert_eq!(cfg.shutdown_timeout, Duration::from_secs(5));
533 }
534
535 #[test]
536 fn replay_state_records_and_forgets() {
537 let st = ReplayState::new();
538 st.record(PathBuf::from("/tmp/foo.rs"), "fn main() {}".into());
539 let snap = st.snapshot();
540 assert_eq!(snap.len(), 1);
541 assert_eq!(snap[0].0, PathBuf::from("/tmp/foo.rs"));
542 st.forget(Path::new("/tmp/foo.rs"));
543 assert!(st.is_empty());
544 assert_eq!(st.len(), 0);
545 }
546
547 #[test]
548 fn error_shut_down_carries_server_name() {
549 let e = LspError::shut_down("rust-analyzer");
550 assert!(matches!(e, LspError::ShutDown { ref server } if server == "rust-analyzer"));
551 }
552}