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