Rust · 16004 bytes Raw Blame History
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 }
513