Skip to main content

codetether_agent/lsp/
client.rs

1//! LSP Client - manages language server connections and operations
2//!
3//! Provides high-level API for LSP operations:
4//! - Initialize/shutdown lifecycle
5//! - Document synchronization
6//! - Code intelligence (definition, references, hover, etc.)
7
8use super::transport::LspTransport;
9use super::types::*;
10use anyhow::Result;
11use lsp_types::{
12    ClientCapabilities, CompletionContext, CompletionParams, CompletionTriggerKind,
13    DocumentSymbolParams, HoverParams, Position, TextDocumentIdentifier, TextDocumentItem,
14    TextDocumentPositionParams,
15};
16use std::collections::HashMap;
17use std::path::Path;
18use std::sync::Arc;
19use tokio::sync::RwLock;
20use tracing::{debug, info, warn};
21
22/// LSP Client for a single language server
23pub struct LspClient {
24    transport: LspTransport,
25    config: LspConfig,
26    server_capabilities: RwLock<Option<lsp_types::ServerCapabilities>>,
27    /// Track open documents with their versions
28    open_documents: RwLock<HashMap<String, i32>>,
29}
30
31impl LspClient {
32    /// Create a new LSP client with the given configuration
33    pub async fn new(config: LspConfig) -> Result<Self> {
34        // Auto-install the language server if it's not on PATH
35        super::types::ensure_server_installed(&config).await?;
36
37        let transport = LspTransport::spawn(&config.command, &config.args).await?;
38
39        Ok(Self {
40            transport,
41            config,
42            server_capabilities: RwLock::new(None),
43            open_documents: RwLock::new(HashMap::new()),
44        })
45    }
46
47    /// Create an LSP client for a specific language
48    pub async fn for_language(language: &str, root_uri: Option<String>) -> Result<Self> {
49        let mut config = get_language_server_config(language)
50            .ok_or_else(|| anyhow::anyhow!("Unknown language: {}", language))?;
51        config.root_uri = root_uri;
52        Self::new(config).await
53    }
54
55    /// Initialize the language server
56    pub async fn initialize(&self) -> Result<()> {
57        let root_uri = self.config.root_uri.clone();
58
59        let params = InitializeParams {
60            process_id: Some(std::process::id() as i64),
61            client_info: ClientInfo {
62                name: "codetether".to_string(),
63                version: env!("CARGO_PKG_VERSION").to_string(),
64            },
65            locale: None,
66            root_path: None,
67            root_uri: root_uri.clone(),
68            initialization_options: self.config.initialization_options.clone(),
69            capabilities: ClientCapabilities::default(),
70            trace: None,
71            workspace_folders: None,
72        };
73
74        let response = self
75            .transport
76            .request("initialize", Some(serde_json::to_value(params)?))
77            .await?;
78
79        if let Some(error) = response.error {
80            return Err(anyhow::anyhow!("LSP initialize error: {}", error.message));
81        }
82
83        if let Some(result) = response.result {
84            let init_result: InitializeResult = serde_json::from_value(result)?;
85            *self.server_capabilities.write().await = Some(init_result.capabilities);
86            info!(
87                server_info = ?init_result.server_info,
88                "LSP server initialized"
89            );
90        }
91
92        // Send initialized notification
93        self.transport.notify("initialized", None).await?;
94        self.transport.set_initialized(true);
95
96        Ok(())
97    }
98
99    /// Shutdown the language server
100    pub async fn shutdown(&self) -> Result<()> {
101        let response = self.transport.request("shutdown", None).await?;
102
103        if let Some(error) = response.error {
104            warn!("LSP shutdown error: {}", error.message);
105        }
106
107        self.transport.notify("exit", None).await?;
108        info!("LSP server shutdown complete");
109
110        Ok(())
111    }
112
113    /// Open a text document
114    pub async fn open_document(&self, path: &Path, content: &str) -> Result<()> {
115        let uri = path_to_uri(path);
116        let language_id = detect_language_from_path(path.to_string_lossy().as_ref())
117            .unwrap_or("plaintext")
118            .to_string();
119
120        let text_document = TextDocumentItem {
121            uri: parse_uri(&uri)?,
122            language_id,
123            version: 1,
124            text: content.to_string(),
125        };
126
127        let params = DidOpenTextDocumentParams { text_document };
128        self.transport
129            .notify("textDocument/didOpen", Some(serde_json::to_value(params)?))
130            .await?;
131
132        self.open_documents.write().await.insert(uri, 1);
133        debug!(path = %path.display(), "Opened document");
134
135        Ok(())
136    }
137
138    /// Close a text document
139    pub async fn close_document(&self, path: &Path) -> Result<()> {
140        let uri = path_to_uri(path);
141
142        let text_document = TextDocumentIdentifier {
143            uri: parse_uri(&uri)?,
144        };
145
146        let params = DidCloseTextDocumentParams { text_document };
147        self.transport
148            .notify("textDocument/didClose", Some(serde_json::to_value(params)?))
149            .await?;
150
151        self.open_documents.write().await.remove(&uri);
152        debug!(path = %path.display(), "Closed document");
153
154        Ok(())
155    }
156
157    /// Update a text document
158    pub async fn change_document(&self, path: &Path, content: &str) -> Result<()> {
159        let uri = path_to_uri(path);
160        let mut open_docs = self.open_documents.write().await;
161
162        let version = open_docs.entry(uri.clone()).or_insert(0);
163        *version += 1;
164
165        let text_document = VersionedTextDocumentIdentifier {
166            uri,
167            version: *version,
168        };
169
170        let content_changes = vec![super::types::TextDocumentContentChangeEvent {
171            range: None, // Full document sync
172            range_length: None,
173            text: content.to_string(),
174        }];
175
176        let params = DidChangeTextDocumentParams {
177            text_document,
178            content_changes,
179        };
180
181        self.transport
182            .notify(
183                "textDocument/didChange",
184                Some(serde_json::to_value(params)?),
185            )
186            .await?;
187
188        debug!(path = %path.display(), version = *version, "Changed document");
189
190        Ok(())
191    }
192
193    /// Go to definition
194    pub async fn go_to_definition(
195        &self,
196        path: &Path,
197        line: u32,
198        character: u32,
199    ) -> Result<LspActionResult> {
200        let uri = path_to_uri(path);
201        self.ensure_document_open(path).await?;
202
203        let params = serde_json::json!({
204            "textDocument": { "uri": uri },
205            "position": { "line": line.saturating_sub(1), "character": character.saturating_sub(1) },
206        });
207
208        let response = self
209            .transport
210            .request("textDocument/definition", Some(params))
211            .await?;
212
213        parse_location_response(response, "definition")
214    }
215
216    /// Find references
217    pub async fn find_references(
218        &self,
219        path: &Path,
220        line: u32,
221        character: u32,
222        include_declaration: bool,
223    ) -> Result<LspActionResult> {
224        let uri = path_to_uri(path);
225        self.ensure_document_open(path).await?;
226
227        let params = ReferenceParams {
228            text_document: TextDocumentIdentifier {
229                uri: parse_uri(&uri)?,
230            },
231            position: Position {
232                line: line.saturating_sub(1),
233                character: character.saturating_sub(1),
234            },
235            context: ReferenceContext {
236                include_declaration,
237            },
238        };
239
240        let response = self
241            .transport
242            .request(
243                "textDocument/references",
244                Some(serde_json::to_value(params)?),
245            )
246            .await?;
247
248        parse_location_response(response, "references")
249    }
250
251    /// Get hover information
252    pub async fn hover(&self, path: &Path, line: u32, character: u32) -> Result<LspActionResult> {
253        let uri = path_to_uri(path);
254        self.ensure_document_open(path).await?;
255
256        let params = HoverParams {
257            text_document_position_params: TextDocumentPositionParams {
258                text_document: TextDocumentIdentifier {
259                    uri: parse_uri(&uri)?,
260                },
261                position: Position {
262                    line: line.saturating_sub(1),
263                    character: character.saturating_sub(1),
264                },
265            },
266            work_done_progress_params: Default::default(),
267        };
268
269        let response = self
270            .transport
271            .request("textDocument/hover", Some(serde_json::to_value(params)?))
272            .await?;
273
274        parse_hover_response(response)
275    }
276
277    /// Get document symbols
278    pub async fn document_symbols(&self, path: &Path) -> Result<LspActionResult> {
279        let uri = path_to_uri(path);
280        self.ensure_document_open(path).await?;
281
282        let params = DocumentSymbolParams {
283            text_document: TextDocumentIdentifier {
284                uri: parse_uri(&uri)?,
285            },
286            work_done_progress_params: Default::default(),
287            partial_result_params: Default::default(),
288        };
289
290        let response = self
291            .transport
292            .request(
293                "textDocument/documentSymbol",
294                Some(serde_json::to_value(params)?),
295            )
296            .await?;
297
298        parse_document_symbols_response(response)
299    }
300
301    /// Search workspace symbols
302    pub async fn workspace_symbols(&self, query: &str) -> Result<LspActionResult> {
303        let params = WorkspaceSymbolParams {
304            query: query.to_string(),
305        };
306
307        let response = self
308            .transport
309            .request("workspace/symbol", Some(serde_json::to_value(params)?))
310            .await?;
311
312        parse_workspace_symbols_response(response)
313    }
314
315    /// Go to implementation
316    pub async fn go_to_implementation(
317        &self,
318        path: &Path,
319        line: u32,
320        character: u32,
321    ) -> Result<LspActionResult> {
322        let uri = path_to_uri(path);
323        self.ensure_document_open(path).await?;
324
325        let params = serde_json::json!({
326            "textDocument": { "uri": uri },
327            "position": { "line": line.saturating_sub(1), "character": character.saturating_sub(1) },
328        });
329
330        let response = self
331            .transport
332            .request("textDocument/implementation", Some(params))
333            .await?;
334
335        parse_location_response(response, "implementation")
336    }
337
338    /// Get code completions
339    pub async fn completion(
340        &self,
341        path: &Path,
342        line: u32,
343        character: u32,
344    ) -> Result<LspActionResult> {
345        let uri = path_to_uri(path);
346        self.ensure_document_open(path).await?;
347
348        let params = CompletionParams {
349            text_document_position: TextDocumentPositionParams {
350                text_document: TextDocumentIdentifier {
351                    uri: parse_uri(&uri)?,
352                },
353                position: Position {
354                    line: line.saturating_sub(1),
355                    character: character.saturating_sub(1),
356                },
357            },
358            work_done_progress_params: Default::default(),
359            partial_result_params: Default::default(),
360            context: Some(CompletionContext {
361                trigger_kind: CompletionTriggerKind::INVOKED,
362                trigger_character: None,
363            }),
364        };
365
366        let response = self
367            .transport
368            .request(
369                "textDocument/completion",
370                Some(serde_json::to_value(params)?),
371            )
372            .await?;
373
374        parse_completion_response(response)
375    }
376
377    /// Ensure a document is open (open it if not already)
378    async fn ensure_document_open(&self, path: &Path) -> Result<()> {
379        let uri = path_to_uri(path);
380        if !self.open_documents.read().await.contains_key(&uri) {
381            let content = tokio::fs::read_to_string(path).await?;
382            self.open_document(path, &content).await?;
383        }
384        Ok(())
385    }
386
387    /// Get the server capabilities
388    pub async fn capabilities(&self) -> Option<lsp_types::ServerCapabilities> {
389        self.server_capabilities.read().await.clone()
390    }
391
392    /// Check if this client handles the given file extension
393    pub fn handles_file(&self, path: &Path) -> bool {
394        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
395        self.config.file_extensions.iter().any(|fe| fe == ext)
396    }
397
398    /// Check if this client handles a language by name
399    pub fn handles_language(&self, language: &str) -> bool {
400        let extensions = match language {
401            "rust" => &["rs"][..],
402            "typescript" => &["ts", "tsx"],
403            "javascript" => &["js", "jsx"],
404            "python" => &["py"],
405            "go" => &["go"],
406            "c" => &["c", "h"],
407            "cpp" => &["cpp", "cc", "cxx", "hpp", "h"],
408            _ => &[],
409        };
410
411        extensions
412            .iter()
413            .any(|ext| self.config.file_extensions.iter().any(|fe| fe == *ext))
414    }
415}
416
417/// Convert a file path to a file:// URI
418fn path_to_uri(path: &Path) -> String {
419    let absolute = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
420    format!("file://{}", absolute.display())
421}
422
423/// Parse a string URI into an lsp_types::Uri
424fn parse_uri(uri_str: &str) -> Result<lsp_types::Uri> {
425    uri_str
426        .parse()
427        .map_err(|e| anyhow::anyhow!("Invalid URI: {e}"))
428}
429
430/// Parse a location response (definition, references, implementation)
431fn parse_location_response(response: JsonRpcResponse, _operation: &str) -> Result<LspActionResult> {
432    if let Some(error) = response.error {
433        return Ok(LspActionResult::Error {
434            message: error.message,
435        });
436    }
437
438    let Some(result) = response.result else {
439        return Ok(LspActionResult::Definition { locations: vec![] });
440    };
441
442    // Try to parse as a single location
443    if let Ok(loc) = serde_json::from_value::<lsp_types::Location>(result.clone()) {
444        return Ok(LspActionResult::Definition {
445            locations: vec![LocationInfo::from(loc)],
446        });
447    }
448
449    // Try to parse as an array of locations
450    if let Ok(locs) = serde_json::from_value::<Vec<lsp_types::Location>>(result.clone()) {
451        return Ok(LspActionResult::Definition {
452            locations: locs.into_iter().map(LocationInfo::from).collect(),
453        });
454    }
455
456    // Try to parse as LocationLink array
457    if let Ok(links) = serde_json::from_value::<Vec<lsp_types::LocationLink>>(result) {
458        return Ok(LspActionResult::Definition {
459            locations: links
460                .into_iter()
461                .filter_map(|link| {
462                    Some(LocationInfo {
463                        uri: link.target_uri.to_string(),
464                        range: RangeInfo::from(link.target_selection_range),
465                    })
466                })
467                .collect(),
468        });
469    }
470
471    Ok(LspActionResult::Definition { locations: vec![] })
472}
473
474/// Parse a hover response
475fn parse_hover_response(response: JsonRpcResponse) -> Result<LspActionResult> {
476    if let Some(error) = response.error {
477        return Ok(LspActionResult::Error {
478            message: error.message,
479        });
480    }
481
482    let Some(result) = response.result else {
483        return Ok(LspActionResult::Hover {
484            contents: String::new(),
485            range: None,
486        });
487    };
488
489    if result.is_null() {
490        return Ok(LspActionResult::Hover {
491            contents: "No hover information available".to_string(),
492            range: None,
493        });
494    }
495
496    let hover: lsp_types::Hover = serde_json::from_value(result)?;
497
498    let contents = match hover.contents {
499        lsp_types::HoverContents::Scalar(markup) => match markup {
500            lsp_types::MarkedString::String(s) => s,
501            lsp_types::MarkedString::LanguageString(ls) => ls.value,
502        },
503        lsp_types::HoverContents::Array(markups) => markups
504            .into_iter()
505            .map(|m| match m {
506                lsp_types::MarkedString::String(s) => s,
507                lsp_types::MarkedString::LanguageString(ls) => ls.value,
508            })
509            .collect::<Vec<_>>()
510            .join("\n\n"),
511        lsp_types::HoverContents::Markup(markup) => markup.value,
512    };
513
514    Ok(LspActionResult::Hover {
515        contents,
516        range: hover.range.map(RangeInfo::from),
517    })
518}
519
520/// Parse a document symbols response
521fn parse_document_symbols_response(response: JsonRpcResponse) -> Result<LspActionResult> {
522    if let Some(error) = response.error {
523        return Ok(LspActionResult::Error {
524            message: error.message,
525        });
526    }
527
528    let Some(result) = response.result else {
529        return Ok(LspActionResult::DocumentSymbols { symbols: vec![] });
530    };
531
532    if result.is_null() {
533        return Ok(LspActionResult::DocumentSymbols { symbols: vec![] });
534    }
535
536    // Try DocumentSymbol[] first (hierarchical)
537    if let Ok(symbols) = serde_json::from_value::<Vec<lsp_types::DocumentSymbol>>(result.clone()) {
538        return Ok(LspActionResult::DocumentSymbols {
539            symbols: symbols.into_iter().map(SymbolInfo::from).collect(),
540        });
541    }
542
543    // Try SymbolInformation[] (flat)
544    if let Ok(symbols) = serde_json::from_value::<Vec<lsp_types::SymbolInformation>>(result) {
545        return Ok(LspActionResult::DocumentSymbols {
546            symbols: symbols.into_iter().map(SymbolInfo::from).collect(),
547        });
548    }
549
550    Ok(LspActionResult::DocumentSymbols { symbols: vec![] })
551}
552
553/// Parse a workspace symbols response
554fn parse_workspace_symbols_response(response: JsonRpcResponse) -> Result<LspActionResult> {
555    if let Some(error) = response.error {
556        return Ok(LspActionResult::Error {
557            message: error.message,
558        });
559    }
560
561    let Some(result) = response.result else {
562        return Ok(LspActionResult::WorkspaceSymbols { symbols: vec![] });
563    };
564
565    if result.is_null() {
566        return Ok(LspActionResult::WorkspaceSymbols { symbols: vec![] });
567    }
568
569    // Try SymbolInformation[]
570    if let Ok(symbols) = serde_json::from_value::<Vec<lsp_types::SymbolInformation>>(result.clone())
571    {
572        return Ok(LspActionResult::WorkspaceSymbols {
573            symbols: symbols.into_iter().map(SymbolInfo::from).collect(),
574        });
575    }
576
577    // Try WorkspaceSymbol[] (LSP 3.17+)
578    if let Ok(symbols) = serde_json::from_value::<Vec<lsp_types::WorkspaceSymbol>>(result) {
579        return Ok(LspActionResult::WorkspaceSymbols {
580            symbols: symbols
581                .into_iter()
582                .map(|s| {
583                    let (uri, range) = match s.location {
584                        lsp_types::OneOf::Left(loc) => {
585                            (loc.uri.to_string(), Some(RangeInfo::from(loc.range)))
586                        }
587                        lsp_types::OneOf::Right(wl) => (wl.uri.to_string(), None),
588                    };
589                    SymbolInfo {
590                        name: s.name,
591                        kind: format!("{:?}", s.kind),
592                        detail: None,
593                        uri: Some(uri),
594                        range,
595                        container_name: s.container_name,
596                    }
597                })
598                .collect(),
599        });
600    }
601
602    Ok(LspActionResult::WorkspaceSymbols { symbols: vec![] })
603}
604
605/// Parse a completion response
606fn parse_completion_response(response: JsonRpcResponse) -> Result<LspActionResult> {
607    if let Some(error) = response.error {
608        return Ok(LspActionResult::Error {
609            message: error.message,
610        });
611    }
612
613    let Some(result) = response.result else {
614        return Ok(LspActionResult::Completion { items: vec![] });
615    };
616
617    if result.is_null() {
618        return Ok(LspActionResult::Completion { items: vec![] });
619    }
620
621    // Try CompletionList first
622    if let Ok(list) = serde_json::from_value::<lsp_types::CompletionList>(result.clone()) {
623        return Ok(LspActionResult::Completion {
624            items: list
625                .items
626                .into_iter()
627                .map(CompletionItemInfo::from)
628                .collect(),
629        });
630    }
631
632    // Try CompletionItem[]
633    if let Ok(items) = serde_json::from_value::<Vec<lsp_types::CompletionItem>>(result) {
634        return Ok(LspActionResult::Completion {
635            items: items.into_iter().map(CompletionItemInfo::from).collect(),
636        });
637    }
638
639    Ok(LspActionResult::Completion { items: vec![] })
640}
641
642/// LSP Manager - manages multiple language server connections
643pub struct LspManager {
644    clients: RwLock<HashMap<String, Arc<LspClient>>>,
645    root_uri: Option<String>,
646}
647
648impl LspManager {
649    /// Create a new LSP manager
650    pub fn new(root_uri: Option<String>) -> Self {
651        Self {
652            clients: RwLock::new(HashMap::new()),
653            root_uri,
654        }
655    }
656
657    /// Get or create a client for the given language
658    pub async fn get_client(&self, language: &str) -> Result<Arc<LspClient>> {
659        // Check if we already have a client
660        {
661            let clients = self.clients.read().await;
662            if let Some(client) = clients.get(language) {
663                return Ok(Arc::clone(client));
664            }
665        }
666
667        // Create a new client
668        let client = LspClient::for_language(language, self.root_uri.clone()).await?;
669        client.initialize().await?;
670
671        let client = Arc::new(client);
672        self.clients
673            .write()
674            .await
675            .insert(language.to_string(), Arc::clone(&client));
676
677        Ok(client)
678    }
679
680    /// Get a client for a file path (detects language from extension)
681    pub async fn get_client_for_file(&self, path: &Path) -> Result<Arc<LspClient>> {
682        let language = detect_language_from_path(path.to_string_lossy().as_ref())
683            .ok_or_else(|| anyhow::anyhow!("Unknown language for file: {}", path.display()))?;
684        self.get_client(language).await
685    }
686
687    /// Check if any registered client handles the given file.
688    pub async fn handles_file(&self, path: &Path) -> bool {
689        let clients = self.clients.read().await;
690        clients.values().any(|c| c.handles_file(path))
691    }
692
693    /// Get capabilities for a specific language server.
694    pub async fn capabilities_for(&self, language: &str) -> Option<lsp_types::ServerCapabilities> {
695        let clients = self.clients.read().await;
696        if let Some(client) = clients.get(language) {
697            client.capabilities().await
698        } else {
699            None
700        }
701    }
702
703    /// Close a document across all relevant clients.
704    pub async fn close_document(&self, path: &Path) -> Result<()> {
705        if let Ok(client) = self.get_client_for_file(path).await {
706            client.close_document(path).await?;
707        }
708        Ok(())
709    }
710
711    /// Notify clients of a document change.
712    pub async fn change_document(&self, path: &Path, content: &str) -> Result<()> {
713        if let Ok(client) = self.get_client_for_file(path).await {
714            client.change_document(path, content).await?;
715        }
716        Ok(())
717    }
718
719    /// Shutdown all clients
720    pub async fn shutdown_all(&self) {
721        let clients = self.clients.read().await;
722        for (lang, client) in clients.iter() {
723            if let Err(e) = client.shutdown().await {
724                warn!("Failed to shutdown {} language server: {}", lang, e);
725            }
726        }
727    }
728}
729
730impl Default for LspManager {
731    fn default() -> Self {
732        Self::new(None)
733    }
734}