1use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use lsp_types::{DidOpenTextDocumentParams, TextDocumentItem, Uri};
9use url::Url;
10
11use crate::error::{Error, Result};
12use crate::lsp::LspClient;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct DocumentState {
17 pub uri: Uri,
19 pub language_id: String,
21 pub version: i32,
23 pub content: String,
25}
26
27#[derive(Debug, Clone, Copy)]
29pub struct ResourceLimits {
30 pub max_documents: usize,
32 pub max_file_size: u64,
34}
35
36impl Default for ResourceLimits {
37 fn default() -> Self {
38 Self {
39 max_documents: 100,
40 max_file_size: 10 * 1024 * 1024, }
42 }
43}
44
45#[derive(Debug)]
47pub struct DocumentTracker {
48 documents: HashMap<PathBuf, DocumentState>,
50 limits: ResourceLimits,
52 extension_map: HashMap<String, String>,
54}
55
56impl DocumentTracker {
57 #[must_use]
59 pub fn new(limits: ResourceLimits, extension_map: HashMap<String, String>) -> Self {
60 Self {
61 documents: HashMap::new(),
62 limits,
63 extension_map,
64 }
65 }
66
67 #[must_use]
69 pub fn is_open(&self, path: &Path) -> bool {
70 self.documents.contains_key(path)
71 }
72
73 #[must_use]
75 pub fn get(&self, path: &Path) -> Option<&DocumentState> {
76 self.documents.get(path)
77 }
78
79 #[must_use]
81 pub fn len(&self) -> usize {
82 self.documents.len()
83 }
84
85 #[must_use]
87 pub fn is_empty(&self) -> bool {
88 self.documents.is_empty()
89 }
90
91 pub fn open(&mut self, path: PathBuf, content: String) -> Result<Uri> {
101 if self.limits.max_documents > 0 && self.documents.len() >= self.limits.max_documents {
103 return Err(Error::DocumentLimitExceeded {
104 current: self.documents.len(),
105 max: self.limits.max_documents,
106 });
107 }
108
109 let size = content.len() as u64;
111 if self.limits.max_file_size > 0 && size > self.limits.max_file_size {
112 return Err(Error::FileSizeLimitExceeded {
113 size,
114 max: self.limits.max_file_size,
115 });
116 }
117
118 let uri = path_to_uri(&path);
119 let language_id = detect_language(&path, &self.extension_map);
120
121 let state = DocumentState {
122 uri: uri.clone(),
123 language_id,
124 version: 1,
125 content,
126 };
127
128 self.documents.insert(path, state);
129 Ok(uri)
130 }
131
132 pub fn update(&mut self, path: &Path, content: String) -> Option<i32> {
136 if let Some(state) = self.documents.get_mut(path) {
137 state.version += 1;
138 state.content = content;
139 Some(state.version)
140 } else {
141 None
142 }
143 }
144
145 pub fn close(&mut self, path: &Path) -> Option<DocumentState> {
149 self.documents.remove(path)
150 }
151
152 pub fn close_all(&mut self) -> Vec<DocumentState> {
154 self.documents.drain().map(|(_, state)| state).collect()
155 }
156
157 pub fn open_paths(&self) -> impl Iterator<Item = &Path> {
159 self.documents.keys().map(PathBuf::as_path)
160 }
161
162 pub async fn ensure_open(&mut self, path: &Path, lsp_client: &LspClient) -> Result<Uri> {
175 if let Some(state) = self.documents.get(path) {
176 return Ok(state.uri.clone());
177 }
178
179 let content = tokio::fs::read_to_string(path)
180 .await
181 .map_err(|e| Error::FileIo {
182 path: path.to_path_buf(),
183 source: e,
184 })?;
185
186 let uri = self.open(path.to_path_buf(), content.clone())?;
187 let state = self
188 .documents
189 .get(path)
190 .ok_or_else(|| Error::DocumentNotFound(path.to_path_buf()))?;
191
192 let params = DidOpenTextDocumentParams {
193 text_document: TextDocumentItem {
194 uri: uri.clone(),
195 language_id: state.language_id.clone(),
196 version: state.version,
197 text: content,
198 },
199 };
200
201 lsp_client.notify("textDocument/didOpen", params).await?;
202
203 Ok(uri)
204 }
205}
206
207#[must_use]
214pub fn path_to_uri(path: &Path) -> Uri {
215 let uri_string = if cfg!(windows) {
216 let path_str = path.to_string_lossy();
217 let stripped = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
220 format!("file:///{}", stripped.replace('\\', "/"))
221 } else {
222 format!("file://{}", path.display())
223 };
224 #[allow(clippy::expect_used)]
225 uri_string.parse().expect("failed to create URI from path")
226}
227
228#[must_use]
233pub fn uri_to_path(uri: &Uri) -> Option<PathBuf> {
234 let url = Url::parse(uri.as_str()).ok()?;
235 if url.scheme() != "file" {
236 return None;
237 }
238 if !url.host_str().unwrap_or("").is_empty() {
241 return None;
242 }
243 url.to_file_path().ok()
244}
245
246#[must_use]
251pub fn detect_language(path: &Path, extension_map: &HashMap<String, String>) -> String {
252 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
253
254 extension_map
255 .get(extension)
256 .cloned()
257 .unwrap_or_else(|| "plaintext".to_string())
258}
259
260#[cfg(test)]
261#[allow(clippy::unwrap_used)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_detect_language() {
267 let mut map = HashMap::new();
268 map.insert("rs".to_string(), "rust".to_string());
269 map.insert("py".to_string(), "python".to_string());
270 map.insert("ts".to_string(), "typescript".to_string());
271
272 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
273 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
274 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
275 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
276 }
277
278 #[test]
279 fn test_document_tracker() {
280 let mut map = HashMap::new();
281 map.insert("rs".to_string(), "rust".to_string());
282
283 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
284 let path = PathBuf::from("/test/file.rs");
285
286 assert!(!tracker.is_open(&path));
287
288 tracker
289 .open(path.clone(), "fn main() {}".to_string())
290 .unwrap();
291 assert!(tracker.is_open(&path));
292 assert_eq!(tracker.len(), 1);
293
294 let state = tracker.get(&path).unwrap();
295 assert_eq!(state.version, 1);
296 assert_eq!(state.language_id, "rust");
297
298 let new_version = tracker.update(&path, "fn main() { println!() }".to_string());
299 assert_eq!(new_version, Some(2));
300
301 tracker.close(&path);
302 assert!(!tracker.is_open(&path));
303 assert!(tracker.is_empty());
304 }
305
306 #[test]
307 fn test_document_limit() {
308 let limits = ResourceLimits {
309 max_documents: 2,
310 max_file_size: 100,
311 };
312 let mut map = HashMap::new();
313 map.insert("rs".to_string(), "rust".to_string());
314
315 let mut tracker = DocumentTracker::new(limits, map);
316
317 tracker
319 .open(PathBuf::from("/test/file1.rs"), "fn test1() {}".to_string())
320 .unwrap();
321 tracker
322 .open(PathBuf::from("/test/file2.rs"), "fn test2() {}".to_string())
323 .unwrap();
324
325 let result = tracker.open(PathBuf::from("/test/file3.rs"), "fn test3() {}".to_string());
327 assert!(matches!(result, Err(Error::DocumentLimitExceeded { .. })));
328 }
329
330 #[test]
331 fn test_file_size_limit() {
332 let limits = ResourceLimits {
333 max_documents: 10,
334 max_file_size: 10,
335 };
336 let mut map = HashMap::new();
337 map.insert("rs".to_string(), "rust".to_string());
338
339 let mut tracker = DocumentTracker::new(limits, map);
340
341 tracker
343 .open(PathBuf::from("/test/small.rs"), "fn f(){}".to_string())
344 .unwrap();
345
346 let large_content = "x".repeat(100);
348 let result = tracker.open(PathBuf::from("/test/large.rs"), large_content);
349 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
350 }
351
352 #[test]
353 fn test_resource_limits_default() {
354 let limits = ResourceLimits::default();
355 assert_eq!(limits.max_documents, 100);
356 assert_eq!(limits.max_file_size, 10 * 1024 * 1024);
357 }
358
359 #[test]
360 fn test_resource_limits_custom() {
361 let limits = ResourceLimits {
362 max_documents: 50,
363 max_file_size: 5 * 1024 * 1024,
364 };
365 assert_eq!(limits.max_documents, 50);
366 assert_eq!(limits.max_file_size, 5 * 1024 * 1024);
367 }
368
369 #[test]
370 fn test_resource_limits_zero_unlimited() {
371 let limits = ResourceLimits {
372 max_documents: 0,
373 max_file_size: 0,
374 };
375 let mut map = HashMap::new();
376 map.insert("rs".to_string(), "rust".to_string());
377
378 let mut tracker = DocumentTracker::new(limits, map);
379
380 for i in 0..200 {
382 tracker
383 .open(
384 PathBuf::from(format!("/test/file{i}.rs")),
385 "content".to_string(),
386 )
387 .unwrap();
388 }
389 assert_eq!(tracker.len(), 200);
390
391 let huge_content = "x".repeat(100_000_000);
393 tracker
394 .open(PathBuf::from("/test/huge.rs"), huge_content)
395 .unwrap();
396 }
397
398 #[test]
399 fn test_document_state_clone() {
400 let state = DocumentState {
401 uri: "file:///test.rs".parse().unwrap(),
402 language_id: "rust".to_string(),
403 version: 5,
404 content: "fn main() {}".to_string(),
405 };
406
407 #[allow(clippy::redundant_clone)]
408 let cloned = state.clone();
409 assert_eq!(cloned.uri, state.uri);
410 assert_eq!(cloned.language_id, state.language_id);
411 assert_eq!(cloned.version, 5);
412 assert_eq!(cloned.content, state.content);
413 }
414
415 #[test]
416 fn test_update_nonexistent_document() {
417 let map = HashMap::new();
418 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
419 let path = PathBuf::from("/test/nonexistent.rs");
420
421 let version = tracker.update(&path, "new content".to_string());
422 assert_eq!(
423 version, None,
424 "Updating non-existent document should return None"
425 );
426 }
427
428 #[test]
429 fn test_close_nonexistent_document() {
430 let map = HashMap::new();
431 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
432 let path = PathBuf::from("/test/nonexistent.rs");
433
434 let state = tracker.close(&path);
435 assert_eq!(
436 state, None,
437 "Closing non-existent document should return None"
438 );
439 }
440
441 #[test]
442 fn test_close_all_documents() {
443 let mut map = HashMap::new();
444 map.insert("rs".to_string(), "rust".to_string());
445
446 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
447
448 tracker
449 .open(PathBuf::from("/test/file1.rs"), "content1".to_string())
450 .unwrap();
451 tracker
452 .open(PathBuf::from("/test/file2.rs"), "content2".to_string())
453 .unwrap();
454 tracker
455 .open(PathBuf::from("/test/file3.rs"), "content3".to_string())
456 .unwrap();
457
458 assert_eq!(tracker.len(), 3);
459
460 let closed = tracker.close_all();
461 assert_eq!(closed.len(), 3);
462 assert!(tracker.is_empty());
463 }
464
465 #[test]
466 fn test_get_nonexistent_document() {
467 let map = HashMap::new();
468 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
469 let path = PathBuf::from("/test/nonexistent.rs");
470
471 let state = tracker.get(&path);
472 assert!(
473 state.is_none(),
474 "Getting non-existent document should return None"
475 );
476 }
477
478 #[test]
479 fn test_document_version_increments() {
480 let mut map = HashMap::new();
481 map.insert("rs".to_string(), "rust".to_string());
482
483 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
484 let path = PathBuf::from("/test/versioned.rs");
485
486 tracker.open(path.clone(), "v1".to_string()).unwrap();
487 assert_eq!(tracker.get(&path).unwrap().version, 1);
488
489 tracker.update(&path, "v2".to_string());
490 assert_eq!(tracker.get(&path).unwrap().version, 2);
491
492 tracker.update(&path, "v3".to_string());
493 assert_eq!(tracker.get(&path).unwrap().version, 3);
494
495 tracker.update(&path, "v4".to_string());
496 assert_eq!(tracker.get(&path).unwrap().version, 4);
497 }
498
499 #[test]
500 #[allow(clippy::too_many_lines)]
501 fn test_detect_language_all_extensions() {
502 let mut map = HashMap::new();
503 map.insert("rs".to_string(), "rust".to_string());
504 map.insert("py".to_string(), "python".to_string());
505 map.insert("pyw".to_string(), "python".to_string());
506 map.insert("pyi".to_string(), "python".to_string());
507 map.insert("js".to_string(), "javascript".to_string());
508 map.insert("mjs".to_string(), "javascript".to_string());
509 map.insert("cjs".to_string(), "javascript".to_string());
510 map.insert("ts".to_string(), "typescript".to_string());
511 map.insert("mts".to_string(), "typescript".to_string());
512 map.insert("cts".to_string(), "typescript".to_string());
513 map.insert("tsx".to_string(), "typescriptreact".to_string());
514 map.insert("jsx".to_string(), "javascriptreact".to_string());
515 map.insert("go".to_string(), "go".to_string());
516 map.insert("c".to_string(), "c".to_string());
517 map.insert("h".to_string(), "c".to_string());
518 map.insert("cpp".to_string(), "cpp".to_string());
519 map.insert("cc".to_string(), "cpp".to_string());
520 map.insert("cxx".to_string(), "cpp".to_string());
521 map.insert("hpp".to_string(), "cpp".to_string());
522 map.insert("hh".to_string(), "cpp".to_string());
523 map.insert("hxx".to_string(), "cpp".to_string());
524 map.insert("java".to_string(), "java".to_string());
525 map.insert("rb".to_string(), "ruby".to_string());
526 map.insert("php".to_string(), "php".to_string());
527 map.insert("swift".to_string(), "swift".to_string());
528 map.insert("kt".to_string(), "kotlin".to_string());
529 map.insert("kts".to_string(), "kotlin".to_string());
530 map.insert("scala".to_string(), "scala".to_string());
531 map.insert("sc".to_string(), "scala".to_string());
532 map.insert("zig".to_string(), "zig".to_string());
533 map.insert("lua".to_string(), "lua".to_string());
534 map.insert("sh".to_string(), "shellscript".to_string());
535 map.insert("bash".to_string(), "shellscript".to_string());
536 map.insert("zsh".to_string(), "shellscript".to_string());
537 map.insert("json".to_string(), "json".to_string());
538 map.insert("toml".to_string(), "toml".to_string());
539 map.insert("yaml".to_string(), "yaml".to_string());
540 map.insert("yml".to_string(), "yaml".to_string());
541 map.insert("xml".to_string(), "xml".to_string());
542 map.insert("html".to_string(), "html".to_string());
543 map.insert("htm".to_string(), "html".to_string());
544 map.insert("css".to_string(), "css".to_string());
545 map.insert("scss".to_string(), "scss".to_string());
546 map.insert("less".to_string(), "less".to_string());
547 map.insert("md".to_string(), "markdown".to_string());
548 map.insert("markdown".to_string(), "markdown".to_string());
549
550 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
551 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
552 assert_eq!(detect_language(Path::new("script.pyw"), &map), "python");
553 assert_eq!(detect_language(Path::new("script.pyi"), &map), "python");
554 assert_eq!(detect_language(Path::new("app.js"), &map), "javascript");
555 assert_eq!(detect_language(Path::new("app.mjs"), &map), "javascript");
556 assert_eq!(detect_language(Path::new("app.cjs"), &map), "javascript");
557 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
558 assert_eq!(detect_language(Path::new("app.mts"), &map), "typescript");
559 assert_eq!(detect_language(Path::new("app.cts"), &map), "typescript");
560 assert_eq!(
561 detect_language(Path::new("component.tsx"), &map),
562 "typescriptreact"
563 );
564 assert_eq!(
565 detect_language(Path::new("component.jsx"), &map),
566 "javascriptreact"
567 );
568 assert_eq!(detect_language(Path::new("main.go"), &map), "go");
569 assert_eq!(detect_language(Path::new("main.c"), &map), "c");
570 assert_eq!(detect_language(Path::new("header.h"), &map), "c");
571 assert_eq!(detect_language(Path::new("main.cpp"), &map), "cpp");
572 assert_eq!(detect_language(Path::new("main.cc"), &map), "cpp");
573 assert_eq!(detect_language(Path::new("main.cxx"), &map), "cpp");
574 assert_eq!(detect_language(Path::new("header.hpp"), &map), "cpp");
575 assert_eq!(detect_language(Path::new("header.hh"), &map), "cpp");
576 assert_eq!(detect_language(Path::new("header.hxx"), &map), "cpp");
577 assert_eq!(detect_language(Path::new("Main.java"), &map), "java");
578 assert_eq!(detect_language(Path::new("script.rb"), &map), "ruby");
579 assert_eq!(detect_language(Path::new("index.php"), &map), "php");
580 assert_eq!(detect_language(Path::new("App.swift"), &map), "swift");
581 assert_eq!(detect_language(Path::new("Main.kt"), &map), "kotlin");
582 assert_eq!(detect_language(Path::new("script.kts"), &map), "kotlin");
583 assert_eq!(detect_language(Path::new("Main.scala"), &map), "scala");
584 assert_eq!(detect_language(Path::new("script.sc"), &map), "scala");
585 assert_eq!(detect_language(Path::new("main.zig"), &map), "zig");
586 assert_eq!(detect_language(Path::new("script.lua"), &map), "lua");
587 assert_eq!(detect_language(Path::new("script.sh"), &map), "shellscript");
588 assert_eq!(
589 detect_language(Path::new("script.bash"), &map),
590 "shellscript"
591 );
592 assert_eq!(
593 detect_language(Path::new("script.zsh"), &map),
594 "shellscript"
595 );
596 assert_eq!(detect_language(Path::new("data.json"), &map), "json");
597 assert_eq!(detect_language(Path::new("config.toml"), &map), "toml");
598 assert_eq!(detect_language(Path::new("config.yaml"), &map), "yaml");
599 assert_eq!(detect_language(Path::new("config.yml"), &map), "yaml");
600 assert_eq!(detect_language(Path::new("data.xml"), &map), "xml");
601 assert_eq!(detect_language(Path::new("index.html"), &map), "html");
602 assert_eq!(detect_language(Path::new("index.htm"), &map), "html");
603 assert_eq!(detect_language(Path::new("styles.css"), &map), "css");
604 assert_eq!(detect_language(Path::new("styles.scss"), &map), "scss");
605 assert_eq!(detect_language(Path::new("styles.less"), &map), "less");
606 assert_eq!(detect_language(Path::new("README.md"), &map), "markdown");
607 assert_eq!(
608 detect_language(Path::new("README.markdown"), &map),
609 "markdown"
610 );
611 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
612 assert_eq!(
613 detect_language(Path::new("no_extension"), &map),
614 "plaintext"
615 );
616 }
617
618 #[test]
619 fn test_path_to_uri_unix() {
620 #[cfg(not(windows))]
621 {
622 let path = Path::new("/home/user/project/main.rs");
623 let uri = path_to_uri(path);
624 assert!(
625 uri.as_str()
626 .starts_with("file:///home/user/project/main.rs")
627 );
628 }
629 }
630
631 #[test]
632 fn test_path_to_uri_with_special_chars() {
633 let path = Path::new("/home/user/project-test/main.rs");
634 let uri = path_to_uri(path);
635 assert!(uri.as_str().starts_with("file://"));
636 assert!(uri.as_str().contains("project-test"));
637 }
638
639 #[test]
640 fn test_document_tracker_concurrent_operations() {
641 let mut map = HashMap::new();
642 map.insert("rs".to_string(), "rust".to_string());
643
644 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
645 let path1 = PathBuf::from("/test/file1.rs");
646 let path2 = PathBuf::from("/test/file2.rs");
647
648 tracker.open(path1.clone(), "content1".to_string()).unwrap();
649 tracker.open(path2.clone(), "content2".to_string()).unwrap();
650
651 assert_eq!(tracker.len(), 2);
652 assert!(tracker.is_open(&path1));
653 assert!(tracker.is_open(&path2));
654
655 tracker.update(&path1, "new content1".to_string());
656 assert_eq!(tracker.get(&path1).unwrap().content, "new content1");
657 assert_eq!(tracker.get(&path2).unwrap().content, "content2");
658
659 tracker.close(&path1);
660 assert_eq!(tracker.len(), 1);
661 assert!(!tracker.is_open(&path1));
662 assert!(tracker.is_open(&path2));
663 }
664
665 #[test]
666 fn test_empty_content() {
667 let mut map = HashMap::new();
668 map.insert("rs".to_string(), "rust".to_string());
669
670 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
671 let path = PathBuf::from("/test/empty.rs");
672
673 tracker.open(path.clone(), String::new()).unwrap();
674 assert!(tracker.is_open(&path));
675 assert_eq!(tracker.get(&path).unwrap().content, "");
676 }
677
678 #[test]
679 fn test_unicode_content() {
680 let mut map = HashMap::new();
681 map.insert("rs".to_string(), "rust".to_string());
682
683 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
684 let path = PathBuf::from("/test/unicode.rs");
685 let content = "fn テスト() { println!(\"こんにちは\"); }";
686
687 tracker.open(path.clone(), content.to_string()).unwrap();
688 assert_eq!(tracker.get(&path).unwrap().content, content);
689 }
690
691 #[test]
692 fn test_document_limit_exact_boundary() {
693 let limits = ResourceLimits {
694 max_documents: 5,
695 max_file_size: 1000,
696 };
697 let mut map = HashMap::new();
698 map.insert("rs".to_string(), "rust".to_string());
699
700 let mut tracker = DocumentTracker::new(limits, map);
701
702 for i in 0..5 {
703 tracker
704 .open(
705 PathBuf::from(format!("/test/file{i}.rs")),
706 "content".to_string(),
707 )
708 .unwrap();
709 }
710
711 assert_eq!(tracker.len(), 5);
712
713 let result = tracker.open(PathBuf::from("/test/file6.rs"), "content".to_string());
714 assert!(matches!(result, Err(Error::DocumentLimitExceeded { .. })));
715 }
716
717 #[test]
718 fn test_file_size_exact_boundary() {
719 let limits = ResourceLimits {
720 max_documents: 10,
721 max_file_size: 100,
722 };
723 let mut map = HashMap::new();
724 map.insert("rs".to_string(), "rust".to_string());
725
726 let mut tracker = DocumentTracker::new(limits, map);
727
728 let exact_size_content = "x".repeat(100);
729 tracker
730 .open(PathBuf::from("/test/exact.rs"), exact_size_content)
731 .unwrap();
732
733 let over_size_content = "x".repeat(101);
734 let result = tracker.open(PathBuf::from("/test/over.rs"), over_size_content);
735 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
736 }
737
738 #[test]
739 fn test_detect_language_with_custom_extension() {
740 let mut map = HashMap::new();
741 map.insert("nu".to_string(), "nushell".to_string());
742
743 assert_eq!(detect_language(Path::new("script.nu"), &map), "nushell");
744
745 let empty_map = HashMap::new();
746 assert_eq!(
747 detect_language(Path::new("script.nu"), &empty_map),
748 "plaintext"
749 );
750 }
751
752 #[test]
753 fn test_detect_language_custom_overrides_default() {
754 let mut custom_map = HashMap::new();
755 custom_map.insert("rs".to_string(), "custom-rust".to_string());
756
757 assert_eq!(
758 detect_language(Path::new("main.rs"), &custom_map),
759 "custom-rust"
760 );
761
762 let mut default_map = HashMap::new();
763 default_map.insert("rs".to_string(), "rust".to_string());
764
765 assert_eq!(detect_language(Path::new("main.rs"), &default_map), "rust");
766 }
767
768 #[test]
769 fn test_detect_language_fallback_to_plaintext() {
770 let mut map = HashMap::new();
771 map.insert("nu".to_string(), "nushell".to_string());
772
773 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
775 }
776
777 #[test]
778 fn test_detect_language_empty_map() {
779 let map = HashMap::new();
780 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
781 }
782
783 #[test]
784 fn test_document_tracker_with_extensions() {
785 let mut map = HashMap::new();
786 map.insert("nu".to_string(), "nushell".to_string());
787
788 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
789
790 let path = PathBuf::from("/test/script.nu");
791 tracker
792 .open(path.clone(), "# nushell script".to_string())
793 .unwrap();
794
795 let state = tracker.get(&path).unwrap();
796 assert_eq!(state.language_id, "nushell");
797 }
798
799 #[test]
800 fn test_document_tracker_uses_provided_map() {
801 let mut map = HashMap::new();
802 map.insert("rs".to_string(), "rust".to_string());
803
804 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
805 let path = PathBuf::from("/test/main.rs");
806 tracker
807 .open(path.clone(), "fn main() {}".to_string())
808 .unwrap();
809
810 let state = tracker.get(&path).unwrap();
811 assert_eq!(state.language_id, "rust");
812 }
813
814 #[test]
815 fn test_multiple_extensions_same_language() {
816 let mut map = HashMap::new();
817 map.insert("cpp".to_string(), "c++".to_string());
818 map.insert("cc".to_string(), "c++".to_string());
819 map.insert("cxx".to_string(), "c++".to_string());
820
821 assert_eq!(detect_language(Path::new("main.cpp"), &map), "c++");
822 assert_eq!(detect_language(Path::new("main.cc"), &map), "c++");
823 assert_eq!(detect_language(Path::new("main.cxx"), &map), "c++");
824 }
825
826 #[test]
827 fn test_case_sensitive_extensions() {
828 let mut map = HashMap::new();
829 map.insert("NU".to_string(), "nushell".to_string());
830
831 assert_eq!(detect_language(Path::new("script.nu"), &map), "plaintext");
833 }
834
835 #[cfg(unix)]
840 #[test]
841 fn test_uri_to_path_file_scheme() {
842 let uri: Uri = "file:///home/user/main.rs".parse().unwrap();
843 let path = uri_to_path(&uri).unwrap();
844 assert_eq!(path, PathBuf::from("/home/user/main.rs"));
845 }
846
847 #[test]
848 fn test_uri_to_path_non_file_scheme_returns_none() {
849 let uri: Uri = "https://example.com/file.rs".parse().unwrap();
850 assert!(uri_to_path(&uri).is_none());
851 }
852
853 #[test]
854 fn test_uri_to_path_lsp_diagnostics_scheme_returns_none() {
855 let uri: Uri = "lsp-diagnostics:///home/user/main.rs".parse().unwrap();
857 assert!(uri_to_path(&uri).is_none());
858 }
859
860 #[test]
861 fn test_uri_to_path_with_authority_returns_none() {
862 let result = "file://server/share/path.rs"
866 .parse::<Uri>()
867 .ok()
868 .and_then(|u| uri_to_path(&u));
869 assert!(result.is_none());
870 }
871
872 #[test]
877 fn test_open_paths_empty_tracker() {
878 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
879 assert_eq!(tracker.open_paths().count(), 0);
880 }
881
882 #[test]
883 fn test_open_paths_populated_tracker() {
884 let mut map = HashMap::new();
885 map.insert("rs".to_string(), "rust".to_string());
886 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
887 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
888 tracker.open(PathBuf::from("/b.rs"), String::new()).unwrap();
889 let mut paths: Vec<_> = tracker.open_paths().collect();
890 paths.sort();
891 assert_eq!(paths, [Path::new("/a.rs"), Path::new("/b.rs")]);
892 }
893
894 #[test]
895 fn test_open_paths_after_close() {
896 let mut map = HashMap::new();
897 map.insert("rs".to_string(), "rust".to_string());
898 let mut tracker = DocumentTracker::new(ResourceLimits::default(), map);
899 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
900 tracker.close(Path::new("/a.rs"));
901 assert_eq!(tracker.open_paths().count(), 0);
902 }
903}