mcpls_core/bridge/
translator.rs

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