Skip to main content

rx4/
lsp.rs

1//! LSP client manager — JSON-RPC 2.0 over stdio with Content-Length framing.
2//!
3//! Manages one or more language server processes, sends initialize/didOpen/
4//! didChange/didClose, collects published diagnostics, and forwards
5//! references/definition requests. No external LSP crate; the protocol is
6//! implemented manually over the child process stdin/stdout.
7
8use dashmap::DashMap;
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Value};
11use std::path::Path;
12use std::process::Stdio;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::Arc;
15use thiserror::Error;
16use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
17use tokio::process::{Child, ChildStdin, ChildStdout, Command};
18use tokio::sync::{oneshot, Mutex};
19use tracing::warn;
20
21/// Errors produced by LSP client operations.
22#[derive(Debug, Error)]
23pub enum LspError {
24    #[error("failed to spawn LSP server: {0}")]
25    Spawn(String),
26    #[error("LSP protocol error: {0}")]
27    Protocol(String),
28    #[error("LSP server for language {0} not started")]
29    NotStarted(String),
30    #[error("unknown language: {0}")]
31    UnknownLanguage(String),
32}
33
34/// Simplified view of a server's `ServerCapabilities`.
35#[derive(Debug, Clone, Default, PartialEq, Eq)]
36pub struct LspCapabilities {
37    pub has_diagnostics: bool,
38    pub has_references: bool,
39    pub has_definition: bool,
40}
41
42/// Severity of a diagnostic, matching the LSP integer encoding.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[repr(i32)]
45pub enum DiagnosticSeverity {
46    Error = 1,
47    Warning = 2,
48    Information = 3,
49    Hint = 4,
50}
51
52impl Serialize for DiagnosticSeverity {
53    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
54    where
55        S: serde::Serializer,
56    {
57        serializer.serialize_i32(*self as i32)
58    }
59}
60
61impl<'de> Deserialize<'de> for DiagnosticSeverity {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: serde::Deserializer<'de>,
65    {
66        let value = i32::deserialize(deserializer)?;
67        Ok(match value {
68            1 => Self::Error,
69            2 => Self::Warning,
70            3 => Self::Information,
71            _ => Self::Hint,
72        })
73    }
74}
75
76/// A single diagnostic reported by the server.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct Diagnostic {
79    pub line: u32,
80    pub character: u32,
81    pub message: String,
82    pub severity: DiagnosticSeverity,
83}
84
85/// A location within a text document, derived from an LSP `Location.range.start`.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct Location {
88    pub uri: String,
89    pub line: u32,
90    pub character: u32,
91}
92
93/// A half-open range within a text document.
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct TextRange {
96    pub start_line: u32,
97    pub start_char: u32,
98    pub end_line: u32,
99    pub end_char: u32,
100}
101
102/// A single content change for `textDocument/didChange`.
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct TextDocumentChange {
105    pub range: Option<TextRange>,
106    pub text: String,
107}
108
109/// Manages a single LSP server process over stdio.
110pub struct LspServer {
111    child: Child,
112    stdin: Arc<Mutex<ChildStdin>>,
113    next_id: Arc<AtomicU64>,
114    pending: Arc<DashMap<u64, oneshot::Sender<Value>>>,
115    diagnostics: Arc<DashMap<String, Vec<Diagnostic>>>,
116    capabilities: LspCapabilities,
117}
118
119impl LspServer {
120    /// Spawn an LSP server process and send the `initialize` request.
121    pub async fn spawn(
122        command: &str,
123        args: &[&str],
124        workspace_root: &Path,
125    ) -> Result<Self, LspError> {
126        let mut child = Command::new(command)
127            .args(args)
128            .stdin(Stdio::piped())
129            .stdout(Stdio::piped())
130            .stderr(Stdio::piped())
131            .spawn()
132            .map_err(|e| LspError::Spawn(format!("{command}: {e}")))?;
133
134        let stdin = child
135            .stdin
136            .take()
137            .ok_or_else(|| LspError::Spawn("missing stdin".into()))?;
138        let stdout = child
139            .stdout
140            .take()
141            .ok_or_else(|| LspError::Spawn("missing stdout".into()))?;
142
143        let next_id = Arc::new(AtomicU64::new(0));
144        let pending: Arc<DashMap<u64, oneshot::Sender<Value>>> = Arc::new(DashMap::new());
145        let diagnostics: Arc<DashMap<String, Vec<Diagnostic>>> = Arc::new(DashMap::new());
146
147        let reader_pending = pending.clone();
148        let reader_diagnostics = diagnostics.clone();
149        tokio::spawn(async move {
150            if let Err(e) = read_loop(stdout, reader_pending, reader_diagnostics).await {
151                warn!("LSP reader exited: {e}");
152            }
153        });
154
155        let stdin = Arc::new(Mutex::new(stdin));
156        let mut server = Self {
157            child,
158            stdin,
159            next_id,
160            pending,
161            diagnostics,
162            capabilities: LspCapabilities::default(),
163        };
164        server.initialize(workspace_root).await?;
165        Ok(server)
166    }
167
168    /// Send the `initialize` request and record returned capabilities.
169    pub async fn initialize(&mut self, workspace_root: &Path) -> Result<LspCapabilities, LspError> {
170        let root_uri = path_to_uri(workspace_root);
171        let params = json!({
172            "processId": std::process::id(),
173            "rootUri": root_uri,
174            "capabilities": {
175                "textDocument": {
176                    "publishDiagnostics": {"relatedInformation": false}
177                }
178            }
179        });
180        let result = self.send_request("initialize", params).await?;
181        let caps = result.get("capabilities").cloned().unwrap_or(Value::Null);
182        let has_diagnostics = caps.get("textDocumentSync").is_some()
183            || provider_enabled(caps.get("diagnosticProvider").unwrap_or(&Value::Null));
184        let has_references = caps
185            .get("referencesProvider")
186            .map(provider_enabled)
187            .unwrap_or(false);
188        let has_definition = caps
189            .get("definitionProvider")
190            .map(provider_enabled)
191            .unwrap_or(false);
192        self.capabilities = LspCapabilities {
193            has_diagnostics,
194            has_references,
195            has_definition,
196        };
197        self.send_notification("initialized", json!({})).await?;
198        Ok(self.capabilities.clone())
199    }
200
201    /// Send a `textDocument/didOpen` notification.
202    pub async fn text_document_did_open(
203        &self,
204        uri: &str,
205        language_id: &str,
206        text: &str,
207    ) -> Result<(), LspError> {
208        let params = json!({
209            "textDocument": {
210                "uri": uri,
211                "languageId": language_id,
212                "version": 1,
213                "text": text,
214            }
215        });
216        self.send_notification("textDocument/didOpen", params).await
217    }
218
219    /// Send a `textDocument/didChange` notification.
220    pub async fn text_document_did_change(
221        &self,
222        uri: &str,
223        changes: &[TextDocumentChange],
224    ) -> Result<(), LspError> {
225        let changes_json: Vec<Value> = changes
226            .iter()
227            .map(|c| {
228                let range = c.range.as_ref().map(|r| {
229                    json!({
230                        "start": {"line": r.start_line, "character": r.start_char},
231                        "end": {"line": r.end_line, "character": r.end_char},
232                    })
233                });
234                json!({"range": range, "text": c.text})
235            })
236            .collect();
237        let params = json!({
238            "textDocument": {"uri": uri, "version": 2},
239            "contentChanges": changes_json,
240        });
241        self.send_notification("textDocument/didChange", params)
242            .await
243    }
244
245    /// Send a `textDocument/didClose` notification.
246    pub async fn text_document_did_close(&self, uri: &str) -> Result<(), LspError> {
247        let params = json!({"textDocument": {"uri": uri}});
248        self.send_notification("textDocument/didClose", params)
249            .await
250    }
251
252    /// Return the most recently published diagnostics for `uri`.
253    pub async fn diagnostics(&self, uri: &str) -> Result<Vec<Diagnostic>, LspError> {
254        Ok(self
255            .diagnostics
256            .get(uri)
257            .map(|entry| entry.clone())
258            .unwrap_or_default())
259    }
260
261    /// Call `textDocument/references` at the given position.
262    pub async fn references(
263        &self,
264        uri: &str,
265        line: u32,
266        character: u32,
267    ) -> Result<Vec<Location>, LspError> {
268        let params = json!({
269            "textDocument": {"uri": uri},
270            "position": {"line": line, "character": character},
271            "context": {"includeDeclaration": true},
272        });
273        let result = self.send_request("textDocument/references", params).await?;
274        Ok(parse_locations(&result))
275    }
276
277    /// Call `textDocument/definition` at the given position.
278    pub async fn definition(
279        &self,
280        uri: &str,
281        line: u32,
282        character: u32,
283    ) -> Result<Vec<Location>, LspError> {
284        let params = json!({
285            "textDocument": {"uri": uri},
286            "position": {"line": line, "character": character},
287        });
288        let result = self.send_request("textDocument/definition", params).await?;
289        Ok(parse_locations(&result))
290    }
291
292    /// Send the `shutdown` request.
293    pub async fn shutdown(&mut self) -> Result<(), LspError> {
294        self.send_request("shutdown", Value::Null).await?;
295        Ok(())
296    }
297
298    /// Send the `exit` notification and terminate the server process.
299    pub async fn exit(&mut self) {
300        let _ = self.send_notification("exit", Value::Null).await;
301        let _ = self.child.kill().await;
302    }
303
304    async fn send_request(&self, method: &str, params: Value) -> Result<Value, LspError> {
305        let id = self.next_id.fetch_add(1, Ordering::Relaxed) + 1;
306        let (tx, rx) = oneshot::channel();
307        self.pending.insert(id, tx);
308        let body = json!({
309            "jsonrpc": "2.0",
310            "id": id,
311            "method": method,
312            "params": params,
313        });
314        self.write_message(&body).await?;
315        let response = rx
316            .await
317            .map_err(|_| LspError::Protocol(format!("no response for {method} (id {id})")))?;
318        if let Some(err) = response.get("error") {
319            let message = err
320                .get("message")
321                .and_then(|m| m.as_str())
322                .unwrap_or("unknown error");
323            return Err(LspError::Protocol(format!("{method}: {message}")));
324        }
325        Ok(response.get("result").cloned().unwrap_or(Value::Null))
326    }
327
328    async fn send_notification(&self, method: &str, params: Value) -> Result<(), LspError> {
329        let body = json!({
330            "jsonrpc": "2.0",
331            "method": method,
332            "params": params,
333        });
334        self.write_message(&body).await
335    }
336
337    async fn write_message(&self, body: &Value) -> Result<(), LspError> {
338        let serialized =
339            serde_json::to_string(body).map_err(|e| LspError::Protocol(format!("encode: {e}")))?;
340        let frame = format!("Content-Length: {}\r\n\r\n{}", serialized.len(), serialized);
341        let mut stdin = self.stdin.lock().await;
342        stdin
343            .write_all(frame.as_bytes())
344            .await
345            .map_err(|e| LspError::Protocol(format!("write: {e}")))?;
346        stdin
347            .flush()
348            .await
349            .map_err(|e| LspError::Protocol(format!("flush: {e}")))?;
350        Ok(())
351    }
352}
353
354/// Manages multiple LSP servers keyed by language.
355pub struct LspManager {
356    registered: Vec<(String, String, Vec<String>)>,
357    servers: DashMap<String, Arc<Mutex<LspServer>>>,
358}
359
360impl LspManager {
361    pub fn new() -> Self {
362        Self {
363            registered: Vec::new(),
364            servers: DashMap::new(),
365        }
366    }
367
368    /// Register an LSP server for a language without starting it.
369    pub fn register(
370        &mut self,
371        language: &str,
372        command: &str,
373        args: &[&str],
374    ) -> Result<(), LspError> {
375        if self.is_registered(language) {
376            return Err(LspError::Protocol(format!(
377                "language already registered: {language}"
378            )));
379        }
380        self.registered.push((
381            language.to_string(),
382            command.to_string(),
383            args.iter().map(|s| s.to_string()).collect(),
384        ));
385        Ok(())
386    }
387
388    /// Returns whether a server has been registered for `language`.
389    pub fn is_registered(&self, language: &str) -> bool {
390        self.registered.iter().any(|(l, _, _)| l == language) || self.servers.contains_key(language)
391    }
392
393    /// Start all registered servers against `workspace_root`.
394    pub async fn start(&mut self, workspace_root: &Path) -> Result<(), LspError> {
395        let registered = std::mem::take(&mut self.registered);
396        for (language, command, args) in registered {
397            if self.servers.contains_key(&language) {
398                continue;
399            }
400            let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
401            let server = LspServer::spawn(&command, &arg_refs, workspace_root).await?;
402            self.servers.insert(language, Arc::new(Mutex::new(server)));
403        }
404        Ok(())
405    }
406
407    /// Get diagnostics for `uri` from the server registered for `language`.
408    pub async fn diagnostics(
409        &self,
410        uri: &str,
411        language: &str,
412    ) -> Result<Vec<Diagnostic>, LspError> {
413        let server = self.lookup(language)?;
414        let arc = server.clone();
415        drop(server);
416        let guard = arc.lock().await;
417        guard.diagnostics(uri).await
418    }
419
420    /// Call `textDocument/references` on the server for `language`.
421    pub async fn references(
422        &self,
423        uri: &str,
424        language: &str,
425        line: u32,
426        char: u32,
427    ) -> Result<Vec<Location>, LspError> {
428        let server = self.lookup(language)?;
429        let arc = server.clone();
430        drop(server);
431        let guard = arc.lock().await;
432        guard.references(uri, line, char).await
433    }
434
435    /// Call `textDocument/definition` on the server for `language`.
436    pub async fn definition(
437        &self,
438        uri: &str,
439        language: &str,
440        line: u32,
441        char: u32,
442    ) -> Result<Vec<Location>, LspError> {
443        let server = self.lookup(language)?;
444        let arc = server.clone();
445        drop(server);
446        let guard = arc.lock().await;
447        guard.definition(uri, line, char).await
448    }
449
450    /// Shut down every running server.
451    pub async fn shutdown_all(&mut self) -> Result<(), LspError> {
452        let languages: Vec<String> = self.servers.iter().map(|r| r.key().clone()).collect();
453        for language in languages {
454            if let Some((_, arc)) = self.servers.remove(&language) {
455                let mut guard = arc.lock().await;
456                if let Err(e) = guard.shutdown().await {
457                    warn!("LSP shutdown error for {language}: {e}");
458                }
459                guard.exit().await;
460            }
461        }
462        Ok(())
463    }
464
465    fn lookup(&self, language: &str) -> Result<Arc<Mutex<LspServer>>, LspError> {
466        if let Some(entry) = self.servers.get(language) {
467            Ok(entry.clone())
468        } else if self.is_registered(language) {
469            Err(LspError::NotStarted(language.to_string()))
470        } else {
471            Err(LspError::UnknownLanguage(language.to_string()))
472        }
473    }
474}
475
476impl Default for LspManager {
477    fn default() -> Self {
478        Self::new()
479    }
480}
481
482async fn read_loop(
483    stdout: ChildStdout,
484    pending: Arc<DashMap<u64, oneshot::Sender<Value>>>,
485    diagnostics: Arc<DashMap<String, Vec<Diagnostic>>>,
486) -> Result<(), LspError> {
487    let mut reader = BufReader::new(stdout);
488    loop {
489        let mut content_length: Option<usize> = None;
490        loop {
491            let mut line = String::new();
492            let n = reader
493                .read_line(&mut line)
494                .await
495                .map_err(|e| LspError::Protocol(format!("read header: {e}")))?;
496            if n == 0 {
497                return Ok(());
498            }
499            let trimmed = line.trim_end_matches(['\r', '\n']);
500            if trimmed.is_empty() {
501                break;
502            }
503            if let Some(rest) = trimmed.strip_prefix("Content-Length:") {
504                content_length = Some(
505                    rest.trim()
506                        .parse::<usize>()
507                        .map_err(|e| LspError::Protocol(format!("content-length: {e}")))?,
508                );
509            }
510        }
511        let len =
512            content_length.ok_or_else(|| LspError::Protocol("missing Content-Length".into()))?;
513        let mut buf = vec![0u8; len];
514        reader
515            .read_exact(&mut buf)
516            .await
517            .map_err(|e| LspError::Protocol(format!("read body: {e}")))?;
518        let message: Value = serde_json::from_slice(&buf)
519            .map_err(|e| LspError::Protocol(format!("parse body: {e}")))?;
520        dispatch_message(message, &pending, &diagnostics);
521    }
522}
523
524fn dispatch_message(
525    message: Value,
526    pending: &DashMap<u64, oneshot::Sender<Value>>,
527    diagnostics: &DashMap<String, Vec<Diagnostic>>,
528) {
529    if message.get("id").is_some() {
530        if let Some(id) = message.get("id").and_then(|v| v.as_u64()) {
531            if let Some((_, sender)) = pending.remove(&id) {
532                let _ = sender.send(message);
533            }
534        }
535        return;
536    }
537    if message.get("method").and_then(|m| m.as_str()) == Some("textDocument/publishDiagnostics") {
538        if let Some(params) = message.get("params") {
539            let uri = params
540                .get("uri")
541                .and_then(|u| u.as_str())
542                .unwrap_or("")
543                .to_string();
544            let parsed = parse_diagnostics(params);
545            diagnostics.insert(uri, parsed);
546        }
547    }
548}
549
550fn parse_diagnostics(params: &Value) -> Vec<Diagnostic> {
551    params
552        .get("diagnostics")
553        .and_then(|d| d.as_array())
554        .map(|arr| arr.iter().filter_map(parse_diagnostic).collect())
555        .unwrap_or_default()
556}
557
558fn parse_diagnostic(value: &Value) -> Option<Diagnostic> {
559    let range = value.get("range")?;
560    let start = range.get("start")?;
561    let line = start.get("line").and_then(|l| l.as_u64()).unwrap_or(0) as u32;
562    let character = start.get("character").and_then(|c| c.as_u64()).unwrap_or(0) as u32;
563    let message = value
564        .get("message")
565        .and_then(|m| m.as_str())
566        .unwrap_or("")
567        .to_string();
568    let severity = value
569        .get("severity")
570        .and_then(|s| s.as_i64())
571        .map(|i| match i {
572            1 => DiagnosticSeverity::Error,
573            2 => DiagnosticSeverity::Warning,
574            3 => DiagnosticSeverity::Information,
575            _ => DiagnosticSeverity::Hint,
576        })
577        .unwrap_or(DiagnosticSeverity::Error);
578    Some(Diagnostic {
579        line,
580        character,
581        message,
582        severity,
583    })
584}
585
586fn parse_locations(result: &Value) -> Vec<Location> {
587    let items: Vec<Value> = match result {
588        Value::Null => Vec::new(),
589        Value::Array(arr) => arr.clone(),
590        other => vec![other.clone()],
591    };
592    items.iter().filter_map(parse_location).collect()
593}
594
595fn parse_location(value: &Value) -> Option<Location> {
596    let uri = value.get("uri")?.as_str()?.to_string();
597    let range = value.get("range")?;
598    let start = range.get("start")?;
599    let line = start.get("line").and_then(|l| l.as_u64()).unwrap_or(0) as u32;
600    let character = start.get("character").and_then(|c| c.as_u64()).unwrap_or(0) as u32;
601    Some(Location {
602        uri,
603        line,
604        character,
605    })
606}
607
608fn provider_enabled(value: &Value) -> bool {
609    match value {
610        Value::Bool(b) => *b,
611        Value::Object(_) => true,
612        _ => false,
613    }
614}
615
616fn path_to_uri(path: &Path) -> String {
617    let absolute = if path.is_absolute() {
618        path.to_path_buf()
619    } else {
620        std::env::current_dir().unwrap_or_default().join(path)
621    };
622    format!("file://{}", absolute.display())
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628
629    #[test]
630    fn manager_registration() {
631        let mut manager = LspManager::new();
632        assert!(!manager.is_registered("rust"));
633        manager.register("rust", "rust-analyzer", &[]).unwrap();
634        assert!(manager.is_registered("rust"));
635    }
636
637    #[test]
638    fn manager_duplicate_registration_errors() {
639        let mut manager = LspManager::new();
640        manager.register("rust", "rust-analyzer", &[]).unwrap();
641        let result = manager.register("rust", "rust-analyzer", &[]);
642        assert!(matches!(result, Err(LspError::Protocol(_))));
643    }
644
645    #[tokio::test]
646    async fn manager_unknown_language() {
647        let manager = LspManager::new();
648        let result = manager.diagnostics("file:///x.rs", "python").await;
649        assert!(matches!(result, Err(LspError::UnknownLanguage(_))));
650    }
651
652    #[tokio::test]
653    async fn manager_registered_but_not_started() {
654        let mut manager = LspManager::new();
655        manager.register("rust", "rust-analyzer", &[]).unwrap();
656        let result = manager.diagnostics("file:///x.rs", "rust").await;
657        assert!(matches!(result, Err(LspError::NotStarted(_))));
658    }
659
660    #[test]
661    fn diagnostic_severity_serialization() {
662        assert_eq!(
663            serde_json::to_string(&DiagnosticSeverity::Error).unwrap(),
664            "1"
665        );
666        let warning: DiagnosticSeverity = serde_json::from_str("2").unwrap();
667        assert_eq!(warning, DiagnosticSeverity::Warning);
668        let hint: DiagnosticSeverity = serde_json::from_str("9").unwrap();
669        assert_eq!(hint, DiagnosticSeverity::Hint);
670    }
671
672    #[test]
673    fn location_serialization() {
674        let location = Location {
675            uri: "file:///a.rs".into(),
676            line: 3,
677            character: 5,
678        };
679        let serialized = serde_json::to_string(&location).unwrap();
680        let back: Location = serde_json::from_str(&serialized).unwrap();
681        assert_eq!(back, location);
682    }
683
684    #[test]
685    fn text_document_change_construction() {
686        let range = TextRange {
687            start_line: 0,
688            start_char: 0,
689            end_line: 0,
690            end_char: 5,
691        };
692        let change = TextDocumentChange {
693            range: Some(range),
694            text: "hello".into(),
695        };
696        let serialized = serde_json::to_string(&change).unwrap();
697        assert!(serialized.contains("hello"));
698        let back: TextDocumentChange = serde_json::from_str(&serialized).unwrap();
699        assert_eq!(back.text, "hello");
700        assert!(back.range.is_some());
701    }
702
703    #[test]
704    fn parse_locations_handles_variants() {
705        assert!(parse_locations(&Value::Null).is_empty());
706        let single = json!({
707            "uri": "file:///a.rs",
708            "range": {"start": {"line": 1, "character": 2}, "end": {"line": 1, "character": 4}}
709        });
710        let locs = parse_locations(&single);
711        assert_eq!(locs.len(), 1);
712        assert_eq!(locs[0].line, 1);
713        assert_eq!(locs[0].character, 2);
714        let arr = json!([single, single]);
715        assert_eq!(parse_locations(&arr).len(), 2);
716    }
717
718    #[test]
719    fn parse_diagnostics_from_params() {
720        let params = json!({
721            "uri": "file:///a.rs",
722            "diagnostics": [
723                {"range": {"start": {"line": 4, "character": 7}, "end": {"line": 4, "character": 8}},
724                 "message": "unused", "severity": 2}
725            ]
726        });
727        let diags = parse_diagnostics(&params);
728        assert_eq!(diags.len(), 1);
729        assert_eq!(diags[0].line, 4);
730        assert_eq!(diags[0].character, 7);
731        assert_eq!(diags[0].severity, DiagnosticSeverity::Warning);
732    }
733
734    #[test]
735    fn provider_enabled_truthiness() {
736        assert!(provider_enabled(&json!(true)));
737        assert!(!provider_enabled(&json!(false)));
738        assert!(provider_enabled(&json!({"options": {}})));
739        assert!(!provider_enabled(&Value::Null));
740    }
741}