1use crate::error::{LspError, Result};
4use lsp_types::*;
5use std::path::{Path, PathBuf};
6use url::Url;
7
8pub mod uri {
10 use super::*;
11
12 pub fn from_path<P: AsRef<Path>>(path: P) -> String {
14 let path = path.as_ref();
15
16 #[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 #[cfg(not(windows))]
29 {
30 format!("file://{}", path.display())
31 }
32 }
33
34 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 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 pub fn is_valid(uri: &str) -> bool {
54 Url::parse(uri).is_ok()
55 }
56
57 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 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 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
86pub mod position {
88 use super::*;
89
90 pub fn new(line: u32, character: u32) -> Position {
92 Position { line, character }
93 }
94
95 pub fn line_start(line: u32) -> Position {
97 Position { line, character: 0 }
98 }
99
100 pub fn line_end(line: u32, line_length: u32) -> Position {
102 Position {
103 line,
104 character: line_length,
105 }
106 }
107
108 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 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 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 if (start.line as usize) < line_lengths.len() {
127 total += line_lengths[start.line as usize].saturating_sub(start.character);
128 }
129
130 for line in (start.line + 1)..end.line {
132 if (line as usize) < line_lengths.len() {
133 total += line_lengths[line as usize] + 1; }
135 }
136
137 total += end.character;
139
140 total
141 }
142 }
143}
144
145pub mod range {
147 use super::*;
148
149 pub fn new(start: Position, end: Position) -> Range {
151 Range { start, end }
152 }
153
154 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 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 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 pub fn is_empty(range: &Range) -> bool {
192 range.start == range.end
193 }
194
195 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 pub fn overlaps(range1: &Range, range2: &Range) -> bool {
205 !is_before(range1, range2) && !is_after(range1, range2)
206 }
207
208 pub fn is_before(range1: &Range, range2: &Range) -> bool {
210 position::is_before(&range1.end, &range2.start)
211 }
212
213 pub fn is_after(range1: &Range, range2: &Range) -> bool {
215 position::is_after(&range1.start, &range2.end)
216 }
217
218 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 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
257pub mod document {
259 use super::*;
260
261 pub fn identifier(uri: Uri) -> TextDocumentIdentifier {
263 TextDocumentIdentifier { uri }
264 }
265
266 pub fn versioned_identifier(uri: Uri, version: i32) -> VersionedTextDocumentIdentifier {
268 VersionedTextDocumentIdentifier { uri, version }
269 }
270
271 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 pub fn position_params(uri: Uri, position: Position) -> TextDocumentPositionParams {
287 TextDocumentPositionParams {
288 text_document: identifier(uri),
289 position,
290 }
291 }
292
293 pub fn line_lengths(content: &str) -> Vec<u32> {
295 content.lines().map(|line| line.len() as u32).collect()
296 }
297
298 pub fn get_line(content: &str, line_number: u32) -> Option<&str> {
300 content.lines().nth(line_number as usize)
301 }
302
303 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 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 for line in lines.iter().take(start_line) {
366 result_lines.push(line.to_string());
367 }
368
369 if start_line == end_line {
371 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 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 for line in new_content.lines() {
394 result_lines.push(line.to_string());
395 }
396 }
397
398 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 pub fn apply_edits(content: &str, edits: &[TextEdit]) -> Result<String> {
408 let mut result = content.to_string();
409
410 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
428pub mod completion {
430 use super::*;
431
432 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 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 pub fn sort_by_relevance(items: &mut [CompletionItem]) {
461 items.sort_by(|a, b| {
462 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 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
487pub mod diagnostics {
489 use super::*;
490
491 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 pub fn error<M: Into<String>>(range: Range, message: M) -> Diagnostic {
512 create(range, Some(DiagnosticSeverity::ERROR), message)
513 }
514
515 pub fn warning<M: Into<String>>(range: Range, message: M) -> Diagnostic {
517 create(range, Some(DiagnosticSeverity::WARNING), message)
518 }
519
520 pub fn info<M: Into<String>>(range: Range, message: M) -> Diagnostic {
522 create(range, Some(DiagnosticSeverity::INFORMATION), message)
523 }
524
525 pub fn hint<M: Into<String>>(range: Range, message: M) -> Diagnostic {
527 create(range, Some(DiagnosticSeverity::HINT), message)
528 }
529
530 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 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
554pub mod features {
556 use super::*;
557
558 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 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 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}