1use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use lsp_types::{
7 CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
8 CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams,
9 CallHierarchyPrepareParams as LspCallHierarchyPrepareParams, CompletionParams,
10 CompletionTriggerKind, DocumentFormattingParams, DocumentSymbol, DocumentSymbolParams,
11 FormattingOptions, GotoDefinitionParams, Hover, HoverContents, HoverParams as LspHoverParams,
12 MarkedString, PartialResultParams, ReferenceContext, ReferenceParams,
13 RenameParams as LspRenameParams, TextDocumentIdentifier, TextDocumentPositionParams,
14 WorkDoneProgressParams, WorkspaceEdit, WorkspaceSymbolParams as LspWorkspaceSymbolParams,
15};
16use serde::{Deserialize, Serialize};
17use tokio::time::Duration;
18
19use super::DocumentTracker;
20use super::state::detect_language;
21use crate::bridge::encoding::mcp_to_lsp_position;
22use crate::error::{Error, Result};
23use crate::lsp::{LspClient, LspServer};
24
25#[derive(Debug)]
27pub struct Translator {
28 lsp_clients: HashMap<String, LspClient>,
30 lsp_servers: HashMap<String, LspServer>,
32 document_tracker: DocumentTracker,
34 workspace_roots: Vec<PathBuf>,
36}
37
38impl Translator {
39 #[must_use]
41 pub fn new() -> Self {
42 Self {
43 lsp_clients: HashMap::new(),
44 lsp_servers: HashMap::new(),
45 document_tracker: DocumentTracker::new(),
46 workspace_roots: vec![],
47 }
48 }
49
50 pub fn set_workspace_roots(&mut self, roots: Vec<PathBuf>) {
52 self.workspace_roots = roots;
53 }
54
55 pub fn register_client(&mut self, language_id: String, client: LspClient) {
57 self.lsp_clients.insert(language_id, client);
58 }
59
60 pub fn register_server(&mut self, language_id: String, server: LspServer) {
62 self.lsp_servers.insert(language_id, server);
63 }
64
65 #[must_use]
67 pub const fn document_tracker(&self) -> &DocumentTracker {
68 &self.document_tracker
69 }
70
71 pub const fn document_tracker_mut(&mut self) -> &mut DocumentTracker {
73 &mut self.document_tracker
74 }
75
76 }
81
82impl Default for Translator {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Position2D {
91 pub line: u32,
93 pub character: u32,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct Range {
100 pub start: Position2D,
102 pub end: Position2D,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Location {
109 pub uri: String,
111 pub range: Range,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct HoverResult {
118 pub contents: String,
120 pub range: Option<Range>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct DefinitionResult {
127 pub locations: Vec<Location>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ReferencesResult {
134 pub locations: Vec<Location>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(rename_all = "lowercase")]
141pub enum DiagnosticSeverity {
142 Error,
144 Warning,
146 Information,
148 Hint,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct Diagnostic {
155 pub range: Range,
157 pub severity: DiagnosticSeverity,
159 pub message: String,
161 pub code: Option<String>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct DiagnosticsResult {
168 pub diagnostics: Vec<Diagnostic>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct TextEdit {
175 pub range: Range,
177 pub new_text: String,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct DocumentChanges {
184 pub uri: String,
186 pub edits: Vec<TextEdit>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct RenameResult {
193 pub changes: Vec<DocumentChanges>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct Completion {
200 pub label: String,
202 pub kind: Option<String>,
204 pub detail: Option<String>,
206 pub documentation: Option<String>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct CompletionsResult {
213 pub items: Vec<Completion>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct Symbol {
220 pub name: String,
222 pub kind: String,
224 pub range: Range,
226 pub selection_range: Range,
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub children: Option<Vec<Self>>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct DocumentSymbolsResult {
236 pub symbols: Vec<Symbol>,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct FormatDocumentResult {
243 pub edits: Vec<TextEdit>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct WorkspaceSymbol {
250 pub name: String,
252 pub kind: String,
254 pub location: Location,
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub container_name: Option<String>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct WorkspaceSymbolResult {
264 pub symbols: Vec<WorkspaceSymbol>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct CodeAction {
271 pub title: String,
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub kind: Option<String>,
276 #[serde(skip_serializing_if = "Vec::is_empty", default)]
278 pub diagnostics: Vec<Diagnostic>,
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub edit: Option<WorkspaceEditDescription>,
282 #[serde(skip_serializing_if = "Option::is_none")]
284 pub command: Option<CommandDescription>,
285 #[serde(default)]
287 pub is_preferred: bool,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct WorkspaceEditDescription {
293 pub changes: Vec<DocumentChanges>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct CommandDescription {
300 pub title: String,
302 pub command: String,
304 #[serde(skip_serializing_if = "Vec::is_empty", default)]
306 pub arguments: Vec<serde_json::Value>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct CodeActionsResult {
312 pub actions: Vec<CodeAction>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct CallHierarchyItemResult {
319 pub name: String,
321 pub kind: String,
323 #[serde(skip_serializing_if = "Option::is_none")]
325 pub detail: Option<String>,
326 pub uri: String,
328 pub range: Range,
330 pub selection_range: Range,
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub data: Option<serde_json::Value>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct CallHierarchyPrepareResult {
340 pub items: Vec<CallHierarchyItemResult>,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct IncomingCall {
347 pub from: CallHierarchyItemResult,
349 pub from_ranges: Vec<Range>,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct IncomingCallsResult {
356 pub calls: Vec<IncomingCall>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct OutgoingCall {
363 pub to: CallHierarchyItemResult,
365 pub from_ranges: Vec<Range>,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct OutgoingCallsResult {
372 pub calls: Vec<OutgoingCall>,
374}
375
376const MAX_POSITION_VALUE: u32 = 1_000_000;
378const MAX_RANGE_LINES: u32 = 10_000;
380
381impl Translator {
382 fn validate_path(&self, path: &Path) -> Result<PathBuf> {
388 let canonical = path.canonicalize().map_err(|e| Error::FileIo {
389 path: path.to_path_buf(),
390 source: e,
391 })?;
392
393 if self.workspace_roots.is_empty() {
395 return Ok(canonical);
396 }
397
398 for root in &self.workspace_roots {
400 if let Ok(canonical_root) = root.canonicalize() {
401 if canonical.starts_with(&canonical_root) {
402 return Ok(canonical);
403 }
404 }
405 }
406
407 Err(Error::PathOutsideWorkspace(path.to_path_buf()))
408 }
409
410 fn get_client_for_file(&self, path: &Path) -> Result<LspClient> {
412 let language_id = detect_language(path);
413 self.lsp_clients
414 .get(&language_id)
415 .cloned()
416 .ok_or(Error::NoServerForLanguage(language_id))
417 }
418
419 fn parse_file_uri(&self, uri: &lsp_types::Uri) -> Result<PathBuf> {
427 let uri_str = uri.as_str();
428
429 if !uri_str.starts_with("file://") {
431 return Err(Error::InvalidToolParams(format!(
432 "Invalid URI scheme, expected file:// but got: {uri_str}"
433 )));
434 }
435
436 let path_str = &uri_str["file://".len()..];
438
439 #[cfg(windows)]
442 let path_str = if path_str.len() >= 3
443 && path_str.starts_with('/')
444 && path_str.chars().nth(2) == Some(':')
445 {
446 &path_str[1..]
447 } else {
448 path_str
449 };
450
451 let path = PathBuf::from(path_str);
452
453 self.validate_path(&path)
455 }
456
457 pub async fn handle_hover(
463 &mut self,
464 file_path: String,
465 line: u32,
466 character: u32,
467 ) -> Result<HoverResult> {
468 let path = PathBuf::from(&file_path);
469 let validated_path = self.validate_path(&path)?;
470 let client = self.get_client_for_file(&validated_path)?;
471 let uri = self
472 .document_tracker
473 .ensure_open(&validated_path, &client)
474 .await?;
475 let lsp_position = mcp_to_lsp_position(line, character);
476
477 let params = LspHoverParams {
478 text_document_position_params: TextDocumentPositionParams {
479 text_document: TextDocumentIdentifier { uri },
480 position: lsp_position,
481 },
482 work_done_progress_params: WorkDoneProgressParams::default(),
483 };
484
485 let timeout_duration = Duration::from_secs(30);
486 let response: Option<Hover> = client
487 .request("textDocument/hover", params, timeout_duration)
488 .await?;
489
490 let result = match response {
491 Some(hover) => {
492 let contents = extract_hover_contents(hover.contents);
493 let range = hover.range.map(normalize_range);
494 HoverResult { contents, range }
495 }
496 None => HoverResult {
497 contents: "No hover information available".to_string(),
498 range: None,
499 },
500 };
501
502 Ok(result)
503 }
504
505 pub async fn handle_definition(
511 &mut self,
512 file_path: String,
513 line: u32,
514 character: u32,
515 ) -> Result<DefinitionResult> {
516 let path = PathBuf::from(&file_path);
517 let validated_path = self.validate_path(&path)?;
518 let client = self.get_client_for_file(&validated_path)?;
519 let uri = self
520 .document_tracker
521 .ensure_open(&validated_path, &client)
522 .await?;
523 let lsp_position = mcp_to_lsp_position(line, character);
524
525 let params = GotoDefinitionParams {
526 text_document_position_params: TextDocumentPositionParams {
527 text_document: TextDocumentIdentifier { uri },
528 position: lsp_position,
529 },
530 work_done_progress_params: WorkDoneProgressParams::default(),
531 partial_result_params: PartialResultParams::default(),
532 };
533
534 let timeout_duration = Duration::from_secs(30);
535 let response: Option<lsp_types::GotoDefinitionResponse> = client
536 .request("textDocument/definition", params, timeout_duration)
537 .await?;
538
539 let locations = match response {
540 Some(lsp_types::GotoDefinitionResponse::Scalar(loc)) => vec![loc],
541 Some(lsp_types::GotoDefinitionResponse::Array(locs)) => locs,
542 Some(lsp_types::GotoDefinitionResponse::Link(links)) => links
543 .into_iter()
544 .map(|link| lsp_types::Location {
545 uri: link.target_uri,
546 range: link.target_selection_range,
547 })
548 .collect(),
549 None => vec![],
550 };
551
552 let result = DefinitionResult {
553 locations: locations
554 .into_iter()
555 .map(|loc| Location {
556 uri: loc.uri.to_string(),
557 range: normalize_range(loc.range),
558 })
559 .collect(),
560 };
561
562 Ok(result)
563 }
564
565 pub async fn handle_references(
571 &mut self,
572 file_path: String,
573 line: u32,
574 character: u32,
575 include_declaration: bool,
576 ) -> Result<ReferencesResult> {
577 let path = PathBuf::from(&file_path);
578 let validated_path = self.validate_path(&path)?;
579 let client = self.get_client_for_file(&validated_path)?;
580 let uri = self
581 .document_tracker
582 .ensure_open(&validated_path, &client)
583 .await?;
584 let lsp_position = mcp_to_lsp_position(line, character);
585
586 let params = ReferenceParams {
587 text_document_position: TextDocumentPositionParams {
588 text_document: TextDocumentIdentifier { uri },
589 position: lsp_position,
590 },
591 work_done_progress_params: WorkDoneProgressParams::default(),
592 partial_result_params: PartialResultParams::default(),
593 context: ReferenceContext {
594 include_declaration,
595 },
596 };
597
598 let timeout_duration = Duration::from_secs(30);
599 let response: Option<Vec<lsp_types::Location>> = client
600 .request("textDocument/references", params, timeout_duration)
601 .await?;
602
603 let locations = response.unwrap_or_default();
604
605 let result = ReferencesResult {
606 locations: locations
607 .into_iter()
608 .map(|loc| Location {
609 uri: loc.uri.to_string(),
610 range: normalize_range(loc.range),
611 })
612 .collect(),
613 };
614
615 Ok(result)
616 }
617
618 pub async fn handle_diagnostics(&mut self, file_path: String) -> Result<DiagnosticsResult> {
624 let path = PathBuf::from(&file_path);
625 let validated_path = self.validate_path(&path)?;
626 let client = self.get_client_for_file(&validated_path)?;
627 let uri = self
628 .document_tracker
629 .ensure_open(&validated_path, &client)
630 .await?;
631
632 let params = lsp_types::DocumentDiagnosticParams {
633 text_document: TextDocumentIdentifier { uri },
634 identifier: None,
635 previous_result_id: None,
636 work_done_progress_params: WorkDoneProgressParams::default(),
637 partial_result_params: PartialResultParams::default(),
638 };
639
640 let timeout_duration = Duration::from_secs(30);
641 let response: lsp_types::DocumentDiagnosticReportResult = client
642 .request("textDocument/diagnostic", params, timeout_duration)
643 .await?;
644
645 let diagnostics = match response {
646 lsp_types::DocumentDiagnosticReportResult::Report(report) => match report {
647 lsp_types::DocumentDiagnosticReport::Full(full) => {
648 full.full_document_diagnostic_report.items
649 }
650 lsp_types::DocumentDiagnosticReport::Unchanged(_) => vec![],
651 },
652 lsp_types::DocumentDiagnosticReportResult::Partial(_) => vec![],
653 };
654
655 let result = DiagnosticsResult {
656 diagnostics: diagnostics
657 .into_iter()
658 .map(|diag| Diagnostic {
659 range: normalize_range(diag.range),
660 severity: match diag.severity {
661 Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
662 Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
663 Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
664 DiagnosticSeverity::Information
665 }
666 Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
667 _ => DiagnosticSeverity::Information,
668 },
669 message: diag.message,
670 code: diag.code.map(|c| match c {
671 lsp_types::NumberOrString::Number(n) => n.to_string(),
672 lsp_types::NumberOrString::String(s) => s,
673 }),
674 })
675 .collect(),
676 };
677
678 Ok(result)
679 }
680
681 pub async fn handle_rename(
687 &mut self,
688 file_path: String,
689 line: u32,
690 character: u32,
691 new_name: String,
692 ) -> Result<RenameResult> {
693 let path = PathBuf::from(&file_path);
694 let validated_path = self.validate_path(&path)?;
695 let client = self.get_client_for_file(&validated_path)?;
696 let uri = self
697 .document_tracker
698 .ensure_open(&validated_path, &client)
699 .await?;
700 let lsp_position = mcp_to_lsp_position(line, character);
701
702 let params = LspRenameParams {
703 text_document_position: TextDocumentPositionParams {
704 text_document: TextDocumentIdentifier { uri },
705 position: lsp_position,
706 },
707 new_name,
708 work_done_progress_params: WorkDoneProgressParams::default(),
709 };
710
711 let timeout_duration = Duration::from_secs(30);
712 let response: Option<WorkspaceEdit> = client
713 .request("textDocument/rename", params, timeout_duration)
714 .await?;
715
716 let changes = if let Some(edit) = response {
717 let mut result_changes = Vec::new();
718
719 if let Some(changes_map) = edit.changes {
720 for (uri, edits) in changes_map {
721 result_changes.push(DocumentChanges {
722 uri: uri.to_string(),
723 edits: edits
724 .into_iter()
725 .map(|edit| TextEdit {
726 range: normalize_range(edit.range),
727 new_text: edit.new_text,
728 })
729 .collect(),
730 });
731 }
732 }
733
734 result_changes
735 } else {
736 vec![]
737 };
738
739 Ok(RenameResult { changes })
740 }
741
742 pub async fn handle_completions(
748 &mut self,
749 file_path: String,
750 line: u32,
751 character: u32,
752 trigger: Option<String>,
753 ) -> Result<CompletionsResult> {
754 let path = PathBuf::from(&file_path);
755 let validated_path = self.validate_path(&path)?;
756 let client = self.get_client_for_file(&validated_path)?;
757 let uri = self
758 .document_tracker
759 .ensure_open(&validated_path, &client)
760 .await?;
761 let lsp_position = mcp_to_lsp_position(line, character);
762
763 let context = trigger.map(|trigger_char| lsp_types::CompletionContext {
764 trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
765 trigger_character: Some(trigger_char),
766 });
767
768 let params = CompletionParams {
769 text_document_position: TextDocumentPositionParams {
770 text_document: TextDocumentIdentifier { uri },
771 position: lsp_position,
772 },
773 work_done_progress_params: WorkDoneProgressParams::default(),
774 partial_result_params: PartialResultParams::default(),
775 context,
776 };
777
778 let timeout_duration = Duration::from_secs(10);
779 let response: Option<lsp_types::CompletionResponse> = client
780 .request("textDocument/completion", params, timeout_duration)
781 .await?;
782
783 let items = match response {
784 Some(lsp_types::CompletionResponse::Array(items)) => items,
785 Some(lsp_types::CompletionResponse::List(list)) => list.items,
786 None => vec![],
787 };
788
789 let result = CompletionsResult {
790 items: items
791 .into_iter()
792 .map(|item| Completion {
793 label: item.label,
794 kind: item.kind.map(|k| format!("{k:?}")),
795 detail: item.detail,
796 documentation: item.documentation.map(|doc| match doc {
797 lsp_types::Documentation::String(s) => s,
798 lsp_types::Documentation::MarkupContent(m) => m.value,
799 }),
800 })
801 .collect(),
802 };
803
804 Ok(result)
805 }
806
807 pub async fn handle_document_symbols(
813 &mut self,
814 file_path: String,
815 ) -> Result<DocumentSymbolsResult> {
816 let path = PathBuf::from(&file_path);
817 let validated_path = self.validate_path(&path)?;
818 let client = self.get_client_for_file(&validated_path)?;
819 let uri = self
820 .document_tracker
821 .ensure_open(&validated_path, &client)
822 .await?;
823
824 let params = DocumentSymbolParams {
825 text_document: TextDocumentIdentifier { uri },
826 work_done_progress_params: WorkDoneProgressParams::default(),
827 partial_result_params: PartialResultParams::default(),
828 };
829
830 let timeout_duration = Duration::from_secs(30);
831 let response: Option<lsp_types::DocumentSymbolResponse> = client
832 .request("textDocument/documentSymbol", params, timeout_duration)
833 .await?;
834
835 let symbols = match response {
836 Some(lsp_types::DocumentSymbolResponse::Flat(symbols)) => symbols
837 .into_iter()
838 .map(|sym| Symbol {
839 name: sym.name,
840 kind: format!("{:?}", sym.kind),
841 range: normalize_range(sym.location.range),
842 selection_range: normalize_range(sym.location.range),
843 children: None,
844 })
845 .collect(),
846 Some(lsp_types::DocumentSymbolResponse::Nested(symbols)) => {
847 symbols.into_iter().map(convert_document_symbol).collect()
848 }
849 None => vec![],
850 };
851
852 Ok(DocumentSymbolsResult { symbols })
853 }
854
855 pub async fn handle_format_document(
861 &mut self,
862 file_path: String,
863 tab_size: u32,
864 insert_spaces: bool,
865 ) -> Result<FormatDocumentResult> {
866 let path = PathBuf::from(&file_path);
867 let validated_path = self.validate_path(&path)?;
868 let client = self.get_client_for_file(&validated_path)?;
869 let uri = self
870 .document_tracker
871 .ensure_open(&validated_path, &client)
872 .await?;
873
874 let params = DocumentFormattingParams {
875 text_document: TextDocumentIdentifier { uri },
876 options: FormattingOptions {
877 tab_size,
878 insert_spaces,
879 ..Default::default()
880 },
881 work_done_progress_params: WorkDoneProgressParams::default(),
882 };
883
884 let timeout_duration = Duration::from_secs(30);
885 let response: Option<Vec<lsp_types::TextEdit>> = client
886 .request("textDocument/formatting", params, timeout_duration)
887 .await?;
888
889 let edits = response.unwrap_or_default();
890
891 let result = FormatDocumentResult {
892 edits: edits
893 .into_iter()
894 .map(|edit| TextEdit {
895 range: normalize_range(edit.range),
896 new_text: edit.new_text,
897 })
898 .collect(),
899 };
900
901 Ok(result)
902 }
903
904 pub async fn handle_workspace_symbol(
910 &mut self,
911 query: String,
912 kind_filter: Option<String>,
913 limit: u32,
914 ) -> Result<WorkspaceSymbolResult> {
915 const MAX_QUERY_LENGTH: usize = 1000;
916 const VALID_SYMBOL_KINDS: &[&str] = &[
917 "File",
918 "Module",
919 "Namespace",
920 "Package",
921 "Class",
922 "Method",
923 "Property",
924 "Field",
925 "Constructor",
926 "Enum",
927 "Interface",
928 "Function",
929 "Variable",
930 "Constant",
931 "String",
932 "Number",
933 "Boolean",
934 "Array",
935 "Object",
936 "Key",
937 "Null",
938 "EnumMember",
939 "Struct",
940 "Event",
941 "Operator",
942 "TypeParameter",
943 ];
944
945 if query.len() > MAX_QUERY_LENGTH {
947 return Err(Error::InvalidToolParams(format!(
948 "Query too long: {} chars (max {MAX_QUERY_LENGTH})",
949 query.len()
950 )));
951 }
952
953 if let Some(ref kind) = kind_filter {
955 if !VALID_SYMBOL_KINDS
956 .iter()
957 .any(|k| k.eq_ignore_ascii_case(kind))
958 {
959 return Err(Error::InvalidToolParams(format!(
960 "Invalid kind_filter: '{kind}'. Valid values: {VALID_SYMBOL_KINDS:?}"
961 )));
962 }
963 }
964
965 let client = self
967 .lsp_clients
968 .values()
969 .next()
970 .cloned()
971 .ok_or(Error::NoServerConfigured)?;
972
973 let params = LspWorkspaceSymbolParams {
974 query,
975 work_done_progress_params: WorkDoneProgressParams::default(),
976 partial_result_params: PartialResultParams::default(),
977 };
978
979 let timeout_duration = Duration::from_secs(30);
980 let response: Option<Vec<lsp_types::SymbolInformation>> = client
981 .request("workspace/symbol", params, timeout_duration)
982 .await?;
983
984 let mut symbols: Vec<WorkspaceSymbol> = response
985 .unwrap_or_default()
986 .into_iter()
987 .map(|sym| WorkspaceSymbol {
988 name: sym.name,
989 kind: format!("{:?}", sym.kind),
990 location: Location {
991 uri: sym.location.uri.to_string(),
992 range: normalize_range(sym.location.range),
993 },
994 container_name: sym.container_name,
995 })
996 .collect();
997
998 if let Some(kind) = kind_filter {
1000 symbols.retain(|s| s.kind.eq_ignore_ascii_case(&kind));
1001 }
1002
1003 symbols.truncate(limit as usize);
1005
1006 Ok(WorkspaceSymbolResult { symbols })
1007 }
1008
1009 pub async fn handle_code_actions(
1015 &mut self,
1016 file_path: String,
1017 start_line: u32,
1018 start_character: u32,
1019 end_line: u32,
1020 end_character: u32,
1021 kind_filter: Option<String>,
1022 ) -> Result<CodeActionsResult> {
1023 const VALID_ACTION_KINDS: &[&str] = &[
1024 "quickfix",
1025 "refactor",
1026 "refactor.extract",
1027 "refactor.inline",
1028 "refactor.rewrite",
1029 "source",
1030 "source.organizeImports",
1031 ];
1032
1033 if let Some(ref kind) = kind_filter {
1035 if !VALID_ACTION_KINDS
1036 .iter()
1037 .any(|k| k.eq_ignore_ascii_case(kind))
1038 {
1039 return Err(Error::InvalidToolParams(format!(
1040 "Invalid kind_filter: '{kind}'. Valid values: {VALID_ACTION_KINDS:?}"
1041 )));
1042 }
1043 }
1044
1045 if start_line < 1 || start_character < 1 || end_line < 1 || end_character < 1 {
1047 return Err(Error::InvalidToolParams(
1048 "Line and character positions must be >= 1".to_string(),
1049 ));
1050 }
1051
1052 if start_line > MAX_POSITION_VALUE
1054 || start_character > MAX_POSITION_VALUE
1055 || end_line > MAX_POSITION_VALUE
1056 || end_character > MAX_POSITION_VALUE
1057 {
1058 return Err(Error::InvalidToolParams(format!(
1059 "Position values must be <= {MAX_POSITION_VALUE}"
1060 )));
1061 }
1062
1063 if end_line.saturating_sub(start_line) > MAX_RANGE_LINES {
1065 return Err(Error::InvalidToolParams(format!(
1066 "Range size must be <= {MAX_RANGE_LINES} lines"
1067 )));
1068 }
1069
1070 if start_line > end_line || (start_line == end_line && start_character > end_character) {
1071 return Err(Error::InvalidToolParams(
1072 "Start position must be before or equal to end position".to_string(),
1073 ));
1074 }
1075
1076 let path = PathBuf::from(&file_path);
1077 let validated_path = self.validate_path(&path)?;
1078 let client = self.get_client_for_file(&validated_path)?;
1079 let uri = self
1080 .document_tracker
1081 .ensure_open(&validated_path, &client)
1082 .await?;
1083
1084 let range = lsp_types::Range {
1085 start: mcp_to_lsp_position(start_line, start_character),
1086 end: mcp_to_lsp_position(end_line, end_character),
1087 };
1088
1089 let only = kind_filter.map(|k| vec![lsp_types::CodeActionKind::from(k)]);
1091
1092 let params = lsp_types::CodeActionParams {
1093 text_document: TextDocumentIdentifier { uri },
1094 range,
1095 context: lsp_types::CodeActionContext {
1096 diagnostics: vec![],
1097 only,
1098 trigger_kind: Some(lsp_types::CodeActionTriggerKind::INVOKED),
1099 },
1100 work_done_progress_params: WorkDoneProgressParams::default(),
1101 partial_result_params: PartialResultParams::default(),
1102 };
1103
1104 let timeout_duration = Duration::from_secs(30);
1105 let response: Option<lsp_types::CodeActionResponse> = client
1106 .request("textDocument/codeAction", params, timeout_duration)
1107 .await?;
1108
1109 let response_vec = response.unwrap_or_default();
1110 let mut actions = Vec::with_capacity(response_vec.len());
1111
1112 for action_or_command in response_vec {
1113 let action = match action_or_command {
1114 lsp_types::CodeActionOrCommand::CodeAction(action) => convert_code_action(action),
1115 lsp_types::CodeActionOrCommand::Command(cmd) => {
1116 let arguments = cmd.arguments.unwrap_or_else(Vec::new);
1117 CodeAction {
1118 title: cmd.title.clone(),
1119 kind: None,
1120 diagnostics: Vec::new(),
1121 edit: None,
1122 command: Some(CommandDescription {
1123 title: cmd.title,
1124 command: cmd.command,
1125 arguments,
1126 }),
1127 is_preferred: false,
1128 }
1129 }
1130 };
1131 actions.push(action);
1132 }
1133
1134 Ok(CodeActionsResult { actions })
1135 }
1136
1137 pub async fn handle_call_hierarchy_prepare(
1143 &mut self,
1144 file_path: String,
1145 line: u32,
1146 character: u32,
1147 ) -> Result<CallHierarchyPrepareResult> {
1148 if line < 1 || character < 1 {
1150 return Err(Error::InvalidToolParams(
1151 "Line and character positions must be >= 1".to_string(),
1152 ));
1153 }
1154
1155 if line > MAX_POSITION_VALUE || character > MAX_POSITION_VALUE {
1156 return Err(Error::InvalidToolParams(format!(
1157 "Position values must be <= {MAX_POSITION_VALUE}"
1158 )));
1159 }
1160
1161 let path = PathBuf::from(&file_path);
1162 let validated_path = self.validate_path(&path)?;
1163 let client = self.get_client_for_file(&validated_path)?;
1164 let uri = self
1165 .document_tracker
1166 .ensure_open(&validated_path, &client)
1167 .await?;
1168 let lsp_position = mcp_to_lsp_position(line, character);
1169
1170 let params = LspCallHierarchyPrepareParams {
1171 text_document_position_params: TextDocumentPositionParams {
1172 text_document: TextDocumentIdentifier { uri },
1173 position: lsp_position,
1174 },
1175 work_done_progress_params: WorkDoneProgressParams::default(),
1176 };
1177
1178 let timeout_duration = Duration::from_secs(30);
1179 let response: Option<Vec<CallHierarchyItem>> = client
1180 .request(
1181 "textDocument/prepareCallHierarchy",
1182 params,
1183 timeout_duration,
1184 )
1185 .await?;
1186
1187 let lsp_items = response.unwrap_or_default();
1189 let mut items = Vec::with_capacity(lsp_items.len());
1190 for item in lsp_items {
1191 items.push(convert_call_hierarchy_item(item));
1192 }
1193
1194 Ok(CallHierarchyPrepareResult { items })
1195 }
1196
1197 pub async fn handle_incoming_calls(
1203 &mut self,
1204 item: serde_json::Value,
1205 ) -> Result<IncomingCallsResult> {
1206 let lsp_item: CallHierarchyItem = serde_json::from_value(item)
1207 .map_err(|e| Error::InvalidToolParams(format!("Invalid call hierarchy item: {e}")))?;
1208
1209 let path = self.parse_file_uri(&lsp_item.uri)?;
1211 let client = self.get_client_for_file(&path)?;
1212
1213 let params = CallHierarchyIncomingCallsParams {
1214 item: lsp_item,
1215 work_done_progress_params: WorkDoneProgressParams::default(),
1216 partial_result_params: PartialResultParams::default(),
1217 };
1218
1219 let timeout_duration = Duration::from_secs(30);
1220 let response: Option<Vec<CallHierarchyIncomingCall>> = client
1221 .request("callHierarchy/incomingCalls", params, timeout_duration)
1222 .await?;
1223
1224 let lsp_calls = response.unwrap_or_default();
1226 let mut calls = Vec::with_capacity(lsp_calls.len());
1227
1228 for call in lsp_calls {
1229 let from_ranges = {
1230 let mut ranges = Vec::with_capacity(call.from_ranges.len());
1231 for range in call.from_ranges {
1232 ranges.push(normalize_range(range));
1233 }
1234 ranges
1235 };
1236
1237 calls.push(IncomingCall {
1238 from: convert_call_hierarchy_item(call.from),
1239 from_ranges,
1240 });
1241 }
1242
1243 Ok(IncomingCallsResult { calls })
1244 }
1245
1246 pub async fn handle_outgoing_calls(
1252 &mut self,
1253 item: serde_json::Value,
1254 ) -> Result<OutgoingCallsResult> {
1255 let lsp_item: CallHierarchyItem = serde_json::from_value(item)
1256 .map_err(|e| Error::InvalidToolParams(format!("Invalid call hierarchy item: {e}")))?;
1257
1258 let path = self.parse_file_uri(&lsp_item.uri)?;
1260 let client = self.get_client_for_file(&path)?;
1261
1262 let params = CallHierarchyOutgoingCallsParams {
1263 item: lsp_item,
1264 work_done_progress_params: WorkDoneProgressParams::default(),
1265 partial_result_params: PartialResultParams::default(),
1266 };
1267
1268 let timeout_duration = Duration::from_secs(30);
1269 let response: Option<Vec<CallHierarchyOutgoingCall>> = client
1270 .request("callHierarchy/outgoingCalls", params, timeout_duration)
1271 .await?;
1272
1273 let lsp_calls = response.unwrap_or_default();
1275 let mut calls = Vec::with_capacity(lsp_calls.len());
1276
1277 for call in lsp_calls {
1278 let from_ranges = {
1279 let mut ranges = Vec::with_capacity(call.from_ranges.len());
1280 for range in call.from_ranges {
1281 ranges.push(normalize_range(range));
1282 }
1283 ranges
1284 };
1285
1286 calls.push(OutgoingCall {
1287 to: convert_call_hierarchy_item(call.to),
1288 from_ranges,
1289 });
1290 }
1291
1292 Ok(OutgoingCallsResult { calls })
1293 }
1294}
1295
1296fn extract_hover_contents(contents: HoverContents) -> String {
1298 match contents {
1299 HoverContents::Scalar(marked_string) => marked_string_to_string(marked_string),
1300 HoverContents::Array(marked_strings) => marked_strings
1301 .into_iter()
1302 .map(marked_string_to_string)
1303 .collect::<Vec<_>>()
1304 .join("\n\n"),
1305 HoverContents::Markup(markup) => markup.value,
1306 }
1307}
1308
1309fn marked_string_to_string(marked: MarkedString) -> String {
1311 match marked {
1312 MarkedString::String(s) => s,
1313 MarkedString::LanguageString(ls) => format!("```{}\n{}\n```", ls.language, ls.value),
1314 }
1315}
1316
1317const fn normalize_range(range: lsp_types::Range) -> Range {
1319 Range {
1320 start: Position2D {
1321 line: range.start.line + 1,
1322 character: range.start.character + 1,
1323 },
1324 end: Position2D {
1325 line: range.end.line + 1,
1326 character: range.end.character + 1,
1327 },
1328 }
1329}
1330
1331fn convert_document_symbol(symbol: DocumentSymbol) -> Symbol {
1333 Symbol {
1334 name: symbol.name,
1335 kind: format!("{:?}", symbol.kind),
1336 range: normalize_range(symbol.range),
1337 selection_range: normalize_range(symbol.selection_range),
1338 children: symbol
1339 .children
1340 .map(|children| children.into_iter().map(convert_document_symbol).collect()),
1341 }
1342}
1343
1344fn convert_call_hierarchy_item(item: CallHierarchyItem) -> CallHierarchyItemResult {
1346 CallHierarchyItemResult {
1347 name: item.name,
1348 kind: format!("{:?}", item.kind),
1349 detail: item.detail,
1350 uri: item.uri.to_string(),
1351 range: normalize_range(item.range),
1352 selection_range: normalize_range(item.selection_range),
1353 data: item.data,
1354 }
1355}
1356
1357fn convert_code_action(action: lsp_types::CodeAction) -> CodeAction {
1359 let diagnostics = action.diagnostics.map_or_else(Vec::new, |diags| {
1360 let mut result = Vec::with_capacity(diags.len());
1361 for d in diags {
1362 result.push(Diagnostic {
1363 range: normalize_range(d.range),
1364 severity: match d.severity {
1365 Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
1366 Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
1367 Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
1368 DiagnosticSeverity::Information
1369 }
1370 Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
1371 _ => DiagnosticSeverity::Information,
1372 },
1373 message: d.message,
1374 code: d.code.map(|c| match c {
1375 lsp_types::NumberOrString::Number(n) => n.to_string(),
1376 lsp_types::NumberOrString::String(s) => s,
1377 }),
1378 });
1379 }
1380 result
1381 });
1382
1383 let edit = action.edit.map(|edit| {
1384 let changes = edit.changes.map_or_else(Vec::new, |changes_map| {
1385 let mut result = Vec::with_capacity(changes_map.len());
1386 for (uri, edits) in changes_map {
1387 let mut text_edits = Vec::with_capacity(edits.len());
1388 for e in edits {
1389 text_edits.push(TextEdit {
1390 range: normalize_range(e.range),
1391 new_text: e.new_text,
1392 });
1393 }
1394 result.push(DocumentChanges {
1395 uri: uri.to_string(),
1396 edits: text_edits,
1397 });
1398 }
1399 result
1400 });
1401 WorkspaceEditDescription { changes }
1402 });
1403
1404 let command = action.command.map(|cmd| {
1405 let arguments = cmd.arguments.unwrap_or_else(Vec::new);
1406 CommandDescription {
1407 title: cmd.title,
1408 command: cmd.command,
1409 arguments,
1410 }
1411 });
1412
1413 CodeAction {
1414 title: action.title,
1415 kind: action.kind.map(|k| k.as_str().to_string()),
1416 diagnostics,
1417 edit,
1418 command,
1419 is_preferred: action.is_preferred.unwrap_or(false),
1420 }
1421}
1422
1423#[cfg(test)]
1424#[allow(clippy::unwrap_used)]
1425mod tests {
1426 use std::fs;
1427
1428 use tempfile::TempDir;
1429 use url::Url;
1430
1431 use super::*;
1432
1433 #[test]
1434 fn test_translator_new() {
1435 let translator = Translator::new();
1436 assert_eq!(translator.workspace_roots.len(), 0);
1437 assert_eq!(translator.lsp_clients.len(), 0);
1438 assert_eq!(translator.lsp_servers.len(), 0);
1439 }
1440
1441 #[test]
1442 fn test_set_workspace_roots() {
1443 let mut translator = Translator::new();
1444 let roots = vec![PathBuf::from("/test/root1"), PathBuf::from("/test/root2")];
1445 translator.set_workspace_roots(roots.clone());
1446 assert_eq!(translator.workspace_roots, roots);
1447 }
1448
1449 #[test]
1450 fn test_register_server() {
1451 let translator = Translator::new();
1452
1453 assert_eq!(translator.lsp_servers.len(), 0);
1455
1456 }
1465
1466 #[test]
1467 fn test_validate_path_no_workspace_roots() {
1468 let translator = Translator::new();
1469 let temp_dir = TempDir::new().unwrap();
1470 let test_file = temp_dir.path().join("test.rs");
1471 fs::write(&test_file, "fn main() {}").unwrap();
1472
1473 let result = translator.validate_path(&test_file);
1475 assert!(result.is_ok());
1476 }
1477
1478 #[test]
1479 fn test_validate_path_within_workspace() {
1480 let mut translator = Translator::new();
1481 let temp_dir = TempDir::new().unwrap();
1482 let workspace_root = temp_dir.path().to_path_buf();
1483 translator.set_workspace_roots(vec![workspace_root]);
1484
1485 let test_file = temp_dir.path().join("test.rs");
1486 fs::write(&test_file, "fn main() {}").unwrap();
1487
1488 let result = translator.validate_path(&test_file);
1489 assert!(result.is_ok());
1490 }
1491
1492 #[test]
1493 fn test_validate_path_outside_workspace() {
1494 let mut translator = Translator::new();
1495 let temp_dir1 = TempDir::new().unwrap();
1496 let temp_dir2 = TempDir::new().unwrap();
1497
1498 translator.set_workspace_roots(vec![temp_dir1.path().to_path_buf()]);
1500
1501 let test_file = temp_dir2.path().join("test.rs");
1503 fs::write(&test_file, "fn main() {}").unwrap();
1504
1505 let result = translator.validate_path(&test_file);
1506 assert!(matches!(result, Err(Error::PathOutsideWorkspace(_))));
1507 }
1508
1509 #[test]
1510 fn test_normalize_range() {
1511 let lsp_range = lsp_types::Range {
1512 start: lsp_types::Position {
1513 line: 0,
1514 character: 0,
1515 },
1516 end: lsp_types::Position {
1517 line: 2,
1518 character: 5,
1519 },
1520 };
1521
1522 let mcp_range = normalize_range(lsp_range);
1523 assert_eq!(mcp_range.start.line, 1);
1524 assert_eq!(mcp_range.start.character, 1);
1525 assert_eq!(mcp_range.end.line, 3);
1526 assert_eq!(mcp_range.end.character, 6);
1527 }
1528
1529 #[test]
1530 fn test_extract_hover_contents_string() {
1531 let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
1532 let contents = lsp_types::HoverContents::Scalar(marked_string);
1533 let result = extract_hover_contents(contents);
1534 assert_eq!(result, "Test hover");
1535 }
1536
1537 #[test]
1538 fn test_extract_hover_contents_language_string() {
1539 let marked_string = lsp_types::MarkedString::LanguageString(lsp_types::LanguageString {
1540 language: "rust".to_string(),
1541 value: "fn main() {}".to_string(),
1542 });
1543 let contents = lsp_types::HoverContents::Scalar(marked_string);
1544 let result = extract_hover_contents(contents);
1545 assert_eq!(result, "```rust\nfn main() {}\n```");
1546 }
1547
1548 #[test]
1549 fn test_extract_hover_contents_markup() {
1550 let markup = lsp_types::MarkupContent {
1551 kind: lsp_types::MarkupKind::Markdown,
1552 value: "# Documentation".to_string(),
1553 };
1554 let contents = lsp_types::HoverContents::Markup(markup);
1555 let result = extract_hover_contents(contents);
1556 assert_eq!(result, "# Documentation");
1557 }
1558
1559 #[tokio::test]
1560 async fn test_handle_workspace_symbol_no_server() {
1561 let mut translator = Translator::new();
1562 let result = translator
1563 .handle_workspace_symbol("test".to_string(), None, 100)
1564 .await;
1565 assert!(matches!(result, Err(Error::NoServerConfigured)));
1566 }
1567
1568 #[tokio::test]
1569 async fn test_handle_code_actions_invalid_kind() {
1570 let mut translator = Translator::new();
1571 let result = translator
1572 .handle_code_actions(
1573 "/tmp/test.rs".to_string(),
1574 1,
1575 1,
1576 1,
1577 10,
1578 Some("invalid_kind".to_string()),
1579 )
1580 .await;
1581 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1582 }
1583
1584 #[tokio::test]
1585 async fn test_handle_code_actions_valid_kind_quickfix() {
1586 use tempfile::TempDir;
1587
1588 let mut translator = Translator::new();
1589 let temp_dir = TempDir::new().unwrap();
1590 let test_file = temp_dir.path().join("test.rs");
1591 fs::write(&test_file, "fn main() {}").unwrap();
1592
1593 let result = translator
1594 .handle_code_actions(
1595 test_file.to_str().unwrap().to_string(),
1596 1,
1597 1,
1598 1,
1599 10,
1600 Some("quickfix".to_string()),
1601 )
1602 .await;
1603 assert!(result.is_err());
1605 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1606 }
1607
1608 #[tokio::test]
1609 async fn test_handle_code_actions_valid_kind_refactor() {
1610 use tempfile::TempDir;
1611
1612 let mut translator = Translator::new();
1613 let temp_dir = TempDir::new().unwrap();
1614 let test_file = temp_dir.path().join("test.rs");
1615 fs::write(&test_file, "fn main() {}").unwrap();
1616
1617 let result = translator
1618 .handle_code_actions(
1619 test_file.to_str().unwrap().to_string(),
1620 1,
1621 1,
1622 1,
1623 10,
1624 Some("refactor".to_string()),
1625 )
1626 .await;
1627 assert!(result.is_err());
1628 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1629 }
1630
1631 #[tokio::test]
1632 async fn test_handle_code_actions_valid_kind_refactor_extract() {
1633 use tempfile::TempDir;
1634
1635 let mut translator = Translator::new();
1636 let temp_dir = TempDir::new().unwrap();
1637 let test_file = temp_dir.path().join("test.rs");
1638 fs::write(&test_file, "fn main() {}").unwrap();
1639
1640 let result = translator
1641 .handle_code_actions(
1642 test_file.to_str().unwrap().to_string(),
1643 1,
1644 1,
1645 1,
1646 10,
1647 Some("refactor.extract".to_string()),
1648 )
1649 .await;
1650 assert!(result.is_err());
1651 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1652 }
1653
1654 #[tokio::test]
1655 async fn test_handle_code_actions_valid_kind_source() {
1656 use tempfile::TempDir;
1657
1658 let mut translator = Translator::new();
1659 let temp_dir = TempDir::new().unwrap();
1660 let test_file = temp_dir.path().join("test.rs");
1661 fs::write(&test_file, "fn main() {}").unwrap();
1662
1663 let result = translator
1664 .handle_code_actions(
1665 test_file.to_str().unwrap().to_string(),
1666 1,
1667 1,
1668 1,
1669 10,
1670 Some("source.organizeImports".to_string()),
1671 )
1672 .await;
1673 assert!(result.is_err());
1674 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1675 }
1676
1677 #[tokio::test]
1678 async fn test_handle_code_actions_invalid_range_zero() {
1679 let mut translator = Translator::new();
1680 let result = translator
1681 .handle_code_actions("/tmp/test.rs".to_string(), 0, 1, 1, 10, None)
1682 .await;
1683 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1684 }
1685
1686 #[tokio::test]
1687 async fn test_handle_code_actions_invalid_range_order() {
1688 let mut translator = Translator::new();
1689 let result = translator
1690 .handle_code_actions("/tmp/test.rs".to_string(), 10, 5, 5, 1, None)
1691 .await;
1692 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1693 }
1694
1695 #[tokio::test]
1696 async fn test_handle_code_actions_empty_range() {
1697 use tempfile::TempDir;
1698
1699 let mut translator = Translator::new();
1700 let temp_dir = TempDir::new().unwrap();
1701 let test_file = temp_dir.path().join("test.rs");
1702 fs::write(&test_file, "fn main() {}").unwrap();
1703
1704 let result = translator
1706 .handle_code_actions(test_file.to_str().unwrap().to_string(), 1, 5, 1, 5, None)
1707 .await;
1708 assert!(result.is_err());
1710 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1711 }
1712
1713 #[test]
1714 fn test_convert_code_action_minimal() {
1715 let lsp_action = lsp_types::CodeAction {
1716 title: "Fix issue".to_string(),
1717 kind: None,
1718 diagnostics: None,
1719 edit: None,
1720 command: None,
1721 is_preferred: None,
1722 disabled: None,
1723 data: None,
1724 };
1725
1726 let result = convert_code_action(lsp_action);
1727 assert_eq!(result.title, "Fix issue");
1728 assert!(result.kind.is_none());
1729 assert!(result.diagnostics.is_empty());
1730 assert!(result.edit.is_none());
1731 assert!(result.command.is_none());
1732 assert!(!result.is_preferred);
1733 }
1734
1735 #[test]
1736 #[allow(clippy::too_many_lines)]
1737 fn test_convert_code_action_with_diagnostics_all_severities() {
1738 let lsp_diagnostics = vec![
1739 lsp_types::Diagnostic {
1740 range: lsp_types::Range {
1741 start: lsp_types::Position {
1742 line: 0,
1743 character: 0,
1744 },
1745 end: lsp_types::Position {
1746 line: 0,
1747 character: 5,
1748 },
1749 },
1750 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
1751 message: "Error message".to_string(),
1752 code: Some(lsp_types::NumberOrString::Number(1)),
1753 source: None,
1754 code_description: None,
1755 related_information: None,
1756 tags: None,
1757 data: None,
1758 },
1759 lsp_types::Diagnostic {
1760 range: lsp_types::Range {
1761 start: lsp_types::Position {
1762 line: 1,
1763 character: 0,
1764 },
1765 end: lsp_types::Position {
1766 line: 1,
1767 character: 5,
1768 },
1769 },
1770 severity: Some(lsp_types::DiagnosticSeverity::WARNING),
1771 message: "Warning message".to_string(),
1772 code: Some(lsp_types::NumberOrString::String("W001".to_string())),
1773 source: None,
1774 code_description: None,
1775 related_information: None,
1776 tags: None,
1777 data: None,
1778 },
1779 lsp_types::Diagnostic {
1780 range: lsp_types::Range {
1781 start: lsp_types::Position {
1782 line: 2,
1783 character: 0,
1784 },
1785 end: lsp_types::Position {
1786 line: 2,
1787 character: 5,
1788 },
1789 },
1790 severity: Some(lsp_types::DiagnosticSeverity::INFORMATION),
1791 message: "Info message".to_string(),
1792 code: None,
1793 source: None,
1794 code_description: None,
1795 related_information: None,
1796 tags: None,
1797 data: None,
1798 },
1799 lsp_types::Diagnostic {
1800 range: lsp_types::Range {
1801 start: lsp_types::Position {
1802 line: 3,
1803 character: 0,
1804 },
1805 end: lsp_types::Position {
1806 line: 3,
1807 character: 5,
1808 },
1809 },
1810 severity: Some(lsp_types::DiagnosticSeverity::HINT),
1811 message: "Hint message".to_string(),
1812 code: None,
1813 source: None,
1814 code_description: None,
1815 related_information: None,
1816 tags: None,
1817 data: None,
1818 },
1819 ];
1820
1821 let lsp_action = lsp_types::CodeAction {
1822 title: "Fix all issues".to_string(),
1823 kind: Some(lsp_types::CodeActionKind::QUICKFIX),
1824 diagnostics: Some(lsp_diagnostics),
1825 edit: None,
1826 command: None,
1827 is_preferred: None,
1828 disabled: None,
1829 data: None,
1830 };
1831
1832 let result = convert_code_action(lsp_action);
1833 assert_eq!(result.diagnostics.len(), 4);
1834 assert!(matches!(
1835 result.diagnostics[0].severity,
1836 DiagnosticSeverity::Error
1837 ));
1838 assert!(matches!(
1839 result.diagnostics[1].severity,
1840 DiagnosticSeverity::Warning
1841 ));
1842 assert!(matches!(
1843 result.diagnostics[2].severity,
1844 DiagnosticSeverity::Information
1845 ));
1846 assert!(matches!(
1847 result.diagnostics[3].severity,
1848 DiagnosticSeverity::Hint
1849 ));
1850 assert_eq!(result.diagnostics[0].code, Some("1".to_string()));
1851 assert_eq!(result.diagnostics[1].code, Some("W001".to_string()));
1852 }
1853
1854 #[test]
1855 #[allow(clippy::mutable_key_type)]
1856 fn test_convert_code_action_with_workspace_edit() {
1857 use std::collections::HashMap;
1858 use std::str::FromStr;
1859
1860 let uri = lsp_types::Uri::from_str("file:///test.rs").unwrap();
1861 let mut changes_map = HashMap::new();
1862 changes_map.insert(
1863 uri,
1864 vec![lsp_types::TextEdit {
1865 range: lsp_types::Range {
1866 start: lsp_types::Position {
1867 line: 0,
1868 character: 0,
1869 },
1870 end: lsp_types::Position {
1871 line: 0,
1872 character: 5,
1873 },
1874 },
1875 new_text: "fixed".to_string(),
1876 }],
1877 );
1878
1879 let lsp_action = lsp_types::CodeAction {
1880 title: "Apply fix".to_string(),
1881 kind: Some(lsp_types::CodeActionKind::QUICKFIX),
1882 diagnostics: None,
1883 edit: Some(lsp_types::WorkspaceEdit {
1884 changes: Some(changes_map),
1885 document_changes: None,
1886 change_annotations: None,
1887 }),
1888 command: None,
1889 is_preferred: Some(true),
1890 disabled: None,
1891 data: None,
1892 };
1893
1894 let result = convert_code_action(lsp_action);
1895 assert!(result.edit.is_some());
1896 let edit = result.edit.unwrap();
1897 assert_eq!(edit.changes.len(), 1);
1898 assert_eq!(edit.changes[0].uri, "file:///test.rs");
1899 assert_eq!(edit.changes[0].edits.len(), 1);
1900 assert_eq!(edit.changes[0].edits[0].new_text, "fixed");
1901 assert!(result.is_preferred);
1902 }
1903
1904 #[test]
1905 fn test_convert_code_action_with_command() {
1906 let lsp_action = lsp_types::CodeAction {
1907 title: "Run command".to_string(),
1908 kind: Some(lsp_types::CodeActionKind::REFACTOR),
1909 diagnostics: None,
1910 edit: None,
1911 command: Some(lsp_types::Command {
1912 title: "Execute refactor".to_string(),
1913 command: "refactor.extract".to_string(),
1914 arguments: Some(vec![serde_json::json!("arg1"), serde_json::json!(42)]),
1915 }),
1916 is_preferred: None,
1917 disabled: None,
1918 data: None,
1919 };
1920
1921 let result = convert_code_action(lsp_action);
1922 assert!(result.command.is_some());
1923 let cmd = result.command.unwrap();
1924 assert_eq!(cmd.title, "Execute refactor");
1925 assert_eq!(cmd.command, "refactor.extract");
1926 assert_eq!(cmd.arguments.len(), 2);
1927 }
1928
1929 #[tokio::test]
1930 async fn test_handle_call_hierarchy_prepare_invalid_position_zero() {
1931 let mut translator = Translator::new();
1932 let result = translator
1933 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 0, 1)
1934 .await;
1935 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1936
1937 let result = translator
1938 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1, 0)
1939 .await;
1940 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1941 }
1942
1943 #[tokio::test]
1944 async fn test_handle_call_hierarchy_prepare_invalid_position_too_large() {
1945 let mut translator = Translator::new();
1946 let result = translator
1947 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1_000_001, 1)
1948 .await;
1949 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1950
1951 let result = translator
1952 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1, 1_000_001)
1953 .await;
1954 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1955 }
1956
1957 #[tokio::test]
1958 async fn test_handle_incoming_calls_invalid_json() {
1959 let mut translator = Translator::new();
1960 let invalid_item = serde_json::json!({"invalid": "structure"});
1961 let result = translator.handle_incoming_calls(invalid_item).await;
1962 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1963 }
1964
1965 #[tokio::test]
1966 async fn test_handle_outgoing_calls_invalid_json() {
1967 let mut translator = Translator::new();
1968 let invalid_item = serde_json::json!({"invalid": "structure"});
1969 let result = translator.handle_outgoing_calls(invalid_item).await;
1970 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1971 }
1972
1973 #[tokio::test]
1974 async fn test_parse_file_uri_invalid_scheme() {
1975 let translator = Translator::new();
1976 let uri: lsp_types::Uri = "http://example.com/file.rs".parse().unwrap();
1977 let result = translator.parse_file_uri(&uri);
1978 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1979 }
1980
1981 #[tokio::test]
1982 async fn test_parse_file_uri_valid_scheme() {
1983 let translator = Translator::new();
1984 let temp_dir = TempDir::new().unwrap();
1985 let test_file = temp_dir.path().join("test.rs");
1986 fs::write(&test_file, "fn main() {}").unwrap();
1987
1988 let file_url = Url::from_file_path(&test_file).unwrap();
1990 let uri: lsp_types::Uri = file_url.as_str().parse().unwrap();
1991 let result = translator.parse_file_uri(&uri);
1992 assert!(result.is_ok());
1993 }
1994}