tenseleyflow/fackr / 69345bd

Browse files

feat: add LSP client infrastructure

Add Language Server Protocol client implementation supporting:
- Process management for spawning language servers
- JSON-RPC message parsing and serialization
- Request/response callback handling
- Multi-server management per language
- Document synchronization (didOpen, didChange, didClose)
- Hover, completion, definition, references, rename requests
- Diagnostics handling via publishDiagnostics notifications

Note: workspaceFolders capability is disabled to ensure pyright
sends diagnostics immediately after didOpen.
Authored by espadonne
SHA
69345bd1789bd6c75b752f75415f5a2ab4a3a53b
Parents
be98840
Tree
aeb12de

7 changed files

StatusFile+-
A src/lsp/client.rs 512 0
A src/lsp/manager.rs 648 0
A src/lsp/message.rs 206 0
A src/lsp/mod.rs 26 0
A src/lsp/process.rs 181 0
A src/lsp/protocol.rs 726 0
A src/lsp/types.rs 474 0
src/lsp/client.rsadded
@@ -0,0 +1,512 @@
1
+//! High-level LSP client API
2
+//!
3
+//! Provides a convenient interface for the editor to interact with language servers.
4
+//!
5
+//! Note: Many methods here are planned LSP features not yet wired to keybindings/UI.
6
+#![allow(dead_code)]
7
+
8
+use anyhow::Result;
9
+use std::collections::HashMap;
10
+use std::sync::mpsc::{self, Receiver, Sender};
11
+use std::sync::{Arc, Mutex};
12
+
13
+use super::manager::LspManager;
14
+use super::protocol;
15
+use super::types::{
16
+    detect_language, path_to_uri, CompletionItem, Diagnostic, DocumentSymbol, HoverInfo, Location,
17
+    Position, Range, TextEdit, WorkspaceEdit,
18
+};
19
+
20
+/// Document state tracked by the LSP client
21
+#[derive(Debug)]
22
+struct DocumentInfo {
23
+    uri: String,
24
+    language_id: String,
25
+    version: i32,
26
+}
27
+
28
+/// High-level LSP client for the editor
29
+pub struct LspClient {
30
+    manager: LspManager,
31
+    /// Tracked documents
32
+    documents: HashMap<String, DocumentInfo>,
33
+    /// Channel for receiving async responses
34
+    response_rx: Receiver<LspResponse>,
35
+    response_tx: Sender<LspResponse>,
36
+    /// Pending diagnostics by URI
37
+    diagnostics: Arc<Mutex<HashMap<String, Vec<Diagnostic>>>>,
38
+}
39
+
40
+/// Response types that can be received asynchronously
41
+#[derive(Debug)]
42
+pub enum LspResponse {
43
+    Completions(i64, Vec<CompletionItem>),
44
+    Hover(i64, Option<HoverInfo>),
45
+    Definition(i64, Vec<Location>),
46
+    References(i64, Vec<Location>),
47
+    Symbols(i64, Vec<DocumentSymbol>),
48
+    Formatting(i64, Vec<TextEdit>),
49
+    Rename(i64, WorkspaceEdit),
50
+    CodeActions(i64, Vec<CodeAction>),
51
+    Error(i64, String),
52
+}
53
+
54
+/// Code action from the server
55
+#[derive(Debug, Clone)]
56
+pub struct CodeAction {
57
+    pub title: String,
58
+    pub kind: Option<String>,
59
+    pub edit: Option<WorkspaceEdit>,
60
+    pub command: Option<String>,
61
+}
62
+
63
+impl LspClient {
64
+    /// Create a new LSP client
65
+    pub fn new(workspace_root: &str) -> Self {
66
+        let (tx, rx) = mpsc::channel();
67
+        let diagnostics = Arc::new(Mutex::new(HashMap::new()));
68
+        let diag_clone = Arc::clone(&diagnostics);
69
+
70
+        let mut manager = LspManager::new(workspace_root);
71
+
72
+        // Set up diagnostics callback
73
+        manager.set_diagnostics_callback(move |uri, diags| {
74
+            if let Ok(mut map) = diag_clone.lock() {
75
+                map.insert(uri, diags);
76
+            }
77
+        });
78
+
79
+        Self {
80
+            manager,
81
+            documents: HashMap::new(),
82
+            response_rx: rx,
83
+            response_tx: tx,
84
+            diagnostics,
85
+        }
86
+    }
87
+
88
+    /// Open a document (notifies the language server)
89
+    pub fn open_document(&mut self, path: &str, content: &str) -> Result<()> {
90
+        let language_id = match detect_language(path) {
91
+            Some(lang) => lang,
92
+            None => return Ok(()), // No LSP support for this file type
93
+        };
94
+
95
+        let uri = path_to_uri(path);
96
+
97
+        // Check if document is already being tracked
98
+        if self.documents.contains_key(path) {
99
+            // Document already tracked - don't send another didOpen
100
+            // The original didOpen (possibly queued) will be sent eventually
101
+            // Just update the content if needed via didChange
102
+            // But only if content actually differs (to avoid unnecessary messages)
103
+            return Ok(());
104
+        }
105
+
106
+        // Track the document
107
+        self.documents.insert(
108
+            path.to_string(),
109
+            DocumentInfo {
110
+                uri: uri.clone(),
111
+                language_id: language_id.to_string(),
112
+                version: 1,
113
+            },
114
+        );
115
+
116
+        // Send didOpen notification
117
+        let notification =
118
+            protocol::create_did_open_notification(&uri, language_id, 1, content);
119
+        self.manager.send_notification(language_id, notification)?;
120
+
121
+        Ok(())
122
+    }
123
+
124
+    /// Notify the server of document changes
125
+    pub fn document_changed(&mut self, path: &str, content: &str) -> Result<()> {
126
+        let doc = match self.documents.get_mut(path) {
127
+            Some(d) => d,
128
+            None => return Ok(()), // Document not tracked
129
+        };
130
+
131
+        doc.version += 1;
132
+        let notification =
133
+            protocol::create_did_change_notification(&doc.uri, doc.version, content);
134
+        self.manager
135
+            .send_notification(&doc.language_id, notification)?;
136
+
137
+        Ok(())
138
+    }
139
+
140
+    /// Notify the server that a document was saved
141
+    pub fn document_saved(&mut self, path: &str, content: Option<&str>) -> Result<()> {
142
+        let doc = match self.documents.get(path) {
143
+            Some(d) => d,
144
+            None => return Ok(()),
145
+        };
146
+
147
+        let notification = protocol::create_did_save_notification(&doc.uri, content);
148
+        self.manager
149
+            .send_notification(&doc.language_id, notification)?;
150
+
151
+        Ok(())
152
+    }
153
+
154
+    /// Close a document
155
+    pub fn close_document(&mut self, path: &str) -> Result<()> {
156
+        let doc = match self.documents.remove(path) {
157
+            Some(d) => d,
158
+            None => return Ok(()),
159
+        };
160
+
161
+        let notification = protocol::create_did_close_notification(&doc.uri);
162
+        self.manager
163
+            .send_notification(&doc.language_id, notification)?;
164
+
165
+        // Clear diagnostics for this file
166
+        if let Ok(mut diags) = self.diagnostics.lock() {
167
+            diags.remove(&doc.uri);
168
+        }
169
+
170
+        Ok(())
171
+    }
172
+
173
+    /// Request completions at a position
174
+    pub fn request_completions(&mut self, path: &str, line: u32, character: u32) -> Result<i64> {
175
+        let doc = self
176
+            .documents
177
+            .get(path)
178
+            .ok_or_else(|| anyhow::anyhow!("Document not open: {}", path))?;
179
+
180
+        let id = protocol::next_request_id();
181
+        let request = protocol::create_completion_request(
182
+            id,
183
+            &doc.uri,
184
+            Position::new(line, character),
185
+        );
186
+
187
+        let tx = self.response_tx.clone();
188
+        self.manager.send_request(
189
+            &doc.language_id,
190
+            request,
191
+            Box::new(move |req_id, result| {
192
+                let response = match result {
193
+                    Ok(value) => {
194
+                        LspResponse::Completions(req_id, protocol::parse_completion_items(&value))
195
+                    }
196
+                    Err(e) => LspResponse::Error(req_id, e.message),
197
+                };
198
+                let _ = tx.send(response);
199
+            }),
200
+        )?;
201
+
202
+        Ok(id)
203
+    }
204
+
205
+    /// Request hover information at a position
206
+    pub fn request_hover(&mut self, path: &str, line: u32, character: u32) -> Result<i64> {
207
+        let doc = self
208
+            .documents
209
+            .get(path)
210
+            .ok_or_else(|| anyhow::anyhow!("Document not open: {}", path))?;
211
+
212
+        let id = protocol::next_request_id();
213
+        let request =
214
+            protocol::create_hover_request(id, &doc.uri, Position::new(line, character));
215
+
216
+        let tx = self.response_tx.clone();
217
+        self.manager.send_request(
218
+            &doc.language_id,
219
+            request,
220
+            Box::new(move |req_id, result| {
221
+                let response = match result {
222
+                    Ok(value) => LspResponse::Hover(req_id, protocol::parse_hover(&value)),
223
+                    Err(e) => LspResponse::Error(req_id, e.message),
224
+                };
225
+                let _ = tx.send(response);
226
+            }),
227
+        )?;
228
+
229
+        Ok(id)
230
+    }
231
+
232
+    /// Request go-to-definition at a position
233
+    pub fn request_definition(&mut self, path: &str, line: u32, character: u32) -> Result<i64> {
234
+        let doc = self
235
+            .documents
236
+            .get(path)
237
+            .ok_or_else(|| anyhow::anyhow!("Document not open: {}", path))?;
238
+
239
+        let id = protocol::next_request_id();
240
+        let request =
241
+            protocol::create_definition_request(id, &doc.uri, Position::new(line, character));
242
+
243
+        let tx = self.response_tx.clone();
244
+        self.manager.send_request(
245
+            &doc.language_id,
246
+            request,
247
+            Box::new(move |req_id, result| {
248
+                let response = match result {
249
+                    Ok(value) => LspResponse::Definition(req_id, protocol::parse_locations(&value)),
250
+                    Err(e) => LspResponse::Error(req_id, e.message),
251
+                };
252
+                let _ = tx.send(response);
253
+            }),
254
+        )?;
255
+
256
+        Ok(id)
257
+    }
258
+
259
+    /// Request find-references at a position
260
+    pub fn request_references(
261
+        &mut self,
262
+        path: &str,
263
+        line: u32,
264
+        character: u32,
265
+        include_declaration: bool,
266
+    ) -> Result<i64> {
267
+        let doc = self
268
+            .documents
269
+            .get(path)
270
+            .ok_or_else(|| anyhow::anyhow!("Document not open: {}", path))?;
271
+
272
+        let id = protocol::next_request_id();
273
+        let request = protocol::create_references_request(
274
+            id,
275
+            &doc.uri,
276
+            Position::new(line, character),
277
+            include_declaration,
278
+        );
279
+
280
+        let tx = self.response_tx.clone();
281
+        self.manager.send_request(
282
+            &doc.language_id,
283
+            request,
284
+            Box::new(move |req_id, result| {
285
+                let response = match result {
286
+                    Ok(value) => LspResponse::References(req_id, protocol::parse_locations(&value)),
287
+                    Err(e) => LspResponse::Error(req_id, e.message),
288
+                };
289
+                let _ = tx.send(response);
290
+            }),
291
+        )?;
292
+
293
+        Ok(id)
294
+    }
295
+
296
+    /// Request document symbols
297
+    pub fn request_document_symbols(&mut self, path: &str) -> Result<i64> {
298
+        let doc = self
299
+            .documents
300
+            .get(path)
301
+            .ok_or_else(|| anyhow::anyhow!("Document not open: {}", path))?;
302
+
303
+        let id = protocol::next_request_id();
304
+        let request = protocol::create_document_symbols_request(id, &doc.uri);
305
+
306
+        let tx = self.response_tx.clone();
307
+        self.manager.send_request(
308
+            &doc.language_id,
309
+            request,
310
+            Box::new(move |req_id, result| {
311
+                let response = match result {
312
+                    Ok(value) => {
313
+                        LspResponse::Symbols(req_id, protocol::parse_document_symbols(&value))
314
+                    }
315
+                    Err(e) => LspResponse::Error(req_id, e.message),
316
+                };
317
+                let _ = tx.send(response);
318
+            }),
319
+        )?;
320
+
321
+        Ok(id)
322
+    }
323
+
324
+    /// Request document formatting
325
+    pub fn request_formatting(&mut self, path: &str, tab_size: u32, use_spaces: bool) -> Result<i64> {
326
+        let doc = self
327
+            .documents
328
+            .get(path)
329
+            .ok_or_else(|| anyhow::anyhow!("Document not open: {}", path))?;
330
+
331
+        let id = protocol::next_request_id();
332
+        let request = protocol::create_formatting_request(id, &doc.uri, tab_size, use_spaces);
333
+
334
+        let tx = self.response_tx.clone();
335
+        self.manager.send_request(
336
+            &doc.language_id,
337
+            request,
338
+            Box::new(move |req_id, result| {
339
+                let response = match result {
340
+                    Ok(value) => {
341
+                        LspResponse::Formatting(req_id, protocol::parse_text_edits(&value))
342
+                    }
343
+                    Err(e) => LspResponse::Error(req_id, e.message),
344
+                };
345
+                let _ = tx.send(response);
346
+            }),
347
+        )?;
348
+
349
+        Ok(id)
350
+    }
351
+
352
+    /// Request rename refactoring
353
+    pub fn request_rename(
354
+        &mut self,
355
+        path: &str,
356
+        line: u32,
357
+        character: u32,
358
+        new_name: &str,
359
+    ) -> Result<i64> {
360
+        let doc = self
361
+            .documents
362
+            .get(path)
363
+            .ok_or_else(|| anyhow::anyhow!("Document not open: {}", path))?;
364
+
365
+        let id = protocol::next_request_id();
366
+        let request = protocol::create_rename_request(
367
+            id,
368
+            &doc.uri,
369
+            Position::new(line, character),
370
+            new_name,
371
+        );
372
+
373
+        let tx = self.response_tx.clone();
374
+        self.manager.send_request(
375
+            &doc.language_id,
376
+            request,
377
+            Box::new(move |req_id, result| {
378
+                let response = match result {
379
+                    Ok(value) => {
380
+                        LspResponse::Rename(req_id, protocol::parse_workspace_edit(&value))
381
+                    }
382
+                    Err(e) => LspResponse::Error(req_id, e.message),
383
+                };
384
+                let _ = tx.send(response);
385
+            }),
386
+        )?;
387
+
388
+        Ok(id)
389
+    }
390
+
391
+    /// Request code actions for a range
392
+    pub fn request_code_actions(
393
+        &mut self,
394
+        path: &str,
395
+        start_line: u32,
396
+        start_char: u32,
397
+        end_line: u32,
398
+        end_char: u32,
399
+    ) -> Result<i64> {
400
+        let doc = self
401
+            .documents
402
+            .get(path)
403
+            .ok_or_else(|| anyhow::anyhow!("Document not open: {}", path))?;
404
+
405
+        let id = protocol::next_request_id();
406
+        let range = Range::new(
407
+            Position::new(start_line, start_char),
408
+            Position::new(end_line, end_char),
409
+        );
410
+        let request = protocol::create_code_action_request(id, &doc.uri, range);
411
+
412
+        let tx = self.response_tx.clone();
413
+        self.manager.send_request(
414
+            &doc.language_id,
415
+            request,
416
+            Box::new(move |req_id, result| {
417
+                let response = match result {
418
+                    Ok(value) => {
419
+                        let actions = parse_code_actions(&value);
420
+                        LspResponse::CodeActions(req_id, actions)
421
+                    }
422
+                    Err(e) => LspResponse::Error(req_id, e.message),
423
+                };
424
+                let _ = tx.send(response);
425
+            }),
426
+        )?;
427
+
428
+        Ok(id)
429
+    }
430
+
431
+    /// Poll for responses (non-blocking)
432
+    pub fn poll_response(&self) -> Option<LspResponse> {
433
+        self.response_rx.try_recv().ok()
434
+    }
435
+
436
+    /// Get diagnostics for a file
437
+    pub fn get_diagnostics(&self, path: &str) -> Vec<Diagnostic> {
438
+        let uri = path_to_uri(path);
439
+        self.diagnostics
440
+            .lock()
441
+            .ok()
442
+            .and_then(|map| map.get(&uri).cloned())
443
+            .unwrap_or_default()
444
+    }
445
+
446
+    /// Get all diagnostics
447
+    pub fn get_all_diagnostics(&self) -> HashMap<String, Vec<Diagnostic>> {
448
+        self.diagnostics
449
+            .lock()
450
+            .ok()
451
+            .map(|map| map.clone())
452
+            .unwrap_or_default()
453
+    }
454
+
455
+    /// Process pending server messages (call this regularly)
456
+    pub fn process_messages(&mut self) {
457
+        self.manager.process_messages();
458
+    }
459
+
460
+    /// Check if LSP is available for a language
461
+    pub fn has_server(&self, language: &str) -> bool {
462
+        self.manager.has_server(language)
463
+    }
464
+
465
+    /// Check if LSP is available for a file
466
+    pub fn has_server_for_file(&self, path: &str) -> bool {
467
+        detect_language(path)
468
+            .map(|lang| self.manager.has_server(lang))
469
+            .unwrap_or(false)
470
+    }
471
+
472
+    /// Shutdown all servers
473
+    pub fn shutdown(&mut self) {
474
+        self.manager.stop_all();
475
+    }
476
+}
477
+
478
+/// Parse code actions from response
479
+fn parse_code_actions(value: &serde_json::Value) -> Vec<CodeAction> {
480
+    value
481
+        .as_array()
482
+        .map(|arr| {
483
+            arr.iter()
484
+                .filter_map(|action| {
485
+                    let title = action.get("title")?.as_str()?.to_string();
486
+                    let kind = action.get("kind").and_then(|v| v.as_str()).map(String::from);
487
+                    let edit = action
488
+                        .get("edit")
489
+                        .map(|e| protocol::parse_workspace_edit(e));
490
+                    let command = action
491
+                        .get("command")
492
+                        .and_then(|c| c.get("command"))
493
+                        .and_then(|v| v.as_str())
494
+                        .map(String::from);
495
+
496
+                    Some(CodeAction {
497
+                        title,
498
+                        kind,
499
+                        edit,
500
+                        command,
501
+                    })
502
+                })
503
+                .collect()
504
+        })
505
+        .unwrap_or_default()
506
+}
507
+
508
+impl Drop for LspClient {
509
+    fn drop(&mut self) {
510
+        self.shutdown();
511
+    }
512
+}
src/lsp/manager.rsadded
@@ -0,0 +1,648 @@
1
+//! LSP server manager
2
+//!
3
+//! Manages multiple language server instances, handles initialization,
4
+//! and routes requests to appropriate servers.
5
+//!
6
+//! Note: Some methods are planned features not yet wired to the UI.
7
+#![allow(dead_code)]
8
+
9
+use anyhow::{anyhow, Result};
10
+use serde_json::Value;
11
+use std::collections::HashMap;
12
+use std::sync::{Arc, Mutex};
13
+
14
+use super::message::{DiagnosticsCallback, MessageHandler, ResponseCallback};
15
+use super::process::ServerProcess;
16
+use super::protocol::{self, LspMessage};
17
+use super::types::{Capabilities, ServerConfig};
18
+
19
+/// State of a language server
20
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21
+pub enum ServerState {
22
+    Starting,
23
+    Initializing,
24
+    Ready,
25
+    ShuttingDown,
26
+    Stopped,
27
+}
28
+
29
+/// A managed language server instance
30
+pub struct ManagedServer {
31
+    pub config: ServerConfig,
32
+    pub process: ServerProcess,
33
+    pub state: ServerState,
34
+    pub capabilities: Capabilities,
35
+    pub handler: MessageHandler,
36
+    /// Queued didOpen notifications (for files opened before initialization)
37
+    pending_opens: Vec<LspMessage>,
38
+}
39
+
40
+impl ManagedServer {
41
+    fn new(config: ServerConfig, process: ServerProcess) -> Self {
42
+        Self {
43
+            config,
44
+            process,
45
+            state: ServerState::Starting,
46
+            capabilities: Capabilities::default(),
47
+            handler: MessageHandler::new(),
48
+            pending_opens: Vec::new(),
49
+        }
50
+    }
51
+}
52
+
53
+/// Manager for multiple language servers
54
+pub struct LspManager {
55
+    /// Workspace root path
56
+    workspace_root: String,
57
+    /// Server configurations by language
58
+    configs: HashMap<String, Vec<ServerConfig>>,
59
+    /// Active servers by language
60
+    servers: HashMap<String, Vec<ManagedServer>>,
61
+    /// Global diagnostics callback
62
+    diagnostics_callback: Option<Arc<Mutex<DiagnosticsCallback>>>,
63
+}
64
+
65
+impl LspManager {
66
+    /// Create a new LSP manager
67
+    pub fn new(workspace_root: &str) -> Self {
68
+        let mut manager = Self {
69
+            workspace_root: workspace_root.to_string(),
70
+            configs: HashMap::new(),
71
+            servers: HashMap::new(),
72
+            diagnostics_callback: None,
73
+        };
74
+        manager.register_default_configs();
75
+        manager
76
+    }
77
+
78
+    /// Set the global diagnostics callback
79
+    pub fn set_diagnostics_callback<F>(&mut self, callback: F)
80
+    where
81
+        F: Fn(String, Vec<super::types::Diagnostic>) + Send + 'static,
82
+    {
83
+        self.diagnostics_callback = Some(Arc::new(Mutex::new(Box::new(callback))));
84
+    }
85
+
86
+    /// Register default server configurations
87
+    fn register_default_configs(&mut self) {
88
+        // Rust - rust-analyzer
89
+        self.register_config(ServerConfig::new("rust-analyzer", "rust", vec!["rust-analyzer"]));
90
+
91
+        // Python - pyright and ruff
92
+        self.register_config(ServerConfig::new(
93
+            "pyright",
94
+            "python",
95
+            vec!["pyright-langserver", "--stdio"],
96
+        ));
97
+        self.register_config(
98
+            ServerConfig::new("ruff", "python", vec!["ruff", "server"]).with_capabilities(
99
+                Capabilities {
100
+                    completion: false,
101
+                    hover: false,
102
+                    definition: false,
103
+                    references: false,
104
+                    rename: false,
105
+                    code_actions: true,
106
+                    formatting: true,
107
+                    diagnostics: true,
108
+                    document_symbols: false,
109
+                    workspace_symbols: false,
110
+                    signature_help: false,
111
+                },
112
+            ),
113
+        );
114
+
115
+        // TypeScript/JavaScript - typescript-language-server
116
+        self.register_config(ServerConfig::new(
117
+            "typescript-language-server",
118
+            "typescript",
119
+            vec!["typescript-language-server", "--stdio"],
120
+        ));
121
+        self.register_config(ServerConfig::new(
122
+            "typescript-language-server",
123
+            "typescriptreact",
124
+            vec!["typescript-language-server", "--stdio"],
125
+        ));
126
+        self.register_config(ServerConfig::new(
127
+            "typescript-language-server",
128
+            "javascript",
129
+            vec!["typescript-language-server", "--stdio"],
130
+        ));
131
+        self.register_config(ServerConfig::new(
132
+            "typescript-language-server",
133
+            "javascriptreact",
134
+            vec!["typescript-language-server", "--stdio"],
135
+        ));
136
+
137
+        // Go - gopls
138
+        self.register_config(ServerConfig::new("gopls", "go", vec!["gopls"]));
139
+
140
+        // C/C++ - clangd
141
+        self.register_config(ServerConfig::new("clangd", "c", vec!["clangd"]));
142
+        self.register_config(ServerConfig::new("clangd", "cpp", vec!["clangd"]));
143
+
144
+        // Java - jdtls (Eclipse JDT Language Server)
145
+        self.register_config(ServerConfig::new("jdtls", "java", vec!["jdtls"]));
146
+
147
+        // Kotlin - kotlin-language-server
148
+        self.register_config(ServerConfig::new(
149
+            "kotlin-language-server",
150
+            "kotlin",
151
+            vec!["kotlin-language-server"],
152
+        ));
153
+
154
+        // Ruby - solargraph
155
+        self.register_config(ServerConfig::new(
156
+            "solargraph",
157
+            "ruby",
158
+            vec!["solargraph", "stdio"],
159
+        ));
160
+
161
+        // PHP - intelephense
162
+        self.register_config(ServerConfig::new(
163
+            "intelephense",
164
+            "php",
165
+            vec!["intelephense", "--stdio"],
166
+        ));
167
+
168
+        // C# - omnisharp
169
+        self.register_config(ServerConfig::new(
170
+            "omnisharp",
171
+            "csharp",
172
+            vec!["omnisharp", "--languageserver"],
173
+        ));
174
+
175
+        // Lua - lua-language-server
176
+        self.register_config(ServerConfig::new("lua-ls", "lua", vec!["lua-language-server"]));
177
+
178
+        // Zig - zls
179
+        self.register_config(ServerConfig::new("zls", "zig", vec!["zls"]));
180
+
181
+        // Haskell - haskell-language-server
182
+        self.register_config(ServerConfig::new(
183
+            "hls",
184
+            "haskell",
185
+            vec!["haskell-language-server-wrapper", "--lsp"],
186
+        ));
187
+
188
+        // OCaml - ocamllsp
189
+        self.register_config(ServerConfig::new("ocamllsp", "ocaml", vec!["ocamllsp"]));
190
+
191
+        // Elixir - elixir-ls
192
+        self.register_config(ServerConfig::new("elixir-ls", "elixir", vec!["elixir-ls"]));
193
+
194
+        // Erlang - erlang_ls
195
+        self.register_config(ServerConfig::new("erlang_ls", "erlang", vec!["erlang_ls"]));
196
+
197
+        // Julia - julia-ls
198
+        self.register_config(ServerConfig::new("julia-ls", "julia", vec!["julia", "--project=@.", "-e", "using LanguageServer; runserver()"]));
199
+
200
+        // Bash - bash-language-server
201
+        self.register_config(ServerConfig::new(
202
+            "bash-ls",
203
+            "shellscript",
204
+            vec!["bash-language-server", "start"],
205
+        ));
206
+
207
+        // HTML - vscode-html-language-server
208
+        self.register_config(ServerConfig::new(
209
+            "html-ls",
210
+            "html",
211
+            vec!["vscode-html-language-server", "--stdio"],
212
+        ));
213
+
214
+        // CSS - vscode-css-language-server
215
+        self.register_config(ServerConfig::new(
216
+            "css-ls",
217
+            "css",
218
+            vec!["vscode-css-language-server", "--stdio"],
219
+        ));
220
+
221
+        // JSON - vscode-json-language-server
222
+        self.register_config(ServerConfig::new(
223
+            "json-ls",
224
+            "json",
225
+            vec!["vscode-json-language-server", "--stdio"],
226
+        ));
227
+
228
+        // YAML - yaml-language-server
229
+        self.register_config(ServerConfig::new(
230
+            "yaml-ls",
231
+            "yaml",
232
+            vec!["yaml-language-server", "--stdio"],
233
+        ));
234
+
235
+        // TOML - taplo
236
+        self.register_config(ServerConfig::new("taplo", "toml", vec!["taplo", "lsp", "stdio"]));
237
+
238
+        // Markdown - marksman
239
+        self.register_config(ServerConfig::new("marksman", "markdown", vec!["marksman", "server"]));
240
+
241
+        // Docker - dockerfile-language-server
242
+        self.register_config(ServerConfig::new(
243
+            "docker-ls",
244
+            "dockerfile",
245
+            vec!["docker-langserver", "--stdio"],
246
+        ));
247
+
248
+        // Terraform - terraform-ls
249
+        self.register_config(ServerConfig::new(
250
+            "terraform-ls",
251
+            "terraform",
252
+            vec!["terraform-ls", "serve"],
253
+        ));
254
+
255
+        // Nix - nil
256
+        self.register_config(ServerConfig::new("nil", "nix", vec!["nil"]));
257
+
258
+        // SQL - sqls
259
+        self.register_config(ServerConfig::new("sqls", "sql", vec!["sqls"]));
260
+
261
+        // Vue - volar
262
+        self.register_config(ServerConfig::new(
263
+            "vue-ls",
264
+            "vue",
265
+            vec!["vue-language-server", "--stdio"],
266
+        ));
267
+
268
+        // Svelte - svelte-language-server
269
+        self.register_config(ServerConfig::new(
270
+            "svelte-ls",
271
+            "svelte",
272
+            vec!["svelteserver", "--stdio"],
273
+        ));
274
+
275
+        // Elm - elm-language-server
276
+        self.register_config(ServerConfig::new("elm-ls", "elm", vec!["elm-language-server"]));
277
+
278
+        // Scala - metals
279
+        self.register_config(ServerConfig::new("metals", "scala", vec!["metals"]));
280
+
281
+        // Dart - dart analysis server
282
+        self.register_config(ServerConfig::new(
283
+            "dart-ls",
284
+            "dart",
285
+            vec!["dart", "language-server", "--protocol=lsp"],
286
+        ));
287
+
288
+        // Clojure - clojure-lsp
289
+        self.register_config(ServerConfig::new("clojure-lsp", "clojure", vec!["clojure-lsp"]));
290
+
291
+        // Fortran - fortls
292
+        self.register_config(ServerConfig::new("fortls", "fortran", vec!["fortls"]));
293
+
294
+        // D - serve-d
295
+        self.register_config(ServerConfig::new("serve-d", "d", vec!["serve-d"]));
296
+
297
+        // Nim - nimlsp
298
+        self.register_config(ServerConfig::new("nimlsp", "nim", vec!["nimlsp"]));
299
+
300
+        // V - vls
301
+        self.register_config(ServerConfig::new("vls", "v", vec!["vls"]));
302
+
303
+        // Perl - perlnavigator
304
+        self.register_config(ServerConfig::new(
305
+            "perlnavigator",
306
+            "perl",
307
+            vec!["perlnavigator"],
308
+        ));
309
+
310
+        // R - languageserver
311
+        self.register_config(ServerConfig::new(
312
+            "r-ls",
313
+            "r",
314
+            vec!["R", "--slave", "-e", "languageserver::run()"],
315
+        ));
316
+
317
+        // GraphQL - graphql-lsp
318
+        self.register_config(ServerConfig::new(
319
+            "graphql-lsp",
320
+            "graphql",
321
+            vec!["graphql-lsp", "server", "-m", "stream"],
322
+        ));
323
+
324
+        // CMake - cmake-language-server
325
+        self.register_config(ServerConfig::new(
326
+            "cmake-ls",
327
+            "cmake",
328
+            vec!["cmake-language-server"],
329
+        ));
330
+
331
+        // Groovy - groovy-language-server
332
+        self.register_config(ServerConfig::new(
333
+            "groovy-ls",
334
+            "groovy",
335
+            vec!["groovy-language-server"],
336
+        ));
337
+
338
+        // Swift - sourcekit-lsp
339
+        self.register_config(ServerConfig::new(
340
+            "sourcekit-lsp",
341
+            "swift",
342
+            vec!["sourcekit-lsp"],
343
+        ));
344
+
345
+        // F# - fsautocomplete
346
+        self.register_config(ServerConfig::new(
347
+            "fsautocomplete",
348
+            "fsharp",
349
+            vec!["fsautocomplete", "--adaptive-lsp-server-enabled"],
350
+        ));
351
+
352
+        // PowerShell - PowerShellEditorServices
353
+        self.register_config(ServerConfig::new(
354
+            "pwsh-ls",
355
+            "powershell",
356
+            vec![
357
+                "pwsh",
358
+                "-NoLogo",
359
+                "-NoProfile",
360
+                "-Command",
361
+                "Import-Module PowerShellEditorServices; Start-EditorServices -HostName 'fackr' -HostProfileId 'fackr' -HostVersion '1.0.0' -Stdio",
362
+            ],
363
+        ));
364
+
365
+        // Protocol Buffers - buf
366
+        self.register_config(ServerConfig::new("buf-ls", "proto", vec!["buf", "lsp"]));
367
+
368
+        // Assembly - asm-lsp
369
+        self.register_config(ServerConfig::new("asm-lsp", "asm", vec!["asm-lsp"]));
370
+
371
+        // Odin - ols
372
+        self.register_config(ServerConfig::new("ols", "odin", vec!["ols"]));
373
+    }
374
+
375
+    /// Register a server configuration
376
+    pub fn register_config(&mut self, config: ServerConfig) {
377
+        self.configs
378
+            .entry(config.language.clone())
379
+            .or_default()
380
+            .push(config);
381
+    }
382
+
383
+    /// Start a server for a language
384
+    pub fn start_server(&mut self, language: &str) -> Result<()> {
385
+        let configs = self
386
+            .configs
387
+            .get(language)
388
+            .ok_or_else(|| anyhow!("No LSP server configured for language: {}", language))?
389
+            .clone();
390
+
391
+        // Start the first available server for the language
392
+        for config in configs {
393
+            match self.start_server_with_config(&config) {
394
+                Ok(()) => return Ok(()),
395
+                Err(_) => {
396
+                    // Server not available, try next one
397
+                    continue;
398
+                }
399
+            }
400
+        }
401
+
402
+        Err(anyhow!(
403
+            "Failed to start any LSP server for language: {}",
404
+            language
405
+        ))
406
+    }
407
+
408
+    /// Start all configured servers for a language
409
+    pub fn start_all_servers(&mut self, language: &str) -> Result<()> {
410
+        let configs = self
411
+            .configs
412
+            .get(language)
413
+            .ok_or_else(|| anyhow!("No LSP server configured for language: {}", language))?
414
+            .clone();
415
+
416
+        let mut started = 0;
417
+        for config in configs {
418
+            match self.start_server_with_config(&config) {
419
+                Ok(()) => started += 1,
420
+                Err(_) => {
421
+                    // Server not available, continue
422
+                }
423
+            }
424
+        }
425
+
426
+        if started > 0 {
427
+            Ok(())
428
+        } else {
429
+            Err(anyhow!(
430
+                "Failed to start any LSP server for language: {}",
431
+                language
432
+            ))
433
+        }
434
+    }
435
+
436
+    /// Start a server with a specific config
437
+    fn start_server_with_config(&mut self, config: &ServerConfig) -> Result<()> {
438
+        // Check if server is already running
439
+        if let Some(servers) = self.servers.get(&config.language) {
440
+            if servers.iter().any(|s| s.config.name == config.name) {
441
+                return Ok(()); // Already running
442
+            }
443
+        }
444
+
445
+        // Spawn the server process
446
+        let process = ServerProcess::spawn(&config.command)?;
447
+
448
+        // Create managed server
449
+        let mut server = ManagedServer::new(config.clone(), process);
450
+
451
+        // Set up diagnostics callback if configured
452
+        if let Some(ref callback) = self.diagnostics_callback {
453
+            let cb = Arc::clone(callback);
454
+            server.handler.set_diagnostics_callback(Box::new(
455
+                move |uri, diags| {
456
+                    if let Ok(cb) = cb.lock() {
457
+                        cb(uri, diags);
458
+                    }
459
+                },
460
+            ));
461
+        }
462
+
463
+        // Send initialize request
464
+        let id = protocol::next_request_id();
465
+        let init_msg = protocol::create_initialize_request(id, &self.workspace_root, "fackr");
466
+
467
+        server.process.send(&init_msg.to_string())?;
468
+        server.state = ServerState::Initializing;
469
+
470
+        // Store the server
471
+        self.servers
472
+            .entry(config.language.clone())
473
+            .or_default()
474
+            .push(server);
475
+
476
+        Ok(())
477
+    }
478
+
479
+    /// Get a server for a language (start if needed)
480
+    pub fn get_or_start_server(&mut self, language: &str) -> Result<&mut ManagedServer> {
481
+        if !self.servers.contains_key(language) || self.servers.get(language).map_or(true, |s| s.is_empty()) {
482
+            self.start_server(language)?;
483
+        }
484
+
485
+        self.servers
486
+            .get_mut(language)
487
+            .and_then(|servers| servers.first_mut())
488
+            .ok_or_else(|| anyhow!("No server available for language: {}", language))
489
+    }
490
+
491
+    /// Get a server with a specific capability
492
+    pub fn get_server_with_capability(
493
+        &mut self,
494
+        language: &str,
495
+        check: impl Fn(&Capabilities) -> bool,
496
+    ) -> Option<&mut ManagedServer> {
497
+        self.servers
498
+            .get_mut(language)?
499
+            .iter_mut()
500
+            .find(|s| s.state == ServerState::Ready && check(&s.capabilities))
501
+    }
502
+
503
+    /// Process messages from all servers (call this regularly)
504
+    pub fn process_messages(&mut self) {
505
+        for (_lang, servers) in self.servers.iter_mut() {
506
+            for server in servers.iter_mut() {
507
+                Self::process_server_messages(server, &self.workspace_root);
508
+            }
509
+        }
510
+    }
511
+
512
+    /// Process messages for a single server
513
+    fn process_server_messages(server: &mut ManagedServer, _workspace_root: &str) {
514
+        while let Some(json_str) = server.process.try_recv() {
515
+            if let Ok(value) = serde_json::from_str::<Value>(&json_str) {
516
+                if let Some(msg) = LspMessage::from_json(value.clone()) {
517
+                    // Handle initialization response specially
518
+                    if let LspMessage::Response { ref result, .. } = msg {
519
+                        if server.state == ServerState::Initializing {
520
+                            if let Some(result) = result {
521
+                                // Parse capabilities
522
+                                server.capabilities = protocol::parse_capabilities(result);
523
+                                server.state = ServerState::Ready;
524
+
525
+                                // Send initialized notification
526
+                                let init_notif = protocol::create_initialized_notification();
527
+                                let _ = server.process.send(&init_notif.to_string());
528
+
529
+                                // Send any pending didOpen notifications
530
+                                for pending in server.pending_opens.drain(..) {
531
+                                    let _ = server.process.send(&pending.to_string());
532
+                                }
533
+                            }
534
+                        }
535
+                    }
536
+
537
+                    // Let the handler process the message
538
+                    if let Some(response) = server.handler.handle_message(msg) {
539
+                        // Send response back to server
540
+                        let _ = server.process.send(&response.to_string());
541
+                    }
542
+                }
543
+            }
544
+        }
545
+    }
546
+
547
+    /// Send a request to a server and register callback
548
+    pub fn send_request(
549
+        &mut self,
550
+        language: &str,
551
+        message: LspMessage,
552
+        callback: ResponseCallback,
553
+    ) -> Result<()> {
554
+        // Clone workspace_root to avoid borrow conflict
555
+        let workspace_root = self.workspace_root.clone();
556
+        let server = self.get_or_start_server(language)?;
557
+
558
+        // Wait for server to be ready
559
+        if server.state != ServerState::Ready {
560
+            // Process messages until ready or timeout
561
+            for _ in 0..50 {
562
+                Self::process_server_messages(server, &workspace_root);
563
+                if server.state == ServerState::Ready {
564
+                    break;
565
+                }
566
+                std::thread::sleep(std::time::Duration::from_millis(100));
567
+            }
568
+        }
569
+
570
+        if let LspMessage::Request { id, .. } = &message {
571
+            server.handler.register_callback(*id, callback);
572
+        }
573
+
574
+        server.process.send(&message.to_string())?;
575
+        Ok(())
576
+    }
577
+
578
+    /// Send a notification to a server
579
+    pub fn send_notification(&mut self, language: &str, message: LspMessage) -> Result<()> {
580
+        let server = self.get_or_start_server(language)?;
581
+
582
+        // Queue didOpen if server not ready
583
+        if server.state != ServerState::Ready {
584
+            if let LspMessage::Notification { ref method, .. } = message {
585
+                if method == "textDocument/didOpen" {
586
+                    server.pending_opens.push(message);
587
+                    return Ok(());
588
+                }
589
+            }
590
+        }
591
+
592
+        server.process.send(&message.to_string())?;
593
+        Ok(())
594
+    }
595
+
596
+    /// Stop a server for a language
597
+    pub fn stop_server(&mut self, language: &str) -> Result<()> {
598
+        if let Some(servers) = self.servers.get_mut(language) {
599
+            for server in servers.iter_mut() {
600
+                server.state = ServerState::ShuttingDown;
601
+
602
+                // Send shutdown request
603
+                let id = protocol::next_request_id();
604
+                let shutdown = protocol::create_shutdown_request(id);
605
+                let _ = server.process.send(&shutdown.to_string());
606
+
607
+                // Wait briefly for shutdown acknowledgment
608
+                std::thread::sleep(std::time::Duration::from_millis(100));
609
+
610
+                // Send exit notification
611
+                let exit = protocol::create_exit_notification();
612
+                let _ = server.process.send(&exit.to_string());
613
+
614
+                // Kill the process
615
+                let _ = server.process.kill();
616
+                server.state = ServerState::Stopped;
617
+            }
618
+            servers.clear();
619
+        }
620
+        Ok(())
621
+    }
622
+
623
+    /// Stop all servers
624
+    pub fn stop_all(&mut self) {
625
+        let languages: Vec<String> = self.servers.keys().cloned().collect();
626
+        for lang in languages {
627
+            let _ = self.stop_server(&lang);
628
+        }
629
+    }
630
+
631
+    /// Check if a server is running for a language
632
+    pub fn has_server(&self, language: &str) -> bool {
633
+        self.servers
634
+            .get(language)
635
+            .map_or(false, |s| !s.is_empty() && s.iter().any(|s| s.state == ServerState::Ready))
636
+    }
637
+
638
+    /// Get the workspace root
639
+    pub fn workspace_root(&self) -> &str {
640
+        &self.workspace_root
641
+    }
642
+}
643
+
644
+impl Drop for LspManager {
645
+    fn drop(&mut self) {
646
+        self.stop_all();
647
+    }
648
+}
src/lsp/message.rsadded
@@ -0,0 +1,206 @@
1
+//! LSP message handling and callback management
2
+//!
3
+//! Handles routing of LSP responses to appropriate callbacks.
4
+//!
5
+//! Note: ParsedResponse helpers are for planned features.
6
+#![allow(dead_code)]
7
+
8
+use serde_json::Value;
9
+use std::collections::HashMap;
10
+
11
+use super::protocol::{LspMessage, ResponseError};
12
+use super::types::{
13
+    CompletionItem, Diagnostic, DocumentSymbol, HoverInfo, Location, TextEdit, WorkspaceEdit,
14
+};
15
+
16
+/// Result type for LSP responses
17
+pub type LspResult<T> = Result<T, ResponseError>;
18
+
19
+/// Callback for LSP responses
20
+pub type ResponseCallback = Box<dyn FnOnce(i64, LspResult<Value>) + Send>;
21
+
22
+/// Callback for diagnostics notifications
23
+pub type DiagnosticsCallback = Box<dyn Fn(String, Vec<Diagnostic>) + Send>;
24
+
25
+/// Tracks pending requests and their callbacks
26
+pub struct MessageHandler {
27
+    /// Pending request callbacks indexed by request ID
28
+    pending: HashMap<i64, ResponseCallback>,
29
+    /// Callback for diagnostics notifications
30
+    diagnostics_callback: Option<DiagnosticsCallback>,
31
+}
32
+
33
+impl MessageHandler {
34
+    pub fn new() -> Self {
35
+        Self {
36
+            pending: HashMap::new(),
37
+            diagnostics_callback: None,
38
+        }
39
+    }
40
+
41
+    /// Register a callback for a request
42
+    pub fn register_callback(&mut self, id: i64, callback: ResponseCallback) {
43
+        self.pending.insert(id, callback);
44
+    }
45
+
46
+    /// Set the diagnostics callback
47
+    pub fn set_diagnostics_callback(&mut self, callback: DiagnosticsCallback) {
48
+        self.diagnostics_callback = Some(callback);
49
+    }
50
+
51
+    /// Handle an incoming message
52
+    pub fn handle_message(&mut self, message: LspMessage) -> Option<LspMessage> {
53
+        match message {
54
+            LspMessage::Response { id, result, error } => {
55
+                self.handle_response(id, result, error);
56
+                None
57
+            }
58
+            LspMessage::Notification { method, params } => {
59
+                self.handle_notification(&method, params);
60
+                None
61
+            }
62
+            LspMessage::Request { id, method, params } => {
63
+                // Handle server-to-client requests
64
+                self.handle_server_request(id, &method, params)
65
+            }
66
+        }
67
+    }
68
+
69
+    /// Handle a response message
70
+    fn handle_response(&mut self, id: i64, result: Option<Value>, error: Option<ResponseError>) {
71
+        if let Some(callback) = self.pending.remove(&id) {
72
+            let response = if let Some(err) = error {
73
+                Err(err)
74
+            } else {
75
+                Ok(result.unwrap_or(Value::Null))
76
+            };
77
+            callback(id, response);
78
+        }
79
+    }
80
+
81
+    /// Handle a notification message
82
+    fn handle_notification(&mut self, method: &str, params: Option<Value>) {
83
+        match method {
84
+            "textDocument/publishDiagnostics" => {
85
+                if let (Some(params), Some(callback)) = (params, &self.diagnostics_callback) {
86
+                    let (uri, diagnostics) = super::protocol::parse_diagnostics(&params);
87
+                    callback(uri, diagnostics);
88
+                }
89
+            }
90
+            "window/logMessage" | "window/showMessage" => {
91
+                // Silently ignore server log messages
92
+                // These could be surfaced to the status bar via a callback if needed
93
+                let _ = params;
94
+            }
95
+            _ => {
96
+                // Ignore other notifications
97
+            }
98
+        }
99
+    }
100
+
101
+    /// Handle a server-to-client request (return a response if needed)
102
+    fn handle_server_request(
103
+        &mut self,
104
+        id: i64,
105
+        method: &str,
106
+        _params: Option<Value>,
107
+    ) -> Option<LspMessage> {
108
+        match method {
109
+            "workspace/configuration" => {
110
+                // Return empty configuration
111
+                Some(LspMessage::Response {
112
+                    id,
113
+                    result: Some(Value::Array(vec![])),
114
+                    error: None,
115
+                })
116
+            }
117
+            "client/registerCapability" | "client/unregisterCapability" => {
118
+                // Acknowledge capability registration
119
+                Some(LspMessage::Response {
120
+                    id,
121
+                    result: Some(Value::Null),
122
+                    error: None,
123
+                })
124
+            }
125
+            "window/workDoneProgress/create" => {
126
+                // Acknowledge progress creation
127
+                Some(LspMessage::Response {
128
+                    id,
129
+                    result: Some(Value::Null),
130
+                    error: None,
131
+                })
132
+            }
133
+            _ => {
134
+                // Unknown request - return method not found error
135
+                Some(LspMessage::Response {
136
+                    id,
137
+                    result: None,
138
+                    error: Some(ResponseError {
139
+                        code: -32601, // Method not found
140
+                        message: format!("Method not found: {}", method),
141
+                        data: None,
142
+                    }),
143
+                })
144
+            }
145
+        }
146
+    }
147
+
148
+    /// Check if there are pending requests
149
+    pub fn has_pending(&self) -> bool {
150
+        !self.pending.is_empty()
151
+    }
152
+
153
+    /// Get number of pending requests
154
+    pub fn pending_count(&self) -> usize {
155
+        self.pending.len()
156
+    }
157
+}
158
+
159
+impl Default for MessageHandler {
160
+    fn default() -> Self {
161
+        Self::new()
162
+    }
163
+}
164
+
165
+/// Parsed LSP response types for convenience
166
+pub enum ParsedResponse {
167
+    Completions(Vec<CompletionItem>),
168
+    Hover(Option<HoverInfo>),
169
+    Locations(Vec<Location>),
170
+    Symbols(Vec<DocumentSymbol>),
171
+    TextEdits(Vec<TextEdit>),
172
+    WorkspaceEdit(WorkspaceEdit),
173
+    Empty,
174
+}
175
+
176
+impl ParsedResponse {
177
+    /// Parse a completion response
178
+    pub fn parse_completions(result: &Value) -> Self {
179
+        ParsedResponse::Completions(super::protocol::parse_completion_items(result))
180
+    }
181
+
182
+    /// Parse a hover response
183
+    pub fn parse_hover(result: &Value) -> Self {
184
+        ParsedResponse::Hover(super::protocol::parse_hover(result))
185
+    }
186
+
187
+    /// Parse a definition/references response
188
+    pub fn parse_locations(result: &Value) -> Self {
189
+        ParsedResponse::Locations(super::protocol::parse_locations(result))
190
+    }
191
+
192
+    /// Parse a document symbols response
193
+    pub fn parse_symbols(result: &Value) -> Self {
194
+        ParsedResponse::Symbols(super::protocol::parse_document_symbols(result))
195
+    }
196
+
197
+    /// Parse a formatting response
198
+    pub fn parse_text_edits(result: &Value) -> Self {
199
+        ParsedResponse::TextEdits(super::protocol::parse_text_edits(result))
200
+    }
201
+
202
+    /// Parse a rename response
203
+    pub fn parse_workspace_edit(result: &Value) -> Self {
204
+        ParsedResponse::WorkspaceEdit(super::protocol::parse_workspace_edit(result))
205
+    }
206
+}
src/lsp/mod.rsadded
@@ -0,0 +1,26 @@
1
+//! LSP (Language Server Protocol) client module
2
+//!
3
+//! Provides LSP support for fackr, enabling:
4
+//! - Code completion
5
+//! - Hover information
6
+//! - Go to definition
7
+//! - Find references
8
+//! - Diagnostics
9
+//! - Code actions
10
+//! - Document symbols
11
+//! - Rename refactoring
12
+//! - Document formatting
13
+
14
+mod client;
15
+mod manager;
16
+mod message;
17
+mod process;
18
+mod protocol;
19
+pub mod server_manager;
20
+mod types;
21
+
22
+pub use client::{LspClient, LspResponse};
23
+pub use server_manager::ServerManagerPanel;
24
+pub use types::{
25
+    CompletionItem, Diagnostic, DiagnosticSeverity, HoverInfo, Location, TextEdit, uri_to_path,
26
+};
src/lsp/process.rsadded
@@ -0,0 +1,181 @@
1
+//! LSP server process management
2
+//!
3
+//! Handles spawning and communicating with language server processes.
4
+//!
5
+//! Note: Some process methods are for planned features.
6
+#![allow(dead_code)]
7
+
8
+use anyhow::{anyhow, Result};
9
+use std::io::{Read, Write};
10
+use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
11
+use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};
12
+use std::thread;
13
+
14
+/// A running language server process
15
+pub struct ServerProcess {
16
+    child: Child,
17
+    stdin: ChildStdin,
18
+    message_rx: Receiver<String>,
19
+    /// Buffer for incomplete messages
20
+    read_buffer: String,
21
+}
22
+
23
+impl ServerProcess {
24
+    /// Spawn a new language server process
25
+    pub fn spawn(command: &[String]) -> Result<Self> {
26
+        if command.is_empty() {
27
+            return Err(anyhow!("Empty command"));
28
+        }
29
+
30
+        let mut cmd = Command::new(&command[0]);
31
+        if command.len() > 1 {
32
+            cmd.args(&command[1..]);
33
+        }
34
+
35
+        let mut child = cmd
36
+            .stdin(Stdio::piped())
37
+            .stdout(Stdio::piped())
38
+            .stderr(Stdio::piped())
39
+            .spawn()
40
+            .map_err(|e| anyhow!("Failed to spawn LSP server '{}': {}", command[0], e))?;
41
+
42
+        let stdin = child.stdin.take().ok_or_else(|| anyhow!("No stdin"))?;
43
+        let stdout = child.stdout.take().ok_or_else(|| anyhow!("No stdout"))?;
44
+
45
+        // Spawn a thread to read from stdout asynchronously
46
+        let (tx, rx) = mpsc::channel();
47
+        spawn_reader_thread(stdout, tx);
48
+
49
+        Ok(Self {
50
+            child,
51
+            stdin,
52
+            message_rx: rx,
53
+            read_buffer: String::new(),
54
+        })
55
+    }
56
+
57
+    /// Send a message to the server
58
+    pub fn send(&mut self, message: &str) -> Result<()> {
59
+        self.stdin.write_all(message.as_bytes())?;
60
+        self.stdin.flush()?;
61
+        Ok(())
62
+    }
63
+
64
+    /// Try to receive a complete message from the server (non-blocking)
65
+    pub fn try_recv(&mut self) -> Option<String> {
66
+        // Drain all available data from the channel into our buffer
67
+        loop {
68
+            match self.message_rx.try_recv() {
69
+                Ok(data) => self.read_buffer.push_str(&data),
70
+                Err(TryRecvError::Empty) => break,
71
+                Err(TryRecvError::Disconnected) => break,
72
+            }
73
+        }
74
+
75
+        // Try to parse a complete message from the buffer
76
+        self.parse_message()
77
+    }
78
+
79
+    /// Block until a message is received (with timeout in ms)
80
+    pub fn recv_timeout(&mut self, timeout_ms: u64) -> Option<String> {
81
+        use std::time::{Duration, Instant};
82
+        let deadline = Instant::now() + Duration::from_millis(timeout_ms);
83
+
84
+        loop {
85
+            // First check if we have a complete message buffered
86
+            if let Some(msg) = self.parse_message() {
87
+                return Some(msg);
88
+            }
89
+
90
+            // Wait for more data
91
+            let remaining = deadline.saturating_duration_since(Instant::now());
92
+            if remaining.is_zero() {
93
+                return None;
94
+            }
95
+
96
+            match self.message_rx.recv_timeout(remaining) {
97
+                Ok(data) => self.read_buffer.push_str(&data),
98
+                Err(_) => return None,
99
+            }
100
+        }
101
+    }
102
+
103
+    /// Parse a complete LSP message from the buffer
104
+    fn parse_message(&mut self) -> Option<String> {
105
+        // Look for Content-Length header
106
+        let header_end = self.read_buffer.find("\r\n\r\n")?;
107
+        let header = &self.read_buffer[..header_end];
108
+
109
+        // Parse Content-Length
110
+        let content_length: usize = header
111
+            .lines()
112
+            .find(|line| line.to_lowercase().starts_with("content-length:"))
113
+            .and_then(|line| line.split(':').nth(1))
114
+            .and_then(|len| len.trim().parse().ok())?;
115
+
116
+        // Check if we have the full message
117
+        let message_start = header_end + 4;
118
+        let message_end = message_start + content_length;
119
+
120
+        if self.read_buffer.len() < message_end {
121
+            return None;
122
+        }
123
+
124
+        // Extract the message
125
+        let message = self.read_buffer[message_start..message_end].to_string();
126
+
127
+        // Remove from buffer
128
+        self.read_buffer = self.read_buffer[message_end..].to_string();
129
+
130
+        Some(message)
131
+    }
132
+
133
+    /// Check if the process is still running
134
+    pub fn is_running(&mut self) -> bool {
135
+        match self.child.try_wait() {
136
+            Ok(Some(_)) => false, // Process has exited
137
+            Ok(None) => true,     // Still running
138
+            Err(_) => false,      // Error checking status
139
+        }
140
+    }
141
+
142
+    /// Kill the server process
143
+    pub fn kill(&mut self) -> Result<()> {
144
+        let _ = self.child.kill();
145
+        Ok(())
146
+    }
147
+
148
+    /// Get the process ID
149
+    pub fn pid(&self) -> u32 {
150
+        self.child.id()
151
+    }
152
+}
153
+
154
+impl Drop for ServerProcess {
155
+    fn drop(&mut self) {
156
+        let _ = self.kill();
157
+    }
158
+}
159
+
160
+/// Spawn a thread to read from the server's stdout
161
+fn spawn_reader_thread(mut stdout: ChildStdout, tx: Sender<String>) {
162
+    use std::io::ErrorKind;
163
+
164
+    thread::spawn(move || {
165
+        let mut buffer = [0u8; 8192];
166
+        loop {
167
+            match stdout.read(&mut buffer) {
168
+                Ok(0) => break,
169
+                Ok(n) => {
170
+                    if let Ok(s) = std::str::from_utf8(&buffer[..n]) {
171
+                        if tx.send(s.to_string()).is_err() {
172
+                            break;
173
+                        }
174
+                    }
175
+                }
176
+                Err(e) if e.kind() == ErrorKind::Interrupted => continue,
177
+                Err(_) => break,
178
+            }
179
+        }
180
+    });
181
+}
src/lsp/protocol.rsadded
@@ -0,0 +1,726 @@
1
+//! LSP JSON-RPC protocol implementation
2
+//!
3
+//! Handles message creation, serialization, and parsing for the Language Server Protocol.
4
+//!
5
+//! Note: Some request builders and parsers are for planned features.
6
+#![allow(dead_code)]
7
+
8
+use serde::{Deserialize, Serialize};
9
+use serde_json::{json, Value};
10
+use std::sync::atomic::{AtomicI64, Ordering};
11
+
12
+use super::types::{Capabilities, Position, Range};
13
+
14
+/// Global request ID counter
15
+static NEXT_REQUEST_ID: AtomicI64 = AtomicI64::new(1);
16
+
17
+/// Get the next unique request ID
18
+pub fn next_request_id() -> i64 {
19
+    NEXT_REQUEST_ID.fetch_add(1, Ordering::SeqCst)
20
+}
21
+
22
+/// LSP message types
23
+#[derive(Debug, Clone)]
24
+pub enum LspMessage {
25
+    Request {
26
+        id: i64,
27
+        method: String,
28
+        params: Option<Value>,
29
+    },
30
+    Response {
31
+        id: i64,
32
+        result: Option<Value>,
33
+        error: Option<ResponseError>,
34
+    },
35
+    Notification {
36
+        method: String,
37
+        params: Option<Value>,
38
+    },
39
+}
40
+
41
+/// LSP response error
42
+#[derive(Debug, Clone, Serialize, Deserialize)]
43
+pub struct ResponseError {
44
+    pub code: i32,
45
+    pub message: String,
46
+    pub data: Option<Value>,
47
+}
48
+
49
+impl LspMessage {
50
+    /// Serialize message to JSON-RPC format with Content-Length header
51
+    pub fn to_string(&self) -> String {
52
+        let json = match self {
53
+            LspMessage::Request { id, method, params } => {
54
+                let mut obj = json!({
55
+                    "jsonrpc": "2.0",
56
+                    "id": id,
57
+                    "method": method,
58
+                });
59
+                if let Some(p) = params {
60
+                    obj["params"] = p.clone();
61
+                }
62
+                obj
63
+            }
64
+            LspMessage::Response { id, result, error } => {
65
+                let mut obj = json!({
66
+                    "jsonrpc": "2.0",
67
+                    "id": id,
68
+                });
69
+                if let Some(r) = result {
70
+                    obj["result"] = r.clone();
71
+                }
72
+                if let Some(e) = error {
73
+                    obj["error"] = serde_json::to_value(e).unwrap_or(Value::Null);
74
+                }
75
+                obj
76
+            }
77
+            LspMessage::Notification { method, params } => {
78
+                let mut obj = json!({
79
+                    "jsonrpc": "2.0",
80
+                    "method": method,
81
+                });
82
+                if let Some(p) = params {
83
+                    obj["params"] = p.clone();
84
+                }
85
+                obj
86
+            }
87
+        };
88
+
89
+        let content = serde_json::to_string(&json).unwrap_or_default();
90
+        format!("Content-Length: {}\r\n\r\n{}", content.len(), content)
91
+    }
92
+
93
+    /// Parse a JSON-RPC message from JSON value
94
+    pub fn from_json(value: Value) -> Option<Self> {
95
+        let obj = value.as_object()?;
96
+
97
+        // Check for response (has id and result/error)
98
+        if let Some(id) = obj.get("id").and_then(|v| v.as_i64()) {
99
+            if obj.contains_key("method") {
100
+                // Request
101
+                let method = obj.get("method")?.as_str()?.to_string();
102
+                let params = obj.get("params").cloned();
103
+                Some(LspMessage::Request { id, method, params })
104
+            } else {
105
+                // Response
106
+                let result = obj.get("result").cloned();
107
+                let error = obj
108
+                    .get("error")
109
+                    .and_then(|e| serde_json::from_value(e.clone()).ok());
110
+                Some(LspMessage::Response { id, result, error })
111
+            }
112
+        } else if let Some(method) = obj.get("method").and_then(|v| v.as_str()) {
113
+            // Notification (no id)
114
+            let params = obj.get("params").cloned();
115
+            Some(LspMessage::Notification {
116
+                method: method.to_string(),
117
+                params,
118
+            })
119
+        } else {
120
+            None
121
+        }
122
+    }
123
+}
124
+
125
+// ============================================================================
126
+// Request Builders
127
+// ============================================================================
128
+
129
+/// Create initialize request
130
+pub fn create_initialize_request(
131
+    id: i64,
132
+    workspace_root: &str,
133
+    client_name: &str,
134
+) -> LspMessage {
135
+    let capabilities = json!({
136
+        "textDocument": {
137
+            "completion": {
138
+                "completionItem": {
139
+                    "snippetSupport": false,
140
+                    "documentationFormat": ["plaintext", "markdown"],
141
+                    "deprecatedSupport": true,
142
+                    "labelDetailsSupport": true
143
+                },
144
+                "contextSupport": true
145
+            },
146
+            "hover": {
147
+                "contentFormat": ["plaintext", "markdown"]
148
+            },
149
+            "definition": {
150
+                "linkSupport": true
151
+            },
152
+            "references": {},
153
+            "documentSymbol": {
154
+                "hierarchicalDocumentSymbolSupport": true
155
+            },
156
+            "codeAction": {
157
+                "codeActionLiteralSupport": {
158
+                    "codeActionKind": {
159
+                        "valueSet": [
160
+                            "quickfix",
161
+                            "refactor",
162
+                            "refactor.extract",
163
+                            "refactor.inline",
164
+                            "refactor.rewrite",
165
+                            "source",
166
+                            "source.organizeImports"
167
+                        ]
168
+                    }
169
+                }
170
+            },
171
+            "rename": {
172
+                "prepareSupport": true
173
+            },
174
+            "publishDiagnostics": {
175
+                "relatedInformation": true,
176
+                "tagSupport": {
177
+                    "valueSet": [1, 2]
178
+                }
179
+            },
180
+            "signatureHelp": {
181
+                "signatureInformation": {
182
+                    "documentationFormat": ["plaintext", "markdown"],
183
+                    "parameterInformation": {
184
+                        "labelOffsetSupport": true
185
+                    }
186
+                }
187
+            },
188
+            "formatting": {},
189
+            "synchronization": {
190
+                "didSave": true,
191
+                "willSave": false,
192
+                "willSaveWaitUntil": false
193
+            }
194
+        },
195
+        "workspace": {
196
+            // Note: workspaceFolders must be false for pyright to send diagnostics
197
+            // after didOpen. With true, pyright waits for workspace folder change events.
198
+            "workspaceFolders": false,
199
+            "symbol": {
200
+                "symbolKind": {
201
+                    "valueSet": (1..=26).collect::<Vec<i32>>()
202
+                }
203
+            },
204
+            "applyEdit": true,
205
+            "workspaceEdit": {
206
+                "documentChanges": true
207
+            }
208
+        }
209
+    });
210
+
211
+    let params = json!({
212
+        "processId": std::process::id(),
213
+        "clientInfo": {
214
+            "name": client_name,
215
+            "version": env!("CARGO_PKG_VERSION")
216
+        },
217
+        "rootUri": format!("file://{}", workspace_root),
218
+        "rootPath": workspace_root,
219
+        "capabilities": capabilities,
220
+        "workspaceFolders": [{
221
+            "uri": format!("file://{}", workspace_root),
222
+            "name": workspace_root.rsplit('/').next().unwrap_or(workspace_root)
223
+        }]
224
+    });
225
+
226
+    LspMessage::Request {
227
+        id,
228
+        method: "initialize".to_string(),
229
+        params: Some(params),
230
+    }
231
+}
232
+
233
+/// Create initialized notification (sent after initialize response)
234
+pub fn create_initialized_notification() -> LspMessage {
235
+    LspMessage::Notification {
236
+        method: "initialized".to_string(),
237
+        params: Some(json!({})),
238
+    }
239
+}
240
+
241
+/// Create shutdown request
242
+pub fn create_shutdown_request(id: i64) -> LspMessage {
243
+    LspMessage::Request {
244
+        id,
245
+        method: "shutdown".to_string(),
246
+        params: None,
247
+    }
248
+}
249
+
250
+/// Create exit notification
251
+pub fn create_exit_notification() -> LspMessage {
252
+    LspMessage::Notification {
253
+        method: "exit".to_string(),
254
+        params: None,
255
+    }
256
+}
257
+
258
+// ============================================================================
259
+// Document Synchronization
260
+// ============================================================================
261
+
262
+/// Create textDocument/didOpen notification
263
+pub fn create_did_open_notification(uri: &str, language_id: &str, version: i32, text: &str) -> LspMessage {
264
+    LspMessage::Notification {
265
+        method: "textDocument/didOpen".to_string(),
266
+        params: Some(json!({
267
+            "textDocument": {
268
+                "uri": uri,
269
+                "languageId": language_id,
270
+                "version": version,
271
+                "text": text
272
+            }
273
+        })),
274
+    }
275
+}
276
+
277
+/// Create textDocument/didChange notification (full sync)
278
+pub fn create_did_change_notification(uri: &str, version: i32, text: &str) -> LspMessage {
279
+    LspMessage::Notification {
280
+        method: "textDocument/didChange".to_string(),
281
+        params: Some(json!({
282
+            "textDocument": {
283
+                "uri": uri,
284
+                "version": version
285
+            },
286
+            "contentChanges": [{
287
+                "text": text
288
+            }]
289
+        })),
290
+    }
291
+}
292
+
293
+/// Create textDocument/didSave notification
294
+pub fn create_did_save_notification(uri: &str, text: Option<&str>) -> LspMessage {
295
+    let mut params = json!({
296
+        "textDocument": {
297
+            "uri": uri
298
+        }
299
+    });
300
+    if let Some(t) = text {
301
+        params["text"] = json!(t);
302
+    }
303
+    LspMessage::Notification {
304
+        method: "textDocument/didSave".to_string(),
305
+        params: Some(params),
306
+    }
307
+}
308
+
309
+/// Create textDocument/didClose notification
310
+pub fn create_did_close_notification(uri: &str) -> LspMessage {
311
+    LspMessage::Notification {
312
+        method: "textDocument/didClose".to_string(),
313
+        params: Some(json!({
314
+            "textDocument": {
315
+                "uri": uri
316
+            }
317
+        })),
318
+    }
319
+}
320
+
321
+// ============================================================================
322
+// Language Features
323
+// ============================================================================
324
+
325
+fn position_params(uri: &str, pos: Position) -> Value {
326
+    json!({
327
+        "textDocument": { "uri": uri },
328
+        "position": { "line": pos.line, "character": pos.character }
329
+    })
330
+}
331
+
332
+/// Create textDocument/completion request
333
+pub fn create_completion_request(id: i64, uri: &str, pos: Position) -> LspMessage {
334
+    let mut params = position_params(uri, pos);
335
+    params["context"] = json!({ "triggerKind": 1 }); // Invoked
336
+    LspMessage::Request {
337
+        id,
338
+        method: "textDocument/completion".to_string(),
339
+        params: Some(params),
340
+    }
341
+}
342
+
343
+/// Create textDocument/hover request
344
+pub fn create_hover_request(id: i64, uri: &str, pos: Position) -> LspMessage {
345
+    LspMessage::Request {
346
+        id,
347
+        method: "textDocument/hover".to_string(),
348
+        params: Some(position_params(uri, pos)),
349
+    }
350
+}
351
+
352
+/// Create textDocument/definition request
353
+pub fn create_definition_request(id: i64, uri: &str, pos: Position) -> LspMessage {
354
+    LspMessage::Request {
355
+        id,
356
+        method: "textDocument/definition".to_string(),
357
+        params: Some(position_params(uri, pos)),
358
+    }
359
+}
360
+
361
+/// Create textDocument/references request
362
+pub fn create_references_request(
363
+    id: i64,
364
+    uri: &str,
365
+    pos: Position,
366
+    include_declaration: bool,
367
+) -> LspMessage {
368
+    let mut params = position_params(uri, pos);
369
+    params["context"] = json!({ "includeDeclaration": include_declaration });
370
+    LspMessage::Request {
371
+        id,
372
+        method: "textDocument/references".to_string(),
373
+        params: Some(params),
374
+    }
375
+}
376
+
377
+/// Create textDocument/rename request
378
+pub fn create_rename_request(id: i64, uri: &str, pos: Position, new_name: &str) -> LspMessage {
379
+    let mut params = position_params(uri, pos);
380
+    params["newName"] = json!(new_name);
381
+    LspMessage::Request {
382
+        id,
383
+        method: "textDocument/rename".to_string(),
384
+        params: Some(params),
385
+    }
386
+}
387
+
388
+/// Create textDocument/codeAction request
389
+pub fn create_code_action_request(id: i64, uri: &str, range: Range) -> LspMessage {
390
+    LspMessage::Request {
391
+        id,
392
+        method: "textDocument/codeAction".to_string(),
393
+        params: Some(json!({
394
+            "textDocument": { "uri": uri },
395
+            "range": {
396
+                "start": { "line": range.start.line, "character": range.start.character },
397
+                "end": { "line": range.end.line, "character": range.end.character }
398
+            },
399
+            "context": {
400
+                "diagnostics": []
401
+            }
402
+        })),
403
+    }
404
+}
405
+
406
+/// Create textDocument/documentSymbol request
407
+pub fn create_document_symbols_request(id: i64, uri: &str) -> LspMessage {
408
+    LspMessage::Request {
409
+        id,
410
+        method: "textDocument/documentSymbol".to_string(),
411
+        params: Some(json!({
412
+            "textDocument": { "uri": uri }
413
+        })),
414
+    }
415
+}
416
+
417
+/// Create workspace/symbol request
418
+pub fn create_workspace_symbols_request(id: i64, query: &str) -> LspMessage {
419
+    LspMessage::Request {
420
+        id,
421
+        method: "workspace/symbol".to_string(),
422
+        params: Some(json!({ "query": query })),
423
+    }
424
+}
425
+
426
+/// Create textDocument/signatureHelp request
427
+pub fn create_signature_help_request(id: i64, uri: &str, pos: Position) -> LspMessage {
428
+    LspMessage::Request {
429
+        id,
430
+        method: "textDocument/signatureHelp".to_string(),
431
+        params: Some(position_params(uri, pos)),
432
+    }
433
+}
434
+
435
+/// Create textDocument/formatting request
436
+pub fn create_formatting_request(id: i64, uri: &str, tab_size: u32, use_spaces: bool) -> LspMessage {
437
+    LspMessage::Request {
438
+        id,
439
+        method: "textDocument/formatting".to_string(),
440
+        params: Some(json!({
441
+            "textDocument": { "uri": uri },
442
+            "options": {
443
+                "tabSize": tab_size,
444
+                "insertSpaces": use_spaces,
445
+                "trimTrailingWhitespace": true,
446
+                "insertFinalNewline": true
447
+            }
448
+        })),
449
+    }
450
+}
451
+
452
+// ============================================================================
453
+// Response Parsing
454
+// ============================================================================
455
+
456
+/// Parse server capabilities from initialize response
457
+pub fn parse_capabilities(result: &Value) -> Capabilities {
458
+    let caps = result.get("capabilities").unwrap_or(result);
459
+
460
+    Capabilities {
461
+        completion: caps.get("completionProvider").is_some(),
462
+        hover: caps.get("hoverProvider").map_or(false, |v| !v.is_null()),
463
+        definition: caps.get("definitionProvider").map_or(false, |v| !v.is_null()),
464
+        references: caps.get("referencesProvider").map_or(false, |v| !v.is_null()),
465
+        rename: caps.get("renameProvider").map_or(false, |v| !v.is_null()),
466
+        code_actions: caps.get("codeActionProvider").map_or(false, |v| !v.is_null()),
467
+        formatting: caps.get("documentFormattingProvider").map_or(false, |v| !v.is_null()),
468
+        diagnostics: true, // Always assume diagnostics are supported
469
+        document_symbols: caps.get("documentSymbolProvider").map_or(false, |v| !v.is_null()),
470
+        workspace_symbols: caps.get("workspaceSymbolProvider").map_or(false, |v| !v.is_null()),
471
+        signature_help: caps.get("signatureHelpProvider").is_some(),
472
+    }
473
+}
474
+
475
+/// Parse Position from JSON
476
+pub fn parse_position(value: &Value) -> Option<super::types::Position> {
477
+    Some(super::types::Position {
478
+        line: value.get("line")?.as_u64()? as u32,
479
+        character: value.get("character")?.as_u64()? as u32,
480
+    })
481
+}
482
+
483
+/// Parse Range from JSON
484
+pub fn parse_range(value: &Value) -> Option<super::types::Range> {
485
+    Some(super::types::Range {
486
+        start: parse_position(value.get("start")?)?,
487
+        end: parse_position(value.get("end")?)?,
488
+    })
489
+}
490
+
491
+/// Parse Location from JSON
492
+pub fn parse_location(value: &Value) -> Option<super::types::Location> {
493
+    Some(super::types::Location {
494
+        uri: value.get("uri")?.as_str()?.to_string(),
495
+        range: parse_range(value.get("range")?)?,
496
+    })
497
+}
498
+
499
+/// Parse completion items from response
500
+pub fn parse_completion_items(result: &Value) -> Vec<super::types::CompletionItem> {
501
+    let items = if let Some(arr) = result.as_array() {
502
+        arr
503
+    } else if let Some(arr) = result.get("items").and_then(|v| v.as_array()) {
504
+        arr
505
+    } else {
506
+        return Vec::new();
507
+    };
508
+
509
+    items
510
+        .iter()
511
+        .filter_map(|item| {
512
+            let label = item.get("label")?.as_str()?.to_string();
513
+            Some(super::types::CompletionItem {
514
+                label,
515
+                kind: item
516
+                    .get("kind")
517
+                    .and_then(|v| v.as_u64())
518
+                    .and_then(|k| super::types::CompletionItemKind::from_u32(k as u32)),
519
+                detail: item.get("detail").and_then(|v| v.as_str()).map(String::from),
520
+                documentation: item
521
+                    .get("documentation")
522
+                    .and_then(|v| {
523
+                        if let Some(s) = v.as_str() {
524
+                            Some(s.to_string())
525
+                        } else {
526
+                            v.get("value").and_then(|v| v.as_str()).map(String::from)
527
+                        }
528
+                    }),
529
+                insert_text: item.get("insertText").and_then(|v| v.as_str()).map(String::from),
530
+                text_edit: item.get("textEdit").and_then(|te| {
531
+                    Some(super::types::TextEdit {
532
+                        range: parse_range(te.get("range")?)?,
533
+                        new_text: te.get("newText")?.as_str()?.to_string(),
534
+                    })
535
+                }),
536
+                sort_text: item.get("sortText").and_then(|v| v.as_str()).map(String::from),
537
+                filter_text: item.get("filterText").and_then(|v| v.as_str()).map(String::from),
538
+            })
539
+        })
540
+        .collect()
541
+}
542
+
543
+/// Parse hover info from response
544
+pub fn parse_hover(result: &Value) -> Option<super::types::HoverInfo> {
545
+    let contents = result.get("contents")?;
546
+    let text = if let Some(s) = contents.as_str() {
547
+        s.to_string()
548
+    } else if let Some(arr) = contents.as_array() {
549
+        arr.iter()
550
+            .filter_map(|v| {
551
+                if let Some(s) = v.as_str() {
552
+                    Some(s.to_string())
553
+                } else {
554
+                    v.get("value").and_then(|v| v.as_str()).map(String::from)
555
+                }
556
+            })
557
+            .collect::<Vec<_>>()
558
+            .join("\n\n")
559
+    } else if let Some(value) = contents.get("value") {
560
+        value.as_str()?.to_string()
561
+    } else {
562
+        return None;
563
+    };
564
+
565
+    Some(super::types::HoverInfo {
566
+        contents: text,
567
+        range: result.get("range").and_then(parse_range),
568
+    })
569
+}
570
+
571
+/// Parse locations from definition/references response
572
+pub fn parse_locations(result: &Value) -> Vec<super::types::Location> {
573
+    if let Some(loc) = parse_location(result) {
574
+        vec![loc]
575
+    } else if let Some(arr) = result.as_array() {
576
+        arr.iter().filter_map(parse_location).collect()
577
+    } else {
578
+        Vec::new()
579
+    }
580
+}
581
+
582
+/// Parse document symbols from response
583
+pub fn parse_document_symbols(result: &Value) -> Vec<super::types::DocumentSymbol> {
584
+    fn parse_symbol(value: &Value) -> Option<super::types::DocumentSymbol> {
585
+        let name = value.get("name")?.as_str()?.to_string();
586
+        let kind = value.get("kind")?.as_u64()?;
587
+
588
+        // Handle both DocumentSymbol and SymbolInformation formats
589
+        let (range, selection_range) = if let Some(r) = value.get("range") {
590
+            let range = parse_range(r)?;
591
+            let sel = value.get("selectionRange").and_then(parse_range).unwrap_or(range);
592
+            (range, sel)
593
+        } else if let Some(loc) = value.get("location") {
594
+            let range = parse_range(loc.get("range")?)?;
595
+            (range, range)
596
+        } else {
597
+            return None;
598
+        };
599
+
600
+        let children = value
601
+            .get("children")
602
+            .and_then(|c| c.as_array())
603
+            .map(|arr| arr.iter().filter_map(parse_symbol).collect())
604
+            .unwrap_or_default();
605
+
606
+        Some(super::types::DocumentSymbol {
607
+            name,
608
+            kind: super::types::SymbolKind::from_u32(kind as u32)?,
609
+            range,
610
+            selection_range,
611
+            children,
612
+        })
613
+    }
614
+
615
+    result
616
+        .as_array()
617
+        .map(|arr| arr.iter().filter_map(parse_symbol).collect())
618
+        .unwrap_or_default()
619
+}
620
+
621
+/// Parse diagnostics from publishDiagnostics notification
622
+pub fn parse_diagnostics(params: &Value) -> (String, Vec<super::types::Diagnostic>) {
623
+    let uri = params
624
+        .get("uri")
625
+        .and_then(|v| v.as_str())
626
+        .unwrap_or("")
627
+        .to_string();
628
+
629
+    let diagnostics = params
630
+        .get("diagnostics")
631
+        .and_then(|v| v.as_array())
632
+        .map(|arr| {
633
+            arr.iter()
634
+                .filter_map(|d| {
635
+                    Some(super::types::Diagnostic {
636
+                        range: parse_range(d.get("range")?)?,
637
+                        severity: d
638
+                            .get("severity")
639
+                            .and_then(|v| v.as_u64())
640
+                            .and_then(|s| super::types::DiagnosticSeverity::from_u32(s as u32)),
641
+                        code: d.get("code").and_then(|v| {
642
+                            if let Some(s) = v.as_str() {
643
+                                Some(s.to_string())
644
+                            } else if let Some(n) = v.as_i64() {
645
+                                Some(n.to_string())
646
+                            } else {
647
+                                None
648
+                            }
649
+                        }),
650
+                        source: d.get("source").and_then(|v| v.as_str()).map(String::from),
651
+                        message: d.get("message")?.as_str()?.to_string(),
652
+                    })
653
+                })
654
+                .collect()
655
+        })
656
+        .unwrap_or_default();
657
+
658
+    (uri, diagnostics)
659
+}
660
+
661
+/// Parse text edits from formatting response
662
+pub fn parse_text_edits(result: &Value) -> Vec<super::types::TextEdit> {
663
+    result
664
+        .as_array()
665
+        .map(|arr| {
666
+            arr.iter()
667
+                .filter_map(|edit| {
668
+                    Some(super::types::TextEdit {
669
+                        range: parse_range(edit.get("range")?)?,
670
+                        new_text: edit.get("newText")?.as_str()?.to_string(),
671
+                    })
672
+                })
673
+                .collect()
674
+        })
675
+        .unwrap_or_default()
676
+}
677
+
678
+/// Parse workspace edit from rename response
679
+pub fn parse_workspace_edit(result: &Value) -> super::types::WorkspaceEdit {
680
+    let mut edit = super::types::WorkspaceEdit::default();
681
+
682
+    if let Some(changes) = result.get("changes").and_then(|v| v.as_object()) {
683
+        for (uri, edits) in changes {
684
+            let text_edits = edits
685
+                .as_array()
686
+                .map(|arr| {
687
+                    arr.iter()
688
+                        .filter_map(|e| {
689
+                            Some(super::types::TextEdit {
690
+                                range: parse_range(e.get("range")?)?,
691
+                                new_text: e.get("newText")?.as_str()?.to_string(),
692
+                            })
693
+                        })
694
+                        .collect()
695
+                })
696
+                .unwrap_or_default();
697
+            edit.changes.insert(uri.clone(), text_edits);
698
+        }
699
+    }
700
+
701
+    // Handle documentChanges format
702
+    if let Some(doc_changes) = result.get("documentChanges").and_then(|v| v.as_array()) {
703
+        for change in doc_changes {
704
+            if let Some(text_doc) = change.get("textDocument") {
705
+                let uri = text_doc.get("uri").and_then(|v| v.as_str()).unwrap_or("");
706
+                let text_edits = change
707
+                    .get("edits")
708
+                    .and_then(|v| v.as_array())
709
+                    .map(|arr| {
710
+                        arr.iter()
711
+                            .filter_map(|e| {
712
+                                Some(super::types::TextEdit {
713
+                                    range: parse_range(e.get("range")?)?,
714
+                                    new_text: e.get("newText")?.as_str()?.to_string(),
715
+                                })
716
+                            })
717
+                            .collect()
718
+                    })
719
+                    .unwrap_or_default();
720
+                edit.changes.insert(uri.to_string(), text_edits);
721
+            }
722
+        }
723
+    }
724
+
725
+    edit
726
+}
src/lsp/types.rsadded
@@ -0,0 +1,474 @@
1
+//! LSP type definitions
2
+//!
3
+//! Core types used throughout the LSP client implementation.
4
+//!
5
+//! Note: Some types and methods are for planned features.
6
+#![allow(dead_code)]
7
+
8
+use std::collections::HashMap;
9
+use std::path::PathBuf;
10
+
11
+/// Position in a document (0-based line and character)
12
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13
+pub struct Position {
14
+    pub line: u32,
15
+    pub character: u32,
16
+}
17
+
18
+impl Position {
19
+    pub fn new(line: u32, character: u32) -> Self {
20
+        Self { line, character }
21
+    }
22
+}
23
+
24
+/// Range in a document
25
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26
+pub struct Range {
27
+    pub start: Position,
28
+    pub end: Position,
29
+}
30
+
31
+impl Range {
32
+    pub fn new(start: Position, end: Position) -> Self {
33
+        Self { start, end }
34
+    }
35
+
36
+    pub fn point(pos: Position) -> Self {
37
+        Self {
38
+            start: pos,
39
+            end: pos,
40
+        }
41
+    }
42
+}
43
+
44
+/// Location in a document
45
+#[derive(Debug, Clone, PartialEq, Eq)]
46
+pub struct Location {
47
+    pub uri: String,
48
+    pub range: Range,
49
+}
50
+
51
+impl Location {
52
+    /// Convert URI to file path
53
+    pub fn to_path(&self) -> Option<PathBuf> {
54
+        if self.uri.starts_with("file://") {
55
+            Some(PathBuf::from(&self.uri[7..]))
56
+        } else {
57
+            None
58
+        }
59
+    }
60
+}
61
+
62
+/// Text edit operation
63
+#[derive(Debug, Clone, PartialEq, Eq)]
64
+pub struct TextEdit {
65
+    pub range: Range,
66
+    pub new_text: String,
67
+}
68
+
69
+/// Workspace edit (multiple file edits)
70
+#[derive(Debug, Clone, Default)]
71
+pub struct WorkspaceEdit {
72
+    pub changes: HashMap<String, Vec<TextEdit>>,
73
+}
74
+
75
+/// Diagnostic severity levels
76
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77
+pub enum DiagnosticSeverity {
78
+    Error = 1,
79
+    Warning = 2,
80
+    Information = 3,
81
+    Hint = 4,
82
+}
83
+
84
+impl DiagnosticSeverity {
85
+    pub fn from_u32(value: u32) -> Option<Self> {
86
+        match value {
87
+            1 => Some(Self::Error),
88
+            2 => Some(Self::Warning),
89
+            3 => Some(Self::Information),
90
+            4 => Some(Self::Hint),
91
+            _ => None,
92
+        }
93
+    }
94
+}
95
+
96
+/// A diagnostic message from the language server
97
+#[derive(Debug, Clone)]
98
+pub struct Diagnostic {
99
+    pub range: Range,
100
+    pub severity: Option<DiagnosticSeverity>,
101
+    pub code: Option<String>,
102
+    pub source: Option<String>,
103
+    pub message: String,
104
+}
105
+
106
+/// Completion item kind
107
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108
+pub enum CompletionItemKind {
109
+    Text = 1,
110
+    Method = 2,
111
+    Function = 3,
112
+    Constructor = 4,
113
+    Field = 5,
114
+    Variable = 6,
115
+    Class = 7,
116
+    Interface = 8,
117
+    Module = 9,
118
+    Property = 10,
119
+    Unit = 11,
120
+    Value = 12,
121
+    Enum = 13,
122
+    Keyword = 14,
123
+    Snippet = 15,
124
+    Color = 16,
125
+    File = 17,
126
+    Reference = 18,
127
+    Folder = 19,
128
+    EnumMember = 20,
129
+    Constant = 21,
130
+    Struct = 22,
131
+    Event = 23,
132
+    Operator = 24,
133
+    TypeParameter = 25,
134
+}
135
+
136
+impl CompletionItemKind {
137
+    pub fn from_u32(value: u32) -> Option<Self> {
138
+        match value {
139
+            1 => Some(Self::Text),
140
+            2 => Some(Self::Method),
141
+            3 => Some(Self::Function),
142
+            4 => Some(Self::Constructor),
143
+            5 => Some(Self::Field),
144
+            6 => Some(Self::Variable),
145
+            7 => Some(Self::Class),
146
+            8 => Some(Self::Interface),
147
+            9 => Some(Self::Module),
148
+            10 => Some(Self::Property),
149
+            11 => Some(Self::Unit),
150
+            12 => Some(Self::Value),
151
+            13 => Some(Self::Enum),
152
+            14 => Some(Self::Keyword),
153
+            15 => Some(Self::Snippet),
154
+            16 => Some(Self::Color),
155
+            17 => Some(Self::File),
156
+            18 => Some(Self::Reference),
157
+            19 => Some(Self::Folder),
158
+            20 => Some(Self::EnumMember),
159
+            21 => Some(Self::Constant),
160
+            22 => Some(Self::Struct),
161
+            23 => Some(Self::Event),
162
+            24 => Some(Self::Operator),
163
+            25 => Some(Self::TypeParameter),
164
+            _ => None,
165
+        }
166
+    }
167
+
168
+    pub fn icon(&self) -> &'static str {
169
+        match self {
170
+            Self::Text => "t",
171
+            Self::Method => "m",
172
+            Self::Function => "f",
173
+            Self::Constructor => "C",
174
+            Self::Field => "F",
175
+            Self::Variable => "v",
176
+            Self::Class => "c",
177
+            Self::Interface => "i",
178
+            Self::Module => "M",
179
+            Self::Property => "p",
180
+            Self::Unit => "u",
181
+            Self::Value => "V",
182
+            Self::Enum => "E",
183
+            Self::Keyword => "k",
184
+            Self::Snippet => "s",
185
+            Self::Color => "#",
186
+            Self::File => "f",
187
+            Self::Reference => "r",
188
+            Self::Folder => "D",
189
+            Self::EnumMember => "e",
190
+            Self::Constant => "K",
191
+            Self::Struct => "S",
192
+            Self::Event => "!",
193
+            Self::Operator => "o",
194
+            Self::TypeParameter => "T",
195
+        }
196
+    }
197
+}
198
+
199
+/// A completion item
200
+#[derive(Debug, Clone)]
201
+pub struct CompletionItem {
202
+    pub label: String,
203
+    pub kind: Option<CompletionItemKind>,
204
+    pub detail: Option<String>,
205
+    pub documentation: Option<String>,
206
+    pub insert_text: Option<String>,
207
+    pub text_edit: Option<TextEdit>,
208
+    pub sort_text: Option<String>,
209
+    pub filter_text: Option<String>,
210
+}
211
+
212
+/// Symbol kind (for document/workspace symbols)
213
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214
+pub enum SymbolKind {
215
+    File = 1,
216
+    Module = 2,
217
+    Namespace = 3,
218
+    Package = 4,
219
+    Class = 5,
220
+    Method = 6,
221
+    Property = 7,
222
+    Field = 8,
223
+    Constructor = 9,
224
+    Enum = 10,
225
+    Interface = 11,
226
+    Function = 12,
227
+    Variable = 13,
228
+    Constant = 14,
229
+    String = 15,
230
+    Number = 16,
231
+    Boolean = 17,
232
+    Array = 18,
233
+    Object = 19,
234
+    Key = 20,
235
+    Null = 21,
236
+    EnumMember = 22,
237
+    Struct = 23,
238
+    Event = 24,
239
+    Operator = 25,
240
+    TypeParameter = 26,
241
+}
242
+
243
+impl SymbolKind {
244
+    pub fn from_u32(value: u32) -> Option<Self> {
245
+        match value {
246
+            1 => Some(Self::File),
247
+            2 => Some(Self::Module),
248
+            3 => Some(Self::Namespace),
249
+            4 => Some(Self::Package),
250
+            5 => Some(Self::Class),
251
+            6 => Some(Self::Method),
252
+            7 => Some(Self::Property),
253
+            8 => Some(Self::Field),
254
+            9 => Some(Self::Constructor),
255
+            10 => Some(Self::Enum),
256
+            11 => Some(Self::Interface),
257
+            12 => Some(Self::Function),
258
+            13 => Some(Self::Variable),
259
+            14 => Some(Self::Constant),
260
+            15 => Some(Self::String),
261
+            16 => Some(Self::Number),
262
+            17 => Some(Self::Boolean),
263
+            18 => Some(Self::Array),
264
+            19 => Some(Self::Object),
265
+            20 => Some(Self::Key),
266
+            21 => Some(Self::Null),
267
+            22 => Some(Self::EnumMember),
268
+            23 => Some(Self::Struct),
269
+            24 => Some(Self::Event),
270
+            25 => Some(Self::Operator),
271
+            26 => Some(Self::TypeParameter),
272
+            _ => None,
273
+        }
274
+    }
275
+
276
+    pub fn icon(&self) -> &'static str {
277
+        match self {
278
+            Self::File => "󰈔",
279
+            Self::Module => "󰆧",
280
+            Self::Namespace => "󰅩",
281
+            Self::Package => "󰏗",
282
+            Self::Class => "󰠱",
283
+            Self::Method => "󰆧",
284
+            Self::Property => "󰜢",
285
+            Self::Field => "󰜢",
286
+            Self::Constructor => "󰆧",
287
+            Self::Enum => "󰒻",
288
+            Self::Interface => "󰜰",
289
+            Self::Function => "󰊕",
290
+            Self::Variable => "󰀫",
291
+            Self::Constant => "󰏿",
292
+            Self::String => "󰀬",
293
+            Self::Number => "󰎠",
294
+            Self::Boolean => "󰨙",
295
+            Self::Array => "󰅪",
296
+            Self::Object => "󰅩",
297
+            Self::Key => "󰌋",
298
+            Self::Null => "󰟢",
299
+            Self::EnumMember => "󰒻",
300
+            Self::Struct => "󰙅",
301
+            Self::Event => "󰉁",
302
+            Self::Operator => "󰆕",
303
+            Self::TypeParameter => "󰊄",
304
+        }
305
+    }
306
+}
307
+
308
+/// A document symbol
309
+#[derive(Debug, Clone)]
310
+pub struct DocumentSymbol {
311
+    pub name: String,
312
+    pub kind: SymbolKind,
313
+    pub range: Range,
314
+    pub selection_range: Range,
315
+    pub children: Vec<DocumentSymbol>,
316
+}
317
+
318
+/// Hover information
319
+#[derive(Debug, Clone)]
320
+pub struct HoverInfo {
321
+    pub contents: String,
322
+    pub range: Option<Range>,
323
+}
324
+
325
+/// Server capabilities
326
+#[derive(Debug, Clone, Default)]
327
+pub struct Capabilities {
328
+    pub completion: bool,
329
+    pub hover: bool,
330
+    pub definition: bool,
331
+    pub references: bool,
332
+    pub rename: bool,
333
+    pub code_actions: bool,
334
+    pub formatting: bool,
335
+    pub diagnostics: bool,
336
+    pub document_symbols: bool,
337
+    pub workspace_symbols: bool,
338
+    pub signature_help: bool,
339
+}
340
+
341
+impl Capabilities {
342
+    pub fn all() -> Self {
343
+        Self {
344
+            completion: true,
345
+            hover: true,
346
+            definition: true,
347
+            references: true,
348
+            rename: true,
349
+            code_actions: true,
350
+            formatting: true,
351
+            diagnostics: true,
352
+            document_symbols: true,
353
+            workspace_symbols: true,
354
+            signature_help: true,
355
+        }
356
+    }
357
+}
358
+
359
+/// Configuration for an LSP server
360
+#[derive(Debug, Clone)]
361
+pub struct ServerConfig {
362
+    pub name: String,
363
+    pub language: String,
364
+    pub command: Vec<String>,
365
+    pub file_patterns: Vec<String>,
366
+    pub capabilities: Capabilities,
367
+}
368
+
369
+impl ServerConfig {
370
+    pub fn new(name: &str, language: &str, command: Vec<&str>) -> Self {
371
+        Self {
372
+            name: name.to_string(),
373
+            language: language.to_string(),
374
+            command: command.into_iter().map(String::from).collect(),
375
+            file_patterns: Vec::new(),
376
+            capabilities: Capabilities::all(),
377
+        }
378
+    }
379
+
380
+    pub fn with_patterns(mut self, patterns: Vec<&str>) -> Self {
381
+        self.file_patterns = patterns.into_iter().map(String::from).collect();
382
+        self
383
+    }
384
+
385
+    pub fn with_capabilities(mut self, caps: Capabilities) -> Self {
386
+        self.capabilities = caps;
387
+        self
388
+    }
389
+}
390
+
391
+/// Language ID detection from file extension
392
+pub fn detect_language(path: &str) -> Option<&'static str> {
393
+    let ext = path.rsplit('.').next()?;
394
+    match ext.to_lowercase().as_str() {
395
+        "rs" => Some("rust"),
396
+        "py" | "pyw" => Some("python"),
397
+        "js" | "mjs" | "cjs" => Some("javascript"),
398
+        "ts" | "mts" | "cts" => Some("typescript"),
399
+        "tsx" => Some("typescriptreact"),
400
+        "jsx" => Some("javascriptreact"),
401
+        "c" | "h" => Some("c"),
402
+        "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Some("cpp"),
403
+        "go" => Some("go"),
404
+        "java" => Some("java"),
405
+        "kt" | "kts" => Some("kotlin"),
406
+        "swift" => Some("swift"),
407
+        "rb" | "erb" => Some("ruby"),
408
+        "php" => Some("php"),
409
+        "cs" => Some("csharp"),
410
+        "fs" | "fsi" | "fsx" => Some("fsharp"),
411
+        "scala" | "sc" => Some("scala"),
412
+        "hs" | "lhs" => Some("haskell"),
413
+        "lua" => Some("lua"),
414
+        "pl" | "pm" => Some("perl"),
415
+        "r" | "R" => Some("r"),
416
+        "jl" => Some("julia"),
417
+        "ex" | "exs" => Some("elixir"),
418
+        "erl" | "hrl" => Some("erlang"),
419
+        "clj" | "cljs" | "cljc" => Some("clojure"),
420
+        "f90" | "f95" | "f03" | "f08" | "for" | "ftn" => Some("fortran"),
421
+        "zig" => Some("zig"),
422
+        "nim" => Some("nim"),
423
+        "odin" => Some("odin"),
424
+        "v" => Some("v"),
425
+        "d" => Some("d"),
426
+        "sh" | "bash" => Some("shellscript"),
427
+        "zsh" => Some("shellscript"),
428
+        "fish" => Some("fish"),
429
+        "ps1" | "psm1" => Some("powershell"),
430
+        "sql" => Some("sql"),
431
+        "html" | "htm" => Some("html"),
432
+        "css" => Some("css"),
433
+        "scss" => Some("scss"),
434
+        "less" => Some("less"),
435
+        "json" => Some("json"),
436
+        "jsonc" => Some("jsonc"),
437
+        "yaml" | "yml" => Some("yaml"),
438
+        "toml" => Some("toml"),
439
+        "xml" => Some("xml"),
440
+        "md" | "markdown" => Some("markdown"),
441
+        "dockerfile" => Some("dockerfile"),
442
+        "tf" | "tfvars" => Some("terraform"),
443
+        "nix" => Some("nix"),
444
+        "ml" | "mli" => Some("ocaml"),
445
+        "dart" => Some("dart"),
446
+        "groovy" | "gradle" => Some("groovy"),
447
+        "vue" => Some("vue"),
448
+        "svelte" => Some("svelte"),
449
+        "elm" => Some("elm"),
450
+        "asm" | "s" => Some("asm"),
451
+        "cmake" => Some("cmake"),
452
+        "proto" => Some("proto"),
453
+        "graphql" | "gql" => Some("graphql"),
454
+        _ => None,
455
+    }
456
+}
457
+
458
+/// Convert file path to LSP URI
459
+pub fn path_to_uri(path: &str) -> String {
460
+    if path.starts_with('/') {
461
+        format!("file://{}", path)
462
+    } else {
463
+        format!("file:///{}", path)
464
+    }
465
+}
466
+
467
+/// Convert LSP URI to file path
468
+pub fn uri_to_path(uri: &str) -> Option<String> {
469
+    if uri.starts_with("file://") {
470
+        Some(uri[7..].to_string())
471
+    } else {
472
+        None
473
+    }
474
+}