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;
18
19use super::DocumentTracker;
20use super::state::detect_language;
21use crate::bridge::encoding::mcp_to_lsp_position;
22use crate::error::{Error, Result};
23use crate::lsp::{LspClient, LspServer};
24
25/// Translator handles MCP tool calls by converting them to LSP requests.
26#[derive(Debug)]
27pub struct Translator {
28    /// LSP clients indexed by language ID.
29    lsp_clients: HashMap<String, LspClient>,
30    /// LSP servers indexed by language ID (held for lifetime management).
31    lsp_servers: HashMap<String, LspServer>,
32    /// Document state tracker.
33    document_tracker: DocumentTracker,
34    /// Allowed workspace roots for path validation.
35    workspace_roots: Vec<PathBuf>,
36}
37
38impl Translator {
39    /// Create a new translator.
40    #[must_use]
41    pub fn new() -> Self {
42        Self {
43            lsp_clients: HashMap::new(),
44            lsp_servers: HashMap::new(),
45            document_tracker: DocumentTracker::new(),
46            workspace_roots: vec![],
47        }
48    }
49
50    /// Set the workspace roots for path validation.
51    pub fn set_workspace_roots(&mut self, roots: Vec<PathBuf>) {
52        self.workspace_roots = roots;
53    }
54
55    /// Register an LSP client for a language.
56    pub fn register_client(&mut self, language_id: String, client: LspClient) {
57        self.lsp_clients.insert(language_id, client);
58    }
59
60    /// Register an LSP server for a language.
61    pub fn register_server(&mut self, language_id: String, server: LspServer) {
62        self.lsp_servers.insert(language_id, server);
63    }
64
65    /// Get the document tracker.
66    #[must_use]
67    pub const fn document_tracker(&self) -> &DocumentTracker {
68        &self.document_tracker
69    }
70
71    /// Get a mutable reference to the document tracker.
72    pub const fn document_tracker_mut(&mut self) -> &mut DocumentTracker {
73        &mut self.document_tracker
74    }
75
76    // TODO: These methods will be implemented in Phase 3-5
77    // Initialize and shutdown are now handled by LspServer in lifecycle.rs
78
79    // Future implementation will use LspServer instead of LspClient directly
80}
81
82impl Default for Translator {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88/// Position in a document (1-based for MCP).
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Position2D {
91    /// Line number (1-based).
92    pub line: u32,
93    /// Character offset (1-based).
94    pub character: u32,
95}
96
97/// Range in a document (1-based for MCP).
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct Range {
100    /// Start position.
101    pub start: Position2D,
102    /// End position.
103    pub end: Position2D,
104}
105
106/// Location in a document.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Location {
109    /// URI of the document.
110    pub uri: String,
111    /// Range within the document.
112    pub range: Range,
113}
114
115/// Result of a hover request.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct HoverResult {
118    /// Hover contents as markdown string.
119    pub contents: String,
120    /// Optional range the hover applies to.
121    pub range: Option<Range>,
122}
123
124/// Result of a definition request.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct DefinitionResult {
127    /// Locations of the definition.
128    pub locations: Vec<Location>,
129}
130
131/// Result of a references request.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ReferencesResult {
134    /// Locations of all references.
135    pub locations: Vec<Location>,
136}
137
138/// Diagnostic severity.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(rename_all = "lowercase")]
141pub enum DiagnosticSeverity {
142    /// Error diagnostic.
143    Error,
144    /// Warning diagnostic.
145    Warning,
146    /// Informational diagnostic.
147    Information,
148    /// Hint diagnostic.
149    Hint,
150}
151
152/// A single diagnostic.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct Diagnostic {
155    /// Range where the diagnostic applies.
156    pub range: Range,
157    /// Severity of the diagnostic.
158    pub severity: DiagnosticSeverity,
159    /// Diagnostic message.
160    pub message: String,
161    /// Optional diagnostic code.
162    pub code: Option<String>,
163}
164
165/// Result of a diagnostics request.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct DiagnosticsResult {
168    /// List of diagnostics for the document.
169    pub diagnostics: Vec<Diagnostic>,
170}
171
172/// A text edit operation.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct TextEdit {
175    /// Range to replace.
176    pub range: Range,
177    /// New text.
178    pub new_text: String,
179}
180
181/// Changes to a document.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct DocumentChanges {
184    /// URI of the document.
185    pub uri: String,
186    /// List of edits to apply.
187    pub edits: Vec<TextEdit>,
188}
189
190/// Result of a rename request.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct RenameResult {
193    /// Changes to apply across documents.
194    pub changes: Vec<DocumentChanges>,
195}
196
197/// A completion item.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct Completion {
200    /// Label of the completion.
201    pub label: String,
202    /// Kind of completion.
203    pub kind: Option<String>,
204    /// Detail information.
205    pub detail: Option<String>,
206    /// Documentation.
207    pub documentation: Option<String>,
208}
209
210/// Result of a completions request.
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct CompletionsResult {
213    /// List of completion items.
214    pub items: Vec<Completion>,
215}
216
217/// A document symbol.
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct Symbol {
220    /// Name of the symbol.
221    pub name: String,
222    /// Kind of symbol.
223    pub kind: String,
224    /// Range of the symbol.
225    pub range: Range,
226    /// Selection range (identifier location).
227    pub selection_range: Range,
228    /// Child symbols.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub children: Option<Vec<Self>>,
231}
232
233/// Result of a document symbols request.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct DocumentSymbolsResult {
236    /// List of symbols in the document.
237    pub symbols: Vec<Symbol>,
238}
239
240/// Result of a format document request.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct FormatDocumentResult {
243    /// List of edits to format the document.
244    pub edits: Vec<TextEdit>,
245}
246
247/// A workspace symbol.
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct WorkspaceSymbol {
250    /// Name of the symbol.
251    pub name: String,
252    /// Kind of symbol.
253    pub kind: String,
254    /// Location of the symbol.
255    pub location: Location,
256    /// Optional container name (parent scope).
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub container_name: Option<String>,
259}
260
261/// Result of workspace symbol search.
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct WorkspaceSymbolResult {
264    /// List of symbols found.
265    pub symbols: Vec<WorkspaceSymbol>,
266}
267
268/// A single code action.
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct CodeAction {
271    /// Title of the code action.
272    pub title: String,
273    /// Kind of code action (quickfix, refactor, etc.).
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub kind: Option<String>,
276    /// Diagnostics that this action resolves.
277    #[serde(skip_serializing_if = "Vec::is_empty", default)]
278    pub diagnostics: Vec<Diagnostic>,
279    /// Workspace edit to apply.
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub edit: Option<WorkspaceEditDescription>,
282    /// Command to execute.
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub command: Option<CommandDescription>,
285    /// Whether this is the preferred action.
286    #[serde(default)]
287    pub is_preferred: bool,
288}
289
290/// Description of a workspace edit.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct WorkspaceEditDescription {
293    /// Changes to apply to documents.
294    pub changes: Vec<DocumentChanges>,
295}
296
297/// Description of a command.
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct CommandDescription {
300    /// Title of the command.
301    pub title: String,
302    /// Command identifier.
303    pub command: String,
304    /// Command arguments.
305    #[serde(skip_serializing_if = "Vec::is_empty", default)]
306    pub arguments: Vec<serde_json::Value>,
307}
308
309/// Result of code actions request.
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct CodeActionsResult {
312    /// Available code actions.
313    pub actions: Vec<CodeAction>,
314}
315
316/// A call hierarchy item.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct CallHierarchyItemResult {
319    /// Name of the symbol.
320    pub name: String,
321    /// Kind of symbol.
322    pub kind: String,
323    /// More detail for this item.
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub detail: Option<String>,
326    /// URI of the document.
327    pub uri: String,
328    /// Range of the symbol.
329    pub range: Range,
330    /// Selection range (identifier location).
331    pub selection_range: Range,
332    /// Opaque data to pass to incoming/outgoing calls.
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub data: Option<serde_json::Value>,
335}
336
337/// Result of call hierarchy prepare request.
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct CallHierarchyPrepareResult {
340    /// List of callable items at the position.
341    pub items: Vec<CallHierarchyItemResult>,
342}
343
344/// An incoming call (caller of the current item).
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct IncomingCall {
347    /// The item that calls the current item.
348    pub from: CallHierarchyItemResult,
349    /// Ranges where the call occurs.
350    pub from_ranges: Vec<Range>,
351}
352
353/// Result of incoming calls request.
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct IncomingCallsResult {
356    /// List of incoming calls.
357    pub calls: Vec<IncomingCall>,
358}
359
360/// An outgoing call (callee from the current item).
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct OutgoingCall {
363    /// The item being called.
364    pub to: CallHierarchyItemResult,
365    /// Ranges where the call occurs.
366    pub from_ranges: Vec<Range>,
367}
368
369/// Result of outgoing calls request.
370#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct OutgoingCallsResult {
372    /// List of outgoing calls.
373    pub calls: Vec<OutgoingCall>,
374}
375
376/// Maximum allowed position value for validation.
377const MAX_POSITION_VALUE: u32 = 1_000_000;
378/// Maximum allowed range size in lines.
379const MAX_RANGE_LINES: u32 = 10_000;
380
381impl Translator {
382    /// Validate that a path is within allowed workspace boundaries.
383    ///
384    /// # Errors
385    ///
386    /// Returns `Error::PathOutsideWorkspace` if the path is outside all workspace roots.
387    fn validate_path(&self, path: &Path) -> Result<PathBuf> {
388        let canonical = path.canonicalize().map_err(|e| Error::FileIo {
389            path: path.to_path_buf(),
390            source: e,
391        })?;
392
393        // If no workspace roots configured, allow any path (backward compatibility)
394        if self.workspace_roots.is_empty() {
395            return Ok(canonical);
396        }
397
398        // Check if path is within any workspace root
399        for root in &self.workspace_roots {
400            if let Ok(canonical_root) = root.canonicalize() {
401                if canonical.starts_with(&canonical_root) {
402                    return Ok(canonical);
403                }
404            }
405        }
406
407        Err(Error::PathOutsideWorkspace(path.to_path_buf()))
408    }
409
410    /// Get a cloned LSP client for a file path based on language detection.
411    fn get_client_for_file(&self, path: &Path) -> Result<LspClient> {
412        let language_id = detect_language(path);
413        self.lsp_clients
414            .get(&language_id)
415            .cloned()
416            .ok_or(Error::NoServerForLanguage(language_id))
417    }
418
419    /// Parse and validate a file URI, returning the validated path.
420    ///
421    /// # Errors
422    ///
423    /// Returns an error if:
424    /// - The URI doesn't have a file:// scheme
425    /// - The path is outside workspace boundaries
426    fn parse_file_uri(&self, uri: &lsp_types::Uri) -> Result<PathBuf> {
427        let uri_str = uri.as_str();
428
429        // Validate file:// scheme
430        if !uri_str.starts_with("file://") {
431            return Err(Error::InvalidToolParams(format!(
432                "Invalid URI scheme, expected file:// but got: {uri_str}"
433            )));
434        }
435
436        // Extract path after file://
437        let path_str = &uri_str["file://".len()..];
438
439        // Handle Windows paths: file:///C:/path -> /C:/path -> C:/path
440        // On Windows, URIs have format file:///C:/path, so we need to strip the leading /
441        #[cfg(windows)]
442        let path_str = if path_str.len() >= 3
443            && path_str.starts_with('/')
444            && path_str.chars().nth(2) == Some(':')
445        {
446            &path_str[1..]
447        } else {
448            path_str
449        };
450
451        let path = PathBuf::from(path_str);
452
453        // Validate path is within workspace
454        self.validate_path(&path)
455    }
456
457    /// Handle hover request.
458    ///
459    /// # Errors
460    ///
461    /// Returns an error if the LSP request fails or the file cannot be opened.
462    pub async fn handle_hover(
463        &mut self,
464        file_path: String,
465        line: u32,
466        character: u32,
467    ) -> Result<HoverResult> {
468        let path = PathBuf::from(&file_path);
469        let validated_path = self.validate_path(&path)?;
470        let client = self.get_client_for_file(&validated_path)?;
471        let uri = self
472            .document_tracker
473            .ensure_open(&validated_path, &client)
474            .await?;
475        let lsp_position = mcp_to_lsp_position(line, character);
476
477        let params = LspHoverParams {
478            text_document_position_params: TextDocumentPositionParams {
479                text_document: TextDocumentIdentifier { uri },
480                position: lsp_position,
481            },
482            work_done_progress_params: WorkDoneProgressParams::default(),
483        };
484
485        let timeout_duration = Duration::from_secs(30);
486        let response: Option<Hover> = client
487            .request("textDocument/hover", params, timeout_duration)
488            .await?;
489
490        let result = match response {
491            Some(hover) => {
492                let contents = extract_hover_contents(hover.contents);
493                let range = hover.range.map(normalize_range);
494                HoverResult { contents, range }
495            }
496            None => HoverResult {
497                contents: "No hover information available".to_string(),
498                range: None,
499            },
500        };
501
502        Ok(result)
503    }
504
505    /// Handle definition request.
506    ///
507    /// # Errors
508    ///
509    /// Returns an error if the LSP request fails or the file cannot be opened.
510    pub async fn handle_definition(
511        &mut self,
512        file_path: String,
513        line: u32,
514        character: u32,
515    ) -> Result<DefinitionResult> {
516        let path = PathBuf::from(&file_path);
517        let validated_path = self.validate_path(&path)?;
518        let client = self.get_client_for_file(&validated_path)?;
519        let uri = self
520            .document_tracker
521            .ensure_open(&validated_path, &client)
522            .await?;
523        let lsp_position = mcp_to_lsp_position(line, character);
524
525        let params = GotoDefinitionParams {
526            text_document_position_params: TextDocumentPositionParams {
527                text_document: TextDocumentIdentifier { uri },
528                position: lsp_position,
529            },
530            work_done_progress_params: WorkDoneProgressParams::default(),
531            partial_result_params: PartialResultParams::default(),
532        };
533
534        let timeout_duration = Duration::from_secs(30);
535        let response: Option<lsp_types::GotoDefinitionResponse> = client
536            .request("textDocument/definition", params, timeout_duration)
537            .await?;
538
539        let locations = match response {
540            Some(lsp_types::GotoDefinitionResponse::Scalar(loc)) => vec![loc],
541            Some(lsp_types::GotoDefinitionResponse::Array(locs)) => locs,
542            Some(lsp_types::GotoDefinitionResponse::Link(links)) => links
543                .into_iter()
544                .map(|link| lsp_types::Location {
545                    uri: link.target_uri,
546                    range: link.target_selection_range,
547                })
548                .collect(),
549            None => vec![],
550        };
551
552        let result = DefinitionResult {
553            locations: locations
554                .into_iter()
555                .map(|loc| Location {
556                    uri: loc.uri.to_string(),
557                    range: normalize_range(loc.range),
558                })
559                .collect(),
560        };
561
562        Ok(result)
563    }
564
565    /// Handle references request.
566    ///
567    /// # Errors
568    ///
569    /// Returns an error if the LSP request fails or the file cannot be opened.
570    pub async fn handle_references(
571        &mut self,
572        file_path: String,
573        line: u32,
574        character: u32,
575        include_declaration: bool,
576    ) -> Result<ReferencesResult> {
577        let path = PathBuf::from(&file_path);
578        let validated_path = self.validate_path(&path)?;
579        let client = self.get_client_for_file(&validated_path)?;
580        let uri = self
581            .document_tracker
582            .ensure_open(&validated_path, &client)
583            .await?;
584        let lsp_position = mcp_to_lsp_position(line, character);
585
586        let params = ReferenceParams {
587            text_document_position: TextDocumentPositionParams {
588                text_document: TextDocumentIdentifier { uri },
589                position: lsp_position,
590            },
591            work_done_progress_params: WorkDoneProgressParams::default(),
592            partial_result_params: PartialResultParams::default(),
593            context: ReferenceContext {
594                include_declaration,
595            },
596        };
597
598        let timeout_duration = Duration::from_secs(30);
599        let response: Option<Vec<lsp_types::Location>> = client
600            .request("textDocument/references", params, timeout_duration)
601            .await?;
602
603        let locations = response.unwrap_or_default();
604
605        let result = ReferencesResult {
606            locations: locations
607                .into_iter()
608                .map(|loc| Location {
609                    uri: loc.uri.to_string(),
610                    range: normalize_range(loc.range),
611                })
612                .collect(),
613        };
614
615        Ok(result)
616    }
617
618    /// Handle diagnostics request.
619    ///
620    /// # Errors
621    ///
622    /// Returns an error if the LSP request fails or the file cannot be opened.
623    pub async fn handle_diagnostics(&mut self, file_path: String) -> Result<DiagnosticsResult> {
624        let path = PathBuf::from(&file_path);
625        let validated_path = self.validate_path(&path)?;
626        let client = self.get_client_for_file(&validated_path)?;
627        let uri = self
628            .document_tracker
629            .ensure_open(&validated_path, &client)
630            .await?;
631
632        let params = lsp_types::DocumentDiagnosticParams {
633            text_document: TextDocumentIdentifier { uri },
634            identifier: None,
635            previous_result_id: None,
636            work_done_progress_params: WorkDoneProgressParams::default(),
637            partial_result_params: PartialResultParams::default(),
638        };
639
640        let timeout_duration = Duration::from_secs(30);
641        let response: lsp_types::DocumentDiagnosticReportResult = client
642            .request("textDocument/diagnostic", params, timeout_duration)
643            .await?;
644
645        let diagnostics = match response {
646            lsp_types::DocumentDiagnosticReportResult::Report(report) => match report {
647                lsp_types::DocumentDiagnosticReport::Full(full) => {
648                    full.full_document_diagnostic_report.items
649                }
650                lsp_types::DocumentDiagnosticReport::Unchanged(_) => vec![],
651            },
652            lsp_types::DocumentDiagnosticReportResult::Partial(_) => vec![],
653        };
654
655        let result = DiagnosticsResult {
656            diagnostics: diagnostics
657                .into_iter()
658                .map(|diag| Diagnostic {
659                    range: normalize_range(diag.range),
660                    severity: match diag.severity {
661                        Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
662                        Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
663                        Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
664                            DiagnosticSeverity::Information
665                        }
666                        Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
667                        _ => DiagnosticSeverity::Information,
668                    },
669                    message: diag.message,
670                    code: diag.code.map(|c| match c {
671                        lsp_types::NumberOrString::Number(n) => n.to_string(),
672                        lsp_types::NumberOrString::String(s) => s,
673                    }),
674                })
675                .collect(),
676        };
677
678        Ok(result)
679    }
680
681    /// Handle rename request.
682    ///
683    /// # Errors
684    ///
685    /// Returns an error if the LSP request fails or the file cannot be opened.
686    pub async fn handle_rename(
687        &mut self,
688        file_path: String,
689        line: u32,
690        character: u32,
691        new_name: String,
692    ) -> Result<RenameResult> {
693        let path = PathBuf::from(&file_path);
694        let validated_path = self.validate_path(&path)?;
695        let client = self.get_client_for_file(&validated_path)?;
696        let uri = self
697            .document_tracker
698            .ensure_open(&validated_path, &client)
699            .await?;
700        let lsp_position = mcp_to_lsp_position(line, character);
701
702        let params = LspRenameParams {
703            text_document_position: TextDocumentPositionParams {
704                text_document: TextDocumentIdentifier { uri },
705                position: lsp_position,
706            },
707            new_name,
708            work_done_progress_params: WorkDoneProgressParams::default(),
709        };
710
711        let timeout_duration = Duration::from_secs(30);
712        let response: Option<WorkspaceEdit> = client
713            .request("textDocument/rename", params, timeout_duration)
714            .await?;
715
716        let changes = if let Some(edit) = response {
717            let mut result_changes = Vec::new();
718
719            if let Some(changes_map) = edit.changes {
720                for (uri, edits) in changes_map {
721                    result_changes.push(DocumentChanges {
722                        uri: uri.to_string(),
723                        edits: edits
724                            .into_iter()
725                            .map(|edit| TextEdit {
726                                range: normalize_range(edit.range),
727                                new_text: edit.new_text,
728                            })
729                            .collect(),
730                    });
731                }
732            }
733
734            result_changes
735        } else {
736            vec![]
737        };
738
739        Ok(RenameResult { changes })
740    }
741
742    /// Handle completions request.
743    ///
744    /// # Errors
745    ///
746    /// Returns an error if the LSP request fails or the file cannot be opened.
747    pub async fn handle_completions(
748        &mut self,
749        file_path: String,
750        line: u32,
751        character: u32,
752        trigger: Option<String>,
753    ) -> Result<CompletionsResult> {
754        let path = PathBuf::from(&file_path);
755        let validated_path = self.validate_path(&path)?;
756        let client = self.get_client_for_file(&validated_path)?;
757        let uri = self
758            .document_tracker
759            .ensure_open(&validated_path, &client)
760            .await?;
761        let lsp_position = mcp_to_lsp_position(line, character);
762
763        let context = trigger.map(|trigger_char| lsp_types::CompletionContext {
764            trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
765            trigger_character: Some(trigger_char),
766        });
767
768        let params = CompletionParams {
769            text_document_position: TextDocumentPositionParams {
770                text_document: TextDocumentIdentifier { uri },
771                position: lsp_position,
772            },
773            work_done_progress_params: WorkDoneProgressParams::default(),
774            partial_result_params: PartialResultParams::default(),
775            context,
776        };
777
778        let timeout_duration = Duration::from_secs(10);
779        let response: Option<lsp_types::CompletionResponse> = client
780            .request("textDocument/completion", params, timeout_duration)
781            .await?;
782
783        let items = match response {
784            Some(lsp_types::CompletionResponse::Array(items)) => items,
785            Some(lsp_types::CompletionResponse::List(list)) => list.items,
786            None => vec![],
787        };
788
789        let result = CompletionsResult {
790            items: items
791                .into_iter()
792                .map(|item| Completion {
793                    label: item.label,
794                    kind: item.kind.map(|k| format!("{k:?}")),
795                    detail: item.detail,
796                    documentation: item.documentation.map(|doc| match doc {
797                        lsp_types::Documentation::String(s) => s,
798                        lsp_types::Documentation::MarkupContent(m) => m.value,
799                    }),
800                })
801                .collect(),
802        };
803
804        Ok(result)
805    }
806
807    /// Handle document symbols request.
808    ///
809    /// # Errors
810    ///
811    /// Returns an error if the LSP request fails or the file cannot be opened.
812    pub async fn handle_document_symbols(
813        &mut self,
814        file_path: String,
815    ) -> Result<DocumentSymbolsResult> {
816        let path = PathBuf::from(&file_path);
817        let validated_path = self.validate_path(&path)?;
818        let client = self.get_client_for_file(&validated_path)?;
819        let uri = self
820            .document_tracker
821            .ensure_open(&validated_path, &client)
822            .await?;
823
824        let params = DocumentSymbolParams {
825            text_document: TextDocumentIdentifier { uri },
826            work_done_progress_params: WorkDoneProgressParams::default(),
827            partial_result_params: PartialResultParams::default(),
828        };
829
830        let timeout_duration = Duration::from_secs(30);
831        let response: Option<lsp_types::DocumentSymbolResponse> = client
832            .request("textDocument/documentSymbol", params, timeout_duration)
833            .await?;
834
835        let symbols = match response {
836            Some(lsp_types::DocumentSymbolResponse::Flat(symbols)) => symbols
837                .into_iter()
838                .map(|sym| Symbol {
839                    name: sym.name,
840                    kind: format!("{:?}", sym.kind),
841                    range: normalize_range(sym.location.range),
842                    selection_range: normalize_range(sym.location.range),
843                    children: None,
844                })
845                .collect(),
846            Some(lsp_types::DocumentSymbolResponse::Nested(symbols)) => {
847                symbols.into_iter().map(convert_document_symbol).collect()
848            }
849            None => vec![],
850        };
851
852        Ok(DocumentSymbolsResult { symbols })
853    }
854
855    /// Handle format document request.
856    ///
857    /// # Errors
858    ///
859    /// Returns an error if the LSP request fails or the file cannot be opened.
860    pub async fn handle_format_document(
861        &mut self,
862        file_path: String,
863        tab_size: u32,
864        insert_spaces: bool,
865    ) -> Result<FormatDocumentResult> {
866        let path = PathBuf::from(&file_path);
867        let validated_path = self.validate_path(&path)?;
868        let client = self.get_client_for_file(&validated_path)?;
869        let uri = self
870            .document_tracker
871            .ensure_open(&validated_path, &client)
872            .await?;
873
874        let params = DocumentFormattingParams {
875            text_document: TextDocumentIdentifier { uri },
876            options: FormattingOptions {
877                tab_size,
878                insert_spaces,
879                ..Default::default()
880            },
881            work_done_progress_params: WorkDoneProgressParams::default(),
882        };
883
884        let timeout_duration = Duration::from_secs(30);
885        let response: Option<Vec<lsp_types::TextEdit>> = client
886            .request("textDocument/formatting", params, timeout_duration)
887            .await?;
888
889        let edits = response.unwrap_or_default();
890
891        let result = FormatDocumentResult {
892            edits: edits
893                .into_iter()
894                .map(|edit| TextEdit {
895                    range: normalize_range(edit.range),
896                    new_text: edit.new_text,
897                })
898                .collect(),
899        };
900
901        Ok(result)
902    }
903
904    /// Handle workspace symbol search.
905    ///
906    /// # Errors
907    ///
908    /// Returns an error if the LSP request fails or no server is configured.
909    pub async fn handle_workspace_symbol(
910        &mut self,
911        query: String,
912        kind_filter: Option<String>,
913        limit: u32,
914    ) -> Result<WorkspaceSymbolResult> {
915        const MAX_QUERY_LENGTH: usize = 1000;
916        const VALID_SYMBOL_KINDS: &[&str] = &[
917            "File",
918            "Module",
919            "Namespace",
920            "Package",
921            "Class",
922            "Method",
923            "Property",
924            "Field",
925            "Constructor",
926            "Enum",
927            "Interface",
928            "Function",
929            "Variable",
930            "Constant",
931            "String",
932            "Number",
933            "Boolean",
934            "Array",
935            "Object",
936            "Key",
937            "Null",
938            "EnumMember",
939            "Struct",
940            "Event",
941            "Operator",
942            "TypeParameter",
943        ];
944
945        // Validate query length
946        if query.len() > MAX_QUERY_LENGTH {
947            return Err(Error::InvalidToolParams(format!(
948                "Query too long: {} chars (max {MAX_QUERY_LENGTH})",
949                query.len()
950            )));
951        }
952
953        // Validate kind filter
954        if let Some(ref kind) = kind_filter {
955            if !VALID_SYMBOL_KINDS
956                .iter()
957                .any(|k| k.eq_ignore_ascii_case(kind))
958            {
959                return Err(Error::InvalidToolParams(format!(
960                    "Invalid kind_filter: '{kind}'. Valid values: {VALID_SYMBOL_KINDS:?}"
961                )));
962            }
963        }
964
965        // Workspace search requires at least one LSP client
966        let client = self
967            .lsp_clients
968            .values()
969            .next()
970            .cloned()
971            .ok_or(Error::NoServerConfigured)?;
972
973        let params = LspWorkspaceSymbolParams {
974            query,
975            work_done_progress_params: WorkDoneProgressParams::default(),
976            partial_result_params: PartialResultParams::default(),
977        };
978
979        let timeout_duration = Duration::from_secs(30);
980        let response: Option<Vec<lsp_types::SymbolInformation>> = client
981            .request("workspace/symbol", params, timeout_duration)
982            .await?;
983
984        let mut symbols: Vec<WorkspaceSymbol> = response
985            .unwrap_or_default()
986            .into_iter()
987            .map(|sym| WorkspaceSymbol {
988                name: sym.name,
989                kind: format!("{:?}", sym.kind),
990                location: Location {
991                    uri: sym.location.uri.to_string(),
992                    range: normalize_range(sym.location.range),
993                },
994                container_name: sym.container_name,
995            })
996            .collect();
997
998        // Apply kind filter if specified
999        if let Some(kind) = kind_filter {
1000            symbols.retain(|s| s.kind.eq_ignore_ascii_case(&kind));
1001        }
1002
1003        // Limit results
1004        symbols.truncate(limit as usize);
1005
1006        Ok(WorkspaceSymbolResult { symbols })
1007    }
1008
1009    /// Handle code actions request.
1010    ///
1011    /// # Errors
1012    ///
1013    /// Returns an error if the LSP request fails or the file cannot be opened.
1014    pub async fn handle_code_actions(
1015        &mut self,
1016        file_path: String,
1017        start_line: u32,
1018        start_character: u32,
1019        end_line: u32,
1020        end_character: u32,
1021        kind_filter: Option<String>,
1022    ) -> Result<CodeActionsResult> {
1023        const VALID_ACTION_KINDS: &[&str] = &[
1024            "quickfix",
1025            "refactor",
1026            "refactor.extract",
1027            "refactor.inline",
1028            "refactor.rewrite",
1029            "source",
1030            "source.organizeImports",
1031        ];
1032
1033        // Validate kind filter
1034        if let Some(ref kind) = kind_filter {
1035            if !VALID_ACTION_KINDS
1036                .iter()
1037                .any(|k| k.eq_ignore_ascii_case(kind))
1038            {
1039                return Err(Error::InvalidToolParams(format!(
1040                    "Invalid kind_filter: '{kind}'. Valid values: {VALID_ACTION_KINDS:?}"
1041                )));
1042            }
1043        }
1044
1045        // Validate range
1046        if start_line < 1 || start_character < 1 || end_line < 1 || end_character < 1 {
1047            return Err(Error::InvalidToolParams(
1048                "Line and character positions must be >= 1".to_string(),
1049            ));
1050        }
1051
1052        // Validate position upper bounds
1053        if start_line > MAX_POSITION_VALUE
1054            || start_character > MAX_POSITION_VALUE
1055            || end_line > MAX_POSITION_VALUE
1056            || end_character > MAX_POSITION_VALUE
1057        {
1058            return Err(Error::InvalidToolParams(format!(
1059                "Position values must be <= {MAX_POSITION_VALUE}"
1060            )));
1061        }
1062
1063        // Validate range size
1064        if end_line.saturating_sub(start_line) > MAX_RANGE_LINES {
1065            return Err(Error::InvalidToolParams(format!(
1066                "Range size must be <= {MAX_RANGE_LINES} lines"
1067            )));
1068        }
1069
1070        if start_line > end_line || (start_line == end_line && start_character > end_character) {
1071            return Err(Error::InvalidToolParams(
1072                "Start position must be before or equal to end position".to_string(),
1073            ));
1074        }
1075
1076        let path = PathBuf::from(&file_path);
1077        let validated_path = self.validate_path(&path)?;
1078        let client = self.get_client_for_file(&validated_path)?;
1079        let uri = self
1080            .document_tracker
1081            .ensure_open(&validated_path, &client)
1082            .await?;
1083
1084        let range = lsp_types::Range {
1085            start: mcp_to_lsp_position(start_line, start_character),
1086            end: mcp_to_lsp_position(end_line, end_character),
1087        };
1088
1089        // Build context with optional kind filter
1090        let only = kind_filter.map(|k| vec![lsp_types::CodeActionKind::from(k)]);
1091
1092        let params = lsp_types::CodeActionParams {
1093            text_document: TextDocumentIdentifier { uri },
1094            range,
1095            context: lsp_types::CodeActionContext {
1096                diagnostics: vec![],
1097                only,
1098                trigger_kind: Some(lsp_types::CodeActionTriggerKind::INVOKED),
1099            },
1100            work_done_progress_params: WorkDoneProgressParams::default(),
1101            partial_result_params: PartialResultParams::default(),
1102        };
1103
1104        let timeout_duration = Duration::from_secs(30);
1105        let response: Option<lsp_types::CodeActionResponse> = client
1106            .request("textDocument/codeAction", params, timeout_duration)
1107            .await?;
1108
1109        let response_vec = response.unwrap_or_default();
1110        let mut actions = Vec::with_capacity(response_vec.len());
1111
1112        for action_or_command in response_vec {
1113            let action = match action_or_command {
1114                lsp_types::CodeActionOrCommand::CodeAction(action) => convert_code_action(action),
1115                lsp_types::CodeActionOrCommand::Command(cmd) => {
1116                    let arguments = cmd.arguments.unwrap_or_else(Vec::new);
1117                    CodeAction {
1118                        title: cmd.title.clone(),
1119                        kind: None,
1120                        diagnostics: Vec::new(),
1121                        edit: None,
1122                        command: Some(CommandDescription {
1123                            title: cmd.title,
1124                            command: cmd.command,
1125                            arguments,
1126                        }),
1127                        is_preferred: false,
1128                    }
1129                }
1130            };
1131            actions.push(action);
1132        }
1133
1134        Ok(CodeActionsResult { actions })
1135    }
1136
1137    /// Handle call hierarchy prepare request.
1138    ///
1139    /// # Errors
1140    ///
1141    /// Returns an error if the LSP request fails or the file cannot be opened.
1142    pub async fn handle_call_hierarchy_prepare(
1143        &mut self,
1144        file_path: String,
1145        line: u32,
1146        character: u32,
1147    ) -> Result<CallHierarchyPrepareResult> {
1148        // Validate position bounds
1149        if line < 1 || character < 1 {
1150            return Err(Error::InvalidToolParams(
1151                "Line and character positions must be >= 1".to_string(),
1152            ));
1153        }
1154
1155        if line > MAX_POSITION_VALUE || character > MAX_POSITION_VALUE {
1156            return Err(Error::InvalidToolParams(format!(
1157                "Position values must be <= {MAX_POSITION_VALUE}"
1158            )));
1159        }
1160
1161        let path = PathBuf::from(&file_path);
1162        let validated_path = self.validate_path(&path)?;
1163        let client = self.get_client_for_file(&validated_path)?;
1164        let uri = self
1165            .document_tracker
1166            .ensure_open(&validated_path, &client)
1167            .await?;
1168        let lsp_position = mcp_to_lsp_position(line, character);
1169
1170        let params = LspCallHierarchyPrepareParams {
1171            text_document_position_params: TextDocumentPositionParams {
1172                text_document: TextDocumentIdentifier { uri },
1173                position: lsp_position,
1174            },
1175            work_done_progress_params: WorkDoneProgressParams::default(),
1176        };
1177
1178        let timeout_duration = Duration::from_secs(30);
1179        let response: Option<Vec<CallHierarchyItem>> = client
1180            .request(
1181                "textDocument/prepareCallHierarchy",
1182                params,
1183                timeout_duration,
1184            )
1185            .await?;
1186
1187        // Pre-allocate and build result
1188        let lsp_items = response.unwrap_or_default();
1189        let mut items = Vec::with_capacity(lsp_items.len());
1190        for item in lsp_items {
1191            items.push(convert_call_hierarchy_item(item));
1192        }
1193
1194        Ok(CallHierarchyPrepareResult { items })
1195    }
1196
1197    /// Handle incoming calls request.
1198    ///
1199    /// # Errors
1200    ///
1201    /// Returns an error if the LSP request fails or the item is invalid.
1202    pub async fn handle_incoming_calls(
1203        &mut self,
1204        item: serde_json::Value,
1205    ) -> Result<IncomingCallsResult> {
1206        let lsp_item: CallHierarchyItem = serde_json::from_value(item)
1207            .map_err(|e| Error::InvalidToolParams(format!("Invalid call hierarchy item: {e}")))?;
1208
1209        // Parse and validate the URI
1210        let path = self.parse_file_uri(&lsp_item.uri)?;
1211        let client = self.get_client_for_file(&path)?;
1212
1213        let params = CallHierarchyIncomingCallsParams {
1214            item: lsp_item,
1215            work_done_progress_params: WorkDoneProgressParams::default(),
1216            partial_result_params: PartialResultParams::default(),
1217        };
1218
1219        let timeout_duration = Duration::from_secs(30);
1220        let response: Option<Vec<CallHierarchyIncomingCall>> = client
1221            .request("callHierarchy/incomingCalls", params, timeout_duration)
1222            .await?;
1223
1224        // Pre-allocate and build result
1225        let lsp_calls = response.unwrap_or_default();
1226        let mut calls = Vec::with_capacity(lsp_calls.len());
1227
1228        for call in lsp_calls {
1229            let from_ranges = {
1230                let mut ranges = Vec::with_capacity(call.from_ranges.len());
1231                for range in call.from_ranges {
1232                    ranges.push(normalize_range(range));
1233                }
1234                ranges
1235            };
1236
1237            calls.push(IncomingCall {
1238                from: convert_call_hierarchy_item(call.from),
1239                from_ranges,
1240            });
1241        }
1242
1243        Ok(IncomingCallsResult { calls })
1244    }
1245
1246    /// Handle outgoing calls request.
1247    ///
1248    /// # Errors
1249    ///
1250    /// Returns an error if the LSP request fails or the item is invalid.
1251    pub async fn handle_outgoing_calls(
1252        &mut self,
1253        item: serde_json::Value,
1254    ) -> Result<OutgoingCallsResult> {
1255        let lsp_item: CallHierarchyItem = serde_json::from_value(item)
1256            .map_err(|e| Error::InvalidToolParams(format!("Invalid call hierarchy item: {e}")))?;
1257
1258        // Parse and validate the URI
1259        let path = self.parse_file_uri(&lsp_item.uri)?;
1260        let client = self.get_client_for_file(&path)?;
1261
1262        let params = CallHierarchyOutgoingCallsParams {
1263            item: lsp_item,
1264            work_done_progress_params: WorkDoneProgressParams::default(),
1265            partial_result_params: PartialResultParams::default(),
1266        };
1267
1268        let timeout_duration = Duration::from_secs(30);
1269        let response: Option<Vec<CallHierarchyOutgoingCall>> = client
1270            .request("callHierarchy/outgoingCalls", params, timeout_duration)
1271            .await?;
1272
1273        // Pre-allocate and build result
1274        let lsp_calls = response.unwrap_or_default();
1275        let mut calls = Vec::with_capacity(lsp_calls.len());
1276
1277        for call in lsp_calls {
1278            let from_ranges = {
1279                let mut ranges = Vec::with_capacity(call.from_ranges.len());
1280                for range in call.from_ranges {
1281                    ranges.push(normalize_range(range));
1282                }
1283                ranges
1284            };
1285
1286            calls.push(OutgoingCall {
1287                to: convert_call_hierarchy_item(call.to),
1288                from_ranges,
1289            });
1290        }
1291
1292        Ok(OutgoingCallsResult { calls })
1293    }
1294}
1295
1296/// Extract hover contents as markdown string.
1297fn extract_hover_contents(contents: HoverContents) -> String {
1298    match contents {
1299        HoverContents::Scalar(marked_string) => marked_string_to_string(marked_string),
1300        HoverContents::Array(marked_strings) => marked_strings
1301            .into_iter()
1302            .map(marked_string_to_string)
1303            .collect::<Vec<_>>()
1304            .join("\n\n"),
1305        HoverContents::Markup(markup) => markup.value,
1306    }
1307}
1308
1309/// Convert a marked string to a plain string.
1310fn marked_string_to_string(marked: MarkedString) -> String {
1311    match marked {
1312        MarkedString::String(s) => s,
1313        MarkedString::LanguageString(ls) => format!("```{}\n{}\n```", ls.language, ls.value),
1314    }
1315}
1316
1317/// Convert LSP range to MCP range (0-based to 1-based).
1318const fn normalize_range(range: lsp_types::Range) -> Range {
1319    Range {
1320        start: Position2D {
1321            line: range.start.line + 1,
1322            character: range.start.character + 1,
1323        },
1324        end: Position2D {
1325            line: range.end.line + 1,
1326            character: range.end.character + 1,
1327        },
1328    }
1329}
1330
1331/// Convert LSP document symbol to MCP symbol.
1332fn convert_document_symbol(symbol: DocumentSymbol) -> Symbol {
1333    Symbol {
1334        name: symbol.name,
1335        kind: format!("{:?}", symbol.kind),
1336        range: normalize_range(symbol.range),
1337        selection_range: normalize_range(symbol.selection_range),
1338        children: symbol
1339            .children
1340            .map(|children| children.into_iter().map(convert_document_symbol).collect()),
1341    }
1342}
1343
1344/// Convert LSP call hierarchy item to MCP call hierarchy item.
1345fn convert_call_hierarchy_item(item: CallHierarchyItem) -> CallHierarchyItemResult {
1346    CallHierarchyItemResult {
1347        name: item.name,
1348        kind: format!("{:?}", item.kind),
1349        detail: item.detail,
1350        uri: item.uri.to_string(),
1351        range: normalize_range(item.range),
1352        selection_range: normalize_range(item.selection_range),
1353        data: item.data,
1354    }
1355}
1356
1357/// Convert LSP code action to MCP code action.
1358fn convert_code_action(action: lsp_types::CodeAction) -> CodeAction {
1359    let diagnostics = action.diagnostics.map_or_else(Vec::new, |diags| {
1360        let mut result = Vec::with_capacity(diags.len());
1361        for d in diags {
1362            result.push(Diagnostic {
1363                range: normalize_range(d.range),
1364                severity: match d.severity {
1365                    Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
1366                    Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
1367                    Some(lsp_types::DiagnosticSeverity::INFORMATION) => {
1368                        DiagnosticSeverity::Information
1369                    }
1370                    Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
1371                    _ => DiagnosticSeverity::Information,
1372                },
1373                message: d.message,
1374                code: d.code.map(|c| match c {
1375                    lsp_types::NumberOrString::Number(n) => n.to_string(),
1376                    lsp_types::NumberOrString::String(s) => s,
1377                }),
1378            });
1379        }
1380        result
1381    });
1382
1383    let edit = action.edit.map(|edit| {
1384        let changes = edit.changes.map_or_else(Vec::new, |changes_map| {
1385            let mut result = Vec::with_capacity(changes_map.len());
1386            for (uri, edits) in changes_map {
1387                let mut text_edits = Vec::with_capacity(edits.len());
1388                for e in edits {
1389                    text_edits.push(TextEdit {
1390                        range: normalize_range(e.range),
1391                        new_text: e.new_text,
1392                    });
1393                }
1394                result.push(DocumentChanges {
1395                    uri: uri.to_string(),
1396                    edits: text_edits,
1397                });
1398            }
1399            result
1400        });
1401        WorkspaceEditDescription { changes }
1402    });
1403
1404    let command = action.command.map(|cmd| {
1405        let arguments = cmd.arguments.unwrap_or_else(Vec::new);
1406        CommandDescription {
1407            title: cmd.title,
1408            command: cmd.command,
1409            arguments,
1410        }
1411    });
1412
1413    CodeAction {
1414        title: action.title,
1415        kind: action.kind.map(|k| k.as_str().to_string()),
1416        diagnostics,
1417        edit,
1418        command,
1419        is_preferred: action.is_preferred.unwrap_or(false),
1420    }
1421}
1422
1423#[cfg(test)]
1424#[allow(clippy::unwrap_used)]
1425mod tests {
1426    use std::fs;
1427
1428    use tempfile::TempDir;
1429    use url::Url;
1430
1431    use super::*;
1432
1433    #[test]
1434    fn test_translator_new() {
1435        let translator = Translator::new();
1436        assert_eq!(translator.workspace_roots.len(), 0);
1437        assert_eq!(translator.lsp_clients.len(), 0);
1438        assert_eq!(translator.lsp_servers.len(), 0);
1439    }
1440
1441    #[test]
1442    fn test_set_workspace_roots() {
1443        let mut translator = Translator::new();
1444        let roots = vec![PathBuf::from("/test/root1"), PathBuf::from("/test/root2")];
1445        translator.set_workspace_roots(roots.clone());
1446        assert_eq!(translator.workspace_roots, roots);
1447    }
1448
1449    #[test]
1450    fn test_register_server() {
1451        let translator = Translator::new();
1452
1453        // Initial state: no servers registered
1454        assert_eq!(translator.lsp_servers.len(), 0);
1455
1456        // The register_server method exists and is callable
1457        // Full integration testing with real LspServer is done in integration tests
1458        // This unit test verifies the method signature and basic functionality
1459
1460        // Note: We can't easily construct an LspServer in a unit test without async
1461        // and a real LSP server process. The actual registration functionality is
1462        // tested in integration tests (see rust_analyzer_tests.rs).
1463        // This test verifies the data structure is properly initialized.
1464    }
1465
1466    #[test]
1467    fn test_validate_path_no_workspace_roots() {
1468        let translator = Translator::new();
1469        let temp_dir = TempDir::new().unwrap();
1470        let test_file = temp_dir.path().join("test.rs");
1471        fs::write(&test_file, "fn main() {}").unwrap();
1472
1473        // With no workspace roots, any valid path should be accepted
1474        let result = translator.validate_path(&test_file);
1475        assert!(result.is_ok());
1476    }
1477
1478    #[test]
1479    fn test_validate_path_within_workspace() {
1480        let mut translator = Translator::new();
1481        let temp_dir = TempDir::new().unwrap();
1482        let workspace_root = temp_dir.path().to_path_buf();
1483        translator.set_workspace_roots(vec![workspace_root]);
1484
1485        let test_file = temp_dir.path().join("test.rs");
1486        fs::write(&test_file, "fn main() {}").unwrap();
1487
1488        let result = translator.validate_path(&test_file);
1489        assert!(result.is_ok());
1490    }
1491
1492    #[test]
1493    fn test_validate_path_outside_workspace() {
1494        let mut translator = Translator::new();
1495        let temp_dir1 = TempDir::new().unwrap();
1496        let temp_dir2 = TempDir::new().unwrap();
1497
1498        // Set workspace root to temp_dir1
1499        translator.set_workspace_roots(vec![temp_dir1.path().to_path_buf()]);
1500
1501        // Create file in temp_dir2 (outside workspace)
1502        let test_file = temp_dir2.path().join("test.rs");
1503        fs::write(&test_file, "fn main() {}").unwrap();
1504
1505        let result = translator.validate_path(&test_file);
1506        assert!(matches!(result, Err(Error::PathOutsideWorkspace(_))));
1507    }
1508
1509    #[test]
1510    fn test_normalize_range() {
1511        let lsp_range = lsp_types::Range {
1512            start: lsp_types::Position {
1513                line: 0,
1514                character: 0,
1515            },
1516            end: lsp_types::Position {
1517                line: 2,
1518                character: 5,
1519            },
1520        };
1521
1522        let mcp_range = normalize_range(lsp_range);
1523        assert_eq!(mcp_range.start.line, 1);
1524        assert_eq!(mcp_range.start.character, 1);
1525        assert_eq!(mcp_range.end.line, 3);
1526        assert_eq!(mcp_range.end.character, 6);
1527    }
1528
1529    #[test]
1530    fn test_extract_hover_contents_string() {
1531        let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
1532        let contents = lsp_types::HoverContents::Scalar(marked_string);
1533        let result = extract_hover_contents(contents);
1534        assert_eq!(result, "Test hover");
1535    }
1536
1537    #[test]
1538    fn test_extract_hover_contents_language_string() {
1539        let marked_string = lsp_types::MarkedString::LanguageString(lsp_types::LanguageString {
1540            language: "rust".to_string(),
1541            value: "fn main() {}".to_string(),
1542        });
1543        let contents = lsp_types::HoverContents::Scalar(marked_string);
1544        let result = extract_hover_contents(contents);
1545        assert_eq!(result, "```rust\nfn main() {}\n```");
1546    }
1547
1548    #[test]
1549    fn test_extract_hover_contents_markup() {
1550        let markup = lsp_types::MarkupContent {
1551            kind: lsp_types::MarkupKind::Markdown,
1552            value: "# Documentation".to_string(),
1553        };
1554        let contents = lsp_types::HoverContents::Markup(markup);
1555        let result = extract_hover_contents(contents);
1556        assert_eq!(result, "# Documentation");
1557    }
1558
1559    #[tokio::test]
1560    async fn test_handle_workspace_symbol_no_server() {
1561        let mut translator = Translator::new();
1562        let result = translator
1563            .handle_workspace_symbol("test".to_string(), None, 100)
1564            .await;
1565        assert!(matches!(result, Err(Error::NoServerConfigured)));
1566    }
1567
1568    #[tokio::test]
1569    async fn test_handle_code_actions_invalid_kind() {
1570        let mut translator = Translator::new();
1571        let result = translator
1572            .handle_code_actions(
1573                "/tmp/test.rs".to_string(),
1574                1,
1575                1,
1576                1,
1577                10,
1578                Some("invalid_kind".to_string()),
1579            )
1580            .await;
1581        assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1582    }
1583
1584    #[tokio::test]
1585    async fn test_handle_code_actions_valid_kind_quickfix() {
1586        use tempfile::TempDir;
1587
1588        let mut translator = Translator::new();
1589        let temp_dir = TempDir::new().unwrap();
1590        let test_file = temp_dir.path().join("test.rs");
1591        fs::write(&test_file, "fn main() {}").unwrap();
1592
1593        let result = translator
1594            .handle_code_actions(
1595                test_file.to_str().unwrap().to_string(),
1596                1,
1597                1,
1598                1,
1599                10,
1600                Some("quickfix".to_string()),
1601            )
1602            .await;
1603        // Will fail due to no LSP server, but validates kind is accepted
1604        assert!(result.is_err());
1605        assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1606    }
1607
1608    #[tokio::test]
1609    async fn test_handle_code_actions_valid_kind_refactor() {
1610        use tempfile::TempDir;
1611
1612        let mut translator = Translator::new();
1613        let temp_dir = TempDir::new().unwrap();
1614        let test_file = temp_dir.path().join("test.rs");
1615        fs::write(&test_file, "fn main() {}").unwrap();
1616
1617        let result = translator
1618            .handle_code_actions(
1619                test_file.to_str().unwrap().to_string(),
1620                1,
1621                1,
1622                1,
1623                10,
1624                Some("refactor".to_string()),
1625            )
1626            .await;
1627        assert!(result.is_err());
1628        assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1629    }
1630
1631    #[tokio::test]
1632    async fn test_handle_code_actions_valid_kind_refactor_extract() {
1633        use tempfile::TempDir;
1634
1635        let mut translator = Translator::new();
1636        let temp_dir = TempDir::new().unwrap();
1637        let test_file = temp_dir.path().join("test.rs");
1638        fs::write(&test_file, "fn main() {}").unwrap();
1639
1640        let result = translator
1641            .handle_code_actions(
1642                test_file.to_str().unwrap().to_string(),
1643                1,
1644                1,
1645                1,
1646                10,
1647                Some("refactor.extract".to_string()),
1648            )
1649            .await;
1650        assert!(result.is_err());
1651        assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1652    }
1653
1654    #[tokio::test]
1655    async fn test_handle_code_actions_valid_kind_source() {
1656        use tempfile::TempDir;
1657
1658        let mut translator = Translator::new();
1659        let temp_dir = TempDir::new().unwrap();
1660        let test_file = temp_dir.path().join("test.rs");
1661        fs::write(&test_file, "fn main() {}").unwrap();
1662
1663        let result = translator
1664            .handle_code_actions(
1665                test_file.to_str().unwrap().to_string(),
1666                1,
1667                1,
1668                1,
1669                10,
1670                Some("source.organizeImports".to_string()),
1671            )
1672            .await;
1673        assert!(result.is_err());
1674        assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1675    }
1676
1677    #[tokio::test]
1678    async fn test_handle_code_actions_invalid_range_zero() {
1679        let mut translator = Translator::new();
1680        let result = translator
1681            .handle_code_actions("/tmp/test.rs".to_string(), 0, 1, 1, 10, None)
1682            .await;
1683        assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1684    }
1685
1686    #[tokio::test]
1687    async fn test_handle_code_actions_invalid_range_order() {
1688        let mut translator = Translator::new();
1689        let result = translator
1690            .handle_code_actions("/tmp/test.rs".to_string(), 10, 5, 5, 1, None)
1691            .await;
1692        assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1693    }
1694
1695    #[tokio::test]
1696    async fn test_handle_code_actions_empty_range() {
1697        use tempfile::TempDir;
1698
1699        let mut translator = Translator::new();
1700        let temp_dir = TempDir::new().unwrap();
1701        let test_file = temp_dir.path().join("test.rs");
1702        fs::write(&test_file, "fn main() {}").unwrap();
1703
1704        // Empty range (same position) should be valid
1705        let result = translator
1706            .handle_code_actions(test_file.to_str().unwrap().to_string(), 1, 5, 1, 5, None)
1707            .await;
1708        // Will fail due to no LSP server, but validates range is accepted
1709        assert!(result.is_err());
1710        assert!(!matches!(result, Err(Error::InvalidToolParams(_))));
1711    }
1712
1713    #[test]
1714    fn test_convert_code_action_minimal() {
1715        let lsp_action = lsp_types::CodeAction {
1716            title: "Fix issue".to_string(),
1717            kind: None,
1718            diagnostics: None,
1719            edit: None,
1720            command: None,
1721            is_preferred: None,
1722            disabled: None,
1723            data: None,
1724        };
1725
1726        let result = convert_code_action(lsp_action);
1727        assert_eq!(result.title, "Fix issue");
1728        assert!(result.kind.is_none());
1729        assert!(result.diagnostics.is_empty());
1730        assert!(result.edit.is_none());
1731        assert!(result.command.is_none());
1732        assert!(!result.is_preferred);
1733    }
1734
1735    #[test]
1736    #[allow(clippy::too_many_lines)]
1737    fn test_convert_code_action_with_diagnostics_all_severities() {
1738        let lsp_diagnostics = vec![
1739            lsp_types::Diagnostic {
1740                range: lsp_types::Range {
1741                    start: lsp_types::Position {
1742                        line: 0,
1743                        character: 0,
1744                    },
1745                    end: lsp_types::Position {
1746                        line: 0,
1747                        character: 5,
1748                    },
1749                },
1750                severity: Some(lsp_types::DiagnosticSeverity::ERROR),
1751                message: "Error message".to_string(),
1752                code: Some(lsp_types::NumberOrString::Number(1)),
1753                source: None,
1754                code_description: None,
1755                related_information: None,
1756                tags: None,
1757                data: None,
1758            },
1759            lsp_types::Diagnostic {
1760                range: lsp_types::Range {
1761                    start: lsp_types::Position {
1762                        line: 1,
1763                        character: 0,
1764                    },
1765                    end: lsp_types::Position {
1766                        line: 1,
1767                        character: 5,
1768                    },
1769                },
1770                severity: Some(lsp_types::DiagnosticSeverity::WARNING),
1771                message: "Warning message".to_string(),
1772                code: Some(lsp_types::NumberOrString::String("W001".to_string())),
1773                source: None,
1774                code_description: None,
1775                related_information: None,
1776                tags: None,
1777                data: None,
1778            },
1779            lsp_types::Diagnostic {
1780                range: lsp_types::Range {
1781                    start: lsp_types::Position {
1782                        line: 2,
1783                        character: 0,
1784                    },
1785                    end: lsp_types::Position {
1786                        line: 2,
1787                        character: 5,
1788                    },
1789                },
1790                severity: Some(lsp_types::DiagnosticSeverity::INFORMATION),
1791                message: "Info message".to_string(),
1792                code: None,
1793                source: None,
1794                code_description: None,
1795                related_information: None,
1796                tags: None,
1797                data: None,
1798            },
1799            lsp_types::Diagnostic {
1800                range: lsp_types::Range {
1801                    start: lsp_types::Position {
1802                        line: 3,
1803                        character: 0,
1804                    },
1805                    end: lsp_types::Position {
1806                        line: 3,
1807                        character: 5,
1808                    },
1809                },
1810                severity: Some(lsp_types::DiagnosticSeverity::HINT),
1811                message: "Hint message".to_string(),
1812                code: None,
1813                source: None,
1814                code_description: None,
1815                related_information: None,
1816                tags: None,
1817                data: None,
1818            },
1819        ];
1820
1821        let lsp_action = lsp_types::CodeAction {
1822            title: "Fix all issues".to_string(),
1823            kind: Some(lsp_types::CodeActionKind::QUICKFIX),
1824            diagnostics: Some(lsp_diagnostics),
1825            edit: None,
1826            command: None,
1827            is_preferred: None,
1828            disabled: None,
1829            data: None,
1830        };
1831
1832        let result = convert_code_action(lsp_action);
1833        assert_eq!(result.diagnostics.len(), 4);
1834        assert!(matches!(
1835            result.diagnostics[0].severity,
1836            DiagnosticSeverity::Error
1837        ));
1838        assert!(matches!(
1839            result.diagnostics[1].severity,
1840            DiagnosticSeverity::Warning
1841        ));
1842        assert!(matches!(
1843            result.diagnostics[2].severity,
1844            DiagnosticSeverity::Information
1845        ));
1846        assert!(matches!(
1847            result.diagnostics[3].severity,
1848            DiagnosticSeverity::Hint
1849        ));
1850        assert_eq!(result.diagnostics[0].code, Some("1".to_string()));
1851        assert_eq!(result.diagnostics[1].code, Some("W001".to_string()));
1852    }
1853
1854    #[test]
1855    #[allow(clippy::mutable_key_type)]
1856    fn test_convert_code_action_with_workspace_edit() {
1857        use std::collections::HashMap;
1858        use std::str::FromStr;
1859
1860        let uri = lsp_types::Uri::from_str("file:///test.rs").unwrap();
1861        let mut changes_map = HashMap::new();
1862        changes_map.insert(
1863            uri,
1864            vec![lsp_types::TextEdit {
1865                range: lsp_types::Range {
1866                    start: lsp_types::Position {
1867                        line: 0,
1868                        character: 0,
1869                    },
1870                    end: lsp_types::Position {
1871                        line: 0,
1872                        character: 5,
1873                    },
1874                },
1875                new_text: "fixed".to_string(),
1876            }],
1877        );
1878
1879        let lsp_action = lsp_types::CodeAction {
1880            title: "Apply fix".to_string(),
1881            kind: Some(lsp_types::CodeActionKind::QUICKFIX),
1882            diagnostics: None,
1883            edit: Some(lsp_types::WorkspaceEdit {
1884                changes: Some(changes_map),
1885                document_changes: None,
1886                change_annotations: None,
1887            }),
1888            command: None,
1889            is_preferred: Some(true),
1890            disabled: None,
1891            data: None,
1892        };
1893
1894        let result = convert_code_action(lsp_action);
1895        assert!(result.edit.is_some());
1896        let edit = result.edit.unwrap();
1897        assert_eq!(edit.changes.len(), 1);
1898        assert_eq!(edit.changes[0].uri, "file:///test.rs");
1899        assert_eq!(edit.changes[0].edits.len(), 1);
1900        assert_eq!(edit.changes[0].edits[0].new_text, "fixed");
1901        assert!(result.is_preferred);
1902    }
1903
1904    #[test]
1905    fn test_convert_code_action_with_command() {
1906        let lsp_action = lsp_types::CodeAction {
1907            title: "Run command".to_string(),
1908            kind: Some(lsp_types::CodeActionKind::REFACTOR),
1909            diagnostics: None,
1910            edit: None,
1911            command: Some(lsp_types::Command {
1912                title: "Execute refactor".to_string(),
1913                command: "refactor.extract".to_string(),
1914                arguments: Some(vec![serde_json::json!("arg1"), serde_json::json!(42)]),
1915            }),
1916            is_preferred: None,
1917            disabled: None,
1918            data: None,
1919        };
1920
1921        let result = convert_code_action(lsp_action);
1922        assert!(result.command.is_some());
1923        let cmd = result.command.unwrap();
1924        assert_eq!(cmd.title, "Execute refactor");
1925        assert_eq!(cmd.command, "refactor.extract");
1926        assert_eq!(cmd.arguments.len(), 2);
1927    }
1928
1929    #[tokio::test]
1930    async fn test_handle_call_hierarchy_prepare_invalid_position_zero() {
1931        let mut translator = Translator::new();
1932        let result = translator
1933            .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 0, 1)
1934            .await;
1935        assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1936
1937        let result = translator
1938            .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1, 0)
1939            .await;
1940        assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1941    }
1942
1943    #[tokio::test]
1944    async fn test_handle_call_hierarchy_prepare_invalid_position_too_large() {
1945        let mut translator = Translator::new();
1946        let result = translator
1947            .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1_000_001, 1)
1948            .await;
1949        assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1950
1951        let result = translator
1952            .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1, 1_000_001)
1953            .await;
1954        assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1955    }
1956
1957    #[tokio::test]
1958    async fn test_handle_incoming_calls_invalid_json() {
1959        let mut translator = Translator::new();
1960        let invalid_item = serde_json::json!({"invalid": "structure"});
1961        let result = translator.handle_incoming_calls(invalid_item).await;
1962        assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1963    }
1964
1965    #[tokio::test]
1966    async fn test_handle_outgoing_calls_invalid_json() {
1967        let mut translator = Translator::new();
1968        let invalid_item = serde_json::json!({"invalid": "structure"});
1969        let result = translator.handle_outgoing_calls(invalid_item).await;
1970        assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1971    }
1972
1973    #[tokio::test]
1974    async fn test_parse_file_uri_invalid_scheme() {
1975        let translator = Translator::new();
1976        let uri: lsp_types::Uri = "http://example.com/file.rs".parse().unwrap();
1977        let result = translator.parse_file_uri(&uri);
1978        assert!(matches!(result, Err(Error::InvalidToolParams(_))));
1979    }
1980
1981    #[tokio::test]
1982    async fn test_parse_file_uri_valid_scheme() {
1983        let translator = Translator::new();
1984        let temp_dir = TempDir::new().unwrap();
1985        let test_file = temp_dir.path().join("test.rs");
1986        fs::write(&test_file, "fn main() {}").unwrap();
1987
1988        // Use url crate for cross-platform file URI creation
1989        let file_url = Url::from_file_path(&test_file).unwrap();
1990        let uri: lsp_types::Uri = file_url.as_str().parse().unwrap();
1991        let result = translator.parse_file_uri(&uri);
1992        assert!(result.is_ok());
1993    }
1994}