1use std::collections::{HashMap, HashSet};
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 InlayHintLabel, InlayHintParams, MarkedString, PartialResultParams, ReferenceContext,
13 ReferenceParams, RenameParams as LspRenameParams,
14 SignatureHelpParams as LspSignatureHelpParams, TextDocumentIdentifier,
15 TextDocumentPositionParams, WorkDoneProgressParams, WorkspaceEdit,
16 WorkspaceSymbolParams as LspWorkspaceSymbolParams,
17};
18use serde::{Deserialize, Serialize};
19use tokio::time::Duration;
20
21use super::state::{ResourceLimits, detect_language, path_to_uri};
22use super::{DocumentTracker, NotificationCache};
23use crate::bridge::encoding::mcp_to_lsp_position;
24use crate::error::{Error, Result};
25use crate::lsp::{LspClient, LspServer};
26
27#[derive(Debug)]
29pub struct Translator {
30 lsp_clients: HashMap<String, LspClient>,
32 lsp_servers: HashMap<String, LspServer>,
34 document_tracker: DocumentTracker,
36 notification_cache: NotificationCache,
38 workspace_roots: Vec<PathBuf>,
40 extension_map: HashMap<String, String>,
42 expected_languages: HashSet<String>,
46}
47
48impl Translator {
49 #[must_use]
51 pub fn new() -> Self {
52 Self {
53 lsp_clients: HashMap::new(),
54 lsp_servers: HashMap::new(),
55 document_tracker: DocumentTracker::new(ResourceLimits::default(), HashMap::new()),
56 notification_cache: NotificationCache::new(),
57 workspace_roots: vec![],
58 extension_map: HashMap::new(),
59 expected_languages: HashSet::new(),
60 }
61 }
62
63 pub fn set_workspace_roots(&mut self, roots: Vec<PathBuf>) {
65 self.workspace_roots = roots;
66 }
67
68 pub fn set_expected_languages(&mut self, languages: HashSet<String>) {
71 self.expected_languages = languages;
72 }
73
74 pub fn clear_expected_languages(&mut self) {
76 self.expected_languages.clear();
77 }
78
79 #[must_use]
84 pub fn with_extensions(mut self, extension_map: HashMap<String, String>) -> Self {
85 self.document_tracker =
86 DocumentTracker::new(ResourceLimits::default(), extension_map.clone());
87 self.extension_map = extension_map;
88 self
89 }
90
91 pub fn register_client(&mut self, language_id: String, client: LspClient) {
93 self.lsp_clients.insert(language_id, client);
94 }
95
96 pub fn register_server(&mut self, language_id: String, server: LspServer) {
98 self.lsp_servers.insert(language_id, server);
99 }
100
101 #[must_use]
103 pub const fn document_tracker(&self) -> &DocumentTracker {
104 &self.document_tracker
105 }
106
107 pub const fn document_tracker_mut(&mut self) -> &mut DocumentTracker {
109 &mut self.document_tracker
110 }
111
112 #[must_use]
114 pub const fn notification_cache(&self) -> &NotificationCache {
115 &self.notification_cache
116 }
117
118 pub const fn notification_cache_mut(&mut self) -> &mut NotificationCache {
120 &mut self.notification_cache
121 }
122
123 }
128
129impl Default for Translator {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135#[derive(Debug, Serialize)]
136#[serde(rename_all = "camelCase")]
137struct DiagnosticRequestParams {
138 text_document: TextDocumentIdentifier,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 identifier: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 previous_result_id: Option<String>,
143 #[serde(flatten)]
144 work_done_progress_params: WorkDoneProgressParams,
145 #[serde(flatten)]
146 partial_result_params: PartialResultParams,
147}
148
149fn diagnostic_request_params(text_document: TextDocumentIdentifier) -> DiagnosticRequestParams {
150 DiagnosticRequestParams {
151 text_document,
152 identifier: None,
153 previous_result_id: None,
154 work_done_progress_params: WorkDoneProgressParams::default(),
155 partial_result_params: PartialResultParams::default(),
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct Position2D {
162 pub line: u32,
164 pub character: u32,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct Range {
171 pub start: Position2D,
173 pub end: Position2D,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct Location {
180 pub uri: String,
182 pub range: Range,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct HoverResult {
189 pub contents: String,
191 pub range: Option<Range>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct DefinitionResult {
198 pub locations: Vec<Location>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ReferencesResult {
205 pub locations: Vec<Location>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211#[serde(rename_all = "lowercase")]
212pub enum DiagnosticSeverity {
213 Error,
215 Warning,
217 Information,
219 Hint,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct Diagnostic {
226 pub range: Range,
228 pub severity: DiagnosticSeverity,
230 pub message: String,
232 pub code: Option<String>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct DiagnosticsResult {
239 pub diagnostics: Vec<Diagnostic>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct TextEdit {
246 pub range: Range,
248 pub new_text: String,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct DocumentChanges {
255 pub uri: String,
257 pub edits: Vec<TextEdit>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct RenameResult {
264 pub changes: Vec<DocumentChanges>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct Completion {
271 pub label: String,
273 pub kind: Option<String>,
275 pub detail: Option<String>,
277 pub documentation: Option<String>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct CompletionsResult {
284 pub items: Vec<Completion>,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct Symbol {
291 pub name: String,
293 pub kind: String,
295 pub range: Range,
297 pub selection_range: Range,
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub children: Option<Vec<Self>>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct DocumentSymbolsResult {
307 pub symbols: Vec<Symbol>,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct FormatDocumentResult {
314 pub edits: Vec<TextEdit>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct WorkspaceSymbol {
321 pub name: String,
323 pub kind: String,
325 pub location: Location,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub container_name: Option<String>,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct WorkspaceSymbolResult {
335 pub symbols: Vec<WorkspaceSymbol>,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct CodeAction {
342 pub title: String,
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub kind: Option<String>,
347 #[serde(skip_serializing_if = "Vec::is_empty", default)]
349 pub diagnostics: Vec<Diagnostic>,
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub edit: Option<WorkspaceEditDescription>,
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub command: Option<CommandDescription>,
356 #[serde(default)]
358 pub is_preferred: bool,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct WorkspaceEditDescription {
364 pub changes: Vec<DocumentChanges>,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct CommandDescription {
371 pub title: String,
373 pub command: String,
375 #[serde(skip_serializing_if = "Vec::is_empty", default)]
377 pub arguments: Vec<serde_json::Value>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct CodeActionsResult {
383 pub actions: Vec<CodeAction>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct CallHierarchyItemResult {
390 pub name: String,
392 pub kind: u32,
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub detail: Option<String>,
397 pub uri: String,
399 pub range: Range,
401 #[serde(rename = "selectionRange")]
408 pub selection_range: Range,
409 #[serde(skip_serializing_if = "Option::is_none")]
411 pub data: Option<serde_json::Value>,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct CallHierarchyPrepareResult {
417 pub items: Vec<CallHierarchyItemResult>,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct IncomingCall {
424 pub from: CallHierarchyItemResult,
426 pub from_ranges: Vec<Range>,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct IncomingCallsResult {
433 pub calls: Vec<IncomingCall>,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct OutgoingCall {
440 pub to: CallHierarchyItemResult,
442 pub from_ranges: Vec<Range>,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct OutgoingCallsResult {
449 pub calls: Vec<OutgoingCall>,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct ServerLogsResult {
456 pub logs: Vec<crate::bridge::notifications::LogEntry>,
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct ServerMessagesResult {
463 pub messages: Vec<crate::bridge::notifications::ServerMessage>,
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct SignatureParameter {
470 pub label: String,
472 #[serde(skip_serializing_if = "Option::is_none")]
474 pub documentation: Option<String>,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct SignatureInfo {
480 pub label: String,
482 #[serde(skip_serializing_if = "Option::is_none")]
484 pub documentation: Option<String>,
485 pub parameters: Vec<SignatureParameter>,
487}
488
489#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct SignatureHelpResult {
492 pub signatures: Vec<SignatureInfo>,
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub active_signature: Option<u32>,
497 #[serde(skip_serializing_if = "Option::is_none")]
499 pub active_parameter: Option<u32>,
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct LocationsResult {
505 pub locations: Vec<Location>,
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct InlayHintEntry {
512 pub position: Position2D,
514 pub label: String,
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub kind: Option<u8>,
519 #[serde(skip_serializing_if = "Option::is_none")]
521 pub padding_left: Option<bool>,
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub padding_right: Option<bool>,
525 #[serde(skip_serializing_if = "Option::is_none")]
527 pub tooltip: Option<String>,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct InlayHintsResult {
533 pub hints: Vec<InlayHintEntry>,
535}
536
537const MAX_POSITION_VALUE: u32 = 1_000_000;
539const MAX_RANGE_LINES: u32 = 10_000;
541
542impl Translator {
543 pub(crate) fn validate_path(&self, path: &Path) -> Result<PathBuf> {
549 let canonical = path.canonicalize().map_err(|e| Error::FileIo {
550 path: path.to_path_buf(),
551 source: e,
552 })?;
553
554 if self.workspace_roots.is_empty() {
556 return Ok(canonical);
557 }
558
559 for root in &self.workspace_roots {
561 if let Ok(canonical_root) = root.canonicalize()
562 && canonical.starts_with(&canonical_root)
563 {
564 return Ok(canonical);
565 }
566 }
567
568 Err(Error::PathOutsideWorkspace(path.to_path_buf()))
569 }
570
571 fn get_client_for_file(&self, path: &Path) -> Result<LspClient> {
573 let language_id = detect_language(path, &self.extension_map);
574 self.lsp_clients.get(&language_id).cloned().ok_or_else(|| {
575 if self.expected_languages.contains(&language_id) {
580 Error::ServerInitializing(language_id)
581 } else {
582 Error::NoServerForLanguage(language_id)
583 }
584 })
585 }
586
587 fn parse_file_uri(&self, uri: &lsp_types::Uri) -> Result<PathBuf> {
595 let uri_str = uri.as_str();
596
597 if !uri_str.starts_with("file://") {
599 return Err(Error::InvalidToolParams(format!(
600 "Invalid URI scheme, expected file:// but got: {uri_str}"
601 )));
602 }
603
604 let path_str = &uri_str["file://".len()..];
606
607 #[cfg(windows)]
610 let path_str = if path_str.len() >= 3
611 && path_str.starts_with('/')
612 && path_str.chars().nth(2) == Some(':')
613 {
614 &path_str[1..]
615 } else {
616 path_str
617 };
618
619 let path = PathBuf::from(path_str);
620
621 self.validate_path(&path)
623 }
624
625 pub async fn handle_hover(
631 &mut self,
632 file_path: String,
633 line: u32,
634 character: u32,
635 ) -> Result<HoverResult> {
636 let path = PathBuf::from(&file_path);
637 let validated_path = self.validate_path(&path)?;
638 let client = self.get_client_for_file(&validated_path)?;
639 let uri = self
640 .document_tracker
641 .ensure_open(&validated_path, &client)
642 .await?;
643 let lsp_position = mcp_to_lsp_position(line, character);
644
645 let params = LspHoverParams {
646 text_document_position_params: TextDocumentPositionParams {
647 text_document: TextDocumentIdentifier { uri },
648 position: lsp_position,
649 },
650 work_done_progress_params: WorkDoneProgressParams::default(),
651 };
652
653 let timeout_duration = Duration::from_secs(30);
654 let response: Option<Hover> = client
655 .request("textDocument/hover", params, timeout_duration)
656 .await?;
657
658 let result = match response {
659 Some(hover) => {
660 let contents = extract_hover_contents(hover.contents);
661 let range = hover.range.map(normalize_range);
662 HoverResult { contents, range }
663 }
664 None => HoverResult {
665 contents: "No hover information available".to_string(),
666 range: None,
667 },
668 };
669
670 Ok(result)
671 }
672
673 pub async fn handle_definition(
679 &mut self,
680 file_path: String,
681 line: u32,
682 character: u32,
683 ) -> Result<DefinitionResult> {
684 let path = PathBuf::from(&file_path);
685 let validated_path = self.validate_path(&path)?;
686 let client = self.get_client_for_file(&validated_path)?;
687 let uri = self
688 .document_tracker
689 .ensure_open(&validated_path, &client)
690 .await?;
691 let lsp_position = mcp_to_lsp_position(line, character);
692
693 let params = GotoDefinitionParams {
694 text_document_position_params: TextDocumentPositionParams {
695 text_document: TextDocumentIdentifier { uri },
696 position: lsp_position,
697 },
698 work_done_progress_params: WorkDoneProgressParams::default(),
699 partial_result_params: PartialResultParams::default(),
700 };
701
702 let timeout_duration = Duration::from_secs(30);
703 let response: Option<lsp_types::GotoDefinitionResponse> = client
704 .request("textDocument/definition", params, timeout_duration)
705 .await?;
706
707 let locations = match response {
708 Some(lsp_types::GotoDefinitionResponse::Scalar(loc)) => vec![loc],
709 Some(lsp_types::GotoDefinitionResponse::Array(locs)) => locs,
710 Some(lsp_types::GotoDefinitionResponse::Link(links)) => links
711 .into_iter()
712 .map(|link| lsp_types::Location {
713 uri: link.target_uri,
714 range: link.target_selection_range,
715 })
716 .collect(),
717 None => vec![],
718 };
719
720 let result = DefinitionResult {
721 locations: locations
722 .into_iter()
723 .map(|loc| Location {
724 uri: loc.uri.to_string(),
725 range: normalize_range(loc.range),
726 })
727 .collect(),
728 };
729
730 Ok(result)
731 }
732
733 pub async fn handle_references(
739 &mut self,
740 file_path: String,
741 line: u32,
742 character: u32,
743 include_declaration: bool,
744 ) -> Result<ReferencesResult> {
745 let path = PathBuf::from(&file_path);
746 let validated_path = self.validate_path(&path)?;
747 let client = self.get_client_for_file(&validated_path)?;
748 let uri = self
749 .document_tracker
750 .ensure_open(&validated_path, &client)
751 .await?;
752 let lsp_position = mcp_to_lsp_position(line, character);
753
754 let params = ReferenceParams {
755 text_document_position: TextDocumentPositionParams {
756 text_document: TextDocumentIdentifier { uri },
757 position: lsp_position,
758 },
759 work_done_progress_params: WorkDoneProgressParams::default(),
760 partial_result_params: PartialResultParams::default(),
761 context: ReferenceContext {
762 include_declaration,
763 },
764 };
765
766 let timeout_duration = Duration::from_secs(30);
767 let response: Option<Vec<lsp_types::Location>> = client
768 .request("textDocument/references", params, timeout_duration)
769 .await?;
770
771 let locations = response.unwrap_or_default();
772
773 let result = ReferencesResult {
774 locations: locations
775 .into_iter()
776 .map(|loc| Location {
777 uri: loc.uri.to_string(),
778 range: normalize_range(loc.range),
779 })
780 .collect(),
781 };
782
783 Ok(result)
784 }
785
786 pub async fn handle_diagnostics(&mut self, file_path: String) -> Result<DiagnosticsResult> {
792 let path = PathBuf::from(&file_path);
793 let validated_path = self.validate_path(&path)?;
794 let client = self.get_client_for_file(&validated_path)?;
795 let uri = self
796 .document_tracker
797 .ensure_open(&validated_path, &client)
798 .await?;
799
800 let params = diagnostic_request_params(TextDocumentIdentifier { uri });
801
802 let timeout_duration = Duration::from_secs(30);
803 let response: lsp_types::DocumentDiagnosticReportResult = client
804 .request("textDocument/diagnostic", params, timeout_duration)
805 .await?;
806
807 let diagnostics = match response {
808 lsp_types::DocumentDiagnosticReportResult::Report(report) => match report {
809 lsp_types::DocumentDiagnosticReport::Full(full) => {
810 full.full_document_diagnostic_report.items
811 }
812 lsp_types::DocumentDiagnosticReport::Unchanged(_) => vec![],
813 },
814 lsp_types::DocumentDiagnosticReportResult::Partial(_) => vec![],
815 };
816
817 let result = DiagnosticsResult {
818 diagnostics: diagnostics
819 .into_iter()
820 .map(|diag| Diagnostic {
821 range: normalize_range(diag.range),
822 severity: match diag.severity {
823 Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
824 Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
825 Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
826 DiagnosticSeverity::Information
827 }
828 Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
829 _ => DiagnosticSeverity::Information,
830 },
831 message: diag.message,
832 code: diag.code.map(|c| match c {
833 lsp_types::NumberOrString::Number(n) => n.to_string(),
834 lsp_types::NumberOrString::String(s) => s,
835 }),
836 })
837 .collect(),
838 };
839
840 Ok(result)
841 }
842
843 pub async fn handle_rename(
849 &mut self,
850 file_path: String,
851 line: u32,
852 character: u32,
853 new_name: String,
854 ) -> Result<RenameResult> {
855 let path = PathBuf::from(&file_path);
856 let validated_path = self.validate_path(&path)?;
857 let client = self.get_client_for_file(&validated_path)?;
858 let uri = self
859 .document_tracker
860 .ensure_open(&validated_path, &client)
861 .await?;
862 let lsp_position = mcp_to_lsp_position(line, character);
863
864 let params = LspRenameParams {
865 text_document_position: TextDocumentPositionParams {
866 text_document: TextDocumentIdentifier { uri },
867 position: lsp_position,
868 },
869 new_name,
870 work_done_progress_params: WorkDoneProgressParams::default(),
871 };
872
873 let timeout_duration = Duration::from_secs(30);
874 let response: Option<WorkspaceEdit> = client
875 .request("textDocument/rename", params, timeout_duration)
876 .await?;
877
878 let changes = if let Some(edit) = response {
879 let mut result_changes = Vec::new();
880
881 if let Some(changes_map) = edit.changes {
883 for (uri, edits) in changes_map {
884 result_changes.push(DocumentChanges {
885 uri: uri.to_string(),
886 edits: edits
887 .into_iter()
888 .map(|e| TextEdit {
889 range: normalize_range(e.range),
890 new_text: e.new_text,
891 })
892 .collect(),
893 });
894 }
895 }
896
897 if result_changes.is_empty() {
899 let text_doc_edits = match edit.document_changes {
900 Some(lsp_types::DocumentChanges::Edits(edits)) => edits,
901 Some(lsp_types::DocumentChanges::Operations(ops)) => ops
902 .into_iter()
903 .filter_map(|op| match op {
904 lsp_types::DocumentChangeOperation::Edit(e) => Some(e),
905 lsp_types::DocumentChangeOperation::Op(_) => None,
906 })
907 .collect(),
908 None => vec![],
909 };
910 for tde in text_doc_edits {
911 result_changes.push(DocumentChanges {
912 uri: tde.text_document.uri.to_string(),
913 edits: tde
914 .edits
915 .into_iter()
916 .map(|one_of| match one_of {
917 lsp_types::OneOf::Left(te) => TextEdit {
918 range: normalize_range(te.range),
919 new_text: te.new_text,
920 },
921 lsp_types::OneOf::Right(ate) => TextEdit {
922 range: normalize_range(ate.text_edit.range),
923 new_text: ate.text_edit.new_text,
924 },
925 })
926 .collect(),
927 });
928 }
929 }
930
931 result_changes
932 } else {
933 vec![]
934 };
935
936 Ok(RenameResult { changes })
937 }
938
939 pub async fn handle_completions(
945 &mut self,
946 file_path: String,
947 line: u32,
948 character: u32,
949 trigger: Option<String>,
950 ) -> Result<CompletionsResult> {
951 let path = PathBuf::from(&file_path);
952 let validated_path = self.validate_path(&path)?;
953 let client = self.get_client_for_file(&validated_path)?;
954 let uri = self
955 .document_tracker
956 .ensure_open(&validated_path, &client)
957 .await?;
958 let lsp_position = mcp_to_lsp_position(line, character);
959
960 let context = trigger.map(|trigger_char| lsp_types::CompletionContext {
961 trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
962 trigger_character: Some(trigger_char),
963 });
964
965 let params = CompletionParams {
966 text_document_position: TextDocumentPositionParams {
967 text_document: TextDocumentIdentifier { uri },
968 position: lsp_position,
969 },
970 work_done_progress_params: WorkDoneProgressParams::default(),
971 partial_result_params: PartialResultParams::default(),
972 context,
973 };
974
975 let timeout_duration = Duration::from_secs(10);
976 let response: Option<lsp_types::CompletionResponse> = client
977 .request("textDocument/completion", params, timeout_duration)
978 .await?;
979
980 let items = match response {
981 Some(lsp_types::CompletionResponse::Array(items)) => items,
982 Some(lsp_types::CompletionResponse::List(list)) => list.items,
983 None => vec![],
984 };
985
986 let result = CompletionsResult {
987 items: items
988 .into_iter()
989 .map(|item| Completion {
990 label: item.label,
991 kind: item.kind.map(|k| format!("{k:?}")),
992 detail: item.detail,
993 documentation: item.documentation.map(|doc| match doc {
994 lsp_types::Documentation::String(s) => s,
995 lsp_types::Documentation::MarkupContent(m) => m.value,
996 }),
997 })
998 .collect(),
999 };
1000
1001 Ok(result)
1002 }
1003
1004 pub async fn handle_document_symbols(
1010 &mut self,
1011 file_path: String,
1012 ) -> Result<DocumentSymbolsResult> {
1013 let path = PathBuf::from(&file_path);
1014 let validated_path = self.validate_path(&path)?;
1015 let client = self.get_client_for_file(&validated_path)?;
1016 let uri = self
1017 .document_tracker
1018 .ensure_open(&validated_path, &client)
1019 .await?;
1020
1021 let params = DocumentSymbolParams {
1022 text_document: TextDocumentIdentifier { uri },
1023 work_done_progress_params: WorkDoneProgressParams::default(),
1024 partial_result_params: PartialResultParams::default(),
1025 };
1026
1027 let timeout_duration = Duration::from_secs(30);
1028 let response: Option<lsp_types::DocumentSymbolResponse> = client
1029 .request("textDocument/documentSymbol", params, timeout_duration)
1030 .await?;
1031
1032 let symbols = match response {
1033 Some(lsp_types::DocumentSymbolResponse::Flat(symbols)) => symbols
1034 .into_iter()
1035 .map(|sym| Symbol {
1036 name: sym.name,
1037 kind: format!("{:?}", sym.kind),
1038 range: normalize_range(sym.location.range),
1039 selection_range: normalize_range(sym.location.range),
1040 children: None,
1041 })
1042 .collect(),
1043 Some(lsp_types::DocumentSymbolResponse::Nested(symbols)) => {
1044 symbols.into_iter().map(convert_document_symbol).collect()
1045 }
1046 None => vec![],
1047 };
1048
1049 Ok(DocumentSymbolsResult { symbols })
1050 }
1051
1052 pub async fn handle_format_document(
1058 &mut self,
1059 file_path: String,
1060 tab_size: u32,
1061 insert_spaces: bool,
1062 ) -> Result<FormatDocumentResult> {
1063 let path = PathBuf::from(&file_path);
1064 let validated_path = self.validate_path(&path)?;
1065 let client = self.get_client_for_file(&validated_path)?;
1066 let uri = self
1067 .document_tracker
1068 .ensure_open(&validated_path, &client)
1069 .await?;
1070
1071 let params = DocumentFormattingParams {
1072 text_document: TextDocumentIdentifier { uri },
1073 options: FormattingOptions {
1074 tab_size,
1075 insert_spaces,
1076 ..Default::default()
1077 },
1078 work_done_progress_params: WorkDoneProgressParams::default(),
1079 };
1080
1081 let timeout_duration = Duration::from_secs(30);
1082 let response: Option<Vec<lsp_types::TextEdit>> = client
1083 .request("textDocument/formatting", params, timeout_duration)
1084 .await?;
1085
1086 let edits = response.unwrap_or_default();
1087
1088 let result = FormatDocumentResult {
1089 edits: edits
1090 .into_iter()
1091 .map(|edit| TextEdit {
1092 range: normalize_range(edit.range),
1093 new_text: edit.new_text,
1094 })
1095 .collect(),
1096 };
1097
1098 Ok(result)
1099 }
1100
1101 pub async fn handle_workspace_symbol(
1107 &mut self,
1108 query: String,
1109 kind_filter: Option<String>,
1110 limit: u32,
1111 ) -> Result<WorkspaceSymbolResult> {
1112 const MAX_QUERY_LENGTH: usize = 1000;
1113 const VALID_SYMBOL_KINDS: &[&str] = &[
1114 "File",
1115 "Module",
1116 "Namespace",
1117 "Package",
1118 "Class",
1119 "Method",
1120 "Property",
1121 "Field",
1122 "Constructor",
1123 "Enum",
1124 "Interface",
1125 "Function",
1126 "Variable",
1127 "Constant",
1128 "String",
1129 "Number",
1130 "Boolean",
1131 "Array",
1132 "Object",
1133 "Key",
1134 "Null",
1135 "EnumMember",
1136 "Struct",
1137 "Event",
1138 "Operator",
1139 "TypeParameter",
1140 ];
1141
1142 if query.len() > MAX_QUERY_LENGTH {
1144 return Err(Error::InvalidToolParams(format!(
1145 "Query too long: {} chars (max {MAX_QUERY_LENGTH})",
1146 query.len()
1147 )));
1148 }
1149
1150 if let Some(ref kind) = kind_filter
1152 && !VALID_SYMBOL_KINDS
1153 .iter()
1154 .any(|k| k.eq_ignore_ascii_case(kind))
1155 {
1156 return Err(Error::InvalidToolParams(format!(
1157 "Invalid kind_filter: '{kind}'. Valid values: {VALID_SYMBOL_KINDS:?}"
1158 )));
1159 }
1160
1161 let client = self.lsp_clients.values().next().cloned().ok_or_else(|| {
1165 self.expected_languages
1166 .iter()
1167 .next()
1168 .map_or(Error::NoServerConfigured, |lang| {
1169 Error::ServerInitializing(lang.clone())
1170 })
1171 })?;
1172
1173 let params = LspWorkspaceSymbolParams {
1174 query,
1175 work_done_progress_params: WorkDoneProgressParams::default(),
1176 partial_result_params: PartialResultParams::default(),
1177 };
1178
1179 let timeout_duration = Duration::from_secs(30);
1180 let response: Option<Vec<lsp_types::SymbolInformation>> = client
1181 .request("workspace/symbol", params, timeout_duration)
1182 .await?;
1183
1184 let mut symbols: Vec<WorkspaceSymbol> = response
1185 .unwrap_or_default()
1186 .into_iter()
1187 .map(|sym| WorkspaceSymbol {
1188 name: sym.name,
1189 kind: format!("{:?}", sym.kind),
1190 location: Location {
1191 uri: sym.location.uri.to_string(),
1192 range: normalize_range(sym.location.range),
1193 },
1194 container_name: sym.container_name,
1195 })
1196 .collect();
1197
1198 if let Some(kind) = kind_filter {
1200 symbols.retain(|s| s.kind.eq_ignore_ascii_case(&kind));
1201 }
1202
1203 symbols.truncate(limit as usize);
1205
1206 Ok(WorkspaceSymbolResult { symbols })
1207 }
1208
1209 pub async fn handle_code_actions(
1215 &mut self,
1216 file_path: String,
1217 start_line: u32,
1218 start_character: u32,
1219 end_line: u32,
1220 end_character: u32,
1221 kind_filter: Option<String>,
1222 ) -> Result<CodeActionsResult> {
1223 validate_code_action_params(
1224 start_line,
1225 start_character,
1226 end_line,
1227 end_character,
1228 kind_filter.as_deref(),
1229 )?;
1230
1231 let path = PathBuf::from(&file_path);
1232 let validated_path = self.validate_path(&path)?;
1233 let client = self.get_client_for_file(&validated_path)?;
1234 let uri = self
1235 .document_tracker
1236 .ensure_open(&validated_path, &client)
1237 .await?;
1238
1239 let range = lsp_types::Range {
1240 start: mcp_to_lsp_position(start_line, start_character),
1241 end: mcp_to_lsp_position(end_line, end_character),
1242 };
1243
1244 let only = kind_filter.map(|k| vec![lsp_types::CodeActionKind::from(k)]);
1246
1247 let context_diagnostics: Vec<lsp_types::Diagnostic> = vec![];
1252
1253 let params = lsp_types::CodeActionParams {
1254 text_document: TextDocumentIdentifier { uri },
1255 range,
1256 context: lsp_types::CodeActionContext {
1257 diagnostics: context_diagnostics,
1258 only,
1259 trigger_kind: Some(lsp_types::CodeActionTriggerKind::INVOKED),
1260 },
1261 work_done_progress_params: WorkDoneProgressParams::default(),
1262 partial_result_params: PartialResultParams::default(),
1263 };
1264
1265 let timeout_duration = Duration::from_secs(30);
1266 let response: Option<lsp_types::CodeActionResponse> = client
1267 .request("textDocument/codeAction", params, timeout_duration)
1268 .await?;
1269 let response_vec = response.unwrap_or_default();
1270 let mut actions = Vec::with_capacity(response_vec.len());
1271
1272 for action_or_command in response_vec {
1273 let action = match action_or_command {
1274 lsp_types::CodeActionOrCommand::CodeAction(action) => convert_code_action(action),
1275 lsp_types::CodeActionOrCommand::Command(cmd) => {
1276 let arguments = cmd.arguments.unwrap_or_else(Vec::new);
1277 CodeAction {
1278 title: cmd.title.clone(),
1279 kind: None,
1280 diagnostics: Vec::new(),
1281 edit: None,
1282 command: Some(CommandDescription {
1283 title: cmd.title,
1284 command: cmd.command,
1285 arguments,
1286 }),
1287 is_preferred: false,
1288 }
1289 }
1290 };
1291 actions.push(action);
1292 }
1293
1294 Ok(CodeActionsResult { actions })
1295 }
1296
1297 pub async fn handle_call_hierarchy_prepare(
1303 &mut self,
1304 file_path: String,
1305 line: u32,
1306 character: u32,
1307 ) -> Result<CallHierarchyPrepareResult> {
1308 if line < 1 || character < 1 {
1310 return Err(Error::InvalidToolParams(
1311 "Line and character positions must be >= 1".to_string(),
1312 ));
1313 }
1314
1315 if line > MAX_POSITION_VALUE || character > MAX_POSITION_VALUE {
1316 return Err(Error::InvalidToolParams(format!(
1317 "Position values must be <= {MAX_POSITION_VALUE}"
1318 )));
1319 }
1320
1321 let path = PathBuf::from(&file_path);
1322 let validated_path = self.validate_path(&path)?;
1323 let client = self.get_client_for_file(&validated_path)?;
1324 let uri = self
1325 .document_tracker
1326 .ensure_open(&validated_path, &client)
1327 .await?;
1328 let lsp_position = mcp_to_lsp_position(line, character);
1329
1330 let params = LspCallHierarchyPrepareParams {
1331 text_document_position_params: TextDocumentPositionParams {
1332 text_document: TextDocumentIdentifier { uri },
1333 position: lsp_position,
1334 },
1335 work_done_progress_params: WorkDoneProgressParams::default(),
1336 };
1337
1338 let timeout_duration = Duration::from_secs(30);
1339 let response: Option<Vec<CallHierarchyItem>> = client
1340 .request(
1341 "textDocument/prepareCallHierarchy",
1342 params,
1343 timeout_duration,
1344 )
1345 .await?;
1346
1347 let lsp_items = response.unwrap_or_default();
1349 let mut items = Vec::with_capacity(lsp_items.len());
1350 for item in lsp_items {
1351 items.push(convert_call_hierarchy_item(item));
1352 }
1353
1354 Ok(CallHierarchyPrepareResult { items })
1355 }
1356
1357 pub async fn handle_incoming_calls(
1363 &mut self,
1364 item: serde_json::Value,
1365 ) -> Result<IncomingCallsResult> {
1366 let lsp_item = mcp_item_to_lsp(item)?;
1368
1369 let path = self.parse_file_uri(&lsp_item.uri)?;
1371 let client = self.get_client_for_file(&path)?;
1372
1373 let params = CallHierarchyIncomingCallsParams {
1374 item: lsp_item,
1375 work_done_progress_params: WorkDoneProgressParams::default(),
1376 partial_result_params: PartialResultParams::default(),
1377 };
1378
1379 let timeout_duration = Duration::from_secs(30);
1380 let response: Option<Vec<CallHierarchyIncomingCall>> = client
1381 .request("callHierarchy/incomingCalls", params, timeout_duration)
1382 .await?;
1383
1384 let lsp_calls = response.unwrap_or_default();
1386 let mut calls = Vec::with_capacity(lsp_calls.len());
1387
1388 for call in lsp_calls {
1389 let from_ranges = {
1390 let mut ranges = Vec::with_capacity(call.from_ranges.len());
1391 for range in call.from_ranges {
1392 ranges.push(normalize_range(range));
1393 }
1394 ranges
1395 };
1396
1397 calls.push(IncomingCall {
1398 from: convert_call_hierarchy_item(call.from),
1399 from_ranges,
1400 });
1401 }
1402
1403 Ok(IncomingCallsResult { calls })
1404 }
1405
1406 pub async fn handle_outgoing_calls(
1412 &mut self,
1413 item: serde_json::Value,
1414 ) -> Result<OutgoingCallsResult> {
1415 let lsp_item = mcp_item_to_lsp(item)?;
1417
1418 let path = self.parse_file_uri(&lsp_item.uri)?;
1420 let client = self.get_client_for_file(&path)?;
1421
1422 let params = CallHierarchyOutgoingCallsParams {
1423 item: lsp_item,
1424 work_done_progress_params: WorkDoneProgressParams::default(),
1425 partial_result_params: PartialResultParams::default(),
1426 };
1427
1428 let timeout_duration = Duration::from_secs(30);
1429 let response: Option<Vec<CallHierarchyOutgoingCall>> = client
1430 .request("callHierarchy/outgoingCalls", params, timeout_duration)
1431 .await?;
1432
1433 let lsp_calls = response.unwrap_or_default();
1435 let mut calls = Vec::with_capacity(lsp_calls.len());
1436
1437 for call in lsp_calls {
1438 let from_ranges = {
1439 let mut ranges = Vec::with_capacity(call.from_ranges.len());
1440 for range in call.from_ranges {
1441 ranges.push(normalize_range(range));
1442 }
1443 ranges
1444 };
1445
1446 calls.push(OutgoingCall {
1447 to: convert_call_hierarchy_item(call.to),
1448 from_ranges,
1449 });
1450 }
1451
1452 Ok(OutgoingCallsResult { calls })
1453 }
1454
1455 pub fn handle_cached_diagnostics(&mut self, file_path: &str) -> Result<DiagnosticsResult> {
1461 let path = PathBuf::from(file_path);
1462 let validated_path = self.validate_path(&path)?;
1463
1464 let uri = path_to_uri(&validated_path).to_string();
1467
1468 let diagnostics =
1469 self.notification_cache
1470 .get_diagnostics(&uri)
1471 .map_or_else(Vec::new, |diag_info| {
1472 diag_info
1473 .diagnostics
1474 .iter()
1475 .map(|diag| Diagnostic {
1476 range: normalize_range(diag.range),
1477 severity: match diag.severity {
1478 Some(lsp_types::DiagnosticSeverity::ERROR) => {
1479 DiagnosticSeverity::Error
1480 }
1481 Some(lsp_types::DiagnosticSeverity::WARNING) => {
1482 DiagnosticSeverity::Warning
1483 }
1484 Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
1485 DiagnosticSeverity::Information
1486 }
1487 Some(lsp_types::DiagnosticSeverity::HINT) => {
1488 DiagnosticSeverity::Hint
1489 }
1490 _ => DiagnosticSeverity::Information,
1491 },
1492 message: diag.message.clone(),
1493 code: diag.code.as_ref().map(|c| match c {
1494 lsp_types::NumberOrString::Number(n) => n.to_string(),
1495 lsp_types::NumberOrString::String(s) => s.clone(),
1496 }),
1497 })
1498 .collect()
1499 });
1500
1501 Ok(DiagnosticsResult { diagnostics })
1502 }
1503
1504 pub fn handle_server_logs(
1510 &mut self,
1511 limit: usize,
1512 min_level: Option<String>,
1513 ) -> Result<ServerLogsResult> {
1514 use crate::bridge::notifications::LogLevel;
1515
1516 let min_level_filter = if let Some(level_str) = min_level {
1517 let level = match level_str.to_lowercase().as_str() {
1518 "error" => LogLevel::Error,
1519 "warning" => LogLevel::Warning,
1520 "info" => LogLevel::Info,
1521 "debug" => LogLevel::Debug,
1522 _ => {
1523 return Err(Error::InvalidToolParams(format!(
1524 "Invalid min_level: '{level_str}'. Valid values: error, warning, info, debug"
1525 )));
1526 }
1527 };
1528 Some(level)
1529 } else {
1530 None
1531 };
1532
1533 let all_logs = self.notification_cache.get_logs();
1534
1535 let logs: Vec<_> = all_logs
1536 .iter()
1537 .filter(|log| {
1538 min_level_filter.is_none_or(|min| match min {
1539 LogLevel::Error => matches!(log.level, LogLevel::Error),
1540 LogLevel::Warning => matches!(log.level, LogLevel::Error | LogLevel::Warning),
1541 LogLevel::Info => !matches!(log.level, LogLevel::Debug),
1542 LogLevel::Debug => true,
1543 })
1544 })
1545 .take(limit)
1546 .cloned()
1547 .collect();
1548
1549 Ok(ServerLogsResult { logs })
1550 }
1551
1552 pub fn handle_server_messages(&mut self, limit: usize) -> Result<ServerMessagesResult> {
1558 let all_messages = self.notification_cache.get_messages();
1559 let messages: Vec<_> = all_messages.iter().take(limit).cloned().collect();
1560 Ok(ServerMessagesResult { messages })
1561 }
1562
1563 pub async fn handle_signature_help(
1572 &mut self,
1573 file_path: String,
1574 line: u32,
1575 character: u32,
1576 ) -> Result<SignatureHelpResult> {
1577 let path = PathBuf::from(&file_path);
1578 let validated_path = self.validate_path(&path)?;
1579 let client = self.get_client_for_file(&validated_path)?;
1580 let uri = self
1581 .document_tracker
1582 .ensure_open(&validated_path, &client)
1583 .await?;
1584 let lsp_position = mcp_to_lsp_position(line, character);
1585
1586 let params = LspSignatureHelpParams {
1587 text_document_position_params: TextDocumentPositionParams {
1588 text_document: TextDocumentIdentifier { uri },
1589 position: lsp_position,
1590 },
1591 work_done_progress_params: WorkDoneProgressParams::default(),
1592 context: None,
1593 };
1594
1595 let timeout_duration = Duration::from_secs(30);
1596 let response: Option<lsp_types::SignatureHelp> = client
1597 .request("textDocument/signatureHelp", params, timeout_duration)
1598 .await?;
1599
1600 let result = match response {
1601 Some(sig_help) => SignatureHelpResult {
1602 signatures: sig_help
1603 .signatures
1604 .into_iter()
1605 .map(|sig| SignatureInfo {
1606 label: sig.label,
1607 documentation: sig.documentation.map(extract_documentation),
1608 parameters: sig
1609 .parameters
1610 .unwrap_or_default()
1611 .into_iter()
1612 .map(|p| SignatureParameter {
1613 label: match p.label {
1614 lsp_types::ParameterLabel::Simple(s) => s,
1615 lsp_types::ParameterLabel::LabelOffsets([start, end]) => {
1616 format!("[{start},{end}]")
1617 }
1618 },
1619 documentation: p.documentation.map(extract_documentation),
1620 })
1621 .collect(),
1622 })
1623 .collect(),
1624 active_signature: sig_help.active_signature,
1625 active_parameter: sig_help.active_parameter,
1626 },
1627 None => SignatureHelpResult {
1628 signatures: vec![],
1629 active_signature: None,
1630 active_parameter: None,
1631 },
1632 };
1633
1634 Ok(result)
1635 }
1636
1637 pub async fn handle_implementation(
1645 &mut self,
1646 file_path: String,
1647 line: u32,
1648 character: u32,
1649 ) -> Result<LocationsResult> {
1650 let path = PathBuf::from(&file_path);
1651 let validated_path = self.validate_path(&path)?;
1652 let client = self.get_client_for_file(&validated_path)?;
1653 let uri = self
1654 .document_tracker
1655 .ensure_open(&validated_path, &client)
1656 .await?;
1657 let lsp_position = mcp_to_lsp_position(line, character);
1658
1659 let params = GotoDefinitionParams {
1660 text_document_position_params: TextDocumentPositionParams {
1661 text_document: TextDocumentIdentifier { uri },
1662 position: lsp_position,
1663 },
1664 work_done_progress_params: WorkDoneProgressParams::default(),
1665 partial_result_params: PartialResultParams::default(),
1666 };
1667
1668 let timeout_duration = Duration::from_secs(30);
1669 let response: Option<lsp_types::GotoDefinitionResponse> = client
1670 .request("textDocument/implementation", params, timeout_duration)
1671 .await?;
1672
1673 Ok(LocationsResult {
1674 locations: goto_response_to_locations(response),
1675 })
1676 }
1677
1678 pub async fn handle_type_definition(
1687 &mut self,
1688 file_path: String,
1689 line: u32,
1690 character: u32,
1691 ) -> Result<LocationsResult> {
1692 let path = PathBuf::from(&file_path);
1693 let validated_path = self.validate_path(&path)?;
1694 let client = self.get_client_for_file(&validated_path)?;
1695 let uri = self
1696 .document_tracker
1697 .ensure_open(&validated_path, &client)
1698 .await?;
1699 let lsp_position = mcp_to_lsp_position(line, character);
1700
1701 let params = GotoDefinitionParams {
1702 text_document_position_params: TextDocumentPositionParams {
1703 text_document: TextDocumentIdentifier { uri },
1704 position: lsp_position,
1705 },
1706 work_done_progress_params: WorkDoneProgressParams::default(),
1707 partial_result_params: PartialResultParams::default(),
1708 };
1709
1710 let timeout_duration = Duration::from_secs(30);
1711 let response: Option<lsp_types::GotoDefinitionResponse> = client
1712 .request("textDocument/typeDefinition", params, timeout_duration)
1713 .await?;
1714
1715 Ok(LocationsResult {
1716 locations: goto_response_to_locations(response),
1717 })
1718 }
1719
1720 pub async fn handle_inlay_hints(
1729 &mut self,
1730 file_path: String,
1731 start_line: u32,
1732 start_character: u32,
1733 end_line: u32,
1734 end_character: u32,
1735 ) -> Result<InlayHintsResult> {
1736 use crate::bridge::encoding::lsp_to_mcp_position;
1737
1738 let path = PathBuf::from(&file_path);
1739 let validated_path = self.validate_path(&path)?;
1740 let client = self.get_client_for_file(&validated_path)?;
1741 let uri = self
1742 .document_tracker
1743 .ensure_open(&validated_path, &client)
1744 .await?;
1745
1746 let lsp_start = mcp_to_lsp_position(start_line, start_character);
1747 let lsp_end = mcp_to_lsp_position(end_line, end_character);
1748
1749 let params = InlayHintParams {
1750 text_document: TextDocumentIdentifier { uri },
1751 range: lsp_types::Range {
1752 start: lsp_start,
1753 end: lsp_end,
1754 },
1755 work_done_progress_params: WorkDoneProgressParams::default(),
1756 };
1757
1758 let timeout_duration = Duration::from_secs(30);
1759 let response: Option<Vec<lsp_types::InlayHint>> = client
1760 .request("textDocument/inlayHint", params, timeout_duration)
1761 .await?;
1762
1763 let hints = response
1764 .unwrap_or_default()
1765 .into_iter()
1766 .map(|hint| {
1767 let (mcp_line, mcp_character) = lsp_to_mcp_position(hint.position);
1768 let label = match hint.label {
1769 InlayHintLabel::String(s) => s,
1770 InlayHintLabel::LabelParts(parts) => parts
1771 .into_iter()
1772 .map(|p| p.value)
1773 .collect::<Vec<_>>()
1774 .concat(),
1775 };
1776 let tooltip = hint.tooltip.map(|t| match t {
1777 lsp_types::InlayHintTooltip::String(s) => s,
1778 lsp_types::InlayHintTooltip::MarkupContent(m) => m.value,
1779 });
1780 InlayHintEntry {
1781 position: Position2D {
1782 line: mcp_line,
1783 character: mcp_character,
1784 },
1785 label,
1786 kind: hint.kind.and_then(|k| {
1787 serde_json::to_value(k)
1788 .ok()
1789 .and_then(|v| v.as_i64())
1790 .and_then(|n| u8::try_from(n).ok())
1791 }),
1792 padding_left: hint.padding_left,
1793 padding_right: hint.padding_right,
1794 tooltip,
1795 }
1796 })
1797 .collect();
1798
1799 Ok(InlayHintsResult { hints })
1800 }
1801}
1802
1803fn extract_documentation(doc: lsp_types::Documentation) -> String {
1806 match doc {
1807 lsp_types::Documentation::String(s) => s,
1808 lsp_types::Documentation::MarkupContent(m) => m.value,
1809 }
1810}
1811
1812fn goto_response_to_locations(
1814 response: Option<lsp_types::GotoDefinitionResponse>,
1815) -> Vec<Location> {
1816 let lsp_locs: Vec<lsp_types::Location> = match response {
1817 Some(lsp_types::GotoDefinitionResponse::Scalar(loc)) => vec![loc],
1818 Some(lsp_types::GotoDefinitionResponse::Array(locs)) => locs,
1819 Some(lsp_types::GotoDefinitionResponse::Link(links)) => links
1820 .into_iter()
1821 .map(|link| lsp_types::Location {
1822 uri: link.target_uri,
1823 range: link.target_selection_range,
1824 })
1825 .collect(),
1826 None => vec![],
1827 };
1828
1829 lsp_locs
1830 .into_iter()
1831 .map(|loc| Location {
1832 uri: loc.uri.to_string(),
1833 range: normalize_range(loc.range),
1834 })
1835 .collect()
1836}
1837
1838fn extract_hover_contents(contents: HoverContents) -> String {
1839 match contents {
1840 HoverContents::Scalar(marked_string) => marked_string_to_string(marked_string),
1841 HoverContents::Array(marked_strings) => marked_strings
1842 .into_iter()
1843 .map(marked_string_to_string)
1844 .collect::<Vec<_>>()
1845 .join("\n\n"),
1846 HoverContents::Markup(markup) => markup.value,
1847 }
1848}
1849
1850fn marked_string_to_string(marked: MarkedString) -> String {
1852 match marked {
1853 MarkedString::String(s) => s,
1854 MarkedString::LanguageString(ls) => format!("```{}\n{}\n```", ls.language, ls.value),
1855 }
1856}
1857
1858fn validate_code_action_params(
1861 start_line: u32,
1862 start_character: u32,
1863 end_line: u32,
1864 end_character: u32,
1865 kind_filter: Option<&str>,
1866) -> Result<()> {
1867 const VALID_ACTION_KINDS: &[&str] = &[
1868 "quickfix",
1869 "refactor",
1870 "refactor.extract",
1871 "refactor.inline",
1872 "refactor.rewrite",
1873 "source",
1874 "source.organizeImports",
1875 ];
1876
1877 if let Some(kind) = kind_filter
1878 && !VALID_ACTION_KINDS
1879 .iter()
1880 .any(|k| k.eq_ignore_ascii_case(kind))
1881 {
1882 return Err(Error::InvalidToolParams(format!(
1883 "Invalid kind_filter: '{kind}'. Valid values: {VALID_ACTION_KINDS:?}"
1884 )));
1885 }
1886
1887 if start_line < 1 || start_character < 1 || end_line < 1 || end_character < 1 {
1888 return Err(Error::InvalidToolParams(
1889 "Line and character positions must be >= 1".to_string(),
1890 ));
1891 }
1892
1893 if start_line > MAX_POSITION_VALUE
1894 || start_character > MAX_POSITION_VALUE
1895 || end_line > MAX_POSITION_VALUE
1896 || end_character > MAX_POSITION_VALUE
1897 {
1898 return Err(Error::InvalidToolParams(format!(
1899 "Position values must be <= {MAX_POSITION_VALUE}"
1900 )));
1901 }
1902
1903 if end_line.saturating_sub(start_line) > MAX_RANGE_LINES {
1904 return Err(Error::InvalidToolParams(format!(
1905 "Range size must be <= {MAX_RANGE_LINES} lines"
1906 )));
1907 }
1908
1909 if start_line > end_line || (start_line == end_line && start_character > end_character) {
1910 return Err(Error::InvalidToolParams(
1911 "Start position must be before or equal to end position".to_string(),
1912 ));
1913 }
1914
1915 Ok(())
1916}
1917
1918fn mcp_item_to_lsp(item: serde_json::Value) -> Result<CallHierarchyItem> {
1926 let mcp: CallHierarchyItemResult = serde_json::from_value(item)
1927 .map_err(|e| Error::InvalidToolParams(format!("Invalid call hierarchy item: {e}")))?;
1928
1929 let uri = mcp.uri.parse::<lsp_types::Uri>().map_err(|e| {
1930 Error::InvalidToolParams(format!("Invalid URI in call hierarchy item: {e}"))
1931 })?;
1932
1933 let detail = mcp.detail;
1934 let data = mcp.data;
1935
1936 let kind: lsp_types::SymbolKind = serde_json::from_value(serde_json::json!(mcp.kind))
1939 .unwrap_or(lsp_types::SymbolKind::FUNCTION);
1940
1941 Ok(CallHierarchyItem {
1942 name: mcp.name,
1943 kind,
1944 tags: None,
1945 detail,
1946 uri,
1947 range: denormalize_range(&mcp.range),
1948 selection_range: denormalize_range(&mcp.selection_range),
1949 data,
1950 })
1951}
1952
1953const fn denormalize_range(range: &Range) -> lsp_types::Range {
1958 lsp_types::Range {
1959 start: lsp_types::Position {
1960 line: range.start.line.saturating_sub(1),
1961 character: range.start.character.saturating_sub(1),
1962 },
1963 end: lsp_types::Position {
1964 line: range.end.line.saturating_sub(1),
1965 character: range.end.character.saturating_sub(1),
1966 },
1967 }
1968}
1969
1970const fn normalize_range(range: lsp_types::Range) -> Range {
1971 Range {
1972 start: Position2D {
1973 line: range.start.line + 1,
1974 character: range.start.character + 1,
1975 },
1976 end: Position2D {
1977 line: range.end.line + 1,
1978 character: range.end.character + 1,
1979 },
1980 }
1981}
1982
1983fn convert_document_symbol(symbol: DocumentSymbol) -> Symbol {
1985 Symbol {
1986 name: symbol.name,
1987 kind: format!("{:?}", symbol.kind),
1988 range: normalize_range(symbol.range),
1989 selection_range: normalize_range(symbol.selection_range),
1990 children: symbol
1991 .children
1992 .map(|children| children.into_iter().map(convert_document_symbol).collect()),
1993 }
1994}
1995
1996fn convert_call_hierarchy_item(item: CallHierarchyItem) -> CallHierarchyItemResult {
1998 CallHierarchyItemResult {
1999 name: item.name,
2000 kind: serde_json::to_value(item.kind)
2001 .ok()
2002 .and_then(|v| v.as_u64())
2003 .and_then(|n| u32::try_from(n).ok())
2004 .unwrap_or(0),
2005 detail: item.detail,
2006 uri: item.uri.to_string(),
2007 range: normalize_range(item.range),
2008 selection_range: normalize_range(item.selection_range),
2009 data: item.data,
2010 }
2011}
2012
2013fn convert_code_action(action: lsp_types::CodeAction) -> CodeAction {
2015 let diagnostics = action.diagnostics.map_or_else(Vec::new, |diags| {
2016 let mut result = Vec::with_capacity(diags.len());
2017 for d in diags {
2018 result.push(Diagnostic {
2019 range: normalize_range(d.range),
2020 severity: match d.severity {
2021 Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
2022 Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
2023 Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
2024 DiagnosticSeverity::Information
2025 }
2026 Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
2027 _ => DiagnosticSeverity::Information,
2028 },
2029 message: d.message,
2030 code: d.code.map(|c| match c {
2031 lsp_types::NumberOrString::Number(n) => n.to_string(),
2032 lsp_types::NumberOrString::String(s) => s,
2033 }),
2034 });
2035 }
2036 result
2037 });
2038
2039 let edit = action.edit.map(|edit| {
2040 let changes = edit.changes.map_or_else(Vec::new, |changes_map| {
2041 let mut result = Vec::with_capacity(changes_map.len());
2042 for (uri, edits) in changes_map {
2043 let mut text_edits = Vec::with_capacity(edits.len());
2044 for e in edits {
2045 text_edits.push(TextEdit {
2046 range: normalize_range(e.range),
2047 new_text: e.new_text,
2048 });
2049 }
2050 result.push(DocumentChanges {
2051 uri: uri.to_string(),
2052 edits: text_edits,
2053 });
2054 }
2055 result
2056 });
2057 WorkspaceEditDescription { changes }
2058 });
2059
2060 let command = action.command.map(|cmd| {
2061 let arguments = cmd.arguments.unwrap_or_else(Vec::new);
2062 CommandDescription {
2063 title: cmd.title,
2064 command: cmd.command,
2065 arguments,
2066 }
2067 });
2068
2069 CodeAction {
2070 title: action.title,
2071 kind: action.kind.map(|k| k.as_str().to_string()),
2072 diagnostics,
2073 edit,
2074 command,
2075 is_preferred: action.is_preferred.unwrap_or(false),
2076 }
2077}
2078
2079#[cfg(test)]
2080#[allow(clippy::unwrap_used)]
2081mod tests {
2082 use std::fs;
2083
2084 use tempfile::TempDir;
2085 use url::Url;
2086
2087 use super::*;
2088
2089 #[test]
2090 fn test_translator_new() {
2091 let translator = Translator::new();
2092 assert_eq!(translator.workspace_roots.len(), 0);
2093 assert_eq!(translator.lsp_clients.len(), 0);
2094 assert_eq!(translator.lsp_servers.len(), 0);
2095 }
2096
2097 #[test]
2098 fn test_set_workspace_roots() {
2099 let mut translator = Translator::new();
2100 let roots = vec![PathBuf::from("/test/root1"), PathBuf::from("/test/root2")];
2101 translator.set_workspace_roots(roots.clone());
2102 assert_eq!(translator.workspace_roots, roots);
2103 }
2104
2105 #[test]
2106 fn test_register_server() {
2107 let translator = Translator::new();
2108
2109 assert_eq!(translator.lsp_servers.len(), 0);
2111
2112 }
2121
2122 #[test]
2123 fn test_get_client_for_file_server_initializing_when_expected() {
2124 let mut translator = Translator::new();
2128 let path = PathBuf::from("/ws/Assets/Scripts/Player.cs");
2129 let lang = detect_language(&path, &translator.extension_map);
2130
2131 let mut expected = HashSet::new();
2132 expected.insert(lang.clone());
2133 translator.set_expected_languages(expected);
2134
2135 let err = translator.get_client_for_file(&path).unwrap_err();
2136 assert!(matches!(err, Error::ServerInitializing(ref l) if *l == lang));
2137 }
2138
2139 #[test]
2140 fn test_get_client_for_file_no_server_when_not_expected() {
2141 let translator = Translator::new();
2144 let path = PathBuf::from("/ws/Assets/Scripts/Player.cs");
2145 let lang = detect_language(&path, &translator.extension_map);
2146
2147 let err = translator.get_client_for_file(&path).unwrap_err();
2148 assert!(matches!(err, Error::NoServerForLanguage(ref l) if *l == lang));
2149 }
2150
2151 #[test]
2152 fn test_clear_expected_languages_reverts_to_no_server() {
2153 let mut translator = Translator::new();
2157 let path = PathBuf::from("/ws/Assets/Scripts/Player.cs");
2158 let lang = detect_language(&path, &translator.extension_map);
2159
2160 let mut expected = HashSet::new();
2161 expected.insert(lang);
2162 translator.set_expected_languages(expected);
2163 translator.clear_expected_languages();
2164
2165 let err = translator.get_client_for_file(&path).unwrap_err();
2166 assert!(matches!(err, Error::NoServerForLanguage(_)));
2167 }
2168
2169 #[test]
2170 fn test_diagnostic_request_params_omit_optional_null_fields() {
2171 let uri = "file:///test.ts".parse().unwrap();
2172 let params = diagnostic_request_params(TextDocumentIdentifier { uri });
2173 let value = serde_json::to_value(params).unwrap();
2174
2175 assert_eq!(value["textDocument"]["uri"], "file:///test.ts");
2176 assert!(value.get("identifier").is_none());
2177 assert!(value.get("previousResultId").is_none());
2178 }
2179
2180 #[test]
2181 fn test_validate_path_no_workspace_roots() {
2182 let translator = Translator::new();
2183 let temp_dir = TempDir::new().unwrap();
2184 let test_file = temp_dir.path().join("test.rs");
2185 fs::write(&test_file, "fn main() {}").unwrap();
2186
2187 let result = translator.validate_path(&test_file);
2189 assert!(result.is_ok());
2190 }
2191
2192 #[test]
2193 fn test_validate_path_within_workspace() {
2194 let mut translator = Translator::new();
2195 let temp_dir = TempDir::new().unwrap();
2196 let workspace_root = temp_dir.path().to_path_buf();
2197 translator.set_workspace_roots(vec![workspace_root]);
2198
2199 let test_file = temp_dir.path().join("test.rs");
2200 fs::write(&test_file, "fn main() {}").unwrap();
2201
2202 let result = translator.validate_path(&test_file);
2203 assert!(result.is_ok());
2204 }
2205
2206 #[test]
2207 fn test_validate_path_outside_workspace() {
2208 let mut translator = Translator::new();
2209 let temp_dir1 = TempDir::new().unwrap();
2210 let temp_dir2 = TempDir::new().unwrap();
2211
2212 translator.set_workspace_roots(vec![temp_dir1.path().to_path_buf()]);
2214
2215 let test_file = temp_dir2.path().join("test.rs");
2217 fs::write(&test_file, "fn main() {}").unwrap();
2218
2219 let result = translator.validate_path(&test_file);
2220 assert!(matches!(result, Err(Error::PathOutsideWorkspace(_))));
2221 }
2222
2223 #[test]
2224 fn test_normalize_range() {
2225 let lsp_range = lsp_types::Range {
2226 start: lsp_types::Position {
2227 line: 0,
2228 character: 0,
2229 },
2230 end: lsp_types::Position {
2231 line: 2,
2232 character: 5,
2233 },
2234 };
2235
2236 let mcp_range = normalize_range(lsp_range);
2237 assert_eq!(mcp_range.start.line, 1);
2238 assert_eq!(mcp_range.start.character, 1);
2239 assert_eq!(mcp_range.end.line, 3);
2240 assert_eq!(mcp_range.end.character, 6);
2241 }
2242
2243 #[test]
2244 fn test_extract_hover_contents_string() {
2245 let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
2246 let contents = lsp_types::HoverContents::Scalar(marked_string);
2247 let result = extract_hover_contents(contents);
2248 assert_eq!(result, "Test hover");
2249 }
2250
2251 #[test]
2252 fn test_extract_hover_contents_language_string() {
2253 let marked_string = lsp_types::MarkedString::LanguageString(lsp_types::LanguageString {
2254 language: "rust".to_string(),
2255 value: "fn main() {}".to_string(),
2256 });
2257 let contents = lsp_types::HoverContents::Scalar(marked_string);
2258 let result = extract_hover_contents(contents);
2259 assert_eq!(result, "```rust\nfn main() {}\n```");
2260 }
2261
2262 #[test]
2263 fn test_extract_hover_contents_markup() {
2264 let markup = lsp_types::MarkupContent {
2265 kind: lsp_types::MarkupKind::Markdown,
2266 value: "# Documentation".to_string(),
2267 };
2268 let contents = lsp_types::HoverContents::Markup(markup);
2269 let result = extract_hover_contents(contents);
2270 assert_eq!(result, "# Documentation");
2271 }
2272
2273 #[tokio::test]
2274 async fn test_handle_workspace_symbol_no_server() {
2275 let mut translator = Translator::new();
2276 let result = translator
2277 .handle_workspace_symbol("test".to_string(), None, 100)
2278 .await;
2279 assert!(matches!(result, Err(Error::NoServerConfigured)));
2280 }
2281
2282 #[tokio::test]
2283 async fn test_handle_code_actions_invalid_kind() {
2284 let mut translator = Translator::new();
2285 let result = translator
2286 .handle_code_actions(
2287 "/tmp/test.rs".to_string(),
2288 1,
2289 1,
2290 1,
2291 10,
2292 Some("invalid_kind".to_string()),
2293 )
2294 .await;
2295 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2296 }
2297
2298 #[tokio::test]
2299 async fn test_handle_code_actions_valid_kind_quickfix() {
2300 use tempfile::TempDir;
2301
2302 let mut translator = Translator::new();
2303 let temp_dir = TempDir::new().unwrap();
2304 let test_file = temp_dir.path().join("test.rs");
2305 fs::write(&test_file, "fn main() {}").unwrap();
2306
2307 let result = translator
2308 .handle_code_actions(
2309 test_file.to_str().unwrap().to_string(),
2310 1,
2311 1,
2312 1,
2313 10,
2314 Some("quickfix".to_string()),
2315 )
2316 .await;
2317 assert!(result.is_err());
2319 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
2320 }
2321
2322 #[tokio::test]
2323 async fn test_handle_code_actions_valid_kind_refactor() {
2324 use tempfile::TempDir;
2325
2326 let mut translator = Translator::new();
2327 let temp_dir = TempDir::new().unwrap();
2328 let test_file = temp_dir.path().join("test.rs");
2329 fs::write(&test_file, "fn main() {}").unwrap();
2330
2331 let result = translator
2332 .handle_code_actions(
2333 test_file.to_str().unwrap().to_string(),
2334 1,
2335 1,
2336 1,
2337 10,
2338 Some("refactor".to_string()),
2339 )
2340 .await;
2341 assert!(result.is_err());
2342 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
2343 }
2344
2345 #[tokio::test]
2346 async fn test_handle_code_actions_valid_kind_refactor_extract() {
2347 use tempfile::TempDir;
2348
2349 let mut translator = Translator::new();
2350 let temp_dir = TempDir::new().unwrap();
2351 let test_file = temp_dir.path().join("test.rs");
2352 fs::write(&test_file, "fn main() {}").unwrap();
2353
2354 let result = translator
2355 .handle_code_actions(
2356 test_file.to_str().unwrap().to_string(),
2357 1,
2358 1,
2359 1,
2360 10,
2361 Some("refactor.extract".to_string()),
2362 )
2363 .await;
2364 assert!(result.is_err());
2365 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
2366 }
2367
2368 #[tokio::test]
2369 async fn test_handle_code_actions_valid_kind_source() {
2370 use tempfile::TempDir;
2371
2372 let mut translator = Translator::new();
2373 let temp_dir = TempDir::new().unwrap();
2374 let test_file = temp_dir.path().join("test.rs");
2375 fs::write(&test_file, "fn main() {}").unwrap();
2376
2377 let result = translator
2378 .handle_code_actions(
2379 test_file.to_str().unwrap().to_string(),
2380 1,
2381 1,
2382 1,
2383 10,
2384 Some("source.organizeImports".to_string()),
2385 )
2386 .await;
2387 assert!(result.is_err());
2388 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
2389 }
2390
2391 #[tokio::test]
2392 async fn test_handle_code_actions_invalid_range_zero() {
2393 let mut translator = Translator::new();
2394 let result = translator
2395 .handle_code_actions("/tmp/test.rs".to_string(), 0, 1, 1, 10, None)
2396 .await;
2397 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2398 }
2399
2400 #[tokio::test]
2401 async fn test_handle_code_actions_invalid_range_order() {
2402 let mut translator = Translator::new();
2403 let result = translator
2404 .handle_code_actions("/tmp/test.rs".to_string(), 10, 5, 5, 1, None)
2405 .await;
2406 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2407 }
2408
2409 #[tokio::test]
2410 async fn test_handle_code_actions_empty_range() {
2411 use tempfile::TempDir;
2412
2413 let mut translator = Translator::new();
2414 let temp_dir = TempDir::new().unwrap();
2415 let test_file = temp_dir.path().join("test.rs");
2416 fs::write(&test_file, "fn main() {}").unwrap();
2417
2418 let result = translator
2420 .handle_code_actions(test_file.to_str().unwrap().to_string(), 1, 5, 1, 5, None)
2421 .await;
2422 assert!(result.is_err());
2424 assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
2425 }
2426
2427 #[test]
2428 fn test_convert_code_action_minimal() {
2429 let lsp_action = lsp_types::CodeAction {
2430 title: "Fix issue".to_string(),
2431 kind: None,
2432 diagnostics: None,
2433 edit: None,
2434 command: None,
2435 is_preferred: None,
2436 disabled: None,
2437 data: None,
2438 };
2439
2440 let result = convert_code_action(lsp_action);
2441 assert_eq!(result.title, "Fix issue");
2442 assert!(result.kind.is_none());
2443 assert!(result.diagnostics.is_empty());
2444 assert!(result.edit.is_none());
2445 assert!(result.command.is_none());
2446 assert!(!result.is_preferred);
2447 }
2448
2449 #[test]
2450 #[allow(clippy::too_many_lines)]
2451 fn test_convert_code_action_with_diagnostics_all_severities() {
2452 let lsp_diagnostics = vec![
2453 lsp_types::Diagnostic {
2454 range: lsp_types::Range {
2455 start: lsp_types::Position {
2456 line: 0,
2457 character: 0,
2458 },
2459 end: lsp_types::Position {
2460 line: 0,
2461 character: 5,
2462 },
2463 },
2464 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2465 message: "Error message".to_string(),
2466 code: Some(lsp_types::NumberOrString::Number(1)),
2467 source: None,
2468 code_description: None,
2469 related_information: None,
2470 tags: None,
2471 data: None,
2472 },
2473 lsp_types::Diagnostic {
2474 range: lsp_types::Range {
2475 start: lsp_types::Position {
2476 line: 1,
2477 character: 0,
2478 },
2479 end: lsp_types::Position {
2480 line: 1,
2481 character: 5,
2482 },
2483 },
2484 severity: Some(lsp_types::DiagnosticSeverity::WARNING),
2485 message: "Warning message".to_string(),
2486 code: Some(lsp_types::NumberOrString::String("W001".to_string())),
2487 source: None,
2488 code_description: None,
2489 related_information: None,
2490 tags: None,
2491 data: None,
2492 },
2493 lsp_types::Diagnostic {
2494 range: lsp_types::Range {
2495 start: lsp_types::Position {
2496 line: 2,
2497 character: 0,
2498 },
2499 end: lsp_types::Position {
2500 line: 2,
2501 character: 5,
2502 },
2503 },
2504 severity: Some(lsp_types::DiagnosticSeverity::INFORMATION),
2505 message: "Info message".to_string(),
2506 code: None,
2507 source: None,
2508 code_description: None,
2509 related_information: None,
2510 tags: None,
2511 data: None,
2512 },
2513 lsp_types::Diagnostic {
2514 range: lsp_types::Range {
2515 start: lsp_types::Position {
2516 line: 3,
2517 character: 0,
2518 },
2519 end: lsp_types::Position {
2520 line: 3,
2521 character: 5,
2522 },
2523 },
2524 severity: Some(lsp_types::DiagnosticSeverity::HINT),
2525 message: "Hint message".to_string(),
2526 code: None,
2527 source: None,
2528 code_description: None,
2529 related_information: None,
2530 tags: None,
2531 data: None,
2532 },
2533 ];
2534
2535 let lsp_action = lsp_types::CodeAction {
2536 title: "Fix all issues".to_string(),
2537 kind: Some(lsp_types::CodeActionKind::QUICKFIX),
2538 diagnostics: Some(lsp_diagnostics),
2539 edit: None,
2540 command: None,
2541 is_preferred: None,
2542 disabled: None,
2543 data: None,
2544 };
2545
2546 let result = convert_code_action(lsp_action);
2547 assert_eq!(result.diagnostics.len(), 4);
2548 assert!(matches!(
2549 result.diagnostics[0].severity,
2550 DiagnosticSeverity::Error
2551 ));
2552 assert!(matches!(
2553 result.diagnostics[1].severity,
2554 DiagnosticSeverity::Warning
2555 ));
2556 assert!(matches!(
2557 result.diagnostics[2].severity,
2558 DiagnosticSeverity::Information
2559 ));
2560 assert!(matches!(
2561 result.diagnostics[3].severity,
2562 DiagnosticSeverity::Hint
2563 ));
2564 assert_eq!(result.diagnostics[0].code, Some("1".to_string()));
2565 assert_eq!(result.diagnostics[1].code, Some("W001".to_string()));
2566 }
2567
2568 #[test]
2569 #[allow(clippy::mutable_key_type)]
2570 fn test_convert_code_action_with_workspace_edit() {
2571 use std::collections::HashMap;
2572 use std::str::FromStr;
2573
2574 let uri = lsp_types::Uri::from_str("file:///test.rs").unwrap();
2575 let mut changes_map = HashMap::new();
2576 changes_map.insert(
2577 uri,
2578 vec![lsp_types::TextEdit {
2579 range: lsp_types::Range {
2580 start: lsp_types::Position {
2581 line: 0,
2582 character: 0,
2583 },
2584 end: lsp_types::Position {
2585 line: 0,
2586 character: 5,
2587 },
2588 },
2589 new_text: "fixed".to_string(),
2590 }],
2591 );
2592
2593 let lsp_action = lsp_types::CodeAction {
2594 title: "Apply fix".to_string(),
2595 kind: Some(lsp_types::CodeActionKind::QUICKFIX),
2596 diagnostics: None,
2597 edit: Some(lsp_types::WorkspaceEdit {
2598 changes: Some(changes_map),
2599 document_changes: None,
2600 change_annotations: None,
2601 }),
2602 command: None,
2603 is_preferred: Some(true),
2604 disabled: None,
2605 data: None,
2606 };
2607
2608 let result = convert_code_action(lsp_action);
2609 assert!(result.edit.is_some());
2610 let edit = result.edit.unwrap();
2611 assert_eq!(edit.changes.len(), 1);
2612 assert_eq!(edit.changes[0].uri, "file:///test.rs");
2613 assert_eq!(edit.changes[0].edits.len(), 1);
2614 assert_eq!(edit.changes[0].edits[0].new_text, "fixed");
2615 assert!(result.is_preferred);
2616 }
2617
2618 #[test]
2619 fn test_convert_code_action_with_command() {
2620 let lsp_action = lsp_types::CodeAction {
2621 title: "Run command".to_string(),
2622 kind: Some(lsp_types::CodeActionKind::REFACTOR),
2623 diagnostics: None,
2624 edit: None,
2625 command: Some(lsp_types::Command {
2626 title: "Execute refactor".to_string(),
2627 command: "refactor.extract".to_string(),
2628 arguments: Some(vec![serde_json::json!("arg1"), serde_json::json!(42)]),
2629 }),
2630 is_preferred: None,
2631 disabled: None,
2632 data: None,
2633 };
2634
2635 let result = convert_code_action(lsp_action);
2636 assert!(result.command.is_some());
2637 let cmd = result.command.unwrap();
2638 assert_eq!(cmd.title, "Execute refactor");
2639 assert_eq!(cmd.command, "refactor.extract");
2640 assert_eq!(cmd.arguments.len(), 2);
2641 }
2642
2643 #[tokio::test]
2644 async fn test_handle_call_hierarchy_prepare_invalid_position_zero() {
2645 let mut translator = Translator::new();
2646 let result = translator
2647 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 0, 1)
2648 .await;
2649 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2650
2651 let result = translator
2652 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1, 0)
2653 .await;
2654 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2655 }
2656
2657 #[tokio::test]
2658 async fn test_handle_call_hierarchy_prepare_invalid_position_too_large() {
2659 let mut translator = Translator::new();
2660 let result = translator
2661 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1_000_001, 1)
2662 .await;
2663 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2664
2665 let result = translator
2666 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1, 1_000_001)
2667 .await;
2668 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2669 }
2670
2671 #[tokio::test]
2672 async fn test_handle_incoming_calls_invalid_json() {
2673 let mut translator = Translator::new();
2674 let invalid_item = serde_json::json!({"invalid": "structure"});
2675 let result = translator.handle_incoming_calls(invalid_item).await;
2676 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2677 }
2678
2679 #[tokio::test]
2680 async fn test_handle_outgoing_calls_invalid_json() {
2681 let mut translator = Translator::new();
2682 let invalid_item = serde_json::json!({"invalid": "structure"});
2683 let result = translator.handle_outgoing_calls(invalid_item).await;
2684 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2685 }
2686
2687 #[tokio::test]
2688 async fn test_parse_file_uri_invalid_scheme() {
2689 let translator = Translator::new();
2690 let uri: lsp_types::Uri = "http://example.com/file.rs".parse().unwrap();
2691 let result = translator.parse_file_uri(&uri);
2692 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2693 }
2694
2695 #[tokio::test]
2696 async fn test_parse_file_uri_valid_scheme() {
2697 let translator = Translator::new();
2698 let temp_dir = TempDir::new().unwrap();
2699 let test_file = temp_dir.path().join("test.rs");
2700 fs::write(&test_file, "fn main() {}").unwrap();
2701
2702 let file_url = Url::from_file_path(&test_file).unwrap();
2704 let uri: lsp_types::Uri = file_url.as_str().parse().unwrap();
2705 let result = translator.parse_file_uri(&uri);
2706 assert!(result.is_ok());
2707 }
2708
2709 #[test]
2710 fn test_handle_cached_diagnostics_empty() {
2711 let mut translator = Translator::new();
2712 let temp_dir = TempDir::new().unwrap();
2713 let test_file = temp_dir.path().join("test.rs");
2714 fs::write(&test_file, "fn main() {}").unwrap();
2715
2716 let result = translator.handle_cached_diagnostics(test_file.to_str().unwrap());
2717 assert!(result.is_ok());
2718 let diags = result.unwrap();
2719 assert_eq!(diags.diagnostics.len(), 0);
2720 }
2721
2722 #[test]
2723 fn test_handle_server_logs_with_filter() {
2724 use crate::bridge::notifications::LogLevel;
2725
2726 let mut translator = Translator::new();
2727
2728 translator
2730 .notification_cache_mut()
2731 .store_log(LogLevel::Error, "error msg".to_string());
2732 translator
2733 .notification_cache_mut()
2734 .store_log(LogLevel::Warning, "warning msg".to_string());
2735 translator
2736 .notification_cache_mut()
2737 .store_log(LogLevel::Info, "info msg".to_string());
2738 translator
2739 .notification_cache_mut()
2740 .store_log(LogLevel::Debug, "debug msg".to_string());
2741
2742 let result = translator.handle_server_logs(10, Some("error".to_string()));
2744 assert!(result.is_ok());
2745 let logs = result.unwrap();
2746 assert_eq!(logs.logs.len(), 1);
2747 assert_eq!(logs.logs[0].message, "error msg");
2748
2749 let result = translator.handle_server_logs(10, Some("warning".to_string()));
2751 assert!(result.is_ok());
2752 let logs = result.unwrap();
2753 assert_eq!(logs.logs.len(), 2);
2754
2755 let result = translator.handle_server_logs(10, Some("info".to_string()));
2757 assert!(result.is_ok());
2758 let logs = result.unwrap();
2759 assert_eq!(logs.logs.len(), 3);
2760
2761 let result = translator.handle_server_logs(10, Some("debug".to_string()));
2763 assert!(result.is_ok());
2764 let logs = result.unwrap();
2765 assert_eq!(logs.logs.len(), 4);
2766
2767 let result = translator.handle_server_logs(10, Some("invalid".to_string()));
2769 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
2770 }
2771
2772 #[test]
2773 fn test_handle_server_messages_limit() {
2774 use crate::bridge::notifications::MessageType;
2775
2776 let mut translator = Translator::new();
2777
2778 for i in 0..10 {
2780 translator
2781 .notification_cache_mut()
2782 .store_message(MessageType::Info, format!("message {i}"));
2783 }
2784
2785 let result = translator.handle_server_messages(5);
2787 assert!(result.is_ok());
2788 let messages = result.unwrap();
2789 assert_eq!(messages.messages.len(), 5);
2790 assert_eq!(messages.messages[0].message, "message 0");
2791 assert_eq!(messages.messages[4].message, "message 4");
2792
2793 let result = translator.handle_server_messages(100);
2795 assert!(result.is_ok());
2796 let messages = result.unwrap();
2797 assert_eq!(messages.messages.len(), 10);
2798 }
2799
2800 #[test]
2801 fn test_handle_cached_diagnostics_with_data() {
2802 let mut translator = Translator::new();
2803 let temp_dir = TempDir::new().unwrap();
2804 let test_file = temp_dir.path().join("test.rs");
2805 fs::write(&test_file, "fn main() {}").unwrap();
2806
2807 let canonical_path = test_file.canonicalize().unwrap();
2808 let uri: lsp_types::Uri = Url::from_file_path(&canonical_path)
2809 .unwrap()
2810 .as_str()
2811 .parse()
2812 .unwrap();
2813 let diagnostic = lsp_types::Diagnostic {
2814 range: lsp_types::Range {
2815 start: lsp_types::Position {
2816 line: 0,
2817 character: 0,
2818 },
2819 end: lsp_types::Position {
2820 line: 0,
2821 character: 5,
2822 },
2823 },
2824 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2825 message: "test error".to_string(),
2826 code: Some(lsp_types::NumberOrString::String("E001".to_string())),
2827 source: None,
2828 code_description: None,
2829 related_information: None,
2830 tags: None,
2831 data: None,
2832 };
2833
2834 translator
2835 .notification_cache_mut()
2836 .store_diagnostics(&uri, Some(1), vec![diagnostic]);
2837
2838 let result = translator.handle_cached_diagnostics(test_file.to_str().unwrap());
2839 assert!(result.is_ok());
2840 let diags = result.unwrap();
2841 assert_eq!(diags.diagnostics.len(), 1);
2842 assert_eq!(diags.diagnostics[0].message, "test error");
2843 assert_eq!(diags.diagnostics[0].code, Some("E001".to_string()));
2844 assert!(matches!(
2845 diags.diagnostics[0].severity,
2846 DiagnosticSeverity::Error
2847 ));
2848 assert_eq!(diags.diagnostics[0].range.start.line, 1);
2849 assert_eq!(diags.diagnostics[0].range.start.character, 1);
2850 }
2851
2852 #[test]
2853 #[allow(clippy::too_many_lines)]
2854 fn test_handle_cached_diagnostics_multiple_severities() {
2855 let mut translator = Translator::new();
2856 let temp_dir = TempDir::new().unwrap();
2857 let test_file = temp_dir.path().join("test.rs");
2858 fs::write(&test_file, "fn main() {}").unwrap();
2859
2860 let canonical_path = test_file.canonicalize().unwrap();
2861 let uri: lsp_types::Uri = Url::from_file_path(&canonical_path)
2862 .unwrap()
2863 .as_str()
2864 .parse()
2865 .unwrap();
2866 let diagnostics = vec![
2867 lsp_types::Diagnostic {
2868 range: lsp_types::Range {
2869 start: lsp_types::Position {
2870 line: 0,
2871 character: 0,
2872 },
2873 end: lsp_types::Position {
2874 line: 0,
2875 character: 5,
2876 },
2877 },
2878 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2879 message: "error".to_string(),
2880 code: None,
2881 source: None,
2882 code_description: None,
2883 related_information: None,
2884 tags: None,
2885 data: None,
2886 },
2887 lsp_types::Diagnostic {
2888 range: lsp_types::Range {
2889 start: lsp_types::Position {
2890 line: 1,
2891 character: 0,
2892 },
2893 end: lsp_types::Position {
2894 line: 1,
2895 character: 5,
2896 },
2897 },
2898 severity: Some(lsp_types::DiagnosticSeverity::WARNING),
2899 message: "warning".to_string(),
2900 code: None,
2901 source: None,
2902 code_description: None,
2903 related_information: None,
2904 tags: None,
2905 data: None,
2906 },
2907 lsp_types::Diagnostic {
2908 range: lsp_types::Range {
2909 start: lsp_types::Position {
2910 line: 2,
2911 character: 0,
2912 },
2913 end: lsp_types::Position {
2914 line: 2,
2915 character: 5,
2916 },
2917 },
2918 severity: Some(lsp_types::DiagnosticSeverity::INFORMATION),
2919 message: "info".to_string(),
2920 code: None,
2921 source: None,
2922 code_description: None,
2923 related_information: None,
2924 tags: None,
2925 data: None,
2926 },
2927 lsp_types::Diagnostic {
2928 range: lsp_types::Range {
2929 start: lsp_types::Position {
2930 line: 3,
2931 character: 0,
2932 },
2933 end: lsp_types::Position {
2934 line: 3,
2935 character: 5,
2936 },
2937 },
2938 severity: Some(lsp_types::DiagnosticSeverity::HINT),
2939 message: "hint".to_string(),
2940 code: None,
2941 source: None,
2942 code_description: None,
2943 related_information: None,
2944 tags: None,
2945 data: None,
2946 },
2947 ];
2948
2949 translator
2950 .notification_cache_mut()
2951 .store_diagnostics(&uri, Some(1), diagnostics);
2952
2953 let result = translator.handle_cached_diagnostics(test_file.to_str().unwrap());
2954 assert!(result.is_ok());
2955 let diags = result.unwrap();
2956 assert_eq!(diags.diagnostics.len(), 4);
2957 assert!(matches!(
2958 diags.diagnostics[0].severity,
2959 DiagnosticSeverity::Error
2960 ));
2961 assert!(matches!(
2962 diags.diagnostics[1].severity,
2963 DiagnosticSeverity::Warning
2964 ));
2965 assert!(matches!(
2966 diags.diagnostics[2].severity,
2967 DiagnosticSeverity::Information
2968 ));
2969 assert!(matches!(
2970 diags.diagnostics[3].severity,
2971 DiagnosticSeverity::Hint
2972 ));
2973 }
2974
2975 #[test]
2976 fn test_handle_cached_diagnostics_with_numeric_code() {
2977 let mut translator = Translator::new();
2978 let temp_dir = TempDir::new().unwrap();
2979 let test_file = temp_dir.path().join("test.rs");
2980 fs::write(&test_file, "fn main() {}").unwrap();
2981
2982 let canonical_path = test_file.canonicalize().unwrap();
2983 let uri: lsp_types::Uri = Url::from_file_path(&canonical_path)
2984 .unwrap()
2985 .as_str()
2986 .parse()
2987 .unwrap();
2988 let diagnostic = lsp_types::Diagnostic {
2989 range: lsp_types::Range {
2990 start: lsp_types::Position {
2991 line: 0,
2992 character: 0,
2993 },
2994 end: lsp_types::Position {
2995 line: 0,
2996 character: 5,
2997 },
2998 },
2999 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
3000 message: "test error".to_string(),
3001 code: Some(lsp_types::NumberOrString::Number(42)),
3002 source: None,
3003 code_description: None,
3004 related_information: None,
3005 tags: None,
3006 data: None,
3007 };
3008
3009 translator
3010 .notification_cache_mut()
3011 .store_diagnostics(&uri, Some(1), vec![diagnostic]);
3012
3013 let result = translator.handle_cached_diagnostics(test_file.to_str().unwrap());
3014 assert!(result.is_ok());
3015 let diags = result.unwrap();
3016 assert_eq!(diags.diagnostics.len(), 1);
3017 assert_eq!(diags.diagnostics[0].code, Some("42".to_string()));
3018 }
3019
3020 #[test]
3021 fn test_handle_cached_diagnostics_invalid_path() {
3022 let mut translator = Translator::new();
3023 let result = translator.handle_cached_diagnostics("/nonexistent/path/file.rs");
3024 assert!(matches!(result, Err(Error::FileIo { .. })));
3025 }
3026
3027 #[test]
3028 fn test_handle_server_logs_no_filter() {
3029 use crate::bridge::notifications::LogLevel;
3030
3031 let mut translator = Translator::new();
3032
3033 translator
3034 .notification_cache_mut()
3035 .store_log(LogLevel::Error, "error msg".to_string());
3036 translator
3037 .notification_cache_mut()
3038 .store_log(LogLevel::Warning, "warning msg".to_string());
3039 translator
3040 .notification_cache_mut()
3041 .store_log(LogLevel::Info, "info msg".to_string());
3042 translator
3043 .notification_cache_mut()
3044 .store_log(LogLevel::Debug, "debug msg".to_string());
3045
3046 let result = translator.handle_server_logs(10, None);
3047 assert!(result.is_ok());
3048 let logs = result.unwrap();
3049 assert_eq!(logs.logs.len(), 4);
3050 }
3051
3052 #[test]
3053 fn test_handle_server_logs_error_filter_strict() {
3054 use crate::bridge::notifications::LogLevel;
3055
3056 let mut translator = Translator::new();
3057
3058 translator
3059 .notification_cache_mut()
3060 .store_log(LogLevel::Error, "error msg".to_string());
3061 translator
3062 .notification_cache_mut()
3063 .store_log(LogLevel::Warning, "warning msg".to_string());
3064 translator
3065 .notification_cache_mut()
3066 .store_log(LogLevel::Info, "info msg".to_string());
3067
3068 let result = translator.handle_server_logs(10, Some("error".to_string()));
3069 assert!(result.is_ok());
3070 let logs = result.unwrap();
3071 assert_eq!(logs.logs.len(), 1);
3072 assert_eq!(logs.logs[0].message, "error msg");
3073 }
3074
3075 #[test]
3076 fn test_handle_server_logs_warning_filter_includes_errors() {
3077 use crate::bridge::notifications::LogLevel;
3078
3079 let mut translator = Translator::new();
3080
3081 translator
3082 .notification_cache_mut()
3083 .store_log(LogLevel::Error, "error msg".to_string());
3084 translator
3085 .notification_cache_mut()
3086 .store_log(LogLevel::Warning, "warning msg".to_string());
3087 translator
3088 .notification_cache_mut()
3089 .store_log(LogLevel::Info, "info msg".to_string());
3090
3091 let result = translator.handle_server_logs(10, Some("warning".to_string()));
3092 assert!(result.is_ok());
3093 let logs = result.unwrap();
3094 assert_eq!(logs.logs.len(), 2);
3095 }
3096
3097 #[test]
3098 fn test_handle_server_logs_info_filter_excludes_debug() {
3099 use crate::bridge::notifications::LogLevel;
3100
3101 let mut translator = Translator::new();
3102
3103 translator
3104 .notification_cache_mut()
3105 .store_log(LogLevel::Error, "error msg".to_string());
3106 translator
3107 .notification_cache_mut()
3108 .store_log(LogLevel::Info, "info msg".to_string());
3109 translator
3110 .notification_cache_mut()
3111 .store_log(LogLevel::Debug, "debug msg".to_string());
3112
3113 let result = translator.handle_server_logs(10, Some("info".to_string()));
3114 assert!(result.is_ok());
3115 let logs = result.unwrap();
3116 assert_eq!(logs.logs.len(), 2);
3117 }
3118
3119 #[test]
3120 fn test_handle_server_logs_debug_filter_includes_all() {
3121 use crate::bridge::notifications::LogLevel;
3122
3123 let mut translator = Translator::new();
3124
3125 translator
3126 .notification_cache_mut()
3127 .store_log(LogLevel::Error, "error msg".to_string());
3128 translator
3129 .notification_cache_mut()
3130 .store_log(LogLevel::Warning, "warning msg".to_string());
3131 translator
3132 .notification_cache_mut()
3133 .store_log(LogLevel::Info, "info msg".to_string());
3134 translator
3135 .notification_cache_mut()
3136 .store_log(LogLevel::Debug, "debug msg".to_string());
3137
3138 let result = translator.handle_server_logs(10, Some("debug".to_string()));
3139 assert!(result.is_ok());
3140 let logs = result.unwrap();
3141 assert_eq!(logs.logs.len(), 4);
3142 }
3143
3144 #[test]
3145 fn test_handle_server_logs_limit_applies_after_filter() {
3146 use crate::bridge::notifications::LogLevel;
3147
3148 let mut translator = Translator::new();
3149
3150 for i in 0..10 {
3151 translator
3152 .notification_cache_mut()
3153 .store_log(LogLevel::Error, format!("error {i}"));
3154 }
3155
3156 let result = translator.handle_server_logs(5, Some("error".to_string()));
3157 assert!(result.is_ok());
3158 let logs = result.unwrap();
3159 assert_eq!(logs.logs.len(), 5);
3160 assert_eq!(logs.logs[0].message, "error 0");
3161 assert_eq!(logs.logs[4].message, "error 4");
3162 }
3163
3164 #[test]
3165 fn test_handle_server_logs_case_insensitive_level() {
3166 use crate::bridge::notifications::LogLevel;
3167
3168 let mut translator = Translator::new();
3169
3170 translator
3171 .notification_cache_mut()
3172 .store_log(LogLevel::Error, "error msg".to_string());
3173
3174 let result = translator.handle_server_logs(10, Some("ERROR".to_string()));
3175 assert!(result.is_ok());
3176
3177 let result = translator.handle_server_logs(10, Some("Error".to_string()));
3178 assert!(result.is_ok());
3179
3180 let result = translator.handle_server_logs(10, Some("eRrOr".to_string()));
3181 assert!(result.is_ok());
3182 }
3183
3184 #[test]
3185 fn test_handle_server_messages_empty() {
3186 let mut translator = Translator::new();
3187
3188 let result = translator.handle_server_messages(10);
3189 assert!(result.is_ok());
3190 let messages = result.unwrap();
3191 assert_eq!(messages.messages.len(), 0);
3192 }
3193
3194 #[test]
3195 fn test_handle_server_messages_with_different_types() {
3196 use crate::bridge::notifications::MessageType;
3197
3198 let mut translator = Translator::new();
3199
3200 translator
3201 .notification_cache_mut()
3202 .store_message(MessageType::Error, "error".to_string());
3203 translator
3204 .notification_cache_mut()
3205 .store_message(MessageType::Warning, "warning".to_string());
3206 translator
3207 .notification_cache_mut()
3208 .store_message(MessageType::Info, "info".to_string());
3209 translator
3210 .notification_cache_mut()
3211 .store_message(MessageType::Log, "log".to_string());
3212
3213 let result = translator.handle_server_messages(10);
3214 assert!(result.is_ok());
3215 let messages = result.unwrap();
3216 assert_eq!(messages.messages.len(), 4);
3217 assert_eq!(messages.messages[0].message, "error");
3218 assert_eq!(messages.messages[1].message, "warning");
3219 assert_eq!(messages.messages[2].message, "info");
3220 assert_eq!(messages.messages[3].message, "log");
3221 }
3222
3223 #[test]
3224 fn test_handle_server_messages_zero_limit() {
3225 use crate::bridge::notifications::MessageType;
3226
3227 let mut translator = Translator::new();
3228
3229 translator
3230 .notification_cache_mut()
3231 .store_message(MessageType::Info, "test".to_string());
3232
3233 let result = translator.handle_server_messages(0);
3234 assert!(result.is_ok());
3235 let messages = result.unwrap();
3236 assert_eq!(messages.messages.len(), 0);
3237 }
3238
3239 #[test]
3240 fn test_handle_cached_diagnostics_path_outside_workspace() {
3241 let mut translator = Translator::new();
3242 let temp_dir1 = TempDir::new().unwrap();
3243 let temp_dir2 = TempDir::new().unwrap();
3244
3245 translator.set_workspace_roots(vec![temp_dir1.path().to_path_buf()]);
3246
3247 let test_file = temp_dir2.path().join("test.rs");
3248 fs::write(&test_file, "fn main() {}").unwrap();
3249
3250 let result = translator.handle_cached_diagnostics(test_file.to_str().unwrap());
3251 assert!(matches!(result, Err(Error::PathOutsideWorkspace(_))));
3252 }
3253
3254 #[test]
3255 fn test_translator_with_custom_extensions() {
3256 let mut extension_map = HashMap::new();
3257 extension_map.insert("nu".to_string(), "nushell".to_string());
3258 extension_map.insert("customext".to_string(), "customlang".to_string());
3259
3260 let translator = Translator::new().with_extensions(extension_map.clone());
3261
3262 assert_eq!(translator.extension_map.len(), 2);
3263 assert_eq!(
3264 translator.extension_map.get("nu"),
3265 Some(&"nushell".to_string())
3266 );
3267 assert_eq!(
3268 translator.extension_map.get("customext"),
3269 Some(&"customlang".to_string())
3270 );
3271 }
3272
3273 #[test]
3274 fn test_get_client_for_file_uses_custom_extension() {
3275 let temp_dir = TempDir::new().unwrap();
3276 let test_file = temp_dir.path().join("script.nu");
3277 fs::write(&test_file, "echo hello").unwrap();
3278
3279 let mut extension_map = HashMap::new();
3280 extension_map.insert("nu".to_string(), "nushell".to_string());
3281
3282 let translator = Translator::new().with_extensions(extension_map);
3283
3284 let result = translator.get_client_for_file(&test_file);
3285
3286 assert!(result.is_err());
3287 if let Err(Error::NoServerForLanguage(lang)) = result {
3288 assert_eq!(lang, "nushell");
3289 } else {
3290 panic!("Expected NoServerForLanguage(nushell) error");
3291 }
3292 }
3293
3294 #[test]
3295 fn test_get_client_for_file_falls_back_to_default() {
3296 let temp_dir = TempDir::new().unwrap();
3297 let test_file = temp_dir.path().join("unknown.xyz");
3298 fs::write(&test_file, "content").unwrap();
3299
3300 let mut extension_map = HashMap::new();
3301 extension_map.insert("rs".to_string(), "rust".to_string());
3302
3303 let translator = Translator::new().with_extensions(extension_map);
3304
3305 let result = translator.get_client_for_file(&test_file);
3306
3307 assert!(result.is_err());
3308 if let Err(Error::NoServerForLanguage(lang)) = result {
3309 assert_eq!(lang, "plaintext");
3310 } else {
3311 panic!("Expected NoServerForLanguage(plaintext) error");
3312 }
3313 }
3314
3315 #[tokio::test]
3316 async fn test_serve_initializes_translator_with_extensions() {
3317 use crate::config::{LanguageExtensionMapping, WorkspaceConfig};
3318
3319 let language_extensions = vec![
3320 LanguageExtensionMapping {
3321 extensions: vec!["nu".to_string()],
3322 language_id: "nushell".to_string(),
3323 },
3324 LanguageExtensionMapping {
3325 extensions: vec!["rs".to_string()],
3326 language_id: "rust".to_string(),
3327 },
3328 ];
3329
3330 let config = crate::config::ServerConfig {
3331 workspace: WorkspaceConfig {
3332 roots: vec![PathBuf::from("/tmp/test-workspace")],
3333 position_encodings: vec!["utf-8".to_string()],
3334 language_extensions: language_extensions.clone(),
3335 heuristics_max_depth: 10,
3336 },
3337 lsp_servers: vec![],
3338 };
3339
3340 let extension_map = config.build_effective_extension_map();
3341 assert_eq!(extension_map.get("nu"), Some(&"nushell".to_string()));
3342 assert_eq!(extension_map.get("rs"), Some(&"rust".to_string()));
3343
3344 let result = crate::serve(config).await;
3347 if let Err(ref err) = result {
3348 assert!(
3349 !matches!(err, crate::error::Error::NoServersAvailable(_)),
3350 "serve() must not return NoServersAvailable for empty lsp_servers config"
3351 );
3352 }
3353 }
3354
3355 #[test]
3356 fn test_convert_call_hierarchy_item_kind_is_numeric() {
3357 let item = lsp_types::CallHierarchyItem {
3358 name: "my_fn".to_string(),
3359 kind: lsp_types::SymbolKind::FUNCTION,
3360 tags: None,
3361 detail: None,
3362 uri: "file:///tmp/test.rs".parse().unwrap(),
3363 range: lsp_types::Range {
3364 start: lsp_types::Position {
3365 line: 0,
3366 character: 0,
3367 },
3368 end: lsp_types::Position {
3369 line: 0,
3370 character: 5,
3371 },
3372 },
3373 selection_range: lsp_types::Range {
3374 start: lsp_types::Position {
3375 line: 0,
3376 character: 0,
3377 },
3378 end: lsp_types::Position {
3379 line: 0,
3380 character: 5,
3381 },
3382 },
3383 data: None,
3384 };
3385 let result = convert_call_hierarchy_item(item);
3386 assert_eq!(result.kind, 12u32);
3388 assert_eq!(result.name, "my_fn");
3389 }
3390}