Rust · 24259 bytes Raw Blame History
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 }
727