Skip to main content

lsp_bridge/
utils.rs

1//! Utility functions and helpers for LSP Bridge.
2
3use crate::error::{LspError, Result};
4use lsp_types::*;
5use std::path::{Path, PathBuf};
6use url::Url;
7
8/// Utility functions for working with URIs.
9pub mod uri {
10    use super::*;
11
12    /// Convert a file path to an LSP URI.
13    pub fn from_path<P: AsRef<Path>>(path: P) -> String {
14        let path = path.as_ref();
15
16        // Handle Windows paths
17        #[cfg(windows)]
18        {
19            let path_str = path.to_string_lossy().replace('\\', "/");
20            if path_str.starts_with('/') {
21                format!("file://{path_str}")
22            } else {
23                format!("file:///{path_str}")
24            }
25        }
26
27        // Handle Unix paths
28        #[cfg(not(windows))]
29        {
30            format!("file://{}", path.display())
31        }
32    }
33
34    /// Convert an LSP URI to a file path.
35    pub fn to_path(uri: &str) -> Result<PathBuf> {
36        let url = Url::parse(uri).map_err(|_| LspError::invalid_uri(uri))?;
37
38        if url.scheme() != "file" {
39            return Err(LspError::invalid_uri(uri).into());
40        }
41
42        url.to_file_path()
43            .map_err(|_| LspError::invalid_uri(uri).into())
44    }
45
46    /// Normalize a URI by ensuring it's properly formatted.
47    pub fn normalize(uri: &str) -> Result<String> {
48        let url = Url::parse(uri).map_err(|_| LspError::invalid_uri(uri))?;
49        Ok(url.to_string())
50    }
51
52    /// Check if a URI is valid.
53    pub fn is_valid(uri: &str) -> bool {
54        Url::parse(uri).is_ok()
55    }
56
57    /// Get the file name from a URI.
58    pub fn file_name(uri: &str) -> Option<String> {
59        to_path(uri).ok().and_then(|path| {
60            path.file_name()
61                .and_then(|name| name.to_str())
62                .map(|s| s.to_string())
63        })
64    }
65
66    /// Get the directory from a URI.
67    pub fn directory(uri: &str) -> Option<String> {
68        to_path(uri)
69            .ok()
70            .and_then(|path| path.parent().map(|p| p.to_path_buf()))
71            .map(from_path)
72    }
73
74    /// Join a base URI with a relative path.
75    pub fn join(base: &str, relative: &str) -> Result<String> {
76        let base_url = Url::parse(base).map_err(|_| LspError::invalid_uri(base))?;
77
78        let joined = base_url
79            .join(relative)
80            .map_err(|_| LspError::invalid_uri(relative))?;
81
82        Ok(joined.to_string())
83    }
84}
85
86/// Utility functions for working with positions and ranges.
87pub mod position {
88    use super::*;
89
90    /// Create a new position.
91    pub fn new(line: u32, character: u32) -> Position {
92        Position { line, character }
93    }
94
95    /// Create a position at the start of a line.
96    pub fn line_start(line: u32) -> Position {
97        Position { line, character: 0 }
98    }
99
100    /// Create a position at the end of a line.
101    pub fn line_end(line: u32, line_length: u32) -> Position {
102        Position {
103            line,
104            character: line_length,
105        }
106    }
107
108    /// Check if a position is before another position.
109    pub fn is_before(pos1: &Position, pos2: &Position) -> bool {
110        pos1.line < pos2.line || (pos1.line == pos2.line && pos1.character < pos2.character)
111    }
112
113    /// Check if a position is after another position.
114    pub fn is_after(pos1: &Position, pos2: &Position) -> bool {
115        pos1.line > pos2.line || (pos1.line == pos2.line && pos1.character > pos2.character)
116    }
117
118    /// Get the distance between two positions (in characters).
119    pub fn distance(start: &Position, end: &Position, line_lengths: &[u32]) -> u32 {
120        if start.line == end.line {
121            end.character.saturating_sub(start.character)
122        } else {
123            let mut total = 0;
124
125            // Characters from start to end of start line
126            if (start.line as usize) < line_lengths.len() {
127                total += line_lengths[start.line as usize].saturating_sub(start.character);
128            }
129
130            // Full lines in between
131            for line in (start.line + 1)..end.line {
132                if (line as usize) < line_lengths.len() {
133                    total += line_lengths[line as usize] + 1; // +1 for newline
134                }
135            }
136
137            // Characters from start of end line to end position
138            total += end.character;
139
140            total
141        }
142    }
143}
144
145/// Utility functions for working with ranges.
146pub mod range {
147    use super::*;
148
149    /// Create a new range.
150    pub fn new(start: Position, end: Position) -> Range {
151        Range { start, end }
152    }
153
154    /// Create a range from coordinates.
155    pub fn from_coords(start_line: u32, start_char: u32, end_line: u32, end_char: u32) -> Range {
156        Range {
157            start: Position {
158                line: start_line,
159                character: start_char,
160            },
161            end: Position {
162                line: end_line,
163                character: end_char,
164            },
165        }
166    }
167
168    /// Create a range that covers a single line.
169    pub fn line(line: u32, line_length: u32) -> Range {
170        Range {
171            start: Position { line, character: 0 },
172            end: Position {
173                line,
174                character: line_length,
175            },
176        }
177    }
178
179    /// Create a range for a single character.
180    pub fn character(line: u32, character: u32) -> Range {
181        Range {
182            start: Position { line, character },
183            end: Position {
184                line,
185                character: character + 1,
186            },
187        }
188    }
189
190    /// Check if a range is empty (start == end).
191    pub fn is_empty(range: &Range) -> bool {
192        range.start == range.end
193    }
194
195    /// Check if a position is within a range.
196    pub fn contains_position(range: &Range, position: &Position) -> bool {
197        (position.line > range.start.line
198            || (position.line == range.start.line && position.character >= range.start.character))
199            && (position.line < range.end.line
200                || (position.line == range.end.line && position.character <= range.end.character))
201    }
202
203    /// Check if two ranges overlap.
204    pub fn overlaps(range1: &Range, range2: &Range) -> bool {
205        !is_before(range1, range2) && !is_after(range1, range2)
206    }
207
208    /// Check if range1 is before range2.
209    pub fn is_before(range1: &Range, range2: &Range) -> bool {
210        position::is_before(&range1.end, &range2.start)
211    }
212
213    /// Check if range1 is after range2.
214    pub fn is_after(range1: &Range, range2: &Range) -> bool {
215        position::is_after(&range1.start, &range2.end)
216    }
217
218    /// Get the intersection of two ranges.
219    pub fn intersection(range1: &Range, range2: &Range) -> Option<Range> {
220        if !overlaps(range1, range2) {
221            return None;
222        }
223
224        let start = if position::is_after(&range1.start, &range2.start) {
225            range1.start
226        } else {
227            range2.start
228        };
229
230        let end = if position::is_before(&range1.end, &range2.end) {
231            range1.end
232        } else {
233            range2.end
234        };
235
236        Some(Range { start, end })
237    }
238
239    /// Get the union of two ranges.
240    pub fn union(range1: &Range, range2: &Range) -> Range {
241        let start = if position::is_before(&range1.start, &range2.start) {
242            range1.start
243        } else {
244            range2.start
245        };
246
247        let end = if position::is_after(&range1.end, &range2.end) {
248            range1.end
249        } else {
250            range2.end
251        };
252
253        Range { start, end }
254    }
255}
256
257/// Utility functions for working with text documents.
258pub mod document {
259    use super::*;
260
261    /// Create a text document identifier.
262    pub fn identifier(uri: Uri) -> TextDocumentIdentifier {
263        TextDocumentIdentifier { uri }
264    }
265
266    /// Create a versioned text document identifier.
267    pub fn versioned_identifier(uri: Uri, version: i32) -> VersionedTextDocumentIdentifier {
268        VersionedTextDocumentIdentifier { uri, version }
269    }
270
271    /// Create a text document item.
272    pub fn item<L, T>(uri: Uri, language_id: L, version: i32, text: T) -> TextDocumentItem
273    where
274        L: Into<String>,
275        T: Into<String>,
276    {
277        TextDocumentItem {
278            uri,
279            language_id: language_id.into(),
280            version,
281            text: text.into(),
282        }
283    }
284
285    /// Create text document position parameters.
286    pub fn position_params(uri: Uri, position: Position) -> TextDocumentPositionParams {
287        TextDocumentPositionParams {
288            text_document: identifier(uri),
289            position,
290        }
291    }
292
293    /// Get line lengths from text content.
294    pub fn line_lengths(content: &str) -> Vec<u32> {
295        content.lines().map(|line| line.len() as u32).collect()
296    }
297
298    /// Get the line at a specific line number.
299    pub fn get_line(content: &str, line_number: u32) -> Option<&str> {
300        content.lines().nth(line_number as usize)
301    }
302
303    /// Get text within a range.
304    pub fn get_range_text(content: &str, range: &Range) -> Result<String> {
305        let lines: Vec<&str> = content.lines().collect();
306
307        if range.start.line as usize >= lines.len() || range.end.line as usize >= lines.len() {
308            return Err(LspError::custom("Range out of bounds").into());
309        }
310
311        if range.start.line == range.end.line {
312            let line = lines[range.start.line as usize];
313            let start_char = range.start.character as usize;
314            let end_char = range.end.character as usize;
315
316            if start_char > line.len() || end_char > line.len() {
317                return Err(LspError::custom("Character position out of bounds").into());
318            }
319
320            return Ok(line[start_char..end_char].to_string());
321        }
322
323        let mut result = String::new();
324        for (i, line) in lines.iter().enumerate() {
325            let line_num = i as u32;
326            if line_num >= range.start.line && line_num <= range.end.line {
327                if line_num == range.start.line {
328                    let start_char = range.start.character as usize;
329                    if start_char <= line.len() {
330                        result.push_str(&line[start_char..]);
331                    }
332                } else if line_num == range.end.line {
333                    let end_char = range.end.character as usize;
334                    if end_char <= line.len() {
335                        result.push_str(&line[..end_char]);
336                    }
337                } else {
338                    result.push_str(line);
339                }
340
341                if line_num < range.end.line {
342                    result.push('\n');
343                }
344            }
345        }
346
347        Ok(result)
348    }
349
350    /// Apply a text edit to content.
351    pub fn apply_edit(content: &str, edit: &TextEdit) -> Result<String> {
352        let lines: Vec<&str> = content.lines().collect();
353        let mut result_lines = Vec::new();
354
355        let start_line = edit.range.start.line as usize;
356        let start_char = edit.range.start.character as usize;
357        let end_line = edit.range.end.line as usize;
358        let end_char = edit.range.end.character as usize;
359
360        if start_line >= lines.len() || end_line >= lines.len() {
361            return Err(LspError::custom("Edit range out of bounds").into());
362        }
363
364        // Copy lines before the edit
365        for line in lines.iter().take(start_line) {
366            result_lines.push(line.to_string());
367        }
368
369        // Apply the edit
370        if start_line == end_line {
371            // Single line edit
372            let line = lines[start_line];
373            if start_char <= line.len() && end_char <= line.len() {
374                let mut new_line = String::new();
375                new_line.push_str(&line[..start_char]);
376                new_line.push_str(&edit.new_text);
377                new_line.push_str(&line[end_char..]);
378                result_lines.push(new_line);
379            } else {
380                return Err(LspError::custom("Edit character positions out of bounds").into());
381            }
382        } else {
383            // Multi-line edit
384            let start_line_content = lines[start_line];
385            let end_line_content = lines[end_line];
386
387            let mut new_content = String::new();
388            new_content.push_str(&start_line_content[..start_char.min(start_line_content.len())]);
389            new_content.push_str(&edit.new_text);
390            new_content.push_str(&end_line_content[end_char.min(end_line_content.len())..]);
391
392            // Add the new content as separate lines
393            for line in new_content.lines() {
394                result_lines.push(line.to_string());
395            }
396        }
397
398        // Copy lines after the edit
399        for line in lines.iter().skip(end_line + 1) {
400            result_lines.push(line.to_string());
401        }
402
403        Ok(result_lines.join("\n"))
404    }
405
406    /// Apply multiple text edits to content.
407    pub fn apply_edits(content: &str, edits: &[TextEdit]) -> Result<String> {
408        let mut result = content.to_string();
409
410        // Sort edits by position (reverse order to apply from end to beginning)
411        let mut sorted_edits = edits.to_vec();
412        sorted_edits.sort_by(|a, b| {
413            b.range
414                .start
415                .line
416                .cmp(&a.range.start.line)
417                .then_with(|| b.range.start.character.cmp(&a.range.start.character))
418        });
419
420        for edit in sorted_edits {
421            result = apply_edit(&result, &edit)?;
422        }
423
424        Ok(result)
425    }
426}
427
428/// Utility functions for working with completion items.
429pub mod completion {
430    use super::*;
431
432    /// Create a simple completion item.
433    pub fn item<L: Into<String>>(label: L, kind: Option<CompletionItemKind>) -> CompletionItem {
434        CompletionItem {
435            label: label.into(),
436            kind,
437            ..Default::default()
438        }
439    }
440
441    /// Create a completion item with detail.
442    pub fn item_with_detail<L, D>(
443        label: L,
444        detail: D,
445        kind: Option<CompletionItemKind>,
446    ) -> CompletionItem
447    where
448        L: Into<String>,
449        D: Into<String>,
450    {
451        CompletionItem {
452            label: label.into(),
453            detail: Some(detail.into()),
454            kind,
455            ..Default::default()
456        }
457    }
458
459    /// Sort completion items by relevance.
460    pub fn sort_by_relevance(items: &mut [CompletionItem]) {
461        items.sort_by(|a, b| {
462            // Sort by sort_text if available, otherwise by label
463            let a_sort = a.sort_text.as_ref().unwrap_or(&a.label);
464            let b_sort = b.sort_text.as_ref().unwrap_or(&b.label);
465            a_sort.cmp(b_sort)
466        });
467    }
468
469    /// Filter completion items by prefix.
470    pub fn filter_by_prefix(items: &[CompletionItem], prefix: &str) -> Vec<CompletionItem> {
471        let prefix_lower = prefix.to_lowercase();
472        items
473            .iter()
474            .filter(|item| {
475                item.label.to_lowercase().starts_with(&prefix_lower)
476                    || item
477                        .filter_text
478                        .as_ref()
479                        .map(|text| text.to_lowercase().starts_with(&prefix_lower))
480                        .unwrap_or(false)
481            })
482            .cloned()
483            .collect()
484    }
485}
486
487/// Utility functions for working with diagnostics.
488pub mod diagnostics {
489    use super::*;
490
491    /// Create a diagnostic.
492    pub fn create<M: Into<String>>(
493        range: Range,
494        severity: Option<DiagnosticSeverity>,
495        message: M,
496    ) -> Diagnostic {
497        Diagnostic {
498            range,
499            severity,
500            code: None,
501            code_description: None,
502            source: None,
503            message: message.into(),
504            related_information: None,
505            tags: None,
506            data: None,
507        }
508    }
509
510    /// Create an error diagnostic.
511    pub fn error<M: Into<String>>(range: Range, message: M) -> Diagnostic {
512        create(range, Some(DiagnosticSeverity::ERROR), message)
513    }
514
515    /// Create a warning diagnostic.
516    pub fn warning<M: Into<String>>(range: Range, message: M) -> Diagnostic {
517        create(range, Some(DiagnosticSeverity::WARNING), message)
518    }
519
520    /// Create an info diagnostic.
521    pub fn info<M: Into<String>>(range: Range, message: M) -> Diagnostic {
522        create(range, Some(DiagnosticSeverity::INFORMATION), message)
523    }
524
525    /// Create a hint diagnostic.
526    pub fn hint<M: Into<String>>(range: Range, message: M) -> Diagnostic {
527        create(range, Some(DiagnosticSeverity::HINT), message)
528    }
529
530    /// Filter diagnostics by severity.
531    pub fn filter_by_severity(
532        diagnostics: &[Diagnostic],
533        severity: DiagnosticSeverity,
534    ) -> Vec<Diagnostic> {
535        diagnostics
536            .iter()
537            .filter(|diag| diag.severity == Some(severity))
538            .cloned()
539            .collect()
540    }
541
542    /// Sort diagnostics by position.
543    pub fn sort_by_position(diagnostics: &mut [Diagnostic]) {
544        diagnostics.sort_by(|a, b| {
545            a.range
546                .start
547                .line
548                .cmp(&b.range.start.line)
549                .then_with(|| a.range.start.character.cmp(&b.range.start.character))
550        });
551    }
552}
553
554/// Utility functions for working with language server features.
555pub mod features {
556    use super::*;
557
558    /// Check if a server supports a specific capability.
559    pub fn supports_capability(capabilities: &ServerCapabilities, feature: &str) -> bool {
560        match feature {
561            "textDocumentSync" => capabilities.text_document_sync.is_some(),
562            "completion" => capabilities.completion_provider.is_some(),
563            "hover" => capabilities.hover_provider.is_some(),
564            "signatureHelp" => capabilities.signature_help_provider.is_some(),
565            "definition" => capabilities.definition_provider.is_some(),
566            "typeDefinition" => capabilities.type_definition_provider.is_some(),
567            "implementation" => capabilities.implementation_provider.is_some(),
568            "references" => capabilities.references_provider.is_some(),
569            "documentHighlight" => capabilities.document_highlight_provider.is_some(),
570            "documentSymbol" => capabilities.document_symbol_provider.is_some(),
571            "codeAction" => capabilities.code_action_provider.is_some(),
572            "codeLens" => capabilities.code_lens_provider.is_some(),
573            "documentLink" => capabilities.document_link_provider.is_some(),
574            "colorProvider" => capabilities.color_provider.is_some(),
575            "formatting" => capabilities.document_formatting_provider.is_some(),
576            "rangeFormatting" => capabilities.document_range_formatting_provider.is_some(),
577            "onTypeFormatting" => capabilities.document_on_type_formatting_provider.is_some(),
578            "rename" => capabilities.rename_provider.is_some(),
579            "foldingRange" => capabilities.folding_range_provider.is_some(),
580            "executeCommand" => capabilities.execute_command_provider.is_some(),
581            "selectionRange" => capabilities.selection_range_provider.is_some(),
582            "workspaceSymbol" => capabilities.workspace_symbol_provider.is_some(),
583            _ => false,
584        }
585    }
586
587    /// Get a list of supported features from server capabilities.
588    pub fn list_supported_features(capabilities: &ServerCapabilities) -> Vec<String> {
589        let mut features = Vec::new();
590
591        if capabilities.text_document_sync.is_some() {
592            features.push("textDocumentSync".to_string());
593        }
594        if capabilities.completion_provider.is_some() {
595            features.push("completion".to_string());
596        }
597        if capabilities.hover_provider.is_some() {
598            features.push("hover".to_string());
599        }
600        if capabilities.signature_help_provider.is_some() {
601            features.push("signatureHelp".to_string());
602        }
603        if capabilities.definition_provider.is_some() {
604            features.push("definition".to_string());
605        }
606        if capabilities.type_definition_provider.is_some() {
607            features.push("typeDefinition".to_string());
608        }
609        if capabilities.implementation_provider.is_some() {
610            features.push("implementation".to_string());
611        }
612        if capabilities.references_provider.is_some() {
613            features.push("references".to_string());
614        }
615        if capabilities.document_highlight_provider.is_some() {
616            features.push("documentHighlight".to_string());
617        }
618        if capabilities.document_symbol_provider.is_some() {
619            features.push("documentSymbol".to_string());
620        }
621        if capabilities.code_action_provider.is_some() {
622            features.push("codeAction".to_string());
623        }
624        if capabilities.code_lens_provider.is_some() {
625            features.push("codeLens".to_string());
626        }
627        if capabilities.document_link_provider.is_some() {
628            features.push("documentLink".to_string());
629        }
630        if capabilities.color_provider.is_some() {
631            features.push("colorProvider".to_string());
632        }
633        if capabilities.document_formatting_provider.is_some() {
634            features.push("formatting".to_string());
635        }
636        if capabilities.document_range_formatting_provider.is_some() {
637            features.push("rangeFormatting".to_string());
638        }
639        if capabilities.document_on_type_formatting_provider.is_some() {
640            features.push("onTypeFormatting".to_string());
641        }
642        if capabilities.rename_provider.is_some() {
643            features.push("rename".to_string());
644        }
645        if capabilities.folding_range_provider.is_some() {
646            features.push("foldingRange".to_string());
647        }
648        if capabilities.execute_command_provider.is_some() {
649            features.push("executeCommand".to_string());
650        }
651        if capabilities.selection_range_provider.is_some() {
652            features.push("selectionRange".to_string());
653        }
654        if capabilities.workspace_symbol_provider.is_some() {
655            features.push("workspaceSymbol".to_string());
656        }
657
658        features
659    }
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665
666    #[test]
667    fn test_uri_conversion() {
668        // Use a platform-appropriate path
669        let path = if cfg!(windows) {
670            Path::new("C:\\tmp\\test.rs")
671        } else {
672            Path::new("/tmp/test.rs")
673        };
674
675        let uri = uri::from_path(path);
676        let back_path = uri::to_path(&uri).unwrap();
677        assert_eq!(back_path, path);
678    }
679
680    #[test]
681    fn test_position_utilities() {
682        let pos1 = position::new(1, 5);
683        let pos2 = position::new(2, 3);
684
685        assert!(position::is_before(&pos1, &pos2));
686        assert!(!position::is_after(&pos1, &pos2));
687    }
688
689    #[test]
690    fn test_range_utilities() {
691        let range1 = range::from_coords(1, 0, 1, 10);
692        let range2 = range::from_coords(1, 5, 1, 15);
693
694        assert!(range::overlaps(&range1, &range2));
695
696        let intersection = range::intersection(&range1, &range2).unwrap();
697        assert_eq!(intersection.start.character, 5);
698        assert_eq!(intersection.end.character, 10);
699    }
700
701    #[test]
702    fn test_document_utilities() {
703        let content = "line 1\nline 2\nline 3";
704        let range = range::from_coords(1, 0, 1, 6);
705
706        let range_text = document::get_range_text(content, &range).unwrap();
707        assert_eq!(range_text, "line 2");
708    }
709
710    #[test]
711    fn test_text_edit_application() {
712        let content = "hello world";
713        let edit = TextEdit {
714            range: range::from_coords(0, 6, 0, 11),
715            new_text: "LSP".to_string(),
716        };
717
718        let result = document::apply_edit(content, &edit).unwrap();
719        assert_eq!(result, "hello LSP");
720    }
721}