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