1use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::sync::{Arc, Mutex as StdMutex};
8use std::time::{Duration, SystemTime};
9
10use lsp_types::{
11 DidChangeTextDocumentParams, DidOpenTextDocumentParams, TextDocumentContentChangeEvent,
12 TextDocumentItem, Uri, VersionedTextDocumentIdentifier,
13};
14use tokio::fs;
15use tokio::io::AsyncReadExt;
16use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard};
17use tokio::time::Instant;
18use url::Url;
19
20use super::lock_std;
21use crate::config::ServerId;
22use crate::error::{Error, Result};
23use crate::lsp::LspClient;
24
25const DISK_CHECK_DEBOUNCE: Duration = Duration::from_millis(250);
38
39const MTIME_GRANULARITY: Duration = Duration::from_secs(2);
45
46fn mtime_settled(mtime: Option<SystemTime>, read_at: SystemTime) -> bool {
53 mtime.is_some_and(|m| {
54 m.checked_add(MTIME_GRANULARITY)
55 .is_some_and(|t| t <= read_at)
56 })
57}
58
59#[derive(Debug, Clone, Copy)]
69pub struct DiskSync {
70 pub mtime: Option<SystemTime>,
75 pub size: u64,
77 pub mtime_settled: bool,
80 pub content_checked_at: Instant,
87}
88
89impl PartialEq for DiskSync {
90 fn eq(&self, other: &Self) -> bool {
91 self.mtime == other.mtime
92 && self.size == other.size
93 && self.mtime_settled == other.mtime_settled
94 }
95}
96
97impl Eq for DiskSync {}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct DocumentState {
102 pub uri: Uri,
104 pub language_id: String,
106 pub version: i32,
108 pub content: String,
110 pub disk: Option<DiskSync>,
124 pub synced: HashMap<ServerId, i32>,
132}
133
134#[derive(Debug, Clone, Copy)]
136pub struct ResourceLimits {
137 pub max_documents: usize,
139 pub max_file_size: u64,
141}
142
143impl Default for ResourceLimits {
144 fn default() -> Self {
145 Self {
146 max_documents: 100,
147 max_file_size: 10 * 1024 * 1024, }
149 }
150}
151
152#[derive(Debug)]
159pub struct DocumentTracker {
160 documents: StdMutex<HashMap<PathBuf, DocumentState>>,
163 path_locks: StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
167 limits: ResourceLimits,
169 extension_map: HashMap<String, String>,
171}
172
173impl DocumentTracker {
174 #[must_use]
176 pub fn new(limits: ResourceLimits, extension_map: HashMap<String, String>) -> Self {
177 Self {
178 documents: StdMutex::new(HashMap::new()),
179 path_locks: StdMutex::new(HashMap::new()),
180 limits,
181 extension_map,
182 }
183 }
184
185 #[must_use]
187 pub fn is_open(&self, path: &Path) -> bool {
188 lock_std(&self.documents).contains_key(path)
189 }
190
191 #[must_use]
193 pub fn get(&self, path: &Path) -> Option<DocumentState> {
194 lock_std(&self.documents).get(path).cloned()
195 }
196
197 #[must_use]
199 pub fn len(&self) -> usize {
200 lock_std(&self.documents).len()
201 }
202
203 #[must_use]
205 pub fn is_empty(&self) -> bool {
206 lock_std(&self.documents).is_empty()
207 }
208
209 pub fn open(&self, path: PathBuf, content: String) -> Result<Uri> {
219 self.check_file_size(content.len() as u64)?;
220
221 let uri = path_to_uri(&path);
222 let language_id = detect_language(&path, &self.extension_map);
223
224 let state = DocumentState {
225 uri: uri.clone(),
226 language_id,
227 version: 1,
228 content,
229 disk: None,
230 synced: HashMap::new(),
231 };
232
233 let mut documents = lock_std(&self.documents);
238 if self.limits.max_documents > 0 && documents.len() >= self.limits.max_documents {
239 return Err(Error::DocumentLimitExceeded {
240 current: documents.len(),
241 max: self.limits.max_documents,
242 });
243 }
244 documents.insert(path, state);
245 drop(documents);
246 Ok(uri)
247 }
248
249 pub fn update(&self, path: &Path, content: String) -> Option<i32> {
255 let mut documents = lock_std(&self.documents);
256 if let Some(state) = documents.get_mut(path) {
257 state.version += 1;
258 state.content = content;
259 state.disk = None;
260 Some(state.version)
261 } else {
262 None
263 }
264 }
265
266 const fn check_file_size(&self, size: u64) -> Result<()> {
268 if self.limits.max_file_size > 0 && size > self.limits.max_file_size {
269 return Err(Error::FileSizeLimitExceeded {
270 size,
271 max: self.limits.max_file_size,
272 });
273 }
274 Ok(())
275 }
276
277 fn set_disk(&self, path: &Path, snap: DiskSync) {
284 if let Some(st) = lock_std(&self.documents).get_mut(path) {
285 st.disk = Some(snap);
286 }
287 }
288
289 pub fn close(&self, path: &Path) -> Option<DocumentState> {
293 lock_std(&self.documents).remove(path)
294 }
295
296 pub fn close_all(&self) -> Vec<DocumentState> {
298 lock_std(&self.documents)
299 .drain()
300 .map(|(_, state)| state)
301 .collect()
302 }
303
304 pub fn open_paths(&self) -> Vec<PathBuf> {
306 lock_std(&self.documents).keys().cloned().collect()
307 }
308
309 async fn lock_path(&self, path: &Path) -> PathLockGuard<'_> {
324 let arc = {
325 let mut locks = lock_std(&self.path_locks);
326 locks
327 .entry(path.to_path_buf())
328 .or_insert_with(|| Arc::new(AsyncMutex::new(())))
329 .clone()
330 };
331 let guard = Arc::clone(&arc).lock_owned().await;
332 PathLockGuard {
333 path_locks: &self.path_locks,
334 path: path.to_path_buf(),
335 arc,
336 guard: Some(guard),
337 }
338 }
339
340 pub async fn ensure_open(
409 &self,
410 path: &Path,
411 server: &ServerId,
412 lsp_client: &LspClient,
413 ) -> Result<Uri> {
414 let _path_guard = self.lock_path(path).await;
415 let decision = self.disk_phase(path).await?;
416 self.sync_phase(path, server, lsp_client, decision).await
417 }
418
419 async fn disk_phase(&self, path: &Path) -> Result<Decision> {
424 if !lock_std(&self.documents).contains_key(path) {
425 return self.disk_phase_new(path).await;
426 }
427
428 let read_at = SystemTime::now();
429 let meta = fs::metadata(path).await.map_err(|e| Error::FileIo {
430 path: path.to_path_buf(),
431 source: e,
432 })?;
433 let mtime = meta.modified().ok();
434 let size = meta.len();
435
436 let Some((uri, current_version, fast_path)) =
440 lock_std(&self.documents).get(path).map(|st| {
441 let stat_matches = st.disk.is_some_and(|d| d.mtime == mtime && d.size == size);
442 let fast_path = match st.disk {
443 Some(d) if stat_matches && d.mtime_settled => true,
444 Some(d)
445 if stat_matches && d.content_checked_at.elapsed() < DISK_CHECK_DEBOUNCE =>
446 {
447 true
448 }
449 _ => false,
450 };
451 (st.uri.clone(), st.version, fast_path)
452 })
453 else {
454 return Err(Error::DocumentNotFound(path.to_path_buf()));
455 };
456 if fast_path {
457 return Ok(Decision::unchanged(uri, current_version));
458 }
459
460 let (fresh, ..) = self.read_to_string_checked(path).await?;
461 let snap = DiskSync {
462 mtime,
463 size,
464 mtime_settled: mtime_settled(mtime, read_at),
465 content_checked_at: Instant::now(),
466 };
467
468 let Some(unchanged) = lock_std(&self.documents)
469 .get(path)
470 .map(|st| fresh == st.content)
471 else {
472 return Err(Error::DocumentNotFound(path.to_path_buf()));
473 };
474
475 if unchanged {
476 self.set_disk(path, snap);
477 return Ok(Decision::unchanged(uri, current_version));
478 }
479
480 Ok(Decision {
481 uri,
482 target_version: current_version.saturating_add(1),
483 fresh_content: Some(fresh),
484 snap: Some(snap),
485 })
486 }
487
488 async fn disk_phase_new(&self, path: &Path) -> Result<Decision> {
492 let read_at = SystemTime::now();
493 let (content, mtime, size) = self.read_to_string_checked(path).await?;
494
495 let uri = self.open(path.to_path_buf(), content)?;
496 self.set_disk(
497 path,
498 DiskSync {
499 mtime,
500 size,
501 mtime_settled: mtime_settled(mtime, read_at),
502 content_checked_at: Instant::now(),
503 },
504 );
505
506 Ok(Decision::unchanged(uri, 1))
507 }
508
509 async fn read_to_string_checked(
521 &self,
522 path: &Path,
523 ) -> Result<(String, Option<SystemTime>, u64)> {
524 let mut file = fs::File::open(path).await.map_err(|e| Error::FileIo {
525 path: path.to_path_buf(),
526 source: e,
527 })?;
528 let meta = file.metadata().await.map_err(|e| Error::FileIo {
529 path: path.to_path_buf(),
530 source: e,
531 })?;
532 self.check_file_size(meta.len())?;
533 let mut content = String::new();
534 file.read_to_string(&mut content)
535 .await
536 .map_err(|e| Error::FileIo {
537 path: path.to_path_buf(),
538 source: e,
539 })?;
540 Ok((content, meta.modified().ok(), meta.len()))
541 }
542
543 async fn sync_phase(
547 &self,
548 path: &Path,
549 server: &ServerId,
550 lsp_client: &LspClient,
551 decision: Decision,
552 ) -> Result<Uri> {
553 let Decision {
554 uri,
555 target_version,
556 fresh_content,
557 snap,
558 } = decision;
559
560 let Some(synced_version) = lock_std(&self.documents)
567 .get(path)
568 .map(|st| st.synced.get(server).copied())
569 else {
570 return Err(Error::DocumentNotFound(path.to_path_buf()));
571 };
572 let up_to_date = synced_version.is_some_and(|v| v >= target_version);
573 let is_first_open = synced_version.is_none();
574
575 if up_to_date {
576 return Ok(uri);
577 }
578
579 let Some((language_id, text)) = lock_std(&self.documents).get(path).map(|st| {
580 let text = fresh_content.clone().unwrap_or_else(|| st.content.clone());
581 (st.language_id.clone(), text)
582 }) else {
583 return Err(Error::DocumentNotFound(path.to_path_buf()));
584 };
585
586 let notify_result = if is_first_open {
587 lsp_client
588 .notify(
589 "textDocument/didOpen",
590 DidOpenTextDocumentParams {
591 text_document: TextDocumentItem {
592 uri: uri.clone(),
593 language_id,
594 version: target_version,
595 text,
596 },
597 },
598 )
599 .await
600 } else {
601 lsp_client
602 .notify(
603 "textDocument/didChange",
604 DidChangeTextDocumentParams {
605 text_document: VersionedTextDocumentIdentifier {
606 uri: uri.clone(),
607 version: target_version,
608 },
609 content_changes: vec![TextDocumentContentChangeEvent {
610 range: None,
611 range_length: None,
612 text,
613 }],
614 },
615 )
616 .await
617 };
618
619 if let Err(err) = notify_result {
620 let first_ever_sync = lock_std(&self.documents)
633 .get(path)
634 .is_some_and(|st| st.synced.is_empty());
635 if is_first_open && first_ever_sync {
636 lock_std(&self.documents).remove(path);
637 }
638 return Err(err);
639 }
640
641 let mut documents = lock_std(&self.documents);
644 let Some(st) = documents.get_mut(path) else {
645 return Err(Error::DocumentNotFound(path.to_path_buf()));
646 };
647 if let Some(fresh) = fresh_content {
648 st.version = target_version;
649 st.content = fresh;
650 st.disk = snap;
651 }
652 st.synced.insert(server.clone(), target_version);
653 drop(documents);
654
655 Ok(uri)
656 }
657}
658
659struct PathLockGuard<'a> {
668 path_locks: &'a StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
669 path: PathBuf,
670 arc: Arc<AsyncMutex<()>>,
671 guard: Option<OwnedMutexGuard<()>>,
672}
673
674impl Drop for PathLockGuard<'_> {
675 fn drop(&mut self) {
676 self.guard.take();
679
680 let mut locks = lock_std(self.path_locks);
681 if Arc::strong_count(&self.arc) <= 2 {
693 locks.remove(&self.path);
694 }
695 }
696}
697
698struct Decision {
703 uri: Uri,
704 target_version: i32,
705 fresh_content: Option<String>,
706 snap: Option<DiskSync>,
707}
708
709impl Decision {
710 const fn unchanged(uri: Uri, target_version: i32) -> Self {
713 Self {
714 uri,
715 target_version,
716 fresh_content: None,
717 snap: None,
718 }
719 }
720}
721
722#[must_use]
729pub fn path_to_uri(path: &Path) -> Uri {
730 let uri_string = file_uri_string(path);
731 let uri_string = encode_rfc3986_path_chars(&uri_string);
732 #[allow(clippy::expect_used)]
733 uri_string.parse().expect("failed to create URI from path")
734}
735
736#[cfg(not(windows))]
737fn file_uri_string(path: &Path) -> String {
738 #[allow(clippy::expect_used)]
739 let file_url = Url::from_file_path(path).expect("failed to create file URI from path");
740 file_url.into()
741}
742
743#[cfg(windows)]
744fn file_uri_string(path: &Path) -> String {
745 match Url::from_file_path(path) {
746 Ok(file_url) => file_url.into(),
747 Err(()) if path.has_root() => windows_rooted_path_to_file_uri(path),
748 Err(()) => panic!("failed to create file URI from path"),
749 }
750}
751
752#[cfg(windows)]
753fn windows_rooted_path_to_file_uri(path: &Path) -> String {
754 let path_str = path.to_string_lossy();
755 let stripped = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
756 format!("file:///{}", stripped.replace('\\', "/"))
757}
758
759fn encode_rfc3986_path_chars(uri: &str) -> String {
760 #[allow(clippy::expect_used)]
761 let url = Url::parse(uri).expect("encode called with invalid URI");
762 let prefix = url[..url::Position::BeforePath].to_owned();
763 let encoded = url[url::Position::BeforePath..]
764 .replace('[', "%5B")
765 .replace(']', "%5D")
766 .replace('^', "%5E")
767 .replace('|', "%7C");
768 format!("{prefix}{encoded}")
769}
770
771#[must_use]
776pub fn uri_to_path(uri: &Uri) -> Option<PathBuf> {
777 let url = Url::parse(uri.as_str()).ok()?;
778 if url.scheme() != "file" {
779 return None;
780 }
781 if !url.host_str().unwrap_or("").is_empty() {
784 return None;
785 }
786 url.to_file_path().ok()
787}
788
789#[must_use]
794pub fn detect_language(path: &Path, extension_map: &HashMap<String, String>) -> String {
795 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
796
797 extension_map
798 .get(extension)
799 .cloned()
800 .unwrap_or_else(|| "plaintext".to_string())
801}
802
803#[cfg(test)]
804#[allow(clippy::unwrap_used)]
805mod tests {
806 use super::*;
807
808 #[test]
809 fn test_detect_language() {
810 let mut map = HashMap::new();
811 map.insert("rs".to_string(), "rust".to_string());
812 map.insert("py".to_string(), "python".to_string());
813 map.insert("ts".to_string(), "typescript".to_string());
814
815 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
816 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
817 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
818 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
819 }
820
821 #[test]
822 fn test_document_tracker() {
823 let mut map = HashMap::new();
824 map.insert("rs".to_string(), "rust".to_string());
825
826 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
827 let path = PathBuf::from("/test/file.rs");
828
829 assert!(!tracker.is_open(&path));
830
831 tracker
832 .open(path.clone(), "fn main() {}".to_string())
833 .unwrap();
834 assert!(tracker.is_open(&path));
835 assert_eq!(tracker.len(), 1);
836
837 let state = tracker.get(&path).unwrap();
838 assert_eq!(state.version, 1);
839 assert_eq!(state.language_id, "rust");
840
841 let new_version = tracker.update(&path, "fn main() { println!() }".to_string());
842 assert_eq!(new_version, Some(2));
843
844 tracker.close(&path);
845 assert!(!tracker.is_open(&path));
846 assert!(tracker.is_empty());
847 }
848
849 #[test]
850 fn test_document_limit() {
851 let limits = ResourceLimits {
852 max_documents: 2,
853 max_file_size: 100,
854 };
855 let mut map = HashMap::new();
856 map.insert("rs".to_string(), "rust".to_string());
857
858 let tracker = DocumentTracker::new(limits, map);
859
860 tracker
862 .open(PathBuf::from("/test/file1.rs"), "fn test1() {}".to_string())
863 .unwrap();
864 tracker
865 .open(PathBuf::from("/test/file2.rs"), "fn test2() {}".to_string())
866 .unwrap();
867
868 let result = tracker.open(PathBuf::from("/test/file3.rs"), "fn test3() {}".to_string());
870 assert!(matches!(result, Err(Error::DocumentLimitExceeded { .. })));
871 }
872
873 #[test]
874 fn test_file_size_limit() {
875 let limits = ResourceLimits {
876 max_documents: 10,
877 max_file_size: 10,
878 };
879 let mut map = HashMap::new();
880 map.insert("rs".to_string(), "rust".to_string());
881
882 let tracker = DocumentTracker::new(limits, map);
883
884 tracker
886 .open(PathBuf::from("/test/small.rs"), "fn f(){}".to_string())
887 .unwrap();
888
889 let large_content = "x".repeat(100);
891 let result = tracker.open(PathBuf::from("/test/large.rs"), large_content);
892 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
893 }
894
895 #[test]
896 fn test_resource_limits_default() {
897 let limits = ResourceLimits::default();
898 assert_eq!(limits.max_documents, 100);
899 assert_eq!(limits.max_file_size, 10 * 1024 * 1024);
900 }
901
902 #[test]
903 fn test_resource_limits_custom() {
904 let limits = ResourceLimits {
905 max_documents: 50,
906 max_file_size: 5 * 1024 * 1024,
907 };
908 assert_eq!(limits.max_documents, 50);
909 assert_eq!(limits.max_file_size, 5 * 1024 * 1024);
910 }
911
912 #[test]
913 fn test_resource_limits_zero_unlimited() {
914 let limits = ResourceLimits {
915 max_documents: 0,
916 max_file_size: 0,
917 };
918 let mut map = HashMap::new();
919 map.insert("rs".to_string(), "rust".to_string());
920
921 let tracker = DocumentTracker::new(limits, map);
922
923 for i in 0..200 {
925 tracker
926 .open(
927 PathBuf::from(format!("/test/file{i}.rs")),
928 "content".to_string(),
929 )
930 .unwrap();
931 }
932 assert_eq!(tracker.len(), 200);
933
934 let huge_content = "x".repeat(100_000_000);
936 tracker
937 .open(PathBuf::from("/test/huge.rs"), huge_content)
938 .unwrap();
939 }
940
941 #[test]
942 fn test_document_state_clone() {
943 let state = DocumentState {
944 uri: "file:///test.rs".parse().unwrap(),
945 language_id: "rust".to_string(),
946 version: 5,
947 content: "fn main() {}".to_string(),
948 disk: None,
949 synced: HashMap::new(),
950 };
951
952 #[allow(clippy::redundant_clone)]
953 let cloned = state.clone();
954 assert_eq!(cloned.uri, state.uri);
955 assert_eq!(cloned.language_id, state.language_id);
956 assert_eq!(cloned.version, 5);
957 assert_eq!(cloned.content, state.content);
958 }
959
960 #[test]
961 fn test_update_nonexistent_document() {
962 let map = HashMap::new();
963 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
964 let path = PathBuf::from("/test/nonexistent.rs");
965
966 let version = tracker.update(&path, "new content".to_string());
967 assert_eq!(
968 version, None,
969 "Updating non-existent document should return None"
970 );
971 }
972
973 #[test]
974 fn test_close_nonexistent_document() {
975 let map = HashMap::new();
976 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
977 let path = PathBuf::from("/test/nonexistent.rs");
978
979 let state = tracker.close(&path);
980 assert_eq!(
981 state, None,
982 "Closing non-existent document should return None"
983 );
984 }
985
986 #[test]
987 fn test_close_all_documents() {
988 let mut map = HashMap::new();
989 map.insert("rs".to_string(), "rust".to_string());
990
991 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
992
993 tracker
994 .open(PathBuf::from("/test/file1.rs"), "content1".to_string())
995 .unwrap();
996 tracker
997 .open(PathBuf::from("/test/file2.rs"), "content2".to_string())
998 .unwrap();
999 tracker
1000 .open(PathBuf::from("/test/file3.rs"), "content3".to_string())
1001 .unwrap();
1002
1003 assert_eq!(tracker.len(), 3);
1004
1005 let closed = tracker.close_all();
1006 assert_eq!(closed.len(), 3);
1007 assert!(tracker.is_empty());
1008 }
1009
1010 #[test]
1011 fn test_get_nonexistent_document() {
1012 let map = HashMap::new();
1013 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1014 let path = PathBuf::from("/test/nonexistent.rs");
1015
1016 let state = tracker.get(&path);
1017 assert!(
1018 state.is_none(),
1019 "Getting non-existent document should return None"
1020 );
1021 }
1022
1023 #[test]
1024 fn test_document_version_increments() {
1025 let mut map = HashMap::new();
1026 map.insert("rs".to_string(), "rust".to_string());
1027
1028 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1029 let path = PathBuf::from("/test/versioned.rs");
1030
1031 tracker.open(path.clone(), "v1".to_string()).unwrap();
1032 assert_eq!(tracker.get(&path).unwrap().version, 1);
1033
1034 tracker.update(&path, "v2".to_string());
1035 assert_eq!(tracker.get(&path).unwrap().version, 2);
1036
1037 tracker.update(&path, "v3".to_string());
1038 assert_eq!(tracker.get(&path).unwrap().version, 3);
1039
1040 tracker.update(&path, "v4".to_string());
1041 assert_eq!(tracker.get(&path).unwrap().version, 4);
1042 }
1043
1044 #[test]
1045 #[allow(clippy::too_many_lines)]
1046 fn test_detect_language_all_extensions() {
1047 let mut map = HashMap::new();
1048 map.insert("rs".to_string(), "rust".to_string());
1049 map.insert("py".to_string(), "python".to_string());
1050 map.insert("pyw".to_string(), "python".to_string());
1051 map.insert("pyi".to_string(), "python".to_string());
1052 map.insert("js".to_string(), "javascript".to_string());
1053 map.insert("mjs".to_string(), "javascript".to_string());
1054 map.insert("cjs".to_string(), "javascript".to_string());
1055 map.insert("ts".to_string(), "typescript".to_string());
1056 map.insert("mts".to_string(), "typescript".to_string());
1057 map.insert("cts".to_string(), "typescript".to_string());
1058 map.insert("tsx".to_string(), "typescriptreact".to_string());
1059 map.insert("jsx".to_string(), "javascriptreact".to_string());
1060 map.insert("go".to_string(), "go".to_string());
1061 map.insert("c".to_string(), "c".to_string());
1062 map.insert("h".to_string(), "c".to_string());
1063 map.insert("cpp".to_string(), "cpp".to_string());
1064 map.insert("cc".to_string(), "cpp".to_string());
1065 map.insert("cxx".to_string(), "cpp".to_string());
1066 map.insert("hpp".to_string(), "cpp".to_string());
1067 map.insert("hh".to_string(), "cpp".to_string());
1068 map.insert("hxx".to_string(), "cpp".to_string());
1069 map.insert("java".to_string(), "java".to_string());
1070 map.insert("rb".to_string(), "ruby".to_string());
1071 map.insert("php".to_string(), "php".to_string());
1072 map.insert("swift".to_string(), "swift".to_string());
1073 map.insert("kt".to_string(), "kotlin".to_string());
1074 map.insert("kts".to_string(), "kotlin".to_string());
1075 map.insert("scala".to_string(), "scala".to_string());
1076 map.insert("sc".to_string(), "scala".to_string());
1077 map.insert("zig".to_string(), "zig".to_string());
1078 map.insert("lua".to_string(), "lua".to_string());
1079 map.insert("sh".to_string(), "shellscript".to_string());
1080 map.insert("bash".to_string(), "shellscript".to_string());
1081 map.insert("zsh".to_string(), "shellscript".to_string());
1082 map.insert("json".to_string(), "json".to_string());
1083 map.insert("toml".to_string(), "toml".to_string());
1084 map.insert("yaml".to_string(), "yaml".to_string());
1085 map.insert("yml".to_string(), "yaml".to_string());
1086 map.insert("xml".to_string(), "xml".to_string());
1087 map.insert("html".to_string(), "html".to_string());
1088 map.insert("htm".to_string(), "html".to_string());
1089 map.insert("css".to_string(), "css".to_string());
1090 map.insert("scss".to_string(), "scss".to_string());
1091 map.insert("less".to_string(), "less".to_string());
1092 map.insert("md".to_string(), "markdown".to_string());
1093 map.insert("markdown".to_string(), "markdown".to_string());
1094
1095 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
1096 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
1097 assert_eq!(detect_language(Path::new("script.pyw"), &map), "python");
1098 assert_eq!(detect_language(Path::new("script.pyi"), &map), "python");
1099 assert_eq!(detect_language(Path::new("app.js"), &map), "javascript");
1100 assert_eq!(detect_language(Path::new("app.mjs"), &map), "javascript");
1101 assert_eq!(detect_language(Path::new("app.cjs"), &map), "javascript");
1102 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
1103 assert_eq!(detect_language(Path::new("app.mts"), &map), "typescript");
1104 assert_eq!(detect_language(Path::new("app.cts"), &map), "typescript");
1105 assert_eq!(
1106 detect_language(Path::new("component.tsx"), &map),
1107 "typescriptreact"
1108 );
1109 assert_eq!(
1110 detect_language(Path::new("component.jsx"), &map),
1111 "javascriptreact"
1112 );
1113 assert_eq!(detect_language(Path::new("main.go"), &map), "go");
1114 assert_eq!(detect_language(Path::new("main.c"), &map), "c");
1115 assert_eq!(detect_language(Path::new("header.h"), &map), "c");
1116 assert_eq!(detect_language(Path::new("main.cpp"), &map), "cpp");
1117 assert_eq!(detect_language(Path::new("main.cc"), &map), "cpp");
1118 assert_eq!(detect_language(Path::new("main.cxx"), &map), "cpp");
1119 assert_eq!(detect_language(Path::new("header.hpp"), &map), "cpp");
1120 assert_eq!(detect_language(Path::new("header.hh"), &map), "cpp");
1121 assert_eq!(detect_language(Path::new("header.hxx"), &map), "cpp");
1122 assert_eq!(detect_language(Path::new("Main.java"), &map), "java");
1123 assert_eq!(detect_language(Path::new("script.rb"), &map), "ruby");
1124 assert_eq!(detect_language(Path::new("index.php"), &map), "php");
1125 assert_eq!(detect_language(Path::new("App.swift"), &map), "swift");
1126 assert_eq!(detect_language(Path::new("Main.kt"), &map), "kotlin");
1127 assert_eq!(detect_language(Path::new("script.kts"), &map), "kotlin");
1128 assert_eq!(detect_language(Path::new("Main.scala"), &map), "scala");
1129 assert_eq!(detect_language(Path::new("script.sc"), &map), "scala");
1130 assert_eq!(detect_language(Path::new("main.zig"), &map), "zig");
1131 assert_eq!(detect_language(Path::new("script.lua"), &map), "lua");
1132 assert_eq!(detect_language(Path::new("script.sh"), &map), "shellscript");
1133 assert_eq!(
1134 detect_language(Path::new("script.bash"), &map),
1135 "shellscript"
1136 );
1137 assert_eq!(
1138 detect_language(Path::new("script.zsh"), &map),
1139 "shellscript"
1140 );
1141 assert_eq!(detect_language(Path::new("data.json"), &map), "json");
1142 assert_eq!(detect_language(Path::new("config.toml"), &map), "toml");
1143 assert_eq!(detect_language(Path::new("config.yaml"), &map), "yaml");
1144 assert_eq!(detect_language(Path::new("config.yml"), &map), "yaml");
1145 assert_eq!(detect_language(Path::new("data.xml"), &map), "xml");
1146 assert_eq!(detect_language(Path::new("index.html"), &map), "html");
1147 assert_eq!(detect_language(Path::new("index.htm"), &map), "html");
1148 assert_eq!(detect_language(Path::new("styles.css"), &map), "css");
1149 assert_eq!(detect_language(Path::new("styles.scss"), &map), "scss");
1150 assert_eq!(detect_language(Path::new("styles.less"), &map), "less");
1151 assert_eq!(detect_language(Path::new("README.md"), &map), "markdown");
1152 assert_eq!(
1153 detect_language(Path::new("README.markdown"), &map),
1154 "markdown"
1155 );
1156 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
1157 assert_eq!(
1158 detect_language(Path::new("no_extension"), &map),
1159 "plaintext"
1160 );
1161 }
1162
1163 #[test]
1164 fn test_path_to_uri_unix() {
1165 #[cfg(not(windows))]
1166 {
1167 let path = Path::new("/home/user/project/main.rs");
1168 let uri = path_to_uri(path);
1169 assert!(
1170 uri.as_str()
1171 .starts_with("file:///home/user/project/main.rs")
1172 );
1173 }
1174 }
1175
1176 #[test]
1177 fn test_path_to_uri_with_special_chars() {
1178 let path = Path::new("/home/user/project-test/main.rs");
1179 let uri = path_to_uri(path);
1180 assert!(uri.as_str().starts_with("file://"));
1181 assert!(uri.as_str().contains("project-test"));
1182 }
1183
1184 #[test]
1185 fn test_path_to_uri_percent_encodes_reserved_chars() {
1186 #[cfg(windows)]
1187 let path = Path::new(r"C:\home\user\routes\api\[...]^|.ts");
1188 #[cfg(not(windows))]
1189 let path = Path::new("/home/user/routes/api/[...]^|.ts");
1190
1191 let uri = path_to_uri(path);
1192
1193 #[cfg(windows)]
1194 let expected = "file:///C:/home/user/routes/api/%5B...%5D%5E%7C.ts";
1195 #[cfg(not(windows))]
1196 let expected = "file:///home/user/routes/api/%5B...%5D%5E%7C.ts";
1197
1198 assert_eq!(uri.as_str(), expected);
1199 assert_eq!(
1200 uri_to_path(&uri).as_deref(),
1201 Some(path),
1202 "encoded file URI should round-trip to the original path"
1203 );
1204 }
1205
1206 #[test]
1207 fn test_path_to_uri_percent_encodes_reserved_chars_in_short_path() {
1208 #[cfg(windows)]
1210 let path = Path::new(r"C:\[a].ts");
1211 #[cfg(not(windows))]
1212 let path = Path::new("/[a].ts");
1213
1214 let uri = path_to_uri(path);
1215
1216 assert!(
1217 uri.as_str().ends_with("%5Ba%5D.ts"),
1218 "short path should percent-encode reserved chars, got {}",
1219 uri.as_str()
1220 );
1221 assert_eq!(uri_to_path(&uri).as_deref(), Some(path));
1222 }
1223
1224 #[test]
1225 fn test_document_tracker_concurrent_operations() {
1226 let mut map = HashMap::new();
1227 map.insert("rs".to_string(), "rust".to_string());
1228
1229 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1230 let path1 = PathBuf::from("/test/file1.rs");
1231 let path2 = PathBuf::from("/test/file2.rs");
1232
1233 tracker.open(path1.clone(), "content1".to_string()).unwrap();
1234 tracker.open(path2.clone(), "content2".to_string()).unwrap();
1235
1236 assert_eq!(tracker.len(), 2);
1237 assert!(tracker.is_open(&path1));
1238 assert!(tracker.is_open(&path2));
1239
1240 tracker.update(&path1, "new content1".to_string());
1241 assert_eq!(tracker.get(&path1).unwrap().content, "new content1");
1242 assert_eq!(tracker.get(&path2).unwrap().content, "content2");
1243
1244 tracker.close(&path1);
1245 assert_eq!(tracker.len(), 1);
1246 assert!(!tracker.is_open(&path1));
1247 assert!(tracker.is_open(&path2));
1248 }
1249
1250 #[test]
1251 fn test_empty_content() {
1252 let mut map = HashMap::new();
1253 map.insert("rs".to_string(), "rust".to_string());
1254
1255 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1256 let path = PathBuf::from("/test/empty.rs");
1257
1258 tracker.open(path.clone(), String::new()).unwrap();
1259 assert!(tracker.is_open(&path));
1260 assert_eq!(tracker.get(&path).unwrap().content, "");
1261 }
1262
1263 #[test]
1264 fn test_unicode_content() {
1265 let mut map = HashMap::new();
1266 map.insert("rs".to_string(), "rust".to_string());
1267
1268 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1269 let path = PathBuf::from("/test/unicode.rs");
1270 let content = "fn テスト() { println!(\"こんにちは\"); }";
1271
1272 tracker.open(path.clone(), content.to_string()).unwrap();
1273 assert_eq!(tracker.get(&path).unwrap().content, content);
1274 }
1275
1276 #[test]
1277 fn test_document_limit_exact_boundary() {
1278 let limits = ResourceLimits {
1279 max_documents: 5,
1280 max_file_size: 1000,
1281 };
1282 let mut map = HashMap::new();
1283 map.insert("rs".to_string(), "rust".to_string());
1284
1285 let tracker = DocumentTracker::new(limits, map);
1286
1287 for i in 0..5 {
1288 tracker
1289 .open(
1290 PathBuf::from(format!("/test/file{i}.rs")),
1291 "content".to_string(),
1292 )
1293 .unwrap();
1294 }
1295
1296 assert_eq!(tracker.len(), 5);
1297
1298 let result = tracker.open(PathBuf::from("/test/file6.rs"), "content".to_string());
1299 assert!(matches!(result, Err(Error::DocumentLimitExceeded { .. })));
1300 }
1301
1302 #[test]
1303 fn test_file_size_exact_boundary() {
1304 let limits = ResourceLimits {
1305 max_documents: 10,
1306 max_file_size: 100,
1307 };
1308 let mut map = HashMap::new();
1309 map.insert("rs".to_string(), "rust".to_string());
1310
1311 let tracker = DocumentTracker::new(limits, map);
1312
1313 let exact_size_content = "x".repeat(100);
1314 tracker
1315 .open(PathBuf::from("/test/exact.rs"), exact_size_content)
1316 .unwrap();
1317
1318 let over_size_content = "x".repeat(101);
1319 let result = tracker.open(PathBuf::from("/test/over.rs"), over_size_content);
1320 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
1321 }
1322
1323 #[test]
1324 fn test_detect_language_with_custom_extension() {
1325 let mut map = HashMap::new();
1326 map.insert("nu".to_string(), "nushell".to_string());
1327
1328 assert_eq!(detect_language(Path::new("script.nu"), &map), "nushell");
1329
1330 let empty_map = HashMap::new();
1331 assert_eq!(
1332 detect_language(Path::new("script.nu"), &empty_map),
1333 "plaintext"
1334 );
1335 }
1336
1337 #[test]
1338 fn test_detect_language_custom_overrides_default() {
1339 let mut custom_map = HashMap::new();
1340 custom_map.insert("rs".to_string(), "custom-rust".to_string());
1341
1342 assert_eq!(
1343 detect_language(Path::new("main.rs"), &custom_map),
1344 "custom-rust"
1345 );
1346
1347 let mut default_map = HashMap::new();
1348 default_map.insert("rs".to_string(), "rust".to_string());
1349
1350 assert_eq!(detect_language(Path::new("main.rs"), &default_map), "rust");
1351 }
1352
1353 #[test]
1354 fn test_detect_language_fallback_to_plaintext() {
1355 let mut map = HashMap::new();
1356 map.insert("nu".to_string(), "nushell".to_string());
1357
1358 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
1360 }
1361
1362 #[test]
1363 fn test_detect_language_empty_map() {
1364 let map = HashMap::new();
1365 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
1366 }
1367
1368 #[test]
1369 fn test_document_tracker_with_extensions() {
1370 let mut map = HashMap::new();
1371 map.insert("nu".to_string(), "nushell".to_string());
1372
1373 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1374
1375 let path = PathBuf::from("/test/script.nu");
1376 tracker
1377 .open(path.clone(), "# nushell script".to_string())
1378 .unwrap();
1379
1380 let state = tracker.get(&path).unwrap();
1381 assert_eq!(state.language_id, "nushell");
1382 }
1383
1384 #[test]
1385 fn test_document_tracker_uses_provided_map() {
1386 let mut map = HashMap::new();
1387 map.insert("rs".to_string(), "rust".to_string());
1388
1389 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1390 let path = PathBuf::from("/test/main.rs");
1391 tracker
1392 .open(path.clone(), "fn main() {}".to_string())
1393 .unwrap();
1394
1395 let state = tracker.get(&path).unwrap();
1396 assert_eq!(state.language_id, "rust");
1397 }
1398
1399 #[test]
1400 fn test_multiple_extensions_same_language() {
1401 let mut map = HashMap::new();
1402 map.insert("cpp".to_string(), "c++".to_string());
1403 map.insert("cc".to_string(), "c++".to_string());
1404 map.insert("cxx".to_string(), "c++".to_string());
1405
1406 assert_eq!(detect_language(Path::new("main.cpp"), &map), "c++");
1407 assert_eq!(detect_language(Path::new("main.cc"), &map), "c++");
1408 assert_eq!(detect_language(Path::new("main.cxx"), &map), "c++");
1409 }
1410
1411 #[test]
1412 fn test_case_sensitive_extensions() {
1413 let mut map = HashMap::new();
1414 map.insert("NU".to_string(), "nushell".to_string());
1415
1416 assert_eq!(detect_language(Path::new("script.nu"), &map), "plaintext");
1418 }
1419
1420 #[cfg(unix)]
1425 #[test]
1426 fn test_uri_to_path_file_scheme() {
1427 let uri: Uri = "file:///home/user/main.rs".parse().unwrap();
1428 let path = uri_to_path(&uri).unwrap();
1429 assert_eq!(path, PathBuf::from("/home/user/main.rs"));
1430 }
1431
1432 #[test]
1433 fn test_uri_to_path_non_file_scheme_returns_none() {
1434 let uri: Uri = "https://example.com/file.rs".parse().unwrap();
1435 assert!(uri_to_path(&uri).is_none());
1436 }
1437
1438 #[test]
1439 fn test_uri_to_path_lsp_diagnostics_scheme_returns_none() {
1440 let uri: Uri = "lsp-diagnostics:///home/user/main.rs".parse().unwrap();
1442 assert!(uri_to_path(&uri).is_none());
1443 }
1444
1445 #[test]
1446 fn test_uri_to_path_with_authority_returns_none() {
1447 let result = "file://server/share/path.rs"
1451 .parse::<Uri>()
1452 .ok()
1453 .and_then(|u| uri_to_path(&u));
1454 assert!(result.is_none());
1455 }
1456
1457 #[test]
1462 fn test_open_paths_empty_tracker() {
1463 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1464 assert_eq!(tracker.open_paths().len(), 0);
1465 }
1466
1467 #[test]
1468 fn test_open_paths_populated_tracker() {
1469 let mut map = HashMap::new();
1470 map.insert("rs".to_string(), "rust".to_string());
1471 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1472 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
1473 tracker.open(PathBuf::from("/b.rs"), String::new()).unwrap();
1474 let mut paths = tracker.open_paths();
1475 paths.sort();
1476 assert_eq!(paths, [PathBuf::from("/a.rs"), PathBuf::from("/b.rs")]);
1477 }
1478
1479 #[test]
1480 fn test_open_paths_after_close() {
1481 let mut map = HashMap::new();
1482 map.insert("rs".to_string(), "rust".to_string());
1483 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1484 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
1485 tracker.close(Path::new("/a.rs"));
1486 assert_eq!(tracker.open_paths().len(), 0);
1487 }
1488
1489 use std::process::Stdio;
1494
1495 use tempfile::TempDir;
1496 use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
1497 use tokio::process::{Child, ChildStdin, ChildStdout, Command};
1498
1499 use crate::config::LspServerConfig;
1500 use crate::lsp::LspTransport;
1501
1502 struct FakeServer {
1515 _write_half: Child,
1516 _read_half: Child,
1517 _read_half_stdin: ChildStdin,
1518 write_stdout: ChildStdout,
1519 }
1520
1521 fn fake_lsp_client() -> (LspClient, FakeServer) {
1524 let mut write_half = Command::new("cat")
1525 .stdin(Stdio::piped())
1526 .stdout(Stdio::piped())
1527 .kill_on_drop(true)
1528 .spawn()
1529 .unwrap();
1530 let write_stdin = write_half.stdin.take().unwrap();
1531 let write_stdout = write_half.stdout.take().unwrap();
1532
1533 let mut read_half = Command::new("cat")
1534 .stdin(Stdio::piped())
1535 .stdout(Stdio::piped())
1536 .kill_on_drop(true)
1537 .spawn()
1538 .unwrap();
1539 let read_stdout = read_half.stdout.take().unwrap();
1540 let read_stdin = read_half.stdin.take().unwrap();
1541
1542 let transport = LspTransport::new(write_stdin, read_stdout);
1543 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1544
1545 (
1546 client,
1547 FakeServer {
1548 _write_half: write_half,
1549 _read_half: read_half,
1550 _read_half_stdin: read_stdin,
1551 write_stdout,
1552 },
1553 )
1554 }
1555
1556 fn set_mtime(path: &Path, time: SystemTime) {
1564 let file = std::fs::OpenOptions::new().write(true).open(path).unwrap();
1565 file.set_modified(time).unwrap();
1566 }
1567
1568 fn settled_past() -> SystemTime {
1569 SystemTime::now() - Duration::from_secs(10)
1570 }
1571
1572 async fn read_framed_message(reader: &mut BufReader<&mut ChildStdout>) -> serde_json::Value {
1578 let mut content_length = None;
1579 let mut line = String::new();
1580 loop {
1581 line.clear();
1582 reader.read_line(&mut line).await.unwrap();
1583 if line == "\r\n" || line == "\n" {
1584 break;
1585 }
1586 if let Some((key, value)) = line.trim_end().split_once(':')
1587 && key.trim().eq_ignore_ascii_case("content-length")
1588 {
1589 content_length = Some(value.trim().parse::<usize>().unwrap());
1590 }
1591 }
1592 let mut buf = vec![0u8; content_length.unwrap()];
1593 reader.read_exact(&mut buf).await.unwrap();
1594 serde_json::from_slice(&buf).unwrap()
1595 }
1596
1597 #[test]
1598 fn test_mtime_settled_boundary() {
1599 let read_at = SystemTime::now();
1600 assert!(!mtime_settled(None, read_at), "no mtime is never settled");
1601 assert!(
1602 mtime_settled(Some(read_at - Duration::from_secs(3)), read_at),
1603 "3s older than read_at is past the 2s granularity margin"
1604 );
1605 assert!(
1606 !mtime_settled(Some(read_at - Duration::from_secs(1)), read_at),
1607 "1s older than read_at is within the 2s granularity margin"
1608 );
1609 assert!(
1610 !mtime_settled(Some(read_at + Duration::from_secs(10)), read_at),
1611 "an mtime after read_at is never settled"
1612 );
1613 }
1614
1615 #[tokio::test]
1616 async fn test_ensure_open_unchanged_file_is_fast_path() {
1617 let dir = TempDir::new().unwrap();
1618 let path = dir.path().join("a.rs");
1619 std::fs::write(&path, "fn main() {}").unwrap();
1620 set_mtime(&path, settled_past());
1621
1622 let (client, _server) = fake_lsp_client();
1623 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1624
1625 let uri1 = tracker
1626 .ensure_open(&path, &ServerId::from("rust"), &client)
1627 .await
1628 .unwrap();
1629 assert_eq!(tracker.get(&path).unwrap().version, 1);
1630
1631 let uri2 = tracker
1632 .ensure_open(&path, &ServerId::from("rust"), &client)
1633 .await
1634 .unwrap();
1635 assert_eq!(uri1, uri2);
1636 assert_eq!(tracker.get(&path).unwrap().version, 1);
1637 assert_eq!(tracker.get(&path).unwrap().content, "fn main() {}");
1638 }
1639
1640 #[tokio::test]
1641 async fn test_ensure_open_resyncs_on_size_change() {
1642 let dir = TempDir::new().unwrap();
1643 let path = dir.path().join("a.rs");
1644 std::fs::write(&path, "fn main() {}").unwrap();
1645 set_mtime(&path, settled_past());
1646
1647 let (client, _server) = fake_lsp_client();
1648 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1649 tracker
1650 .ensure_open(&path, &ServerId::from("rust"), &client)
1651 .await
1652 .unwrap();
1653
1654 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
1655 set_mtime(&path, settled_past());
1656
1657 tracker
1658 .ensure_open(&path, &ServerId::from("rust"), &client)
1659 .await
1660 .unwrap();
1661 let state = tracker.get(&path).unwrap();
1662 assert_eq!(state.version, 2);
1663 assert_eq!(state.content, "fn main() { println!(\"hi\"); }");
1664 }
1665
1666 #[tokio::test(start_paused = true)]
1667 async fn test_ensure_open_regression_102_103_racy_same_size_rewrite() {
1668 let dir = TempDir::new().unwrap();
1669 let path = dir.path().join("a.rs");
1670 std::fs::write(&path, "AAAA").unwrap();
1671 let (client, _server) = fake_lsp_client();
1674 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1675 tracker
1676 .ensure_open(&path, &ServerId::from("rust"), &client)
1677 .await
1678 .unwrap();
1679 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
1680
1681 std::fs::write(&path, "BBBB").unwrap();
1684 set_mtime(&path, original_mtime);
1685
1686 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
1687
1688 tracker
1689 .ensure_open(&path, &ServerId::from("rust"), &client)
1690 .await
1691 .unwrap();
1692 let state = tracker.get(&path).unwrap();
1693 assert_eq!(
1694 state.version, 2,
1695 "must resync despite identical (mtime, size)"
1696 );
1697 assert_eq!(state.content, "BBBB");
1698 }
1699
1700 #[tokio::test(start_paused = true)]
1701 async fn test_ensure_open_regression_102_103_settled_mtime_is_the_documented_limit() {
1702 let dir = TempDir::new().unwrap();
1703 let path = dir.path().join("a.rs");
1704 std::fs::write(&path, "AAAA").unwrap();
1705 set_mtime(&path, settled_past());
1706
1707 let (client, _server) = fake_lsp_client();
1708 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1709 tracker
1710 .ensure_open(&path, &ServerId::from("rust"), &client)
1711 .await
1712 .unwrap();
1713 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
1714
1715 std::fs::write(&path, "BBBB").unwrap();
1719 set_mtime(&path, original_mtime);
1720
1721 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
1722
1723 tracker
1724 .ensure_open(&path, &ServerId::from("rust"), &client)
1725 .await
1726 .unwrap();
1727 let state = tracker.get(&path).unwrap();
1728 assert_eq!(state.version, 1, "documented limitation: fast path taken");
1729 assert_eq!(state.content, "AAAA");
1730 }
1731
1732 #[tokio::test(start_paused = true)]
1733 async fn test_ensure_open_stat_is_never_debounced() {
1734 let dir = TempDir::new().unwrap();
1735 let path = dir.path().join("a.rs");
1736 std::fs::write(&path, "AAAA").unwrap();
1737 set_mtime(&path, settled_past());
1738
1739 let (client, _server) = fake_lsp_client();
1740 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1741 tracker
1742 .ensure_open(&path, &ServerId::from("rust"), &client)
1743 .await
1744 .unwrap();
1745
1746 std::fs::write(&path, "BBBBBBBB").unwrap();
1749 tracker
1750 .ensure_open(&path, &ServerId::from("rust"), &client)
1751 .await
1752 .unwrap();
1753
1754 let state = tracker.get(&path).unwrap();
1755 assert_eq!(state.version, 2);
1756 assert_eq!(state.content, "BBBBBBBB");
1757 }
1758
1759 #[tokio::test(start_paused = true)]
1760 async fn test_ensure_open_debounce_gates_reread_only() {
1761 let dir = TempDir::new().unwrap();
1762 let path = dir.path().join("a.rs");
1763 std::fs::write(&path, "AAAA").unwrap();
1764 let (client, _server) = fake_lsp_client();
1767 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1768 tracker
1769 .ensure_open(&path, &ServerId::from("rust"), &client)
1770 .await
1771 .unwrap();
1772 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
1773
1774 std::fs::write(&path, "BBBB").unwrap(); set_mtime(&path, original_mtime); tracker
1779 .ensure_open(&path, &ServerId::from("rust"), &client)
1780 .await
1781 .unwrap();
1782 assert_eq!(tracker.get(&path).unwrap().version, 1);
1783
1784 tokio::time::advance(Duration::from_millis(300)).await;
1785 tracker
1786 .ensure_open(&path, &ServerId::from("rust"), &client)
1787 .await
1788 .unwrap();
1789 let state = tracker.get(&path).unwrap();
1790 assert_eq!(state.version, 2);
1791 assert_eq!(state.content, "BBBB");
1792 }
1793
1794 #[tokio::test]
1795 async fn test_ensure_open_deleted_file_errors_state_untouched() {
1796 let dir = TempDir::new().unwrap();
1797 let path = dir.path().join("a.rs");
1798 std::fs::write(&path, "fn main() {}").unwrap();
1799 set_mtime(&path, settled_past());
1800
1801 let (client, _server) = fake_lsp_client();
1802 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1803 tracker
1804 .ensure_open(&path, &ServerId::from("rust"), &client)
1805 .await
1806 .unwrap();
1807
1808 std::fs::remove_file(&path).unwrap();
1809
1810 let result = tracker
1811 .ensure_open(&path, &ServerId::from("rust"), &client)
1812 .await;
1813 assert!(matches!(result, Err(Error::FileIo { .. })));
1814 assert!(tracker.is_open(&path));
1815 assert_eq!(tracker.get(&path).unwrap().version, 1);
1816 assert_eq!(tracker.get(&path).unwrap().content, "fn main() {}");
1817 }
1818
1819 #[tokio::test]
1820 async fn test_ensure_open_grows_past_limit_errors_state_intact() {
1821 let dir = TempDir::new().unwrap();
1822 let path = dir.path().join("a.rs");
1823 std::fs::write(&path, "small").unwrap();
1824 set_mtime(&path, settled_past());
1825
1826 let limits = ResourceLimits {
1827 max_documents: 10,
1828 max_file_size: 10,
1829 };
1830 let (client, _server) = fake_lsp_client();
1831 let tracker = DocumentTracker::new(limits, HashMap::new());
1832 tracker
1833 .ensure_open(&path, &ServerId::from("rust"), &client)
1834 .await
1835 .unwrap();
1836
1837 std::fs::write(&path, "x".repeat(100)).unwrap();
1838
1839 let result = tracker
1840 .ensure_open(&path, &ServerId::from("rust"), &client)
1841 .await;
1842 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
1843 assert_eq!(tracker.get(&path).unwrap().content, "small");
1844 assert_eq!(tracker.get(&path).unwrap().version, 1);
1845 }
1846
1847 #[tokio::test]
1848 async fn test_ensure_open_resync_at_document_capacity() {
1849 let dir = TempDir::new().unwrap();
1850 let path = dir.path().join("a.rs");
1851 std::fs::write(&path, "AAAA").unwrap();
1852 set_mtime(&path, settled_past());
1853
1854 let limits = ResourceLimits {
1855 max_documents: 1,
1856 max_file_size: 0,
1857 };
1858 let (client, _server) = fake_lsp_client();
1859 let tracker = DocumentTracker::new(limits, HashMap::new());
1860 tracker
1861 .ensure_open(&path, &ServerId::from("rust"), &client)
1862 .await
1863 .unwrap();
1864 assert_eq!(tracker.len(), 1);
1865
1866 std::fs::write(&path, "BBBBBBBB").unwrap();
1867 let result = tracker
1868 .ensure_open(&path, &ServerId::from("rust"), &client)
1869 .await;
1870 assert!(
1871 result.is_ok(),
1872 "resync must not re-run the doc-count check on an already-tracked path"
1873 );
1874 assert_eq!(tracker.len(), 1);
1875 assert_eq!(tracker.get(&path).unwrap().version, 2);
1876 }
1877
1878 #[tokio::test]
1879 async fn test_update_clears_disk_provenance() {
1880 let dir = TempDir::new().unwrap();
1881 let path = dir.path().join("a.rs");
1882 std::fs::write(&path, "fn main() {}").unwrap();
1883 set_mtime(&path, settled_past());
1884
1885 let (client, _server) = fake_lsp_client();
1886 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1887 tracker
1888 .ensure_open(&path, &ServerId::from("rust"), &client)
1889 .await
1890 .unwrap();
1891 assert!(tracker.get(&path).unwrap().disk.is_some());
1892
1893 tracker.update(&path, "fn main() { updated(); }".to_string());
1894 assert!(
1895 tracker.get(&path).unwrap().disk.is_none(),
1896 "update() must clear disk provenance so the next ensure_open re-verifies by content"
1897 );
1898 }
1899
1900 #[tokio::test]
1901 async fn test_first_open_self_heals_when_did_open_notify_fails() {
1902 let dir = TempDir::new().unwrap();
1903 let path = dir.path().join("a.rs");
1904 std::fs::write(&path, "fn main() {}").unwrap();
1905
1906 let (client, _server) = fake_lsp_client();
1907 let notify_will_fail = client.clone();
1913 client.shutdown().await.unwrap();
1914
1915 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1916 let result = tracker
1917 .ensure_open(&path, &ServerId::from("rust"), ¬ify_will_fail)
1918 .await;
1919
1920 assert!(result.is_err(), "notify failure must propagate as an error");
1921 assert!(
1922 !tracker.is_open(&path),
1923 "a failed didOpen must not leave the document tracked, or the server \
1924 and tracker would stay permanently desynced"
1925 );
1926 }
1927
1928 #[tokio::test]
1929 async fn test_resync_sends_didchange_with_full_replacement_over_the_wire() {
1930 let dir = TempDir::new().unwrap();
1931 let path = dir.path().join("a.rs");
1932 std::fs::write(&path, "fn main() {}").unwrap();
1933 set_mtime(&path, settled_past());
1934
1935 let (client, mut server) = fake_lsp_client();
1936 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1937 tracker
1938 .ensure_open(&path, &ServerId::from("rust"), &client)
1939 .await
1940 .unwrap();
1941
1942 let mut wire = BufReader::new(&mut server.write_stdout);
1943 let opened = read_framed_message(&mut wire).await;
1944 assert_eq!(opened["method"], "textDocument/didOpen");
1945
1946 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
1947 set_mtime(&path, settled_past());
1948 tracker
1949 .ensure_open(&path, &ServerId::from("rust"), &client)
1950 .await
1951 .unwrap();
1952
1953 let changed = read_framed_message(&mut wire).await;
1954 assert_eq!(changed["method"], "textDocument/didChange");
1955 let params = &changed["params"];
1956 assert_eq!(params["textDocument"]["version"], 2);
1957 let change = ¶ms["contentChanges"][0];
1958 assert!(
1959 change.get("range").is_none(),
1960 "range must be omitted, not null, for a full-replacement change"
1961 );
1962 assert!(
1963 change.get("rangeLength").is_none(),
1964 "rangeLength must be omitted, not null, for a full-replacement change"
1965 );
1966 assert_eq!(change["text"], "fn main() { println!(\"hi\"); }");
1967 }
1968
1969 #[tokio::test]
1974 async fn test_ensure_open_second_server_gets_didopen_no_disk_change() {
1975 let dir = TempDir::new().unwrap();
1976 let path = dir.path().join("a.rs");
1977 std::fs::write(&path, "fn main() {}").unwrap();
1978 set_mtime(&path, settled_past());
1979
1980 let (client_a, mut server_a) = fake_lsp_client();
1981 let (client_b, mut server_b) = fake_lsp_client();
1982 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1983
1984 let id_a = ServerId::from("server-a");
1985 let id_b = ServerId::from("server-b");
1986
1987 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
1988 let mut wire_a = BufReader::new(&mut server_a.write_stdout);
1989 let opened_a = read_framed_message(&mut wire_a).await;
1990 assert_eq!(opened_a["method"], "textDocument/didOpen");
1991
1992 tracker.ensure_open(&path, &id_b, &client_b).await.unwrap();
1996 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
1997 let opened_b = read_framed_message(&mut wire_b).await;
1998 assert_eq!(opened_b["method"], "textDocument/didOpen");
1999 assert_eq!(opened_b["params"]["textDocument"]["version"], 1);
2000 assert_eq!(opened_b["params"]["textDocument"]["text"], "fn main() {}");
2001 }
2002
2003 #[tokio::test(start_paused = true)]
2007 async fn test_ensure_open_second_server_gets_didopen_unchanged_content_path() {
2008 let dir = TempDir::new().unwrap();
2009 let path = dir.path().join("a.rs");
2010 std::fs::write(&path, "fn main() {}").unwrap();
2011 let (client_a, _server_a) = fake_lsp_client();
2014 let (client_b, mut server_b) = fake_lsp_client();
2015 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2016
2017 tracker
2018 .ensure_open(&path, &ServerId::from("server-a"), &client_a)
2019 .await
2020 .unwrap();
2021
2022 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
2025
2026 tracker
2027 .ensure_open(&path, &ServerId::from("server-b"), &client_b)
2028 .await
2029 .unwrap();
2030 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
2031 let opened_b = read_framed_message(&mut wire_b).await;
2032 assert_eq!(opened_b["method"], "textDocument/didOpen");
2033 }
2034
2035 #[tokio::test]
2042 async fn test_ensure_open_same_server_twice_sends_nothing_second_time() {
2043 let dir = TempDir::new().unwrap();
2044 let path = dir.path().join("a.rs");
2045 std::fs::write(&path, "fn main() {}").unwrap();
2046 set_mtime(&path, settled_past());
2047
2048 let (client, mut server) = fake_lsp_client();
2049 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2050 let id = ServerId::from("rust");
2051
2052 tracker.ensure_open(&path, &id, &client).await.unwrap();
2053 tracker.ensure_open(&path, &id, &client).await.unwrap();
2054
2055 let mut wire = BufReader::new(&mut server.write_stdout);
2056 let opened = read_framed_message(&mut wire).await;
2057 assert_eq!(opened["method"], "textDocument/didOpen");
2058 assert_eq!(
2059 tracker.get(&path).unwrap().synced.get(&id),
2060 Some(&1),
2061 "second call for the same server must not re-open or re-change"
2062 );
2063 }
2064
2065 #[tokio::test]
2069 async fn test_sync_phase_failed_didchange_does_not_disturb_other_server() {
2070 let dir = TempDir::new().unwrap();
2071 let path = dir.path().join("a.rs");
2072 std::fs::write(&path, "fn main() {}").unwrap();
2073 set_mtime(&path, settled_past());
2074
2075 let (client_a, _server_a) = fake_lsp_client();
2076 let (client_b, _server_b) = fake_lsp_client();
2077 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2078 let id_a = ServerId::from("server-a");
2079 let id_b = ServerId::from("server-b");
2080
2081 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
2082 tracker.ensure_open(&path, &id_b, &client_b).await.unwrap();
2083
2084 let client_b_will_fail = client_b.clone();
2087 client_b.shutdown().await.unwrap();
2088
2089 std::fs::write(&path, "fn main() { updated(); }").unwrap();
2090 set_mtime(&path, settled_past());
2091
2092 let result = tracker.ensure_open(&path, &id_b, &client_b_will_fail).await;
2093 assert!(result.is_err(), "B's didChange must fail and propagate");
2094
2095 assert!(tracker.is_open(&path));
2101 assert_eq!(tracker.get(&path).unwrap().content, "fn main() {}");
2102 assert_eq!(tracker.get(&path).unwrap().version, 1);
2103 assert_eq!(tracker.get(&path).unwrap().synced.get(&id_a), Some(&1));
2104 assert_eq!(tracker.get(&path).unwrap().synced.get(&id_b), Some(&1));
2105
2106 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
2110 assert_eq!(
2111 tracker.get(&path).unwrap().content,
2112 "fn main() { updated(); }"
2113 );
2114 assert_eq!(tracker.get(&path).unwrap().synced.get(&id_a), Some(&2));
2115 assert_eq!(tracker.get(&path).unwrap().synced.get(&id_b), Some(&1));
2116 }
2117
2118 #[cfg(unix)]
2134 #[tokio::test]
2135 async fn test_ensure_open_different_paths_do_not_serialize() {
2136 let dir = TempDir::new().unwrap();
2137 let path_a = dir.path().join("a.rs");
2138 let path_b = dir.path().join("b.rs");
2139
2140 std::fs::write(&path_b, "fn b() {}").unwrap();
2141 set_mtime(&path_b, settled_past());
2142
2143 let status = std::process::Command::new("mkfifo")
2144 .arg(&path_a)
2145 .status()
2146 .unwrap();
2147 assert!(status.success(), "mkfifo must succeed to set up this test");
2148
2149 let (client_a, _server_a) = fake_lsp_client();
2150 let (client_b, _server_b) = fake_lsp_client();
2151 let tracker = Arc::new(DocumentTracker::new(
2152 ResourceLimits::default(),
2153 HashMap::new(),
2154 ));
2155
2156 let tracker_for_a = Arc::clone(&tracker);
2159 let path_a_for_task = path_a.clone();
2160 let handle_a = tokio::spawn(async move {
2161 tracker_for_a
2162 .ensure_open(&path_a_for_task, &ServerId::from("server-a"), &client_a)
2163 .await
2164 });
2165
2166 tokio::time::sleep(Duration::from_millis(200)).await;
2169
2170 tokio::time::timeout(
2173 Duration::from_secs(5),
2174 tracker.ensure_open(&path_b, &ServerId::from("server-b"), &client_b),
2175 )
2176 .await
2177 .unwrap()
2178 .unwrap();
2179
2180 let path_a_writer = path_a.clone();
2184 tokio::task::spawn_blocking(move || {
2185 std::fs::write(path_a_writer, "fn a() {}").unwrap();
2186 })
2187 .await
2188 .unwrap();
2189
2190 handle_a.await.unwrap().unwrap();
2191 assert_eq!(tracker.get(&path_a).unwrap().content, "fn a() {}");
2192 }
2193
2194 #[tokio::test]
2200 async fn test_ensure_open_concurrent_same_path_single_didopen() {
2201 let dir = TempDir::new().unwrap();
2202 let path = dir.path().join("a.rs");
2203 std::fs::write(&path, "fn main() {}").unwrap();
2204 set_mtime(&path, settled_past());
2205
2206 let (client, mut server) = fake_lsp_client();
2207 let tracker = Arc::new(DocumentTracker::new(
2208 ResourceLimits::default(),
2209 HashMap::new(),
2210 ));
2211 let id = ServerId::from("rust");
2212
2213 let mut handles = Vec::new();
2214 for _ in 0..8 {
2215 let tracker = Arc::clone(&tracker);
2216 let client = client.clone();
2217 let path = path.clone();
2218 let id = id.clone();
2219 handles.push(tokio::spawn(async move {
2220 tracker.ensure_open(&path, &id, &client).await
2221 }));
2222 }
2223 for handle in handles {
2224 handle.await.unwrap().unwrap();
2225 }
2226
2227 let mut wire = BufReader::new(&mut server.write_stdout);
2228 let opened = read_framed_message(&mut wire).await;
2229 assert_eq!(opened["method"], "textDocument/didOpen");
2230
2231 let extra =
2234 tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut wire)).await;
2235 assert!(
2236 extra.is_err(),
2237 "expected no additional notification after the single didOpen"
2238 );
2239
2240 assert_eq!(tracker.get(&path).unwrap().synced.get(&id), Some(&1));
2241 assert_eq!(tracker.get(&path).unwrap().version, 1);
2242 }
2243
2244 #[tokio::test]
2251 async fn test_ensure_open_path_locks_evicted_after_completion() {
2252 let dir = TempDir::new().unwrap();
2253 let paths: Vec<_> = ["a.rs", "b.rs", "c.rs"]
2254 .iter()
2255 .map(|name| dir.path().join(name))
2256 .collect();
2257 for path in &paths {
2258 std::fs::write(path, "fn f() {}").unwrap();
2259 set_mtime(path, settled_past());
2260 }
2261
2262 let tracker = Arc::new(DocumentTracker::new(
2263 ResourceLimits::default(),
2264 HashMap::new(),
2265 ));
2266 let id = ServerId::from("rust");
2267
2268 let mut handles = Vec::new();
2269 let mut servers = Vec::new();
2270 for path in paths.clone() {
2271 let tracker = Arc::clone(&tracker);
2272 let (client, server) = fake_lsp_client();
2273 servers.push(server);
2274 let id = id.clone();
2275 handles.push(tokio::spawn(async move {
2276 tracker.ensure_open(&path, &id, &client).await
2277 }));
2278 }
2279 for handle in handles {
2280 handle.await.unwrap().unwrap();
2281 }
2282 drop(servers);
2283
2284 assert!(
2285 lock_std(&tracker.path_locks).is_empty(),
2286 "path_locks must be fully evicted once every ensure_open call \
2287 for every path has completed, otherwise the map grows \
2288 unbounded for the lifetime of the process"
2289 );
2290 }
2291}