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;
18use url::Url;
19
20use super::state::detect_language;
21use super::{DocumentTracker, NotificationCache};
22use crate::bridge::encoding::mcp_to_lsp_position;
23use crate::error::{Error, Result};
24use crate::lsp::{LspClient, LspServer};
25
26#[derive(Debug)]
28pub struct Translator {
29 lsp_clients: HashMap<String, LspClient>,
31 lsp_servers: HashMap<String, LspServer>,
33 document_tracker: DocumentTracker,
35 notification_cache: NotificationCache,
37 workspace_roots: Vec<PathBuf>,
39}
40
41impl Translator {
42 #[must_use]
44 pub fn new() -> Self {
45 Self {
46 lsp_clients: HashMap::new(),
47 lsp_servers: HashMap::new(),
48 document_tracker: DocumentTracker::new(),
49 notification_cache: NotificationCache::new(),
50 workspace_roots: vec![],
51 }
52 }
53
54 pub fn set_workspace_roots(&mut self, roots: Vec<PathBuf>) {
56 self.workspace_roots = roots;
57 }
58
59 pub fn register_client(&mut self, language_id: String, client: LspClient) {
61 self.lsp_clients.insert(language_id, client);
62 }
63
64 pub fn register_server(&mut self, language_id: String, server: LspServer) {
66 self.lsp_servers.insert(language_id, server);
67 }
68
69 #[must_use]
71 pub const fn document_tracker(&self) -> &DocumentTracker {
72 &self.document_tracker
73 }
74
75 pub const fn document_tracker_mut(&mut self) -> &mut DocumentTracker {
77 &mut self.document_tracker
78 }
79
80 #[must_use]
82 pub const fn notification_cache(&self) -> &NotificationCache {
83 &self.notification_cache
84 }
85
86 pub const fn notification_cache_mut(&mut self) -> &mut NotificationCache {
88 &mut self.notification_cache
89 }
90
91 }
96
97impl Default for Translator {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct Position2D {
106 pub line: u32,
108 pub character: u32,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Range {
115 pub start: Position2D,
117 pub end: Position2D,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct Location {
124 pub uri: String,
126 pub range: Range,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct HoverResult {
133 pub contents: String,
135 pub range: Option<Range>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct DefinitionResult {
142 pub locations: Vec<Location>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ReferencesResult {
149 pub locations: Vec<Location>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(rename_all = "lowercase")]
156pub enum DiagnosticSeverity {
157 Error,
159 Warning,
161 Information,
163 Hint,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct Diagnostic {
170 pub range: Range,
172 pub severity: DiagnosticSeverity,
174 pub message: String,
176 pub code: Option<String>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct DiagnosticsResult {
183 pub diagnostics: Vec<Diagnostic>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct TextEdit {
190 pub range: Range,
192 pub new_text: String,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct DocumentChanges {
199 pub uri: String,
201 pub edits: Vec<TextEdit>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct RenameResult {
208 pub changes: Vec<DocumentChanges>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct Completion {
215 pub label: String,
217 pub kind: Option<String>,
219 pub detail: Option<String>,
221 pub documentation: Option<String>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct CompletionsResult {
228 pub items: Vec<Completion>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct Symbol {
235 pub name: String,
237 pub kind: String,
239 pub range: Range,
241 pub selection_range: Range,
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub children: Option<Vec<Self>>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct DocumentSymbolsResult {
251 pub symbols: Vec<Symbol>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct FormatDocumentResult {
258 pub edits: Vec<TextEdit>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct WorkspaceSymbol {
265 pub name: String,
267 pub kind: String,
269 pub location: Location,
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub container_name: Option<String>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct WorkspaceSymbolResult {
279 pub symbols: Vec<WorkspaceSymbol>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct CodeAction {
286 pub title: String,
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub kind: Option<String>,
291 #[serde(skip_serializing_if = "Vec::is_empty", default)]
293 pub diagnostics: Vec<Diagnostic>,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub edit: Option<WorkspaceEditDescription>,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub command: Option<CommandDescription>,
300 #[serde(default)]
302 pub is_preferred: bool,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct WorkspaceEditDescription {
308 pub changes: Vec<DocumentChanges>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct CommandDescription {
315 pub title: String,
317 pub command: String,
319 #[serde(skip_serializing_if = "Vec::is_empty", default)]
321 pub arguments: Vec<serde_json::Value>,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct CodeActionsResult {
327 pub actions: Vec<CodeAction>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct CallHierarchyItemResult {
334 pub name: String,
336 pub kind: String,
338 #[serde(skip_serializing_if = "Option::is_none")]
340 pub detail: Option<String>,
341 pub uri: String,
343 pub range: Range,
345 pub selection_range: Range,
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub data: Option<serde_json::Value>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct CallHierarchyPrepareResult {
355 pub items: Vec<CallHierarchyItemResult>,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct IncomingCall {
362 pub from: CallHierarchyItemResult,
364 pub from_ranges: Vec<Range>,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct IncomingCallsResult {
371 pub calls: Vec<IncomingCall>,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct OutgoingCall {
378 pub to: CallHierarchyItemResult,
380 pub from_ranges: Vec<Range>,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct OutgoingCallsResult {
387 pub calls: Vec<OutgoingCall>,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct ServerLogsResult {
394 pub logs: Vec<crate::bridge::notifications::LogEntry>,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct ServerMessagesResult {
401 pub messages: Vec<crate::bridge::notifications::ServerMessage>,
403}
404
405const MAX_POSITION_VALUE: u32 = 1_000_000;
407const MAX_RANGE_LINES: u32 = 10_000;
409
410impl Translator {
411 fn validate_path(&self, path: &Path) -> Result<PathBuf> {
417 let canonical = path.canonicalize().map_err(|e| Error::FileIo {
418 path: path.to_path_buf(),
419 source: e,
420 })?;
421
422 if self.workspace_roots.is_empty() {
424 return Ok(canonical);
425 }
426
427 for root in &self.workspace_roots {
429 if let Ok(canonical_root) = root.canonicalize() {
430 if canonical.starts_with(&canonical_root) {
431 return Ok(canonical);
432 }
433 }
434 }
435
436 Err(Error::PathOutsideWorkspace(path.to_path_buf()))
437 }
438
439 fn get_client_for_file(&self, path: &Path) -> Result<LspClient> {
441 let language_id = detect_language(path);
442 self.lsp_clients
443 .get(&language_id)
444 .cloned()
445 .ok_or(Error::NoServerForLanguage(language_id))
446 }
447
448 fn parse_file_uri(&self, uri: &lsp_types::Uri) -> Result<PathBuf> {
456 let uri_str = uri.as_str();
457
458 if !uri_str.starts_with("file://") {
460 return Err(Error::InvalidToolParams(format!(
461 "Invalid URI scheme, expected file:// but got: {uri_str}"
462 )));
463 }
464
465 let path_str = &uri_str["file://".len()..];
467
468 #[cfg(windows)]
471 let path_str = if path_str.len() >= 3
472 && path_str.starts_with('/')
473 && path_str.chars().nth(2) == Some(':')
474 {
475 &path_str[1..]
476 } else {
477 path_str
478 };
479
480 let path = PathBuf::from(path_str);
481
482 self.validate_path(&path)
484 }
485
486 pub async fn handle_hover(
492 &mut self,
493 file_path: String,
494 line: u32,
495 character: u32,
496 ) -> Result<HoverResult> {
497 let path = PathBuf::from(&file_path);
498 let validated_path = self.validate_path(&path)?;
499 let client = self.get_client_for_file(&validated_path)?;
500 let uri = self
501 .document_tracker
502 .ensure_open(&validated_path, &client)
503 .await?;
504 let lsp_position = mcp_to_lsp_position(line, character);
505
506 let params = LspHoverParams {
507 text_document_position_params: TextDocumentPositionParams {
508 text_document: TextDocumentIdentifier { uri },
509 position: lsp_position,
510 },
511 work_done_progress_params: WorkDoneProgressParams::default(),
512 };
513
514 let timeout_duration = Duration::from_secs(30);
515 let response: Option<Hover> = client
516 .request("textDocument/hover", params, timeout_duration)
517 .await?;
518
519 let result = match response {
520 Some(hover) => {
521 let contents = extract_hover_contents(hover.contents);
522 let range = hover.range.map(normalize_range);
523 HoverResult { contents, range }
524 }
525 None => HoverResult {
526 contents: "No hover information available".to_string(),
527 range: None,
528 },
529 };
530
531 Ok(result)
532 }
533
534 pub async fn handle_definition(
540 &mut self,
541 file_path: String,
542 line: u32,
543 character: u32,
544 ) -> Result<DefinitionResult> {
545 let path = PathBuf::from(&file_path);
546 let validated_path = self.validate_path(&path)?;
547 let client = self.get_client_for_file(&validated_path)?;
548 let uri = self
549 .document_tracker
550 .ensure_open(&validated_path, &client)
551 .await?;
552 let lsp_position = mcp_to_lsp_position(line, character);
553
554 let params = GotoDefinitionParams {
555 text_document_position_params: TextDocumentPositionParams {
556 text_document: TextDocumentIdentifier { uri },
557 position: lsp_position,
558 },
559 work_done_progress_params: WorkDoneProgressParams::default(),
560 partial_result_params: PartialResultParams::default(),
561 };
562
563 let timeout_duration = Duration::from_secs(30);
564 let response: Option<lsp_types::GotoDefinitionResponse> = client
565 .request("textDocument/definition", params, timeout_duration)
566 .await?;
567
568 let locations = match response {
569 Some(lsp_types::GotoDefinitionResponse::Scalar(loc)) => vec![loc],
570 Some(lsp_types::GotoDefinitionResponse::Array(locs)) => locs,
571 Some(lsp_types::GotoDefinitionResponse::Link(links)) => links
572 .into_iter()
573 .map(|link| lsp_types::Location {
574 uri: link.target_uri,
575 range: link.target_selection_range,
576 })
577 .collect(),
578 None => vec![],
579 };
580
581 let result = DefinitionResult {
582 locations: locations
583 .into_iter()
584 .map(|loc| Location {
585 uri: loc.uri.to_string(),
586 range: normalize_range(loc.range),
587 })
588 .collect(),
589 };
590
591 Ok(result)
592 }
593
594 pub async fn handle_references(
600 &mut self,
601 file_path: String,
602 line: u32,
603 character: u32,
604 include_declaration: bool,
605 ) -> Result<ReferencesResult> {
606 let path = PathBuf::from(&file_path);
607 let validated_path = self.validate_path(&path)?;
608 let client = self.get_client_for_file(&validated_path)?;
609 let uri = self
610 .document_tracker
611 .ensure_open(&validated_path, &client)
612 .await?;
613 let lsp_position = mcp_to_lsp_position(line, character);
614
615 let params = ReferenceParams {
616 text_document_position: TextDocumentPositionParams {
617 text_document: TextDocumentIdentifier { uri },
618 position: lsp_position,
619 },
620 work_done_progress_params: WorkDoneProgressParams::default(),
621 partial_result_params: PartialResultParams::default(),
622 context: ReferenceContext {
623 include_declaration,
624 },
625 };
626
627 let timeout_duration = Duration::from_secs(30);
628 let response: Option<Vec<lsp_types::Location>> = client
629 .request("textDocument/references", params, timeout_duration)
630 .await?;
631
632 let locations = response.unwrap_or_default();
633
634 let result = ReferencesResult {
635 locations: locations
636 .into_iter()
637 .map(|loc| Location {
638 uri: loc.uri.to_string(),
639 range: normalize_range(loc.range),
640 })
641 .collect(),
642 };
643
644 Ok(result)
645 }
646
647 pub async fn handle_diagnostics(&mut self, file_path: String) -> Result<DiagnosticsResult> {
653 let path = PathBuf::from(&file_path);
654 let validated_path = self.validate_path(&path)?;
655 let client = self.get_client_for_file(&validated_path)?;
656 let uri = self
657 .document_tracker
658 .ensure_open(&validated_path, &client)
659 .await?;
660
661 let params = lsp_types::DocumentDiagnosticParams {
662 text_document: TextDocumentIdentifier { uri },
663 identifier: None,
664 previous_result_id: None,
665 work_done_progress_params: WorkDoneProgressParams::default(),
666 partial_result_params: PartialResultParams::default(),
667 };
668
669 let timeout_duration = Duration::from_secs(30);
670 let response: lsp_types::DocumentDiagnosticReportResult = client
671 .request("textDocument/diagnostic", params, timeout_duration)
672 .await?;
673
674 let diagnostics = match response {
675 lsp_types::DocumentDiagnosticReportResult::Report(report) => match report {
676 lsp_types::DocumentDiagnosticReport::Full(full) => {
677 full.full_document_diagnostic_report.items
678 }
679 lsp_types::DocumentDiagnosticReport::Unchanged(_) => vec![],
680 },
681 lsp_types::DocumentDiagnosticReportResult::Partial(_) => vec![],
682 };
683
684 let result = DiagnosticsResult {
685 diagnostics: diagnostics
686 .into_iter()
687 .map(|diag| Diagnostic {
688 range: normalize_range(diag.range),
689 severity: match diag.severity {
690 Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
691 Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
692 Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
693 DiagnosticSeverity::Information
694 }
695 Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
696 _ => DiagnosticSeverity::Information,
697 },
698 message: diag.message,
699 code: diag.code.map(|c| match c {
700 lsp_types::NumberOrString::Number(n) => n.to_string(),
701 lsp_types::NumberOrString::String(s) => s,
702 }),
703 })
704 .collect(),
705 };
706
707 Ok(result)
708 }
709
710 pub async fn handle_rename(
716 &mut self,
717 file_path: String,
718 line: u32,
719 character: u32,
720 new_name: String,
721 ) -> Result<RenameResult> {
722 let path = PathBuf::from(&file_path);
723 let validated_path = self.validate_path(&path)?;
724 let client = self.get_client_for_file(&validated_path)?;
725 let uri = self
726 .document_tracker
727 .ensure_open(&validated_path, &client)
728 .await?;
729 let lsp_position = mcp_to_lsp_position(line, character);
730
731 let params = LspRenameParams {
732 text_document_position: TextDocumentPositionParams {
733 text_document: TextDocumentIdentifier { uri },
734 position: lsp_position,
735 },
736 new_name,
737 work_done_progress_params: WorkDoneProgressParams::default(),
738 };
739
740 let timeout_duration = Duration::from_secs(30);
741 let response: Option<WorkspaceEdit> = client
742 .request("textDocument/rename", params, timeout_duration)
743 .await?;
744
745 let changes = if let Some(edit) = response {
746 let mut result_changes = Vec::new();
747
748 if let Some(changes_map) = edit.changes {
749 for (uri, edits) in changes_map {
750 result_changes.push(DocumentChanges {
751 uri: uri.to_string(),
752 edits: edits
753 .into_iter()
754 .map(|edit| TextEdit {
755 range: normalize_range(edit.range),
756 new_text: edit.new_text,
757 })
758 .collect(),
759 });
760 }
761 }
762
763 result_changes
764 } else {
765 vec![]
766 };
767
768 Ok(RenameResult { changes })
769 }
770
771 pub async fn handle_completions(
777 &mut self,
778 file_path: String,
779 line: u32,
780 character: u32,
781 trigger: Option<String>,
782 ) -> Result<CompletionsResult> {
783 let path = PathBuf::from(&file_path);
784 let validated_path = self.validate_path(&path)?;
785 let client = self.get_client_for_file(&validated_path)?;
786 let uri = self
787 .document_tracker
788 .ensure_open(&validated_path, &client)
789 .await?;
790 let lsp_position = mcp_to_lsp_position(line, character);
791
792 let context = trigger.map(|trigger_char| lsp_types::CompletionContext {
793 trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
794 trigger_character: Some(trigger_char),
795 });
796
797 let params = CompletionParams {
798 text_document_position: TextDocumentPositionParams {
799 text_document: TextDocumentIdentifier { uri },
800 position: lsp_position,
801 },
802 work_done_progress_params: WorkDoneProgressParams::default(),
803 partial_result_params: PartialResultParams::default(),
804 context,
805 };
806
807 let timeout_duration = Duration::from_secs(10);
808 let response: Option<lsp_types::CompletionResponse> = client
809 .request("textDocument/completion", params, timeout_duration)
810 .await?;
811
812 let items = match response {
813 Some(lsp_types::CompletionResponse::Array(items)) => items,
814 Some(lsp_types::CompletionResponse::List(list)) => list.items,
815 None => vec![],
816 };
817
818 let result = CompletionsResult {
819 items: items
820 .into_iter()
821 .map(|item| Completion {
822 label: item.label,
823 kind: item.kind.map(|k| format!("{k:?}")),
824 detail: item.detail,
825 documentation: item.documentation.map(|doc| match doc {
826 lsp_types::Documentation::String(s) => s,
827 lsp_types::Documentation::MarkupContent(m) => m.value,
828 }),
829 })
830 .collect(),
831 };
832
833 Ok(result)
834 }
835
836 pub async fn handle_document_symbols(
842 &mut self,
843 file_path: String,
844 ) -> Result<DocumentSymbolsResult> {
845 let path = PathBuf::from(&file_path);
846 let validated_path = self.validate_path(&path)?;
847 let client = self.get_client_for_file(&validated_path)?;
848 let uri = self
849 .document_tracker
850 .ensure_open(&validated_path, &client)
851 .await?;
852
853 let params = DocumentSymbolParams {
854 text_document: TextDocumentIdentifier { uri },
855 work_done_progress_params: WorkDoneProgressParams::default(),
856 partial_result_params: PartialResultParams::default(),
857 };
858
859 let timeout_duration = Duration::from_secs(30);
860 let response: Option<lsp_types::DocumentSymbolResponse> = client
861 .request("textDocument/documentSymbol", params, timeout_duration)
862 .await?;
863
864 let symbols = match response {
865 Some(lsp_types::DocumentSymbolResponse::Flat(symbols)) => symbols
866 .into_iter()
867 .map(|sym| Symbol {
868 name: sym.name,
869 kind: format!("{:?}", sym.kind),
870 range: normalize_range(sym.location.range),
871 selection_range: normalize_range(sym.location.range),
872 children: None,
873 })
874 .collect(),
875 Some(lsp_types::DocumentSymbolResponse::Nested(symbols)) => {
876 symbols.into_iter().map(convert_document_symbol).collect()
877 }
878 None => vec![],
879 };
880
881 Ok(DocumentSymbolsResult { symbols })
882 }
883
884 pub async fn handle_format_document(
890 &mut self,
891 file_path: String,
892 tab_size: u32,
893 insert_spaces: bool,
894 ) -> Result<FormatDocumentResult> {
895 let path = PathBuf::from(&file_path);
896 let validated_path = self.validate_path(&path)?;
897 let client = self.get_client_for_file(&validated_path)?;
898 let uri = self
899 .document_tracker
900 .ensure_open(&validated_path, &client)
901 .await?;
902
903 let params = DocumentFormattingParams {
904 text_document: TextDocumentIdentifier { uri },
905 options: FormattingOptions {
906 tab_size,
907 insert_spaces,
908 ..Default::default()
909 },
910 work_done_progress_params: WorkDoneProgressParams::default(),
911 };
912
913 let timeout_duration = Duration::from_secs(30);
914 let response: Option<Vec<lsp_types::TextEdit>> = client
915 .request("textDocument/formatting", params, timeout_duration)
916 .await?;
917
918 let edits = response.unwrap_or_default();
919
920 let result = FormatDocumentResult {
921 edits: edits
922 .into_iter()
923 .map(|edit| TextEdit {
924 range: normalize_range(edit.range),
925 new_text: edit.new_text,
926 })
927 .collect(),
928 };
929
930 Ok(result)
931 }
932
933 pub async fn handle_workspace_symbol(
939 &mut self,
940 query: String,
941 kind_filter: Option<String>,
942 limit: u32,
943 ) -> Result<WorkspaceSymbolResult> {
944 const MAX_QUERY_LENGTH: usize = 1000;
945 const VALID_SYMBOL_KINDS: &[&str] = &[
946 "File",
947 "Module",
948 "Namespace",
949 "Package",
950 "Class",
951 "Method",
952 "Property",
953 "Field",
954 "Constructor",
955 "Enum",
956 "Interface",
957 "Function",
958 "Variable",
959 "Constant",
960 "String",
961 "Number",
962 "Boolean",
963 "Array",
964 "Object",
965 "Key",
966 "Null",
967 "EnumMember",
968 "Struct",
969 "Event",
970 "Operator",
971 "TypeParameter",
972 ];
973
974 if query.len() > MAX_QUERY_LENGTH {
976 return Err(Error::InvalidToolParams(format!(
977 "Query too long: {} chars (max {MAX_QUERY_LENGTH})",
978 query.len()
979 )));
980 }
981
982 if let Some(ref kind) = kind_filter {
984 if !VALID_SYMBOL_KINDS
985 .iter()
986 .any(|k| k.eq_ignore_ascii_case(kind))
987 {
988 return Err(Error::InvalidToolParams(format!(
989 "Invalid kind_filter: '{kind}'. Valid values: {VALID_SYMBOL_KINDS:?}"
990 )));
991 }
992 }
993
994 let client = self
996 .lsp_clients
997 .values()
998 .next()
999 .cloned()
1000 .ok_or(Error::NoServerConfigured)?;
1001
1002 let params = LspWorkspaceSymbolParams {
1003 query,
1004 work_done_progress_params: WorkDoneProgressParams::default(),
1005 partial_result_params: PartialResultParams::default(),
1006 };
1007
1008 let timeout_duration = Duration::from_secs(30);
1009 let response: Option<Vec<lsp_types::SymbolInformation>> = client
1010 .request("workspace/symbol", params, timeout_duration)
1011 .await?;
1012
1013 let mut symbols: Vec<WorkspaceSymbol> = response
1014 .unwrap_or_default()
1015 .into_iter()
1016 .map(|sym| WorkspaceSymbol {
1017 name: sym.name,
1018 kind: format!("{:?}", sym.kind),
1019 location: Location {
1020 uri: sym.location.uri.to_string(),
1021 range: normalize_range(sym.location.range),
1022 },
1023 container_name: sym.container_name,
1024 })
1025 .collect();
1026
1027 if let Some(kind) = kind_filter {
1029 symbols.retain(|s| s.kind.eq_ignore_ascii_case(&kind));
1030 }
1031
1032 symbols.truncate(limit as usize);
1034
1035 Ok(WorkspaceSymbolResult { symbols })
1036 }
1037
1038 pub async fn handle_code_actions(
1044 &mut self,
1045 file_path: String,
1046 start_line: u32,
1047 start_character: u32,
1048 end_line: u32,
1049 end_character: u32,
1050 kind_filter: Option<String>,
1051 ) -> Result<CodeActionsResult> {
1052 const VALID_ACTION_KINDS: &[&str] = &[
1053 "quickfix",
1054 "refactor",
1055 "refactor.extract",
1056 "refactor.inline",
1057 "refactor.rewrite",
1058 "source",
1059 "source.organizeImports",
1060 ];
1061
1062 if let Some(ref kind) = kind_filter {
1064 if !VALID_ACTION_KINDS
1065 .iter()
1066 .any(|k| k.eq_ignore_ascii_case(kind))
1067 {
1068 return Err(Error::InvalidToolParams(format!(
1069 "Invalid kind_filter: '{kind}'. Valid values: {VALID_ACTION_KINDS:?}"
1070 )));
1071 }
1072 }
1073
1074 if start_line < 1 || start_character < 1 || end_line < 1 || end_character < 1 {
1076 return Err(Error::InvalidToolParams(
1077 "Line and character positions must be >= 1".to_string(),
1078 ));
1079 }
1080
1081 if start_line > MAX_POSITION_VALUE
1083 || start_character > MAX_POSITION_VALUE
1084 || end_line > MAX_POSITION_VALUE
1085 || end_character > MAX_POSITION_VALUE
1086 {
1087 return Err(Error::InvalidToolParams(format!(
1088 "Position values must be <= {MAX_POSITION_VALUE}"
1089 )));
1090 }
1091
1092 if end_line.saturating_sub(start_line) > MAX_RANGE_LINES {
1094 return Err(Error::InvalidToolParams(format!(
1095 "Range size must be <= {MAX_RANGE_LINES} lines"
1096 )));
1097 }
1098
1099 if start_line > end_line || (start_line == end_line && start_character > end_character) {
1100 return Err(Error::InvalidToolParams(
1101 "Start position must be before or equal to end position".to_string(),
1102 ));
1103 }
1104
1105 let path = PathBuf::from(&file_path);
1106 let validated_path = self.validate_path(&path)?;
1107 let client = self.get_client_for_file(&validated_path)?;
1108 let uri = self
1109 .document_tracker
1110 .ensure_open(&validated_path, &client)
1111 .await?;
1112
1113 let range = lsp_types::Range {
1114 start: mcp_to_lsp_position(start_line, start_character),
1115 end: mcp_to_lsp_position(end_line, end_character),
1116 };
1117
1118 let only = kind_filter.map(|k| vec![lsp_types::CodeActionKind::from(k)]);
1120
1121 let params = lsp_types::CodeActionParams {
1122 text_document: TextDocumentIdentifier { uri },
1123 range,
1124 context: lsp_types::CodeActionContext {
1125 diagnostics: vec![],
1126 only,
1127 trigger_kind: Some(lsp_types::CodeActionTriggerKind::INVOKED),
1128 },
1129 work_done_progress_params: WorkDoneProgressParams::default(),
1130 partial_result_params: PartialResultParams::default(),
1131 };
1132
1133 let timeout_duration = Duration::from_secs(30);
1134 let response: Option<lsp_types::CodeActionResponse> = client
1135 .request("textDocument/codeAction", params, timeout_duration)
1136 .await?;
1137
1138 let response_vec = response.unwrap_or_default();
1139 let mut actions = Vec::with_capacity(response_vec.len());
1140
1141 for action_or_command in response_vec {
1142 let action = match action_or_command {
1143 lsp_types::CodeActionOrCommand::CodeAction(action) => convert_code_action(action),
1144 lsp_types::CodeActionOrCommand::Command(cmd) => {
1145 let arguments = cmd.arguments.unwrap_or_else(Vec::new);
1146 CodeAction {
1147 title: cmd.title.clone(),
1148 kind: None,
1149 diagnostics: Vec::new(),
1150 edit: None,
1151 command: Some(CommandDescription {
1152 title: cmd.title,
1153 command: cmd.command,
1154 arguments,
1155 }),
1156 is_preferred: false,
1157 }
1158 }
1159 };
1160 actions.push(action);
1161 }
1162
1163 Ok(CodeActionsResult { actions })
1164 }
1165
1166 pub async fn handle_call_hierarchy_prepare(
1172 &mut self,
1173 file_path: String,
1174 line: u32,
1175 character: u32,
1176 ) -> Result<CallHierarchyPrepareResult> {
1177 if line < 1 || character < 1 {
1179 return Err(Error::InvalidToolParams(
1180 "Line and character positions must be >= 1".to_string(),
1181 ));
1182 }
1183
1184 if line > MAX_POSITION_VALUE || character > MAX_POSITION_VALUE {
1185 return Err(Error::InvalidToolParams(format!(
1186 "Position values must be <= {MAX_POSITION_VALUE}"
1187 )));
1188 }
1189
1190 let path = PathBuf::from(&file_path);
1191 let validated_path = self.validate_path(&path)?;
1192 let client = self.get_client_for_file(&validated_path)?;
1193 let uri = self
1194 .document_tracker
1195 .ensure_open(&validated_path, &client)
1196 .await?;
1197 let lsp_position = mcp_to_lsp_position(line, character);
1198
1199 let params = LspCallHierarchyPrepareParams {
1200 text_document_position_params: TextDocumentPositionParams {
1201 text_document: TextDocumentIdentifier { uri },
1202 position: lsp_position,
1203 },
1204 work_done_progress_params: WorkDoneProgressParams::default(),
1205 };
1206
1207 let timeout_duration = Duration::from_secs(30);
1208 let response: Option<Vec<CallHierarchyItem>> = client
1209 .request(
1210 "textDocument/prepareCallHierarchy",
1211 params,
1212 timeout_duration,
1213 )
1214 .await?;
1215
1216 let lsp_items = response.unwrap_or_default();
1218 let mut items = Vec::with_capacity(lsp_items.len());
1219 for item in lsp_items {
1220 items.push(convert_call_hierarchy_item(item));
1221 }
1222
1223 Ok(CallHierarchyPrepareResult { items })
1224 }
1225
1226 pub async fn handle_incoming_calls(
1232 &mut self,
1233 item: serde_json::Value,
1234 ) -> Result<IncomingCallsResult> {
1235 let lsp_item: CallHierarchyItem = serde_json::from_value(item)
1236 .map_err(|e| Error::InvalidToolParams(format!("Invalid call hierarchy item: {e}")))?;
1237
1238 let path = self.parse_file_uri(&lsp_item.uri)?;
1240 let client = self.get_client_for_file(&path)?;
1241
1242 let params = CallHierarchyIncomingCallsParams {
1243 item: lsp_item,
1244 work_done_progress_params: WorkDoneProgressParams::default(),
1245 partial_result_params: PartialResultParams::default(),
1246 };
1247
1248 let timeout_duration = Duration::from_secs(30);
1249 let response: Option<Vec<CallHierarchyIncomingCall>> = client
1250 .request("callHierarchy/incomingCalls", params, timeout_duration)
1251 .await?;
1252
1253 let lsp_calls = response.unwrap_or_default();
1255 let mut calls = Vec::with_capacity(lsp_calls.len());
1256
1257 for call in lsp_calls {
1258 let from_ranges = {
1259 let mut ranges = Vec::with_capacity(call.from_ranges.len());
1260 for range in call.from_ranges {
1261 ranges.push(normalize_range(range));
1262 }
1263 ranges
1264 };
1265
1266 calls.push(IncomingCall {
1267 from: convert_call_hierarchy_item(call.from),
1268 from_ranges,
1269 });
1270 }
1271
1272 Ok(IncomingCallsResult { calls })
1273 }
1274
1275 pub async fn handle_outgoing_calls(
1281 &mut self,
1282 item: serde_json::Value,
1283 ) -> Result<OutgoingCallsResult> {
1284 let lsp_item: CallHierarchyItem = serde_json::from_value(item)
1285 .map_err(|e| Error::InvalidToolParams(format!("Invalid call hierarchy item: {e}")))?;
1286
1287 let path = self.parse_file_uri(&lsp_item.uri)?;
1289 let client = self.get_client_for_file(&path)?;
1290
1291 let params = CallHierarchyOutgoingCallsParams {
1292 item: lsp_item,
1293 work_done_progress_params: WorkDoneProgressParams::default(),
1294 partial_result_params: PartialResultParams::default(),
1295 };
1296
1297 let timeout_duration = Duration::from_secs(30);
1298 let response: Option<Vec<CallHierarchyOutgoingCall>> = client
1299 .request("callHierarchy/outgoingCalls", params, timeout_duration)
1300 .await?;
1301
1302 let lsp_calls = response.unwrap_or_default();
1304 let mut calls = Vec::with_capacity(lsp_calls.len());
1305
1306 for call in lsp_calls {
1307 let from_ranges = {
1308 let mut ranges = Vec::with_capacity(call.from_ranges.len());
1309 for range in call.from_ranges {
1310 ranges.push(normalize_range(range));
1311 }
1312 ranges
1313 };
1314
1315 calls.push(OutgoingCall {
1316 to: convert_call_hierarchy_item(call.to),
1317 from_ranges,
1318 });
1319 }
1320
1321 Ok(OutgoingCallsResult { calls })
1322 }
1323
1324 pub fn handle_cached_diagnostics(&mut self, file_path: &str) -> Result<DiagnosticsResult> {
1330 let path = PathBuf::from(file_path);
1331 let validated_path = self.validate_path(&path)?;
1332
1333 let uri = Url::from_file_path(&validated_path)
1335 .map_err(|()| Error::InvalidUri(validated_path.display().to_string()))?
1336 .to_string();
1337
1338 let diagnostics =
1339 self.notification_cache
1340 .get_diagnostics(&uri)
1341 .map_or_else(Vec::new, |diag_info| {
1342 diag_info
1343 .diagnostics
1344 .iter()
1345 .map(|diag| Diagnostic {
1346 range: normalize_range(diag.range),
1347 severity: match diag.severity {
1348 Some(lsp_types::DiagnosticSeverity::ERROR) => {
1349 DiagnosticSeverity::Error
1350 }
1351 Some(lsp_types::DiagnosticSeverity::WARNING) => {
1352 DiagnosticSeverity::Warning
1353 }
1354 Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
1355 DiagnosticSeverity::Information
1356 }
1357 Some(lsp_types::DiagnosticSeverity::HINT) => {
1358 DiagnosticSeverity::Hint
1359 }
1360 _ => DiagnosticSeverity::Information,
1361 },
1362 message: diag.message.clone(),
1363 code: diag.code.as_ref().map(|c| match c {
1364 lsp_types::NumberOrString::Number(n) => n.to_string(),
1365 lsp_types::NumberOrString::String(s) => s.clone(),
1366 }),
1367 })
1368 .collect()
1369 });
1370
1371 Ok(DiagnosticsResult { diagnostics })
1372 }
1373
1374 pub fn handle_server_logs(
1380 &mut self,
1381 limit: usize,
1382 min_level: Option<String>,
1383 ) -> Result<ServerLogsResult> {
1384 use crate::bridge::notifications::LogLevel;
1385
1386 let min_level_filter = if let Some(level_str) = min_level {
1387 let level = match level_str.to_lowercase().as_str() {
1388 "error" => LogLevel::Error,
1389 "warning" => LogLevel::Warning,
1390 "info" => LogLevel::Info,
1391 "debug" => LogLevel::Debug,
1392 _ => {
1393 return Err(Error::InvalidToolParams(format!(
1394 "Invalid min_level: '{level_str}'. Valid values: error, warning, info, debug"
1395 )));
1396 }
1397 };
1398 Some(level)
1399 } else {
1400 None
1401 };
1402
1403 let all_logs = self.notification_cache.get_logs();
1404
1405 let logs: Vec<_> = all_logs
1406 .iter()
1407 .filter(|log| {
1408 min_level_filter.is_none_or(|min| match min {
1409 LogLevel::Error => matches!(log.level, LogLevel::Error),
1410 LogLevel::Warning => matches!(log.level, LogLevel::Error | LogLevel::Warning),
1411 LogLevel::Info => !matches!(log.level, LogLevel::Debug),
1412 LogLevel::Debug => true,
1413 })
1414 })
1415 .take(limit)
1416 .cloned()
1417 .collect();
1418
1419 Ok(ServerLogsResult { logs })
1420 }
1421
1422 pub fn handle_server_messages(&mut self, limit: usize) -> Result<ServerMessagesResult> {
1428 let all_messages = self.notification_cache.get_messages();
1429 let messages: Vec<_> = all_messages.iter().take(limit).cloned().collect();
1430 Ok(ServerMessagesResult { messages })
1431 }
1432}
1433
1434fn extract_hover_contents(contents: HoverContents) -> String {
1436 match contents {
1437 HoverContents::Scalar(marked_string) => marked_string_to_string(marked_string),
1438 HoverContents::Array(marked_strings) => marked_strings
1439 .into_iter()
1440 .map(marked_string_to_string)
1441 .collect::<Vec<_>>()
1442 .join("\n\n"),
1443 HoverContents::Markup(markup) => markup.value,
1444 }
1445}
1446
1447fn marked_string_to_string(marked: MarkedString) -> String {
1449 match marked {
1450 MarkedString::String(s) => s,
1451 MarkedString::LanguageString(ls) => format!("```{}\n{}\n```", ls.language, ls.value),
1452 }
1453}
1454
1455const fn normalize_range(range: lsp_types::Range) -> Range {
1457 Range {
1458 start: Position2D {
1459 line: range.start.line + 1,
1460 character: range.start.character + 1,
1461 },
1462 end: Position2D {
1463 line: range.end.line + 1,
1464 character: range.end.character + 1,
1465 },
1466 }
1467}
1468
1469fn convert_document_symbol(symbol: DocumentSymbol) -> Symbol {
1471 Symbol {
1472 name: symbol.name,
1473 kind: format!("{:?}", symbol.kind),
1474 range: normalize_range(symbol.range),
1475 selection_range: normalize_range(symbol.selection_range),
1476 children: symbol
1477 .children
1478 .map(|children| children.into_iter().map(convert_document_symbol).collect()),
1479 }
1480}
1481
1482fn convert_call_hierarchy_item(item: CallHierarchyItem) -> CallHierarchyItemResult {
1484 CallHierarchyItemResult {
1485 name: item.name,
1486 kind: format!("{:?}", item.kind),
1487 detail: item.detail,
1488 uri: item.uri.to_string(),
1489 range: normalize_range(item.range),
1490 selection_range: normalize_range(item.selection_range),
1491 data: item.data,
1492 }
1493}
1494
1495fn convert_code_action(action: lsp_types::CodeAction) -> CodeAction {
1497 let diagnostics = action.diagnostics.map_or_else(Vec::new, |diags| {
1498 let mut result = Vec::with_capacity(diags.len());
1499 for d in diags {
1500 result.push(Diagnostic {
1501 range: normalize_range(d.range),
1502 severity: match d.severity {
1503 Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
1504 Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
1505 Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
1506 DiagnosticSeverity::Information
1507 }
1508 Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
1509 _ => DiagnosticSeverity::Information,
1510 },
1511 message: d.message,
1512 code: d.code.map(|c| match c {
1513 lsp_types::NumberOrString::Number(n) => n.to_string(),
1514 lsp_types::NumberOrString::String(s) => s,
1515 }),
1516 });
1517 }
1518 result
1519 });
1520
1521 let edit = action.edit.map(|edit| {
1522 let changes = edit.changes.map_or_else(Vec::new, |changes_map| {
1523 let mut result = Vec::with_capacity(changes_map.len());
1524 for (uri, edits) in changes_map {
1525 let mut text_edits = Vec::with_capacity(edits.len());
1526 for e in edits {
1527 text_edits.push(TextEdit {
1528 range: normalize_range(e.range),
1529 new_text: e.new_text,
1530 });
1531 }
1532 result.push(DocumentChanges {
1533 uri: uri.to_string(),
1534 edits: text_edits,
1535 });
1536 }
1537 result
1538 });
1539 WorkspaceEditDescription { changes }
1540 });
1541
1542 let command = action.command.map(|cmd| {
1543 let arguments = cmd.arguments.unwrap_or_else(Vec::new);
1544 CommandDescription {
1545 title: cmd.title,
1546 command: cmd.command,
1547 arguments,
1548 }
1549 });
1550
1551 CodeAction {
1552 title: action.title,
1553 kind: action.kind.map(|k| k.as_str().to_string()),
1554 diagnostics,
1555 edit,
1556 command,
1557 is_preferred: action.is_preferred.unwrap_or(false),
1558 }
1559}
1560
1561#[cfg(test)]
1562#[allow(clippy::unwrap_used)]
1563mod tests {
1564 use std::fs;
1565
1566 use tempfile::TempDir;
1567 use url::Url;
1568
1569 use super::*;
1570
1571 #[test]
1572 fn test_translator_new() {
1573 let translator = Translator::new();
1574 assert_eq!(translator.workspace_roots.len(), 0);
1575 assert_eq!(translator.lsp_clients.len(), 0);
1576 assert_eq!(translator.lsp_servers.len(), 0);
1577 }
1578
1579 #[test]
1580 fn test_set_workspace_roots() {
1581 let mut translator = Translator::new();
1582 let roots = vec![PathBuf::from("/test/root1"), PathBuf::from("/test/root2")];
1583 translator.set_workspace_roots(roots.clone());
1584 assert_eq!(translator.workspace_roots, roots);
1585 }
1586
1587 #[test]
1588 fn test_register_server() {
1589 let translator = Translator::new();
1590
1591 assert_eq!(translator.lsp_servers.len(), 0);
1593
1594 }
1603
1604 #[test]
1605 fn test_validate_path_no_workspace_roots() {
1606 let translator = Translator::new();
1607 let temp_dir = TempDir::new().unwrap();
1608 let test_file = temp_dir.path().join("test.rs");
1609 fs::write(&test_file, "fn main() {}").unwrap();
1610
1611 let result = translator.validate_path(&test_file);
1613 assert!(result.is_ok());
1614 }
1615
1616 #[test]
1617 fn test_validate_path_within_workspace() {
1618 let mut translator = Translator::new();
1619 let temp_dir = TempDir::new().unwrap();
1620 let workspace_root = temp_dir.path().to_path_buf();
1621 translator.set_workspace_roots(vec![workspace_root]);
1622
1623 let test_file = temp_dir.path().join("test.rs");
1624 fs::write(&test_file, "fn main() {}").unwrap();
1625
1626 let result = translator.validate_path(&test_file);
1627 assert!(result.is_ok());
1628 }
1629
1630 #[test]
1631 fn test_validate_path_outside_workspace() {
1632 let mut translator = Translator::new();
1633 let temp_dir1 = TempDir::new().unwrap();
1634 let temp_dir2 = TempDir::new().unwrap();
1635
1636 translator.set_workspace_roots(vec![temp_dir1.path().to_path_buf()]);
1638
1639 let test_file = temp_dir2.path().join("test.rs");
1641 fs::write(&test_file, "fn main() {}").unwrap();
1642
1643 let result = translator.validate_path(&test_file);
1644 assert!(matches!(result, Err(Error::PathOutsideWorkspace(_))));
1645 }
1646
1647 #[test]
1648 fn test_normalize_range() {
1649 let lsp_range = lsp_types::Range {
1650 start: lsp_types::Position {
1651 line: 0,
1652 character: 0,
1653 },
1654 end: lsp_types::Position {
1655 line: 2,
1656 character: 5,
1657 },
1658 };
1659
1660 let mcp_range = normalize_range(lsp_range);
1661 assert_eq!(mcp_range.start.line, 1);
1662 assert_eq!(mcp_range.start.character, 1);
1663 assert_eq!(mcp_range.end.line, 3);
1664 assert_eq!(mcp_range.end.character, 6);
1665 }
1666
1667 #[test]
1668 fn test_extract_hover_contents_string() {
1669 let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
1670 let contents = lsp_types::HoverContents::Scalar(marked_string);
1671 let result = extract_hover_contents(contents);
1672 assert_eq!(result, "Test hover");
1673 }
1674
1675 #[test]
1676 fn test_extract_hover_contents_language_string() {
1677 let marked_string = lsp_types::MarkedString::LanguageString(lsp_types::LanguageString {
1678 language: "rust".to_string(),
1679 value: "fn main() {}".to_string(),
1680 });
1681 let contents = lsp_types::HoverContents::Scalar(marked_string);
1682 let result = extract_hover_contents(contents);
1683 assert_eq!(result, "```rust\nfn main() {}\n```");
1684 }
1685
1686 #[test]
1687 fn test_extract_hover_contents_markup() {
1688 let markup = lsp_types::MarkupContent {
1689 kind: lsp_types::MarkupKind::Markdown,
1690 value: "# Documentation".to_string(),
1691 };
1692 let contents = lsp_types::HoverContents::Markup(markup);
1693 let result = extract_hover_contents(contents);
1694 assert_eq!(result, "# Documentation");
1695 }
1696
1697 #[tokio::test]
1698 async fn test_handle_workspace_symbol_no_server() {
1699 let mut translator = Translator::new();
1700 let result = translator
1701 .handle_workspace_symbol("test".to_string(), None, 100)
1702 .await;
1703 assert!(matches!(result, Err(Error::NoServerConfigured)));
1704 }
1705
1706 #[tokio::test]
1707 async fn test_handle_code_actions_invalid_kind() {
1708 let mut translator = Translator::new();
1709 let result = translator
1710 .handle_code_actions(
1711 "/tmp/test.rs".to_string(),
1712 1,
1713 1,
1714 1,
1715 10,
1716 Some("invalid_kind".to_string()),
1717 )
1718 .await;
1719 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1720 }
1721
1722 #[tokio::test]
1723 async fn test_handle_code_actions_valid_kind_quickfix() {
1724 use tempfile::TempDir;
1725
1726 let mut translator = Translator::new();
1727 let temp_dir = TempDir::new().unwrap();
1728 let test_file = temp_dir.path().join("test.rs");
1729 fs::write(&test_file, "fn main() {}").unwrap();
1730
1731 let result = translator
1732 .handle_code_actions(
1733 test_file.to_str().unwrap().to_string(),
1734 1,
1735 1,
1736 1,
1737 10,
1738 Some("quickfix".to_string()),
1739 )
1740 .await;
1741 assert!(result.is_err());
1743 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1744 }
1745
1746 #[tokio::test]
1747 async fn test_handle_code_actions_valid_kind_refactor() {
1748 use tempfile::TempDir;
1749
1750 let mut translator = Translator::new();
1751 let temp_dir = TempDir::new().unwrap();
1752 let test_file = temp_dir.path().join("test.rs");
1753 fs::write(&test_file, "fn main() {}").unwrap();
1754
1755 let result = translator
1756 .handle_code_actions(
1757 test_file.to_str().unwrap().to_string(),
1758 1,
1759 1,
1760 1,
1761 10,
1762 Some("refactor".to_string()),
1763 )
1764 .await;
1765 assert!(result.is_err());
1766 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1767 }
1768
1769 #[tokio::test]
1770 async fn test_handle_code_actions_valid_kind_refactor_extract() {
1771 use tempfile::TempDir;
1772
1773 let mut translator = Translator::new();
1774 let temp_dir = TempDir::new().unwrap();
1775 let test_file = temp_dir.path().join("test.rs");
1776 fs::write(&test_file, "fn main() {}").unwrap();
1777
1778 let result = translator
1779 .handle_code_actions(
1780 test_file.to_str().unwrap().to_string(),
1781 1,
1782 1,
1783 1,
1784 10,
1785 Some("refactor.extract".to_string()),
1786 )
1787 .await;
1788 assert!(result.is_err());
1789 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1790 }
1791
1792 #[tokio::test]
1793 async fn test_handle_code_actions_valid_kind_source() {
1794 use tempfile::TempDir;
1795
1796 let mut translator = Translator::new();
1797 let temp_dir = TempDir::new().unwrap();
1798 let test_file = temp_dir.path().join("test.rs");
1799 fs::write(&test_file, "fn main() {}").unwrap();
1800
1801 let result = translator
1802 .handle_code_actions(
1803 test_file.to_str().unwrap().to_string(),
1804 1,
1805 1,
1806 1,
1807 10,
1808 Some("source.organizeImports".to_string()),
1809 )
1810 .await;
1811 assert!(result.is_err());
1812 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1813 }
1814
1815 #[tokio::test]
1816 async fn test_handle_code_actions_invalid_range_zero() {
1817 let mut translator = Translator::new();
1818 let result = translator
1819 .handle_code_actions("/tmp/test.rs".to_string(), 0, 1, 1, 10, None)
1820 .await;
1821 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1822 }
1823
1824 #[tokio::test]
1825 async fn test_handle_code_actions_invalid_range_order() {
1826 let mut translator = Translator::new();
1827 let result = translator
1828 .handle_code_actions("/tmp/test.rs".to_string(), 10, 5, 5, 1, None)
1829 .await;
1830 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1831 }
1832
1833 #[tokio::test]
1834 async fn test_handle_code_actions_empty_range() {
1835 use tempfile::TempDir;
1836
1837 let mut translator = Translator::new();
1838 let temp_dir = TempDir::new().unwrap();
1839 let test_file = temp_dir.path().join("test.rs");
1840 fs::write(&test_file, "fn main() {}").unwrap();
1841
1842 let result = translator
1844 .handle_code_actions(test_file.to_str().unwrap().to_string(), 1, 5, 1, 5, None)
1845 .await;
1846 assert!(result.is_err());
1848 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1849 }
1850
1851 #[test]
1852 fn test_convert_code_action_minimal() {
1853 let lsp_action = lsp_types::CodeAction {
1854 title: "Fix issue".to_string(),
1855 kind: None,
1856 diagnostics: None,
1857 edit: None,
1858 command: None,
1859 is_preferred: None,
1860 disabled: None,
1861 data: None,
1862 };
1863
1864 let result = convert_code_action(lsp_action);
1865 assert_eq!(result.title, "Fix issue");
1866 assert!(result.kind.is_none());
1867 assert!(result.diagnostics.is_empty());
1868 assert!(result.edit.is_none());
1869 assert!(result.command.is_none());
1870 assert!(!result.is_preferred);
1871 }
1872
1873 #[test]
1874 #[allow(clippy::too_many_lines)]
1875 fn test_convert_code_action_with_diagnostics_all_severities() {
1876 let lsp_diagnostics = vec![
1877 lsp_types::Diagnostic {
1878 range: lsp_types::Range {
1879 start: lsp_types::Position {
1880 line: 0,
1881 character: 0,
1882 },
1883 end: lsp_types::Position {
1884 line: 0,
1885 character: 5,
1886 },
1887 },
1888 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
1889 message: "Error message".to_string(),
1890 code: Some(lsp_types::NumberOrString::Number(1)),
1891 source: None,
1892 code_description: None,
1893 related_information: None,
1894 tags: None,
1895 data: None,
1896 },
1897 lsp_types::Diagnostic {
1898 range: lsp_types::Range {
1899 start: lsp_types::Position {
1900 line: 1,
1901 character: 0,
1902 },
1903 end: lsp_types::Position {
1904 line: 1,
1905 character: 5,
1906 },
1907 },
1908 severity: Some(lsp_types::DiagnosticSeverity::WARNING),
1909 message: "Warning message".to_string(),
1910 code: Some(lsp_types::NumberOrString::String("W001".to_string())),
1911 source: None,
1912 code_description: None,
1913 related_information: None,
1914 tags: None,
1915 data: None,
1916 },
1917 lsp_types::Diagnostic {
1918 range: lsp_types::Range {
1919 start: lsp_types::Position {
1920 line: 2,
1921 character: 0,
1922 },
1923 end: lsp_types::Position {
1924 line: 2,
1925 character: 5,
1926 },
1927 },
1928 severity: Some(lsp_types::DiagnosticSeverity::INFORMATION),
1929 message: "Info message".to_string(),
1930 code: None,
1931 source: None,
1932 code_description: None,
1933 related_information: None,
1934 tags: None,
1935 data: None,
1936 },
1937 lsp_types::Diagnostic {
1938 range: lsp_types::Range {
1939 start: lsp_types::Position {
1940 line: 3,
1941 character: 0,
1942 },
1943 end: lsp_types::Position {
1944 line: 3,
1945 character: 5,
1946 },
1947 },
1948 severity: Some(lsp_types::DiagnosticSeverity::HINT),
1949 message: "Hint message".to_string(),
1950 code: None,
1951 source: None,
1952 code_description: None,
1953 related_information: None,
1954 tags: None,
1955 data: None,
1956 },
1957 ];
1958
1959 let lsp_action = lsp_types::CodeAction {
1960 title: "Fix all issues".to_string(),
1961 kind: Some(lsp_types::CodeActionKind::QUICKFIX),
1962 diagnostics: Some(lsp_diagnostics),
1963 edit: None,
1964 command: None,
1965 is_preferred: None,
1966 disabled: None,
1967 data: None,
1968 };
1969
1970 let result = convert_code_action(lsp_action);
1971 assert_eq!(result.diagnostics.len(), 4);
1972 assert!(matches!(
1973 result.diagnostics[0].severity,
1974 DiagnosticSeverity::Error
1975 ));
1976 assert!(matches!(
1977 result.diagnostics[1].severity,
1978 DiagnosticSeverity::Warning
1979 ));
1980 assert!(matches!(
1981 result.diagnostics[2].severity,
1982 DiagnosticSeverity::Information
1983 ));
1984 assert!(matches!(
1985 result.diagnostics[3].severity,
1986 DiagnosticSeverity::Hint
1987 ));
1988 assert_eq!(result.diagnostics[0].code, Some("1".to_string()));
1989 assert_eq!(result.diagnostics[1].code, Some("W001".to_string()));
1990 }
1991
1992 #[test]
1993 #[allow(clippy::mutable_key_type)]
1994 fn test_convert_code_action_with_workspace_edit() {
1995 use std::collections::HashMap;
1996 use std::str::FromStr;
1997
1998 let uri = lsp_types::Uri::from_str("file:///test.rs").unwrap();
1999 let mut changes_map = HashMap::new();
2000 changes_map.insert(
2001 uri,
2002 vec![lsp_types::TextEdit {
2003 range: lsp_types::Range {
2004 start: lsp_types::Position {
2005 line: 0,
2006 character: 0,
2007 },
2008 end: lsp_types::Position {
2009 line: 0,
2010 character: 5,
2011 },
2012 },
2013 new_text: "fixed".to_string(),
2014 }],
2015 );
2016
2017 let lsp_action = lsp_types::CodeAction {
2018 title: "Apply fix".to_string(),
2019 kind: Some(lsp_types::CodeActionKind::QUICKFIX),
2020 diagnostics: None,
2021 edit: Some(lsp_types::WorkspaceEdit {
2022 changes: Some(changes_map),
2023 document_changes: None,
2024 change_annotations: None,
2025 }),
2026 command: None,
2027 is_preferred: Some(true),
2028 disabled: None,
2029 data: None,
2030 };
2031
2032 let result = convert_code_action(lsp_action);
2033 assert!(result.edit.is_some());
2034 let edit = result.edit.unwrap();
2035 assert_eq!(edit.changes.len(), 1);
2036 assert_eq!(edit.changes[0].uri, "file:///test.rs");
2037 assert_eq!(edit.changes[0].edits.len(), 1);
2038 assert_eq!(edit.changes[0].edits[0].new_text, "fixed");
2039 assert!(result.is_preferred);
2040 }
2041
2042 #[test]
2043 fn test_convert_code_action_with_command() {
2044 let lsp_action = lsp_types::CodeAction {
2045 title: "Run command".to_string(),
2046 kind: Some(lsp_types::CodeActionKind::REFACTOR),
2047 diagnostics: None,
2048 edit: None,
2049 command: Some(lsp_types::Command {
2050 title: "Execute refactor".to_string(),
2051 command: "refactor.extract".to_string(),
2052 arguments: Some(vec![serde_json::json!("arg1"), serde_json::json!(42)]),
2053 }),
2054 is_preferred: None,
2055 disabled: None,
2056 data: None,
2057 };
2058
2059 let result = convert_code_action(lsp_action);
2060 assert!(result.command.is_some());
2061 let cmd = result.command.unwrap();
2062 assert_eq!(cmd.title, "Execute refactor");
2063 assert_eq!(cmd.command, "refactor.extract");
2064 assert_eq!(cmd.arguments.len(), 2);
2065 }
2066
2067 #[tokio::test]
2068 async fn test_handle_call_hierarchy_prepare_invalid_position_zero() {
2069 let mut translator = Translator::new();
2070 let result = translator
2071 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 0, 1)
2072 .await;
2073 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2074
2075 let result = translator
2076 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1, 0)
2077 .await;
2078 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2079 }
2080
2081 #[tokio::test]
2082 async fn test_handle_call_hierarchy_prepare_invalid_position_too_large() {
2083 let mut translator = Translator::new();
2084 let result = translator
2085 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1_000_001, 1)
2086 .await;
2087 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2088
2089 let result = translator
2090 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1, 1_000_001)
2091 .await;
2092 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2093 }
2094
2095 #[tokio::test]
2096 async fn test_handle_incoming_calls_invalid_json() {
2097 let mut translator = Translator::new();
2098 let invalid_item = serde_json::json!({"invalid": "structure"});
2099 let result = translator.handle_incoming_calls(invalid_item).await;
2100 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2101 }
2102
2103 #[tokio::test]
2104 async fn test_handle_outgoing_calls_invalid_json() {
2105 let mut translator = Translator::new();
2106 let invalid_item = serde_json::json!({"invalid": "structure"});
2107 let result = translator.handle_outgoing_calls(invalid_item).await;
2108 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2109 }
2110
2111 #[tokio::test]
2112 async fn test_parse_file_uri_invalid_scheme() {
2113 let translator = Translator::new();
2114 let uri: lsp_types::Uri = "http://example.com/file.rs".parse().unwrap();
2115 let result = translator.parse_file_uri(&uri);
2116 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2117 }
2118
2119 #[tokio::test]
2120 async fn test_parse_file_uri_valid_scheme() {
2121 let translator = Translator::new();
2122 let temp_dir = TempDir::new().unwrap();
2123 let test_file = temp_dir.path().join("test.rs");
2124 fs::write(&test_file, "fn main() {}").unwrap();
2125
2126 let file_url = Url::from_file_path(&test_file).unwrap();
2128 let uri: lsp_types::Uri = file_url.as_str().parse().unwrap();
2129 let result = translator.parse_file_uri(&uri);
2130 assert!(result.is_ok());
2131 }
2132
2133 #[test]
2134 fn test_handle_cached_diagnostics_empty() {
2135 let mut translator = Translator::new();
2136 let temp_dir = TempDir::new().unwrap();
2137 let test_file = temp_dir.path().join("test.rs");
2138 fs::write(&test_file, "fn main() {}").unwrap();
2139
2140 let result = translator.handle_cached_diagnostics(test_file.to_str().unwrap());
2141 assert!(result.is_ok());
2142 let diags = result.unwrap();
2143 assert_eq!(diags.diagnostics.len(), 0);
2144 }
2145
2146 #[test]
2147 fn test_handle_server_logs_with_filter() {
2148 use crate::bridge::notifications::LogLevel;
2149
2150 let mut translator = Translator::new();
2151
2152 translator
2154 .notification_cache_mut()
2155 .store_log(LogLevel::Error, "error msg".to_string());
2156 translator
2157 .notification_cache_mut()
2158 .store_log(LogLevel::Warning, "warning msg".to_string());
2159 translator
2160 .notification_cache_mut()
2161 .store_log(LogLevel::Info, "info msg".to_string());
2162 translator
2163 .notification_cache_mut()
2164 .store_log(LogLevel::Debug, "debug msg".to_string());
2165
2166 let result = translator.handle_server_logs(10, Some("error".to_string()));
2168 assert!(result.is_ok());
2169 let logs = result.unwrap();
2170 assert_eq!(logs.logs.len(), 1);
2171 assert_eq!(logs.logs[0].message, "error msg");
2172
2173 let result = translator.handle_server_logs(10, Some("warning".to_string()));
2175 assert!(result.is_ok());
2176 let logs = result.unwrap();
2177 assert_eq!(logs.logs.len(), 2);
2178
2179 let result = translator.handle_server_logs(10, Some("info".to_string()));
2181 assert!(result.is_ok());
2182 let logs = result.unwrap();
2183 assert_eq!(logs.logs.len(), 3);
2184
2185 let result = translator.handle_server_logs(10, Some("debug".to_string()));
2187 assert!(result.is_ok());
2188 let logs = result.unwrap();
2189 assert_eq!(logs.logs.len(), 4);
2190
2191 let result = translator.handle_server_logs(10, Some("invalid".to_string()));
2193 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2194 }
2195
2196 #[test]
2197 fn test_handle_server_messages_limit() {
2198 use crate::bridge::notifications::MessageType;
2199
2200 let mut translator = Translator::new();
2201
2202 for i in 0..10 {
2204 translator
2205 .notification_cache_mut()
2206 .store_message(MessageType::Info, format!("message {i}"));
2207 }
2208
2209 let result = translator.handle_server_messages(5);
2211 assert!(result.is_ok());
2212 let messages = result.unwrap();
2213 assert_eq!(messages.messages.len(), 5);
2214 assert_eq!(messages.messages[0].message, "message 0");
2215 assert_eq!(messages.messages[4].message, "message 4");
2216
2217 let result = translator.handle_server_messages(100);
2219 assert!(result.is_ok());
2220 let messages = result.unwrap();
2221 assert_eq!(messages.messages.len(), 10);
2222 }
2223
2224 #[test]
2225 fn test_handle_cached_diagnostics_with_data() {
2226 let mut translator = Translator::new();
2227 let temp_dir = TempDir::new().unwrap();
2228 let test_file = temp_dir.path().join("test.rs");
2229 fs::write(&test_file, "fn main() {}").unwrap();
2230
2231 let canonical_path = test_file.canonicalize().unwrap();
2232 let uri: lsp_types::Uri = Url::from_file_path(&canonical_path)
2233 .unwrap()
2234 .as_str()
2235 .parse()
2236 .unwrap();
2237 let diagnostic = lsp_types::Diagnostic {
2238 range: lsp_types::Range {
2239 start: lsp_types::Position {
2240 line: 0,
2241 character: 0,
2242 },
2243 end: lsp_types::Position {
2244 line: 0,
2245 character: 5,
2246 },
2247 },
2248 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2249 message: "test error".to_string(),
2250 code: Some(lsp_types::NumberOrString::String("E001".to_string())),
2251 source: None,
2252 code_description: None,
2253 related_information: None,
2254 tags: None,
2255 data: None,
2256 };
2257
2258 translator
2259 .notification_cache_mut()
2260 .store_diagnostics(&uri, Some(1), vec![diagnostic]);
2261
2262 let result = translator.handle_cached_diagnostics(test_file.to_str().unwrap());
2263 assert!(result.is_ok());
2264 let diags = result.unwrap();
2265 assert_eq!(diags.diagnostics.len(), 1);
2266 assert_eq!(diags.diagnostics[0].message, "test error");
2267 assert_eq!(diags.diagnostics[0].code, Some("E001".to_string()));
2268 assert!(matches!(
2269 diags.diagnostics[0].severity,
2270 DiagnosticSeverity::Error
2271 ));
2272 assert_eq!(diags.diagnostics[0].range.start.line, 1);
2273 assert_eq!(diags.diagnostics[0].range.start.character, 1);
2274 }
2275
2276 #[test]
2277 #[allow(clippy::too_many_lines)]
2278 fn test_handle_cached_diagnostics_multiple_severities() {
2279 let mut translator = Translator::new();
2280 let temp_dir = TempDir::new().unwrap();
2281 let test_file = temp_dir.path().join("test.rs");
2282 fs::write(&test_file, "fn main() {}").unwrap();
2283
2284 let canonical_path = test_file.canonicalize().unwrap();
2285 let uri: lsp_types::Uri = Url::from_file_path(&canonical_path)
2286 .unwrap()
2287 .as_str()
2288 .parse()
2289 .unwrap();
2290 let diagnostics = vec![
2291 lsp_types::Diagnostic {
2292 range: lsp_types::Range {
2293 start: lsp_types::Position {
2294 line: 0,
2295 character: 0,
2296 },
2297 end: lsp_types::Position {
2298 line: 0,
2299 character: 5,
2300 },
2301 },
2302 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2303 message: "error".to_string(),
2304 code: None,
2305 source: None,
2306 code_description: None,
2307 related_information: None,
2308 tags: None,
2309 data: None,
2310 },
2311 lsp_types::Diagnostic {
2312 range: lsp_types::Range {
2313 start: lsp_types::Position {
2314 line: 1,
2315 character: 0,
2316 },
2317 end: lsp_types::Position {
2318 line: 1,
2319 character: 5,
2320 },
2321 },
2322 severity: Some(lsp_types::DiagnosticSeverity::WARNING),
2323 message: "warning".to_string(),
2324 code: None,
2325 source: None,
2326 code_description: None,
2327 related_information: None,
2328 tags: None,
2329 data: None,
2330 },
2331 lsp_types::Diagnostic {
2332 range: lsp_types::Range {
2333 start: lsp_types::Position {
2334 line: 2,
2335 character: 0,
2336 },
2337 end: lsp_types::Position {
2338 line: 2,
2339 character: 5,
2340 },
2341 },
2342 severity: Some(lsp_types::DiagnosticSeverity::INFORMATION),
2343 message: "info".to_string(),
2344 code: None,
2345 source: None,
2346 code_description: None,
2347 related_information: None,
2348 tags: None,
2349 data: None,
2350 },
2351 lsp_types::Diagnostic {
2352 range: lsp_types::Range {
2353 start: lsp_types::Position {
2354 line: 3,
2355 character: 0,
2356 },
2357 end: lsp_types::Position {
2358 line: 3,
2359 character: 5,
2360 },
2361 },
2362 severity: Some(lsp_types::DiagnosticSeverity::HINT),
2363 message: "hint".to_string(),
2364 code: None,
2365 source: None,
2366 code_description: None,
2367 related_information: None,
2368 tags: None,
2369 data: None,
2370 },
2371 ];
2372
2373 translator
2374 .notification_cache_mut()
2375 .store_diagnostics(&uri, Some(1), diagnostics);
2376
2377 let result = translator.handle_cached_diagnostics(test_file.to_str().unwrap());
2378 assert!(result.is_ok());
2379 let diags = result.unwrap();
2380 assert_eq!(diags.diagnostics.len(), 4);
2381 assert!(matches!(
2382 diags.diagnostics[0].severity,
2383 DiagnosticSeverity::Error
2384 ));
2385 assert!(matches!(
2386 diags.diagnostics[1].severity,
2387 DiagnosticSeverity::Warning
2388 ));
2389 assert!(matches!(
2390 diags.diagnostics[2].severity,
2391 DiagnosticSeverity::Information
2392 ));
2393 assert!(matches!(
2394 diags.diagnostics[3].severity,
2395 DiagnosticSeverity::Hint
2396 ));
2397 }
2398
2399 #[test]
2400 fn test_handle_cached_diagnostics_with_numeric_code() {
2401 let mut translator = Translator::new();
2402 let temp_dir = TempDir::new().unwrap();
2403 let test_file = temp_dir.path().join("test.rs");
2404 fs::write(&test_file, "fn main() {}").unwrap();
2405
2406 let canonical_path = test_file.canonicalize().unwrap();
2407 let uri: lsp_types::Uri = Url::from_file_path(&canonical_path)
2408 .unwrap()
2409 .as_str()
2410 .parse()
2411 .unwrap();
2412 let diagnostic = lsp_types::Diagnostic {
2413 range: lsp_types::Range {
2414 start: lsp_types::Position {
2415 line: 0,
2416 character: 0,
2417 },
2418 end: lsp_types::Position {
2419 line: 0,
2420 character: 5,
2421 },
2422 },
2423 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2424 message: "test error".to_string(),
2425 code: Some(lsp_types::NumberOrString::Number(42)),
2426 source: None,
2427 code_description: None,
2428 related_information: None,
2429 tags: None,
2430 data: None,
2431 };
2432
2433 translator
2434 .notification_cache_mut()
2435 .store_diagnostics(&uri, Some(1), vec![diagnostic]);
2436
2437 let result = translator.handle_cached_diagnostics(test_file.to_str().unwrap());
2438 assert!(result.is_ok());
2439 let diags = result.unwrap();
2440 assert_eq!(diags.diagnostics.len(), 1);
2441 assert_eq!(diags.diagnostics[0].code, Some("42".to_string()));
2442 }
2443
2444 #[test]
2445 fn test_handle_cached_diagnostics_invalid_path() {
2446 let mut translator = Translator::new();
2447 let result = translator.handle_cached_diagnostics("/nonexistent/path/file.rs");
2448 assert!(matches!(result, Err(Error::FileIo { .. })));
2449 }
2450
2451 #[test]
2452 fn test_handle_server_logs_no_filter() {
2453 use crate::bridge::notifications::LogLevel;
2454
2455 let mut translator = Translator::new();
2456
2457 translator
2458 .notification_cache_mut()
2459 .store_log(LogLevel::Error, "error msg".to_string());
2460 translator
2461 .notification_cache_mut()
2462 .store_log(LogLevel::Warning, "warning msg".to_string());
2463 translator
2464 .notification_cache_mut()
2465 .store_log(LogLevel::Info, "info msg".to_string());
2466 translator
2467 .notification_cache_mut()
2468 .store_log(LogLevel::Debug, "debug msg".to_string());
2469
2470 let result = translator.handle_server_logs(10, None);
2471 assert!(result.is_ok());
2472 let logs = result.unwrap();
2473 assert_eq!(logs.logs.len(), 4);
2474 }
2475
2476 #[test]
2477 fn test_handle_server_logs_error_filter_strict() {
2478 use crate::bridge::notifications::LogLevel;
2479
2480 let mut translator = Translator::new();
2481
2482 translator
2483 .notification_cache_mut()
2484 .store_log(LogLevel::Error, "error msg".to_string());
2485 translator
2486 .notification_cache_mut()
2487 .store_log(LogLevel::Warning, "warning msg".to_string());
2488 translator
2489 .notification_cache_mut()
2490 .store_log(LogLevel::Info, "info msg".to_string());
2491
2492 let result = translator.handle_server_logs(10, Some("error".to_string()));
2493 assert!(result.is_ok());
2494 let logs = result.unwrap();
2495 assert_eq!(logs.logs.len(), 1);
2496 assert_eq!(logs.logs[0].message, "error msg");
2497 }
2498
2499 #[test]
2500 fn test_handle_server_logs_warning_filter_includes_errors() {
2501 use crate::bridge::notifications::LogLevel;
2502
2503 let mut translator = Translator::new();
2504
2505 translator
2506 .notification_cache_mut()
2507 .store_log(LogLevel::Error, "error msg".to_string());
2508 translator
2509 .notification_cache_mut()
2510 .store_log(LogLevel::Warning, "warning msg".to_string());
2511 translator
2512 .notification_cache_mut()
2513 .store_log(LogLevel::Info, "info msg".to_string());
2514
2515 let result = translator.handle_server_logs(10, Some("warning".to_string()));
2516 assert!(result.is_ok());
2517 let logs = result.unwrap();
2518 assert_eq!(logs.logs.len(), 2);
2519 }
2520
2521 #[test]
2522 fn test_handle_server_logs_info_filter_excludes_debug() {
2523 use crate::bridge::notifications::LogLevel;
2524
2525 let mut translator = Translator::new();
2526
2527 translator
2528 .notification_cache_mut()
2529 .store_log(LogLevel::Error, "error msg".to_string());
2530 translator
2531 .notification_cache_mut()
2532 .store_log(LogLevel::Info, "info msg".to_string());
2533 translator
2534 .notification_cache_mut()
2535 .store_log(LogLevel::Debug, "debug msg".to_string());
2536
2537 let result = translator.handle_server_logs(10, Some("info".to_string()));
2538 assert!(result.is_ok());
2539 let logs = result.unwrap();
2540 assert_eq!(logs.logs.len(), 2);
2541 }
2542
2543 #[test]
2544 fn test_handle_server_logs_debug_filter_includes_all() {
2545 use crate::bridge::notifications::LogLevel;
2546
2547 let mut translator = Translator::new();
2548
2549 translator
2550 .notification_cache_mut()
2551 .store_log(LogLevel::Error, "error msg".to_string());
2552 translator
2553 .notification_cache_mut()
2554 .store_log(LogLevel::Warning, "warning msg".to_string());
2555 translator
2556 .notification_cache_mut()
2557 .store_log(LogLevel::Info, "info msg".to_string());
2558 translator
2559 .notification_cache_mut()
2560 .store_log(LogLevel::Debug, "debug msg".to_string());
2561
2562 let result = translator.handle_server_logs(10, Some("debug".to_string()));
2563 assert!(result.is_ok());
2564 let logs = result.unwrap();
2565 assert_eq!(logs.logs.len(), 4);
2566 }
2567
2568 #[test]
2569 fn test_handle_server_logs_limit_applies_after_filter() {
2570 use crate::bridge::notifications::LogLevel;
2571
2572 let mut translator = Translator::new();
2573
2574 for i in 0..10 {
2575 translator
2576 .notification_cache_mut()
2577 .store_log(LogLevel::Error, format!("error {i}"));
2578 }
2579
2580 let result = translator.handle_server_logs(5, Some("error".to_string()));
2581 assert!(result.is_ok());
2582 let logs = result.unwrap();
2583 assert_eq!(logs.logs.len(), 5);
2584 assert_eq!(logs.logs[0].message, "error 0");
2585 assert_eq!(logs.logs[4].message, "error 4");
2586 }
2587
2588 #[test]
2589 fn test_handle_server_logs_case_insensitive_level() {
2590 use crate::bridge::notifications::LogLevel;
2591
2592 let mut translator = Translator::new();
2593
2594 translator
2595 .notification_cache_mut()
2596 .store_log(LogLevel::Error, "error msg".to_string());
2597
2598 let result = translator.handle_server_logs(10, Some("ERROR".to_string()));
2599 assert!(result.is_ok());
2600
2601 let result = translator.handle_server_logs(10, Some("Error".to_string()));
2602 assert!(result.is_ok());
2603
2604 let result = translator.handle_server_logs(10, Some("eRrOr".to_string()));
2605 assert!(result.is_ok());
2606 }
2607
2608 #[test]
2609 fn test_handle_server_messages_empty() {
2610 let mut translator = Translator::new();
2611
2612 let result = translator.handle_server_messages(10);
2613 assert!(result.is_ok());
2614 let messages = result.unwrap();
2615 assert_eq!(messages.messages.len(), 0);
2616 }
2617
2618 #[test]
2619 fn test_handle_server_messages_with_different_types() {
2620 use crate::bridge::notifications::MessageType;
2621
2622 let mut translator = Translator::new();
2623
2624 translator
2625 .notification_cache_mut()
2626 .store_message(MessageType::Error, "error".to_string());
2627 translator
2628 .notification_cache_mut()
2629 .store_message(MessageType::Warning, "warning".to_string());
2630 translator
2631 .notification_cache_mut()
2632 .store_message(MessageType::Info, "info".to_string());
2633 translator
2634 .notification_cache_mut()
2635 .store_message(MessageType::Log, "log".to_string());
2636
2637 let result = translator.handle_server_messages(10);
2638 assert!(result.is_ok());
2639 let messages = result.unwrap();
2640 assert_eq!(messages.messages.len(), 4);
2641 assert_eq!(messages.messages[0].message, "error");
2642 assert_eq!(messages.messages[1].message, "warning");
2643 assert_eq!(messages.messages[2].message, "info");
2644 assert_eq!(messages.messages[3].message, "log");
2645 }
2646
2647 #[test]
2648 fn test_handle_server_messages_zero_limit() {
2649 use crate::bridge::notifications::MessageType;
2650
2651 let mut translator = Translator::new();
2652
2653 translator
2654 .notification_cache_mut()
2655 .store_message(MessageType::Info, "test".to_string());
2656
2657 let result = translator.handle_server_messages(0);
2658 assert!(result.is_ok());
2659 let messages = result.unwrap();
2660 assert_eq!(messages.messages.len(), 0);
2661 }
2662
2663 #[test]
2664 fn test_handle_cached_diagnostics_path_outside_workspace() {
2665 let mut translator = Translator::new();
2666 let temp_dir1 = TempDir::new().unwrap();
2667 let temp_dir2 = TempDir::new().unwrap();
2668
2669 translator.set_workspace_roots(vec![temp_dir1.path().to_path_buf()]);
2670
2671 let test_file = temp_dir2.path().join("test.rs");
2672 fs::write(&test_file, "fn main() {}").unwrap();
2673
2674 let result = translator.handle_cached_diagnostics(test_file.to_str().unwrap());
2675 assert!(matches!(result, Err(Error::PathOutsideWorkspace(_))));
2676 }
2677}