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)]
123pub struct DocumentState {
124 uri: Uri,
125 language_id: String,
126 version: i32,
127 content: String,
128 disk: Option<DiskSync>,
129 synced: HashMap<ServerId, i32>,
130}
131
132impl DocumentState {
133 fn new(uri: Uri, language_id: String, content: String) -> Self {
136 Self {
137 uri,
138 language_id,
139 version: 1,
140 content,
141 disk: None,
142 synced: HashMap::new(),
143 }
144 }
145
146 #[must_use]
148 pub const fn uri(&self) -> &Uri {
149 &self.uri
150 }
151
152 #[must_use]
154 pub fn language_id(&self) -> &str {
155 &self.language_id
156 }
157
158 #[must_use]
162 pub const fn version(&self) -> i32 {
163 self.version
164 }
165
166 #[must_use]
168 pub fn content(&self) -> &str {
169 &self.content
170 }
171
172 const fn disk(&self) -> Option<DiskSync> {
175 self.disk
176 }
177
178 #[must_use]
187 pub fn synced_version(&self, server: &ServerId) -> Option<i32> {
188 self.synced.get(server).copied()
189 }
190
191 fn has_never_synced(&self) -> bool {
193 self.synced.is_empty()
194 }
195
196 fn apply_local_edit(&mut self, content: String) -> i32 {
200 self.version += 1;
201 self.content = content;
202 self.disk = None;
203 self.version
204 }
205
206 fn commit_reload(&mut self, version: i32, content: String, snap: Option<DiskSync>) {
212 debug_assert!(
213 version >= self.version,
214 "document version must be monotonically increasing"
215 );
216 self.version = version;
217 self.content = content;
218 self.disk = snap;
219 }
220
221 const fn set_disk(&mut self, snap: DiskSync) {
223 self.disk = Some(snap);
224 }
225
226 fn mark_synced(&mut self, server: ServerId, version: i32) {
228 self.synced.insert(server, version);
229 }
230
231 fn forget_server(&mut self, server: &ServerId) {
233 self.synced.remove(server);
234 }
235}
236
237pub const DEFAULT_MAX_DOCUMENTS: usize = 100;
240
241pub const DEFAULT_MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
244
245#[derive(Debug, Clone, Copy)]
247pub struct ResourceLimits {
248 pub max_documents: usize,
250 pub max_file_size: u64,
252}
253
254impl Default for ResourceLimits {
255 fn default() -> Self {
256 Self {
257 max_documents: DEFAULT_MAX_DOCUMENTS,
258 max_file_size: DEFAULT_MAX_FILE_SIZE,
259 }
260 }
261}
262
263#[derive(Debug)]
270pub struct DocumentTracker {
271 documents: StdMutex<HashMap<PathBuf, DocumentState>>,
274 path_locks: StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
278 generations: StdMutex<HashMap<ServerId, u64>>,
285 limits: ResourceLimits,
287 extension_map: HashMap<String, String>,
289}
290
291impl DocumentTracker {
292 #[must_use]
294 pub fn new(limits: ResourceLimits, extension_map: HashMap<String, String>) -> Self {
295 Self {
296 documents: StdMutex::new(HashMap::new()),
297 path_locks: StdMutex::new(HashMap::new()),
298 generations: StdMutex::new(HashMap::new()),
299 limits,
300 extension_map,
301 }
302 }
303
304 #[must_use]
306 pub fn is_open(&self, path: &Path) -> bool {
307 lock_std(&self.documents).contains_key(path)
308 }
309
310 #[must_use]
312 pub fn get(&self, path: &Path) -> Option<DocumentState> {
313 lock_std(&self.documents).get(path).cloned()
314 }
315
316 #[must_use]
324 pub fn line_text(&self, path: &Path, line: u32) -> Option<String> {
325 lock_std(&self.documents)
326 .get(path)?
327 .content
328 .lines()
329 .nth(line as usize)
330 .map(str::to_string)
331 }
332
333 #[must_use]
335 pub fn len(&self) -> usize {
336 lock_std(&self.documents).len()
337 }
338
339 #[must_use]
341 pub fn is_empty(&self) -> bool {
342 lock_std(&self.documents).is_empty()
343 }
344
345 pub fn open(&self, path: PathBuf, content: String) -> Result<Uri> {
355 self.check_file_size(content.len() as u64)?;
356
357 let uri = path_to_uri(&path)?;
358 let language_id = detect_language(&path, &self.extension_map);
359
360 let state = DocumentState::new(uri.clone(), language_id, content);
361
362 let mut documents = lock_std(&self.documents);
367 if self.limits.max_documents > 0 && documents.len() >= self.limits.max_documents {
368 return Err(Error::DocumentLimitExceeded {
369 current: documents.len(),
370 max: self.limits.max_documents,
371 });
372 }
373 documents.insert(path, state);
374 drop(documents);
375 Ok(uri)
376 }
377
378 pub fn update(&self, path: &Path, content: String) -> Option<i32> {
386 lock_std(&self.documents)
387 .get_mut(path)
388 .map(|state| state.apply_local_edit(content))
389 }
390
391 const fn check_file_size(&self, size: u64) -> Result<()> {
393 if self.limits.max_file_size > 0 && size > self.limits.max_file_size {
394 return Err(Error::FileSizeLimitExceeded {
395 size,
396 max: self.limits.max_file_size,
397 });
398 }
399 Ok(())
400 }
401
402 fn set_disk(&self, path: &Path, snap: DiskSync) {
409 if let Some(st) = lock_std(&self.documents).get_mut(path) {
410 st.set_disk(snap);
411 }
412 }
413
414 pub fn close(&self, path: &Path) -> Option<DocumentState> {
418 lock_std(&self.documents).remove(path)
419 }
420
421 pub fn close_all(&self) -> Vec<DocumentState> {
423 lock_std(&self.documents)
424 .drain()
425 .map(|(_, state)| state)
426 .collect()
427 }
428
429 pub fn open_paths(&self) -> Vec<PathBuf> {
431 lock_std(&self.documents).keys().cloned().collect()
432 }
433
434 pub fn forget_server(&self, server: &ServerId) {
453 *lock_std(&self.generations)
454 .entry(server.clone())
455 .or_insert(0) += 1;
456 for state in lock_std(&self.documents).values_mut() {
457 state.forget_server(server);
458 }
459 }
460
461 fn generation(&self, server: &ServerId) -> u64 {
463 lock_std(&self.generations)
464 .get(server)
465 .copied()
466 .unwrap_or(0)
467 }
468
469 async fn lock_path(&self, path: &Path) -> PathLockGuard<'_> {
484 let arc = {
485 let mut locks = lock_std(&self.path_locks);
486 locks
487 .entry(path.to_path_buf())
488 .or_insert_with(|| Arc::new(AsyncMutex::new(())))
489 .clone()
490 };
491 let guard = Arc::clone(&arc).lock_owned().await;
492 PathLockGuard {
493 path_locks: &self.path_locks,
494 path: path.to_path_buf(),
495 arc,
496 guard: Some(guard),
497 }
498 }
499
500 pub async fn ensure_open(
569 &self,
570 path: &Path,
571 server: &ServerId,
572 lsp_client: &LspClient,
573 ) -> Result<Uri> {
574 let _path_guard = self.lock_path(path).await;
575 let generation = self.generation(server);
576 let decision = self.disk_phase(path).await?;
577 self.sync_phase(path, server, lsp_client, decision, generation)
578 .await
579 }
580
581 async fn disk_phase(&self, path: &Path) -> Result<Decision> {
586 if !lock_std(&self.documents).contains_key(path) {
587 return self.disk_phase_new(path).await;
588 }
589
590 let read_at = SystemTime::now();
591 let meta = fs::metadata(path).await.map_err(|e| Error::FileIo {
592 path: path.to_path_buf(),
593 source: e,
594 })?;
595 let mtime = meta.modified().ok();
596 let size = meta.len();
597
598 let Some((uri, current_version, fast_path)) =
602 lock_std(&self.documents).get(path).map(|st| {
603 let stat_matches = st
604 .disk()
605 .is_some_and(|d| d.mtime == mtime && d.size == size);
606 let fast_path = match st.disk() {
607 Some(d) if stat_matches && d.mtime_settled => true,
608 Some(d)
609 if stat_matches && d.content_checked_at.elapsed() < DISK_CHECK_DEBOUNCE =>
610 {
611 true
612 }
613 _ => false,
614 };
615 (st.uri.clone(), st.version, fast_path)
616 })
617 else {
618 return Err(Error::DocumentNotFound(path.to_path_buf()));
619 };
620 if fast_path {
621 return Ok(Decision::unchanged(uri, current_version));
622 }
623
624 let (fresh, ..) = self.read_to_string_checked(path).await?;
625 let snap = DiskSync {
626 mtime,
627 size,
628 mtime_settled: mtime_settled(mtime, read_at),
629 content_checked_at: Instant::now(),
630 };
631
632 let Some(unchanged) = lock_std(&self.documents)
633 .get(path)
634 .map(|st| fresh == st.content)
635 else {
636 return Err(Error::DocumentNotFound(path.to_path_buf()));
637 };
638
639 if unchanged {
640 self.set_disk(path, snap);
641 return Ok(Decision::unchanged(uri, current_version));
642 }
643
644 Ok(Decision {
645 uri,
646 target_version: current_version.saturating_add(1),
647 fresh_content: Some(fresh),
648 snap: Some(snap),
649 })
650 }
651
652 async fn disk_phase_new(&self, path: &Path) -> Result<Decision> {
656 let read_at = SystemTime::now();
657 let (content, mtime, size) = self.read_to_string_checked(path).await?;
658
659 let uri = self.open(path.to_path_buf(), content)?;
660 self.set_disk(
661 path,
662 DiskSync {
663 mtime,
664 size,
665 mtime_settled: mtime_settled(mtime, read_at),
666 content_checked_at: Instant::now(),
667 },
668 );
669
670 Ok(Decision::unchanged(uri, 1))
671 }
672
673 async fn read_to_string_checked(
685 &self,
686 path: &Path,
687 ) -> Result<(String, Option<SystemTime>, u64)> {
688 let mut file = fs::File::open(path).await.map_err(|e| Error::FileIo {
689 path: path.to_path_buf(),
690 source: e,
691 })?;
692 let meta = file.metadata().await.map_err(|e| Error::FileIo {
693 path: path.to_path_buf(),
694 source: e,
695 })?;
696 self.check_file_size(meta.len())?;
697 let mut content = String::new();
698 file.read_to_string(&mut content)
699 .await
700 .map_err(|e| Error::FileIo {
701 path: path.to_path_buf(),
702 source: e,
703 })?;
704 Ok((content, meta.modified().ok(), meta.len()))
705 }
706
707 async fn sync_phase(
718 &self,
719 path: &Path,
720 server: &ServerId,
721 lsp_client: &LspClient,
722 decision: Decision,
723 generation: u64,
724 ) -> Result<Uri> {
725 let Decision {
726 uri,
727 target_version,
728 fresh_content,
729 snap,
730 } = decision;
731
732 let Some(synced_version) = lock_std(&self.documents)
739 .get(path)
740 .map(|st| st.synced_version(server))
741 else {
742 return Err(Error::DocumentNotFound(path.to_path_buf()));
743 };
744 let up_to_date = synced_version.is_some_and(|v| v >= target_version);
745 let is_first_open = synced_version.is_none();
746
747 if up_to_date {
748 return Ok(uri);
749 }
750
751 let Some((language_id, text)) = lock_std(&self.documents).get(path).map(|st| {
752 let text = fresh_content.clone().unwrap_or_else(|| st.content.clone());
753 (st.language_id.clone(), text)
754 }) else {
755 return Err(Error::DocumentNotFound(path.to_path_buf()));
756 };
757
758 let notify_result = if is_first_open {
759 lsp_client
760 .notify(
761 "textDocument/didOpen",
762 DidOpenTextDocumentParams {
763 text_document: TextDocumentItem {
764 uri: uri.clone(),
765 language_id,
766 version: target_version,
767 text,
768 },
769 },
770 )
771 .await
772 } else {
773 lsp_client
774 .notify(
775 "textDocument/didChange",
776 DidChangeTextDocumentParams {
777 text_document: VersionedTextDocumentIdentifier {
778 uri: uri.clone(),
779 version: target_version,
780 },
781 content_changes: vec![TextDocumentContentChangeEvent {
782 range: None,
783 range_length: None,
784 text,
785 }],
786 },
787 )
788 .await
789 };
790
791 if let Err(err) = notify_result {
792 let first_ever_sync = lock_std(&self.documents)
805 .get(path)
806 .is_some_and(DocumentState::has_never_synced);
807 if is_first_open && first_ever_sync {
808 lock_std(&self.documents).remove(path);
809 }
810 return Err(err);
811 }
812
813 let mut documents = lock_std(&self.documents);
816 let Some(st) = documents.get_mut(path) else {
817 return Err(Error::DocumentNotFound(path.to_path_buf()));
818 };
819 if let Some(fresh) = fresh_content {
820 st.commit_reload(target_version, fresh, snap);
821 }
822 if self.generation(server) == generation {
832 st.mark_synced(server.clone(), target_version);
833 }
834 drop(documents);
835
836 Ok(uri)
837 }
838}
839
840struct PathLockGuard<'a> {
849 path_locks: &'a StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
850 path: PathBuf,
851 arc: Arc<AsyncMutex<()>>,
852 guard: Option<OwnedMutexGuard<()>>,
853}
854
855impl Drop for PathLockGuard<'_> {
856 fn drop(&mut self) {
857 self.guard.take();
860
861 let mut locks = lock_std(self.path_locks);
862 if Arc::strong_count(&self.arc) <= 2 {
874 locks.remove(&self.path);
875 }
876 }
877}
878
879struct Decision {
884 uri: Uri,
885 target_version: i32,
886 fresh_content: Option<String>,
887 snap: Option<DiskSync>,
888}
889
890impl Decision {
891 const fn unchanged(uri: Uri, target_version: i32) -> Self {
894 Self {
895 uri,
896 target_version,
897 fresh_content: None,
898 snap: None,
899 }
900 }
901}
902
903pub fn path_to_uri(path: &Path) -> Result<Uri> {
916 try_path_to_uri(path)
917 .ok_or_else(|| Error::InvalidUri(format!("cannot convert path to URI: {}", path.display())))
918}
919
920#[must_use]
926pub fn try_path_to_uri(path: &Path) -> Option<Uri> {
927 let uri_string = encode_rfc3986_path_chars(&file_url(path)?);
928 uri_string.parse().ok()
929}
930
931#[cfg(not(windows))]
932fn file_url(path: &Path) -> Option<Url> {
933 Url::from_file_path(path).ok()
934}
935
936#[cfg(windows)]
937fn file_url(path: &Path) -> Option<Url> {
938 match Url::from_file_path(path) {
939 Ok(file_url) => Some(file_url),
940 Err(()) if path.has_root() => windows_rooted_path_to_file_url(path),
941 Err(()) => None,
942 }
943}
944
945#[cfg(windows)]
946fn windows_rooted_path_to_file_url(path: &Path) -> Option<Url> {
947 let path_str = path.to_string_lossy();
948 let stripped = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
949 let mut file_url = Url::parse("file:///").ok()?;
950 file_url.path_segments_mut().ok()?.clear().extend(
951 stripped
952 .split(['\\', '/'])
953 .filter(|segment| !segment.is_empty()),
954 );
955 Some(file_url)
956}
957
958pub(super) fn encode_rfc3986_path_chars(url: &Url) -> String {
968 let prefix = url[..url::Position::BeforePath].to_owned();
969 let encoded = url[url::Position::BeforePath..]
970 .replace('[', "%5B")
971 .replace(']', "%5D")
972 .replace('^', "%5E")
973 .replace('|', "%7C");
974 format!("{prefix}{encoded}")
975}
976
977#[must_use]
982pub fn uri_to_path(uri: &Uri) -> Option<PathBuf> {
983 let url = Url::parse(uri.as_str()).ok()?;
984 if url.scheme() != "file" {
985 return None;
986 }
987 if !url.host_str().unwrap_or("").is_empty() {
990 return None;
991 }
992 url.to_file_path().ok()
993}
994
995#[must_use]
1000pub fn detect_language(path: &Path, extension_map: &HashMap<String, String>) -> String {
1001 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
1002
1003 extension_map
1004 .get(extension)
1005 .cloned()
1006 .unwrap_or_else(|| "plaintext".to_string())
1007}
1008
1009#[cfg(test)]
1010#[allow(clippy::unwrap_used)]
1011mod tests {
1012 use super::*;
1013
1014 #[test]
1015 fn test_detect_language() {
1016 let mut map = HashMap::new();
1017 map.insert("rs".to_string(), "rust".to_string());
1018 map.insert("py".to_string(), "python".to_string());
1019 map.insert("ts".to_string(), "typescript".to_string());
1020
1021 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
1022 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
1023 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
1024 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
1025 }
1026
1027 #[test]
1028 fn test_document_tracker() {
1029 let mut map = HashMap::new();
1030 map.insert("rs".to_string(), "rust".to_string());
1031
1032 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1033 let path = PathBuf::from("/test/file.rs");
1034
1035 assert!(!tracker.is_open(&path));
1036
1037 tracker
1038 .open(path.clone(), "fn main() {}".to_string())
1039 .unwrap();
1040 assert!(tracker.is_open(&path));
1041 assert_eq!(tracker.len(), 1);
1042
1043 let state = tracker.get(&path).unwrap();
1044 assert_eq!(state.version(), 1);
1045 assert_eq!(state.language_id(), "rust");
1046
1047 let new_version = tracker.update(&path, "fn main() { println!() }".to_string());
1048 assert_eq!(new_version, Some(2));
1049
1050 tracker.close(&path);
1051 assert!(!tracker.is_open(&path));
1052 assert!(tracker.is_empty());
1053 }
1054
1055 #[test]
1061 fn test_forget_server_clears_only_that_servers_synced_version() {
1062 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1063 let path = PathBuf::from("/test/file.rs");
1064 tracker
1065 .open(path.clone(), "fn main() {}".to_string())
1066 .unwrap();
1067
1068 let respawned = ServerId::from("rust-respawned");
1069 let untouched = ServerId::from("rust-diagnostics");
1070 lock_std(&tracker.documents)
1071 .get_mut(&path)
1072 .unwrap()
1073 .synced
1074 .insert(respawned.clone(), 1);
1075 lock_std(&tracker.documents)
1076 .get_mut(&path)
1077 .unwrap()
1078 .synced
1079 .insert(untouched.clone(), 1);
1080
1081 tracker.forget_server(&respawned);
1082
1083 let state = tracker.get(&path).unwrap();
1084 assert!(state.synced_version(&respawned).is_none());
1085 assert!(state.synced_version(&untouched).is_some());
1086 }
1087
1088 #[tokio::test]
1098 async fn test_sync_phase_skips_commit_when_generation_is_stale() {
1099 let dir = TempDir::new().unwrap();
1100 let path = dir.path().join("race.rs");
1101 std::fs::write(&path, "fn main() {}").unwrap();
1102 set_mtime(&path, settled_past());
1103
1104 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1105 let server = ServerId::from("rust");
1106 let generation_before_respawn = 0; tracker.forget_server(&server);
1111
1112 let (stale_client, _guard) = fake_lsp_client();
1113 let decision = tracker.disk_phase(&path).await.unwrap();
1114 tracker
1115 .sync_phase(
1116 &path,
1117 &server,
1118 &stale_client,
1119 decision,
1120 generation_before_respawn,
1121 )
1122 .await
1123 .unwrap();
1124
1125 let state = tracker.get(&path).unwrap();
1126 assert!(
1127 state.synced_version(&server).is_none(),
1128 "a sync_phase call that captured a stale generation must not \
1129 commit `synced`, even though its notify against the \
1130 superseded connection succeeded"
1131 );
1132 }
1133
1134 #[tokio::test]
1139 async fn test_ensure_open_commits_when_generation_is_current() {
1140 let dir = TempDir::new().unwrap();
1141 let path = dir.path().join("no_race.rs");
1142 std::fs::write(&path, "fn main() {}").unwrap();
1143
1144 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1145 let server = ServerId::from("rust");
1146 let (client, _guard) = fake_lsp_client();
1147
1148 tracker.ensure_open(&path, &server, &client).await.unwrap();
1149
1150 let state = tracker.get(&path).unwrap();
1151 assert_eq!(state.synced_version(&server), Some(1));
1152 }
1153
1154 #[test]
1155 fn test_document_limit() {
1156 let limits = ResourceLimits {
1157 max_documents: 2,
1158 max_file_size: 100,
1159 };
1160 let mut map = HashMap::new();
1161 map.insert("rs".to_string(), "rust".to_string());
1162
1163 let tracker = DocumentTracker::new(limits, map);
1164
1165 tracker
1167 .open(PathBuf::from("/test/file1.rs"), "fn test1() {}".to_string())
1168 .unwrap();
1169 tracker
1170 .open(PathBuf::from("/test/file2.rs"), "fn test2() {}".to_string())
1171 .unwrap();
1172
1173 let result = tracker.open(PathBuf::from("/test/file3.rs"), "fn test3() {}".to_string());
1175 assert!(matches!(result, Err(Error::DocumentLimitExceeded { .. })));
1176 }
1177
1178 #[test]
1179 fn test_file_size_limit() {
1180 let limits = ResourceLimits {
1181 max_documents: 10,
1182 max_file_size: 10,
1183 };
1184 let mut map = HashMap::new();
1185 map.insert("rs".to_string(), "rust".to_string());
1186
1187 let tracker = DocumentTracker::new(limits, map);
1188
1189 tracker
1191 .open(PathBuf::from("/test/small.rs"), "fn f(){}".to_string())
1192 .unwrap();
1193
1194 let large_content = "x".repeat(100);
1196 let result = tracker.open(PathBuf::from("/test/large.rs"), large_content);
1197 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
1198 }
1199
1200 #[test]
1201 fn test_resource_limits_default() {
1202 let limits = ResourceLimits::default();
1203 assert_eq!(limits.max_documents, 100);
1204 assert_eq!(limits.max_file_size, 10 * 1024 * 1024);
1205 }
1206
1207 #[test]
1208 fn test_resource_limits_custom() {
1209 let limits = ResourceLimits {
1210 max_documents: 50,
1211 max_file_size: 5 * 1024 * 1024,
1212 };
1213 assert_eq!(limits.max_documents, 50);
1214 assert_eq!(limits.max_file_size, 5 * 1024 * 1024);
1215 }
1216
1217 #[test]
1218 fn test_resource_limits_zero_unlimited() {
1219 let limits = ResourceLimits {
1220 max_documents: 0,
1221 max_file_size: 0,
1222 };
1223 let mut map = HashMap::new();
1224 map.insert("rs".to_string(), "rust".to_string());
1225
1226 let tracker = DocumentTracker::new(limits, map);
1227
1228 for i in 0..200 {
1230 tracker
1231 .open(
1232 PathBuf::from(format!("/test/file{i}.rs")),
1233 "content".to_string(),
1234 )
1235 .unwrap();
1236 }
1237 assert_eq!(tracker.len(), 200);
1238
1239 let huge_content = "x".repeat(100_000_000);
1241 tracker
1242 .open(PathBuf::from("/test/huge.rs"), huge_content)
1243 .unwrap();
1244 }
1245
1246 #[test]
1247 fn test_document_state_clone() {
1248 let state = DocumentState {
1249 uri: "file:///test.rs".parse().unwrap(),
1250 language_id: "rust".to_string(),
1251 version: 5,
1252 content: "fn main() {}".to_string(),
1253 disk: None,
1254 synced: HashMap::new(),
1255 };
1256
1257 #[allow(clippy::redundant_clone)]
1258 let cloned = state.clone();
1259 assert_eq!(cloned.uri(), state.uri());
1260 assert_eq!(cloned.language_id(), state.language_id());
1261 assert_eq!(cloned.version(), 5);
1262 assert_eq!(cloned.content(), state.content());
1263 }
1264
1265 #[test]
1266 fn test_update_nonexistent_document() {
1267 let map = HashMap::new();
1268 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1269 let path = PathBuf::from("/test/nonexistent.rs");
1270
1271 let version = tracker.update(&path, "new content".to_string());
1272 assert_eq!(
1273 version, None,
1274 "Updating non-existent document should return None"
1275 );
1276 }
1277
1278 #[test]
1279 fn test_close_nonexistent_document() {
1280 let map = HashMap::new();
1281 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1282 let path = PathBuf::from("/test/nonexistent.rs");
1283
1284 let state = tracker.close(&path);
1285 assert_eq!(
1286 state, None,
1287 "Closing non-existent document should return None"
1288 );
1289 }
1290
1291 #[test]
1292 fn test_close_all_documents() {
1293 let mut map = HashMap::new();
1294 map.insert("rs".to_string(), "rust".to_string());
1295
1296 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1297
1298 tracker
1299 .open(PathBuf::from("/test/file1.rs"), "content1".to_string())
1300 .unwrap();
1301 tracker
1302 .open(PathBuf::from("/test/file2.rs"), "content2".to_string())
1303 .unwrap();
1304 tracker
1305 .open(PathBuf::from("/test/file3.rs"), "content3".to_string())
1306 .unwrap();
1307
1308 assert_eq!(tracker.len(), 3);
1309
1310 let closed = tracker.close_all();
1311 assert_eq!(closed.len(), 3);
1312 assert!(tracker.is_empty());
1313 }
1314
1315 #[test]
1316 fn test_get_nonexistent_document() {
1317 let map = HashMap::new();
1318 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1319 let path = PathBuf::from("/test/nonexistent.rs");
1320
1321 let state = tracker.get(&path);
1322 assert!(
1323 state.is_none(),
1324 "Getting non-existent document should return None"
1325 );
1326 }
1327
1328 #[test]
1329 fn test_document_version_increments() {
1330 let mut map = HashMap::new();
1331 map.insert("rs".to_string(), "rust".to_string());
1332
1333 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1334 let path = PathBuf::from("/test/versioned.rs");
1335
1336 tracker.open(path.clone(), "v1".to_string()).unwrap();
1337 assert_eq!(tracker.get(&path).unwrap().version(), 1);
1338
1339 tracker.update(&path, "v2".to_string());
1340 assert_eq!(tracker.get(&path).unwrap().version(), 2);
1341
1342 tracker.update(&path, "v3".to_string());
1343 assert_eq!(tracker.get(&path).unwrap().version(), 3);
1344
1345 tracker.update(&path, "v4".to_string());
1346 assert_eq!(tracker.get(&path).unwrap().version(), 4);
1347 }
1348
1349 #[test]
1350 #[allow(clippy::too_many_lines)]
1351 fn test_detect_language_all_extensions() {
1352 let mut map = HashMap::new();
1353 map.insert("rs".to_string(), "rust".to_string());
1354 map.insert("py".to_string(), "python".to_string());
1355 map.insert("pyw".to_string(), "python".to_string());
1356 map.insert("pyi".to_string(), "python".to_string());
1357 map.insert("js".to_string(), "javascript".to_string());
1358 map.insert("mjs".to_string(), "javascript".to_string());
1359 map.insert("cjs".to_string(), "javascript".to_string());
1360 map.insert("ts".to_string(), "typescript".to_string());
1361 map.insert("mts".to_string(), "typescript".to_string());
1362 map.insert("cts".to_string(), "typescript".to_string());
1363 map.insert("tsx".to_string(), "typescriptreact".to_string());
1364 map.insert("jsx".to_string(), "javascriptreact".to_string());
1365 map.insert("go".to_string(), "go".to_string());
1366 map.insert("c".to_string(), "c".to_string());
1367 map.insert("h".to_string(), "c".to_string());
1368 map.insert("cpp".to_string(), "cpp".to_string());
1369 map.insert("cc".to_string(), "cpp".to_string());
1370 map.insert("cxx".to_string(), "cpp".to_string());
1371 map.insert("hpp".to_string(), "cpp".to_string());
1372 map.insert("hh".to_string(), "cpp".to_string());
1373 map.insert("hxx".to_string(), "cpp".to_string());
1374 map.insert("java".to_string(), "java".to_string());
1375 map.insert("rb".to_string(), "ruby".to_string());
1376 map.insert("php".to_string(), "php".to_string());
1377 map.insert("swift".to_string(), "swift".to_string());
1378 map.insert("kt".to_string(), "kotlin".to_string());
1379 map.insert("kts".to_string(), "kotlin".to_string());
1380 map.insert("scala".to_string(), "scala".to_string());
1381 map.insert("sc".to_string(), "scala".to_string());
1382 map.insert("zig".to_string(), "zig".to_string());
1383 map.insert("lua".to_string(), "lua".to_string());
1384 map.insert("sh".to_string(), "shellscript".to_string());
1385 map.insert("bash".to_string(), "shellscript".to_string());
1386 map.insert("zsh".to_string(), "shellscript".to_string());
1387 map.insert("json".to_string(), "json".to_string());
1388 map.insert("toml".to_string(), "toml".to_string());
1389 map.insert("yaml".to_string(), "yaml".to_string());
1390 map.insert("yml".to_string(), "yaml".to_string());
1391 map.insert("xml".to_string(), "xml".to_string());
1392 map.insert("html".to_string(), "html".to_string());
1393 map.insert("htm".to_string(), "html".to_string());
1394 map.insert("css".to_string(), "css".to_string());
1395 map.insert("scss".to_string(), "scss".to_string());
1396 map.insert("less".to_string(), "less".to_string());
1397 map.insert("md".to_string(), "markdown".to_string());
1398 map.insert("markdown".to_string(), "markdown".to_string());
1399
1400 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
1401 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
1402 assert_eq!(detect_language(Path::new("script.pyw"), &map), "python");
1403 assert_eq!(detect_language(Path::new("script.pyi"), &map), "python");
1404 assert_eq!(detect_language(Path::new("app.js"), &map), "javascript");
1405 assert_eq!(detect_language(Path::new("app.mjs"), &map), "javascript");
1406 assert_eq!(detect_language(Path::new("app.cjs"), &map), "javascript");
1407 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
1408 assert_eq!(detect_language(Path::new("app.mts"), &map), "typescript");
1409 assert_eq!(detect_language(Path::new("app.cts"), &map), "typescript");
1410 assert_eq!(
1411 detect_language(Path::new("component.tsx"), &map),
1412 "typescriptreact"
1413 );
1414 assert_eq!(
1415 detect_language(Path::new("component.jsx"), &map),
1416 "javascriptreact"
1417 );
1418 assert_eq!(detect_language(Path::new("main.go"), &map), "go");
1419 assert_eq!(detect_language(Path::new("main.c"), &map), "c");
1420 assert_eq!(detect_language(Path::new("header.h"), &map), "c");
1421 assert_eq!(detect_language(Path::new("main.cpp"), &map), "cpp");
1422 assert_eq!(detect_language(Path::new("main.cc"), &map), "cpp");
1423 assert_eq!(detect_language(Path::new("main.cxx"), &map), "cpp");
1424 assert_eq!(detect_language(Path::new("header.hpp"), &map), "cpp");
1425 assert_eq!(detect_language(Path::new("header.hh"), &map), "cpp");
1426 assert_eq!(detect_language(Path::new("header.hxx"), &map), "cpp");
1427 assert_eq!(detect_language(Path::new("Main.java"), &map), "java");
1428 assert_eq!(detect_language(Path::new("script.rb"), &map), "ruby");
1429 assert_eq!(detect_language(Path::new("index.php"), &map), "php");
1430 assert_eq!(detect_language(Path::new("App.swift"), &map), "swift");
1431 assert_eq!(detect_language(Path::new("Main.kt"), &map), "kotlin");
1432 assert_eq!(detect_language(Path::new("script.kts"), &map), "kotlin");
1433 assert_eq!(detect_language(Path::new("Main.scala"), &map), "scala");
1434 assert_eq!(detect_language(Path::new("script.sc"), &map), "scala");
1435 assert_eq!(detect_language(Path::new("main.zig"), &map), "zig");
1436 assert_eq!(detect_language(Path::new("script.lua"), &map), "lua");
1437 assert_eq!(detect_language(Path::new("script.sh"), &map), "shellscript");
1438 assert_eq!(
1439 detect_language(Path::new("script.bash"), &map),
1440 "shellscript"
1441 );
1442 assert_eq!(
1443 detect_language(Path::new("script.zsh"), &map),
1444 "shellscript"
1445 );
1446 assert_eq!(detect_language(Path::new("data.json"), &map), "json");
1447 assert_eq!(detect_language(Path::new("config.toml"), &map), "toml");
1448 assert_eq!(detect_language(Path::new("config.yaml"), &map), "yaml");
1449 assert_eq!(detect_language(Path::new("config.yml"), &map), "yaml");
1450 assert_eq!(detect_language(Path::new("data.xml"), &map), "xml");
1451 assert_eq!(detect_language(Path::new("index.html"), &map), "html");
1452 assert_eq!(detect_language(Path::new("index.htm"), &map), "html");
1453 assert_eq!(detect_language(Path::new("styles.css"), &map), "css");
1454 assert_eq!(detect_language(Path::new("styles.scss"), &map), "scss");
1455 assert_eq!(detect_language(Path::new("styles.less"), &map), "less");
1456 assert_eq!(detect_language(Path::new("README.md"), &map), "markdown");
1457 assert_eq!(
1458 detect_language(Path::new("README.markdown"), &map),
1459 "markdown"
1460 );
1461 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
1462 assert_eq!(
1463 detect_language(Path::new("no_extension"), &map),
1464 "plaintext"
1465 );
1466 }
1467
1468 #[test]
1469 fn test_path_to_uri_unix() {
1470 #[cfg(not(windows))]
1471 {
1472 let path = Path::new("/home/user/project/main.rs");
1473 let uri = path_to_uri(path).unwrap();
1474 assert!(
1475 uri.as_str()
1476 .starts_with("file:///home/user/project/main.rs")
1477 );
1478 }
1479 }
1480
1481 #[test]
1482 fn test_path_to_uri_with_special_chars() {
1483 let path = Path::new("/home/user/project-test/main.rs");
1484 let uri = path_to_uri(path).unwrap();
1485 assert!(uri.as_str().starts_with("file://"));
1486 assert!(uri.as_str().contains("project-test"));
1487 }
1488
1489 #[test]
1490 fn test_path_to_uri_percent_encodes_reserved_chars() {
1491 #[cfg(windows)]
1492 let path = Path::new(r"C:\home\user\routes\api\[...]^|.ts");
1493 #[cfg(not(windows))]
1494 let path = Path::new("/home/user/routes/api/[...]^|.ts");
1495
1496 let uri = path_to_uri(path).unwrap();
1497
1498 #[cfg(windows)]
1499 let expected = "file:///C:/home/user/routes/api/%5B...%5D%5E%7C.ts";
1500 #[cfg(not(windows))]
1501 let expected = "file:///home/user/routes/api/%5B...%5D%5E%7C.ts";
1502
1503 assert_eq!(uri.as_str(), expected);
1504 assert_eq!(
1505 uri_to_path(&uri).as_deref(),
1506 Some(path),
1507 "encoded file URI should round-trip to the original path"
1508 );
1509 }
1510
1511 #[test]
1512 fn test_try_path_to_uri_returns_none_for_relative_path() {
1513 assert_eq!(try_path_to_uri(Path::new("relative/file.ts")), None);
1514 }
1515
1516 #[test]
1520 fn test_path_to_uri_returns_err_for_relative_path() {
1521 let err = path_to_uri(Path::new("relative/file.ts")).unwrap_err();
1522 assert!(matches!(err, Error::InvalidUri(_)));
1523 }
1524
1525 #[cfg(windows)]
1526 #[test]
1527 fn test_try_path_to_uri_encodes_synthetic_windows_root() {
1528 let uri = try_path_to_uri(Path::new("/home/user/#work %23")).unwrap();
1529
1530 assert_eq!(uri.as_str(), "file:///home/user/%23work%20%2523");
1531 }
1532
1533 #[test]
1534 fn test_path_to_uri_percent_encodes_reserved_chars_in_short_path() {
1535 #[cfg(windows)]
1537 let path = Path::new(r"C:\[a].ts");
1538 #[cfg(not(windows))]
1539 let path = Path::new("/[a].ts");
1540
1541 let uri = path_to_uri(path).unwrap();
1542
1543 assert!(
1544 uri.as_str().ends_with("%5Ba%5D.ts"),
1545 "short path should percent-encode reserved chars, got {}",
1546 uri.as_str()
1547 );
1548 assert_eq!(uri_to_path(&uri).as_deref(), Some(path));
1549 }
1550
1551 #[test]
1552 fn test_path_to_uri_percent_encodes_all_rfc3986_other_reserved_chars() {
1553 #[cfg(windows)]
1557 let path = Path::new(r"C:\home\user\test[]^|{}`.ts");
1558 #[cfg(not(windows))]
1559 let path = Path::new("/home/user/test[]^|{}`.ts");
1560
1561 let uri = try_path_to_uri(path).unwrap();
1562 let uri_str = uri.as_str();
1563
1564 for (raw, encoded) in [
1565 ('[', "%5B"),
1566 (']', "%5D"),
1567 ('^', "%5E"),
1568 ('|', "%7C"),
1569 ('{', "%7B"),
1570 ('}', "%7D"),
1571 ('`', "%60"),
1572 ] {
1573 assert!(
1574 uri_str.contains(encoded),
1575 "expected {raw:?} to be percent-encoded as {encoded} in {uri_str}"
1576 );
1577 }
1578 assert!(
1579 !uri_str.contains(['[', ']', '^', '|', '{', '}', '`']),
1580 "no raw reserved characters should remain in {uri_str}"
1581 );
1582 }
1583
1584 #[test]
1585 fn test_document_tracker_concurrent_operations() {
1586 let mut map = HashMap::new();
1587 map.insert("rs".to_string(), "rust".to_string());
1588
1589 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1590 let path1 = PathBuf::from("/test/file1.rs");
1591 let path2 = PathBuf::from("/test/file2.rs");
1592
1593 tracker.open(path1.clone(), "content1".to_string()).unwrap();
1594 tracker.open(path2.clone(), "content2".to_string()).unwrap();
1595
1596 assert_eq!(tracker.len(), 2);
1597 assert!(tracker.is_open(&path1));
1598 assert!(tracker.is_open(&path2));
1599
1600 tracker.update(&path1, "new content1".to_string());
1601 assert_eq!(tracker.get(&path1).unwrap().content(), "new content1");
1602 assert_eq!(tracker.get(&path2).unwrap().content(), "content2");
1603
1604 tracker.close(&path1);
1605 assert_eq!(tracker.len(), 1);
1606 assert!(!tracker.is_open(&path1));
1607 assert!(tracker.is_open(&path2));
1608 }
1609
1610 #[test]
1611 fn test_empty_content() {
1612 let mut map = HashMap::new();
1613 map.insert("rs".to_string(), "rust".to_string());
1614
1615 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1616 let path = PathBuf::from("/test/empty.rs");
1617
1618 tracker.open(path.clone(), String::new()).unwrap();
1619 assert!(tracker.is_open(&path));
1620 assert_eq!(tracker.get(&path).unwrap().content(), "");
1621 }
1622
1623 #[test]
1624 fn test_unicode_content() {
1625 let mut map = HashMap::new();
1626 map.insert("rs".to_string(), "rust".to_string());
1627
1628 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1629 let path = PathBuf::from("/test/unicode.rs");
1630 let content = "fn テスト() { println!(\"こんにちは\"); }";
1631
1632 tracker.open(path.clone(), content.to_string()).unwrap();
1633 assert_eq!(tracker.get(&path).unwrap().content(), content);
1634 }
1635
1636 #[test]
1637 fn test_document_limit_exact_boundary() {
1638 let limits = ResourceLimits {
1639 max_documents: 5,
1640 max_file_size: 1000,
1641 };
1642 let mut map = HashMap::new();
1643 map.insert("rs".to_string(), "rust".to_string());
1644
1645 let tracker = DocumentTracker::new(limits, map);
1646
1647 for i in 0..5 {
1648 tracker
1649 .open(
1650 PathBuf::from(format!("/test/file{i}.rs")),
1651 "content".to_string(),
1652 )
1653 .unwrap();
1654 }
1655
1656 assert_eq!(tracker.len(), 5);
1657
1658 let result = tracker.open(PathBuf::from("/test/file6.rs"), "content".to_string());
1659 assert!(matches!(result, Err(Error::DocumentLimitExceeded { .. })));
1660 }
1661
1662 #[test]
1663 fn test_file_size_exact_boundary() {
1664 let limits = ResourceLimits {
1665 max_documents: 10,
1666 max_file_size: 100,
1667 };
1668 let mut map = HashMap::new();
1669 map.insert("rs".to_string(), "rust".to_string());
1670
1671 let tracker = DocumentTracker::new(limits, map);
1672
1673 let exact_size_content = "x".repeat(100);
1674 tracker
1675 .open(PathBuf::from("/test/exact.rs"), exact_size_content)
1676 .unwrap();
1677
1678 let over_size_content = "x".repeat(101);
1679 let result = tracker.open(PathBuf::from("/test/over.rs"), over_size_content);
1680 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
1681 }
1682
1683 #[test]
1684 fn test_detect_language_with_custom_extension() {
1685 let mut map = HashMap::new();
1686 map.insert("nu".to_string(), "nushell".to_string());
1687
1688 assert_eq!(detect_language(Path::new("script.nu"), &map), "nushell");
1689
1690 let empty_map = HashMap::new();
1691 assert_eq!(
1692 detect_language(Path::new("script.nu"), &empty_map),
1693 "plaintext"
1694 );
1695 }
1696
1697 #[test]
1698 fn test_detect_language_custom_overrides_default() {
1699 let mut custom_map = HashMap::new();
1700 custom_map.insert("rs".to_string(), "custom-rust".to_string());
1701
1702 assert_eq!(
1703 detect_language(Path::new("main.rs"), &custom_map),
1704 "custom-rust"
1705 );
1706
1707 let mut default_map = HashMap::new();
1708 default_map.insert("rs".to_string(), "rust".to_string());
1709
1710 assert_eq!(detect_language(Path::new("main.rs"), &default_map), "rust");
1711 }
1712
1713 #[test]
1714 fn test_detect_language_fallback_to_plaintext() {
1715 let mut map = HashMap::new();
1716 map.insert("nu".to_string(), "nushell".to_string());
1717
1718 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
1720 }
1721
1722 #[test]
1723 fn test_detect_language_empty_map() {
1724 let map = HashMap::new();
1725 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
1726 }
1727
1728 #[test]
1729 fn test_document_tracker_with_extensions() {
1730 let mut map = HashMap::new();
1731 map.insert("nu".to_string(), "nushell".to_string());
1732
1733 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1734
1735 let path = PathBuf::from("/test/script.nu");
1736 tracker
1737 .open(path.clone(), "# nushell script".to_string())
1738 .unwrap();
1739
1740 let state = tracker.get(&path).unwrap();
1741 assert_eq!(state.language_id(), "nushell");
1742 }
1743
1744 #[test]
1745 fn test_document_tracker_uses_provided_map() {
1746 let mut map = HashMap::new();
1747 map.insert("rs".to_string(), "rust".to_string());
1748
1749 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1750 let path = PathBuf::from("/test/main.rs");
1751 tracker
1752 .open(path.clone(), "fn main() {}".to_string())
1753 .unwrap();
1754
1755 let state = tracker.get(&path).unwrap();
1756 assert_eq!(state.language_id(), "rust");
1757 }
1758
1759 #[test]
1760 fn test_multiple_extensions_same_language() {
1761 let mut map = HashMap::new();
1762 map.insert("cpp".to_string(), "c++".to_string());
1763 map.insert("cc".to_string(), "c++".to_string());
1764 map.insert("cxx".to_string(), "c++".to_string());
1765
1766 assert_eq!(detect_language(Path::new("main.cpp"), &map), "c++");
1767 assert_eq!(detect_language(Path::new("main.cc"), &map), "c++");
1768 assert_eq!(detect_language(Path::new("main.cxx"), &map), "c++");
1769 }
1770
1771 #[test]
1772 fn test_case_sensitive_extensions() {
1773 let mut map = HashMap::new();
1774 map.insert("NU".to_string(), "nushell".to_string());
1775
1776 assert_eq!(detect_language(Path::new("script.nu"), &map), "plaintext");
1778 }
1779
1780 #[cfg(unix)]
1785 #[test]
1786 fn test_uri_to_path_file_scheme() {
1787 let uri: Uri = "file:///home/user/main.rs".parse().unwrap();
1788 let path = uri_to_path(&uri).unwrap();
1789 assert_eq!(path, PathBuf::from("/home/user/main.rs"));
1790 }
1791
1792 #[test]
1793 fn test_uri_to_path_non_file_scheme_returns_none() {
1794 let uri: Uri = "https://example.com/file.rs".parse().unwrap();
1795 assert!(uri_to_path(&uri).is_none());
1796 }
1797
1798 #[test]
1799 fn test_uri_to_path_lsp_diagnostics_scheme_returns_none() {
1800 let uri: Uri = "lsp-diagnostics:///home/user/main.rs".parse().unwrap();
1802 assert!(uri_to_path(&uri).is_none());
1803 }
1804
1805 #[test]
1806 fn test_uri_to_path_with_authority_returns_none() {
1807 let result = "file://server/share/path.rs"
1811 .parse::<Uri>()
1812 .ok()
1813 .and_then(|u| uri_to_path(&u));
1814 assert!(result.is_none());
1815 }
1816
1817 #[test]
1822 fn test_open_paths_empty_tracker() {
1823 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1824 assert_eq!(tracker.open_paths().len(), 0);
1825 }
1826
1827 #[test]
1828 fn test_open_paths_populated_tracker() {
1829 let mut map = HashMap::new();
1830 map.insert("rs".to_string(), "rust".to_string());
1831 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1832 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
1833 tracker.open(PathBuf::from("/b.rs"), String::new()).unwrap();
1834 let mut paths = tracker.open_paths();
1835 paths.sort();
1836 assert_eq!(paths, [PathBuf::from("/a.rs"), PathBuf::from("/b.rs")]);
1837 }
1838
1839 #[test]
1840 fn test_open_paths_after_close() {
1841 let mut map = HashMap::new();
1842 map.insert("rs".to_string(), "rust".to_string());
1843 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1844 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
1845 tracker.close(Path::new("/a.rs"));
1846 assert_eq!(tracker.open_paths().len(), 0);
1847 }
1848
1849 use std::process::Stdio;
1854
1855 use tempfile::TempDir;
1856 use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
1857 use tokio::process::{Child, ChildStdin, ChildStdout, Command};
1858
1859 use crate::config::LspServerConfig;
1860 use crate::lsp::LspTransport;
1861
1862 struct FakeServer {
1875 _write_half: Child,
1876 _read_half: Child,
1877 _read_half_stdin: ChildStdin,
1878 write_stdout: ChildStdout,
1879 }
1880
1881 fn fake_lsp_client() -> (LspClient, FakeServer) {
1884 let mut write_half = Command::new("cat")
1885 .stdin(Stdio::piped())
1886 .stdout(Stdio::piped())
1887 .kill_on_drop(true)
1888 .spawn()
1889 .unwrap();
1890 let write_stdin = write_half.stdin.take().unwrap();
1891 let write_stdout = write_half.stdout.take().unwrap();
1892
1893 let mut read_half = Command::new("cat")
1894 .stdin(Stdio::piped())
1895 .stdout(Stdio::piped())
1896 .kill_on_drop(true)
1897 .spawn()
1898 .unwrap();
1899 let read_stdout = read_half.stdout.take().unwrap();
1900 let read_stdin = read_half.stdin.take().unwrap();
1901
1902 let transport = LspTransport::new(write_stdin, read_stdout);
1903 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1904
1905 (
1906 client,
1907 FakeServer {
1908 _write_half: write_half,
1909 _read_half: read_half,
1910 _read_half_stdin: read_stdin,
1911 write_stdout,
1912 },
1913 )
1914 }
1915
1916 fn set_mtime(path: &Path, time: SystemTime) {
1924 let file = std::fs::OpenOptions::new().write(true).open(path).unwrap();
1925 file.set_modified(time).unwrap();
1926 }
1927
1928 fn settled_past() -> SystemTime {
1929 SystemTime::now() - Duration::from_secs(10)
1930 }
1931
1932 async fn read_framed_message(reader: &mut BufReader<&mut ChildStdout>) -> serde_json::Value {
1938 let mut content_length = None;
1939 let mut line = String::new();
1940 loop {
1941 line.clear();
1942 reader.read_line(&mut line).await.unwrap();
1943 if line == "\r\n" || line == "\n" {
1944 break;
1945 }
1946 if let Some((key, value)) = line.trim_end().split_once(':')
1947 && key.trim().eq_ignore_ascii_case("content-length")
1948 {
1949 content_length = Some(value.trim().parse::<usize>().unwrap());
1950 }
1951 }
1952 let mut buf = vec![0u8; content_length.unwrap()];
1953 reader.read_exact(&mut buf).await.unwrap();
1954 serde_json::from_slice(&buf).unwrap()
1955 }
1956
1957 #[test]
1958 fn test_mtime_settled_boundary() {
1959 let read_at = SystemTime::now();
1960 assert!(!mtime_settled(None, read_at), "no mtime is never settled");
1961 assert!(
1962 mtime_settled(Some(read_at - Duration::from_secs(3)), read_at),
1963 "3s older than read_at is past the 2s granularity margin"
1964 );
1965 assert!(
1966 !mtime_settled(Some(read_at - Duration::from_secs(1)), read_at),
1967 "1s older than read_at is within the 2s granularity margin"
1968 );
1969 assert!(
1970 !mtime_settled(Some(read_at + Duration::from_secs(10)), read_at),
1971 "an mtime after read_at is never settled"
1972 );
1973 }
1974
1975 #[tokio::test]
1976 async fn test_ensure_open_unchanged_file_is_fast_path() {
1977 let dir = TempDir::new().unwrap();
1978 let path = dir.path().join("a.rs");
1979 std::fs::write(&path, "fn main() {}").unwrap();
1980 set_mtime(&path, settled_past());
1981
1982 let (client, _server) = fake_lsp_client();
1983 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1984
1985 let uri1 = tracker
1986 .ensure_open(&path, &ServerId::from("rust"), &client)
1987 .await
1988 .unwrap();
1989 assert_eq!(tracker.get(&path).unwrap().version(), 1);
1990
1991 let uri2 = tracker
1992 .ensure_open(&path, &ServerId::from("rust"), &client)
1993 .await
1994 .unwrap();
1995 assert_eq!(uri1, uri2);
1996 assert_eq!(tracker.get(&path).unwrap().version(), 1);
1997 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
1998 }
1999
2000 #[tokio::test]
2001 async fn test_ensure_open_resyncs_on_size_change() {
2002 let dir = TempDir::new().unwrap();
2003 let path = dir.path().join("a.rs");
2004 std::fs::write(&path, "fn main() {}").unwrap();
2005 set_mtime(&path, settled_past());
2006
2007 let (client, _server) = fake_lsp_client();
2008 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2009 tracker
2010 .ensure_open(&path, &ServerId::from("rust"), &client)
2011 .await
2012 .unwrap();
2013
2014 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
2015 set_mtime(&path, settled_past());
2016
2017 tracker
2018 .ensure_open(&path, &ServerId::from("rust"), &client)
2019 .await
2020 .unwrap();
2021 let state = tracker.get(&path).unwrap();
2022 assert_eq!(state.version(), 2);
2023 assert_eq!(state.content(), "fn main() { println!(\"hi\"); }");
2024 }
2025
2026 #[tokio::test(start_paused = true)]
2027 async fn test_ensure_open_regression_102_103_racy_same_size_rewrite() {
2028 let dir = TempDir::new().unwrap();
2029 let path = dir.path().join("a.rs");
2030 std::fs::write(&path, "AAAA").unwrap();
2031 let (client, _server) = fake_lsp_client();
2034 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2035 tracker
2036 .ensure_open(&path, &ServerId::from("rust"), &client)
2037 .await
2038 .unwrap();
2039 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
2040
2041 std::fs::write(&path, "BBBB").unwrap();
2044 set_mtime(&path, original_mtime);
2045
2046 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
2047
2048 tracker
2049 .ensure_open(&path, &ServerId::from("rust"), &client)
2050 .await
2051 .unwrap();
2052 let state = tracker.get(&path).unwrap();
2053 assert_eq!(
2054 state.version(),
2055 2,
2056 "must resync despite identical (mtime, size)"
2057 );
2058 assert_eq!(state.content(), "BBBB");
2059 }
2060
2061 #[tokio::test(start_paused = true)]
2062 async fn test_ensure_open_regression_102_103_settled_mtime_is_the_documented_limit() {
2063 let dir = TempDir::new().unwrap();
2064 let path = dir.path().join("a.rs");
2065 std::fs::write(&path, "AAAA").unwrap();
2066 set_mtime(&path, settled_past());
2067
2068 let (client, _server) = fake_lsp_client();
2069 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2070 tracker
2071 .ensure_open(&path, &ServerId::from("rust"), &client)
2072 .await
2073 .unwrap();
2074 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
2075
2076 std::fs::write(&path, "BBBB").unwrap();
2080 set_mtime(&path, original_mtime);
2081
2082 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
2083
2084 tracker
2085 .ensure_open(&path, &ServerId::from("rust"), &client)
2086 .await
2087 .unwrap();
2088 let state = tracker.get(&path).unwrap();
2089 assert_eq!(state.version(), 1, "documented limitation: fast path taken");
2090 assert_eq!(state.content(), "AAAA");
2091 }
2092
2093 #[tokio::test(start_paused = true)]
2094 async fn test_ensure_open_stat_is_never_debounced() {
2095 let dir = TempDir::new().unwrap();
2096 let path = dir.path().join("a.rs");
2097 std::fs::write(&path, "AAAA").unwrap();
2098 set_mtime(&path, settled_past());
2099
2100 let (client, _server) = fake_lsp_client();
2101 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2102 tracker
2103 .ensure_open(&path, &ServerId::from("rust"), &client)
2104 .await
2105 .unwrap();
2106
2107 std::fs::write(&path, "BBBBBBBB").unwrap();
2110 tracker
2111 .ensure_open(&path, &ServerId::from("rust"), &client)
2112 .await
2113 .unwrap();
2114
2115 let state = tracker.get(&path).unwrap();
2116 assert_eq!(state.version(), 2);
2117 assert_eq!(state.content(), "BBBBBBBB");
2118 }
2119
2120 #[tokio::test(start_paused = true)]
2121 async fn test_ensure_open_debounce_gates_reread_only() {
2122 let dir = TempDir::new().unwrap();
2123 let path = dir.path().join("a.rs");
2124 std::fs::write(&path, "AAAA").unwrap();
2125 let (client, _server) = fake_lsp_client();
2128 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2129 tracker
2130 .ensure_open(&path, &ServerId::from("rust"), &client)
2131 .await
2132 .unwrap();
2133 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
2134
2135 std::fs::write(&path, "BBBB").unwrap(); set_mtime(&path, original_mtime); tracker
2140 .ensure_open(&path, &ServerId::from("rust"), &client)
2141 .await
2142 .unwrap();
2143 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2144
2145 tokio::time::advance(Duration::from_millis(300)).await;
2146 tracker
2147 .ensure_open(&path, &ServerId::from("rust"), &client)
2148 .await
2149 .unwrap();
2150 let state = tracker.get(&path).unwrap();
2151 assert_eq!(state.version(), 2);
2152 assert_eq!(state.content(), "BBBB");
2153 }
2154
2155 #[tokio::test]
2156 async fn test_ensure_open_deleted_file_errors_state_untouched() {
2157 let dir = TempDir::new().unwrap();
2158 let path = dir.path().join("a.rs");
2159 std::fs::write(&path, "fn main() {}").unwrap();
2160 set_mtime(&path, settled_past());
2161
2162 let (client, _server) = fake_lsp_client();
2163 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2164 tracker
2165 .ensure_open(&path, &ServerId::from("rust"), &client)
2166 .await
2167 .unwrap();
2168
2169 std::fs::remove_file(&path).unwrap();
2170
2171 let result = tracker
2172 .ensure_open(&path, &ServerId::from("rust"), &client)
2173 .await;
2174 assert!(matches!(result, Err(Error::FileIo { .. })));
2175 assert!(tracker.is_open(&path));
2176 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2177 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
2178 }
2179
2180 #[tokio::test]
2181 async fn test_ensure_open_grows_past_limit_errors_state_intact() {
2182 let dir = TempDir::new().unwrap();
2183 let path = dir.path().join("a.rs");
2184 std::fs::write(&path, "small").unwrap();
2185 set_mtime(&path, settled_past());
2186
2187 let limits = ResourceLimits {
2188 max_documents: 10,
2189 max_file_size: 10,
2190 };
2191 let (client, _server) = fake_lsp_client();
2192 let tracker = DocumentTracker::new(limits, HashMap::new());
2193 tracker
2194 .ensure_open(&path, &ServerId::from("rust"), &client)
2195 .await
2196 .unwrap();
2197
2198 std::fs::write(&path, "x".repeat(100)).unwrap();
2199
2200 let result = tracker
2201 .ensure_open(&path, &ServerId::from("rust"), &client)
2202 .await;
2203 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
2204 assert_eq!(tracker.get(&path).unwrap().content(), "small");
2205 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2206 }
2207
2208 #[tokio::test]
2209 async fn test_ensure_open_resync_at_document_capacity() {
2210 let dir = TempDir::new().unwrap();
2211 let path = dir.path().join("a.rs");
2212 std::fs::write(&path, "AAAA").unwrap();
2213 set_mtime(&path, settled_past());
2214
2215 let limits = ResourceLimits {
2216 max_documents: 1,
2217 max_file_size: 0,
2218 };
2219 let (client, _server) = fake_lsp_client();
2220 let tracker = DocumentTracker::new(limits, HashMap::new());
2221 tracker
2222 .ensure_open(&path, &ServerId::from("rust"), &client)
2223 .await
2224 .unwrap();
2225 assert_eq!(tracker.len(), 1);
2226
2227 std::fs::write(&path, "BBBBBBBB").unwrap();
2228 let result = tracker
2229 .ensure_open(&path, &ServerId::from("rust"), &client)
2230 .await;
2231 assert!(
2232 result.is_ok(),
2233 "resync must not re-run the doc-count check on an already-tracked path"
2234 );
2235 assert_eq!(tracker.len(), 1);
2236 assert_eq!(tracker.get(&path).unwrap().version(), 2);
2237 }
2238
2239 #[tokio::test]
2240 async fn test_update_clears_disk_provenance() {
2241 let dir = TempDir::new().unwrap();
2242 let path = dir.path().join("a.rs");
2243 std::fs::write(&path, "fn main() {}").unwrap();
2244 set_mtime(&path, settled_past());
2245
2246 let (client, _server) = fake_lsp_client();
2247 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2248 tracker
2249 .ensure_open(&path, &ServerId::from("rust"), &client)
2250 .await
2251 .unwrap();
2252 assert!(tracker.get(&path).unwrap().disk.is_some());
2253
2254 tracker.update(&path, "fn main() { updated(); }".to_string());
2255 assert!(
2256 tracker.get(&path).unwrap().disk.is_none(),
2257 "update() must clear disk provenance so the next ensure_open re-verifies by content"
2258 );
2259 }
2260
2261 #[tokio::test]
2262 async fn test_first_open_self_heals_when_did_open_notify_fails() {
2263 let dir = TempDir::new().unwrap();
2264 let path = dir.path().join("a.rs");
2265 std::fs::write(&path, "fn main() {}").unwrap();
2266
2267 let (client, _server) = fake_lsp_client();
2268 let notify_will_fail = client.clone();
2274 client.shutdown().await.unwrap();
2275
2276 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2277 let result = tracker
2278 .ensure_open(&path, &ServerId::from("rust"), ¬ify_will_fail)
2279 .await;
2280
2281 assert!(result.is_err(), "notify failure must propagate as an error");
2282 assert!(
2283 !tracker.is_open(&path),
2284 "a failed didOpen must not leave the document tracked, or the server \
2285 and tracker would stay permanently desynced"
2286 );
2287 }
2288
2289 #[tokio::test]
2290 async fn test_resync_sends_didchange_with_full_replacement_over_the_wire() {
2291 let dir = TempDir::new().unwrap();
2292 let path = dir.path().join("a.rs");
2293 std::fs::write(&path, "fn main() {}").unwrap();
2294 set_mtime(&path, settled_past());
2295
2296 let (client, mut server) = fake_lsp_client();
2297 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2298 tracker
2299 .ensure_open(&path, &ServerId::from("rust"), &client)
2300 .await
2301 .unwrap();
2302
2303 let mut wire = BufReader::new(&mut server.write_stdout);
2304 let opened = read_framed_message(&mut wire).await;
2305 assert_eq!(opened["method"], "textDocument/didOpen");
2306
2307 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
2308 set_mtime(&path, settled_past());
2309 tracker
2310 .ensure_open(&path, &ServerId::from("rust"), &client)
2311 .await
2312 .unwrap();
2313
2314 let changed = read_framed_message(&mut wire).await;
2315 assert_eq!(changed["method"], "textDocument/didChange");
2316 let params = &changed["params"];
2317 assert_eq!(params["textDocument"]["version"], 2);
2318 let change = ¶ms["contentChanges"][0];
2319 assert!(
2320 change.get("range").is_none(),
2321 "range must be omitted, not null, for a full-replacement change"
2322 );
2323 assert!(
2324 change.get("rangeLength").is_none(),
2325 "rangeLength must be omitted, not null, for a full-replacement change"
2326 );
2327 assert_eq!(change["text"], "fn main() { println!(\"hi\"); }");
2328 }
2329
2330 #[tokio::test]
2335 async fn test_ensure_open_second_server_gets_didopen_no_disk_change() {
2336 let dir = TempDir::new().unwrap();
2337 let path = dir.path().join("a.rs");
2338 std::fs::write(&path, "fn main() {}").unwrap();
2339 set_mtime(&path, settled_past());
2340
2341 let (client_a, mut server_a) = fake_lsp_client();
2342 let (client_b, mut server_b) = fake_lsp_client();
2343 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2344
2345 let id_a = ServerId::from("server-a");
2346 let id_b = ServerId::from("server-b");
2347
2348 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
2349 let mut wire_a = BufReader::new(&mut server_a.write_stdout);
2350 let opened_a = read_framed_message(&mut wire_a).await;
2351 assert_eq!(opened_a["method"], "textDocument/didOpen");
2352
2353 tracker.ensure_open(&path, &id_b, &client_b).await.unwrap();
2357 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
2358 let opened_b = read_framed_message(&mut wire_b).await;
2359 assert_eq!(opened_b["method"], "textDocument/didOpen");
2360 assert_eq!(opened_b["params"]["textDocument"]["version"], 1);
2361 assert_eq!(opened_b["params"]["textDocument"]["text"], "fn main() {}");
2362 }
2363
2364 #[tokio::test(start_paused = true)]
2368 async fn test_ensure_open_second_server_gets_didopen_unchanged_content_path() {
2369 let dir = TempDir::new().unwrap();
2370 let path = dir.path().join("a.rs");
2371 std::fs::write(&path, "fn main() {}").unwrap();
2372 let (client_a, _server_a) = fake_lsp_client();
2375 let (client_b, mut server_b) = fake_lsp_client();
2376 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2377
2378 tracker
2379 .ensure_open(&path, &ServerId::from("server-a"), &client_a)
2380 .await
2381 .unwrap();
2382
2383 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
2386
2387 tracker
2388 .ensure_open(&path, &ServerId::from("server-b"), &client_b)
2389 .await
2390 .unwrap();
2391 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
2392 let opened_b = read_framed_message(&mut wire_b).await;
2393 assert_eq!(opened_b["method"], "textDocument/didOpen");
2394 }
2395
2396 #[tokio::test]
2403 async fn test_ensure_open_same_server_twice_sends_nothing_second_time() {
2404 let dir = TempDir::new().unwrap();
2405 let path = dir.path().join("a.rs");
2406 std::fs::write(&path, "fn main() {}").unwrap();
2407 set_mtime(&path, settled_past());
2408
2409 let (client, mut server) = fake_lsp_client();
2410 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2411 let id = ServerId::from("rust");
2412
2413 tracker.ensure_open(&path, &id, &client).await.unwrap();
2414 tracker.ensure_open(&path, &id, &client).await.unwrap();
2415
2416 let mut wire = BufReader::new(&mut server.write_stdout);
2417 let opened = read_framed_message(&mut wire).await;
2418 assert_eq!(opened["method"], "textDocument/didOpen");
2419 assert_eq!(
2420 tracker.get(&path).unwrap().synced_version(&id),
2421 Some(1),
2422 "second call for the same server must not re-open or re-change"
2423 );
2424 }
2425
2426 #[tokio::test]
2430 async fn test_sync_phase_failed_didchange_does_not_disturb_other_server() {
2431 let dir = TempDir::new().unwrap();
2432 let path = dir.path().join("a.rs");
2433 std::fs::write(&path, "fn main() {}").unwrap();
2434 set_mtime(&path, settled_past());
2435
2436 let (client_a, _server_a) = fake_lsp_client();
2437 let (client_b, _server_b) = fake_lsp_client();
2438 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2439 let id_a = ServerId::from("server-a");
2440 let id_b = ServerId::from("server-b");
2441
2442 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
2443 tracker.ensure_open(&path, &id_b, &client_b).await.unwrap();
2444
2445 let client_b_will_fail = client_b.clone();
2448 client_b.shutdown().await.unwrap();
2449
2450 std::fs::write(&path, "fn main() { updated(); }").unwrap();
2451 set_mtime(&path, settled_past());
2452
2453 let result = tracker.ensure_open(&path, &id_b, &client_b_will_fail).await;
2454 assert!(result.is_err(), "B's didChange must fail and propagate");
2455
2456 assert!(tracker.is_open(&path));
2462 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
2463 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2464 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_a), Some(1));
2465 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_b), Some(1));
2466
2467 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
2471 assert_eq!(
2472 tracker.get(&path).unwrap().content(),
2473 "fn main() { updated(); }"
2474 );
2475 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_a), Some(2));
2476 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_b), Some(1));
2477 }
2478
2479 #[cfg(unix)]
2495 #[tokio::test]
2496 async fn test_ensure_open_different_paths_do_not_serialize() {
2497 let dir = TempDir::new().unwrap();
2498 let path_a = dir.path().join("a.rs");
2499 let path_b = dir.path().join("b.rs");
2500
2501 std::fs::write(&path_b, "fn b() {}").unwrap();
2502 set_mtime(&path_b, settled_past());
2503
2504 let status = std::process::Command::new("mkfifo")
2505 .arg(&path_a)
2506 .status()
2507 .unwrap();
2508 assert!(status.success(), "mkfifo must succeed to set up this test");
2509
2510 let (client_a, _server_a) = fake_lsp_client();
2511 let (client_b, _server_b) = fake_lsp_client();
2512 let tracker = Arc::new(DocumentTracker::new(
2513 ResourceLimits::default(),
2514 HashMap::new(),
2515 ));
2516
2517 let tracker_for_a = Arc::clone(&tracker);
2520 let path_a_for_task = path_a.clone();
2521 let handle_a = tokio::spawn(async move {
2522 tracker_for_a
2523 .ensure_open(&path_a_for_task, &ServerId::from("server-a"), &client_a)
2524 .await
2525 });
2526
2527 tokio::time::sleep(Duration::from_millis(200)).await;
2530
2531 tokio::time::timeout(
2534 Duration::from_secs(5),
2535 tracker.ensure_open(&path_b, &ServerId::from("server-b"), &client_b),
2536 )
2537 .await
2538 .unwrap()
2539 .unwrap();
2540
2541 let path_a_writer = path_a.clone();
2545 tokio::task::spawn_blocking(move || {
2546 std::fs::write(path_a_writer, "fn a() {}").unwrap();
2547 })
2548 .await
2549 .unwrap();
2550
2551 handle_a.await.unwrap().unwrap();
2552 assert_eq!(tracker.get(&path_a).unwrap().content(), "fn a() {}");
2553 }
2554
2555 #[tokio::test]
2561 async fn test_ensure_open_concurrent_same_path_single_didopen() {
2562 let dir = TempDir::new().unwrap();
2563 let path = dir.path().join("a.rs");
2564 std::fs::write(&path, "fn main() {}").unwrap();
2565 set_mtime(&path, settled_past());
2566
2567 let (client, mut server) = fake_lsp_client();
2568 let tracker = Arc::new(DocumentTracker::new(
2569 ResourceLimits::default(),
2570 HashMap::new(),
2571 ));
2572 let id = ServerId::from("rust");
2573
2574 let mut handles = Vec::new();
2575 for _ in 0..8 {
2576 let tracker = Arc::clone(&tracker);
2577 let client = client.clone();
2578 let path = path.clone();
2579 let id = id.clone();
2580 handles.push(tokio::spawn(async move {
2581 tracker.ensure_open(&path, &id, &client).await
2582 }));
2583 }
2584 for handle in handles {
2585 handle.await.unwrap().unwrap();
2586 }
2587
2588 let mut wire = BufReader::new(&mut server.write_stdout);
2589 let opened = read_framed_message(&mut wire).await;
2590 assert_eq!(opened["method"], "textDocument/didOpen");
2591
2592 let extra =
2595 tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut wire)).await;
2596 assert!(
2597 extra.is_err(),
2598 "expected no additional notification after the single didOpen"
2599 );
2600
2601 assert_eq!(tracker.get(&path).unwrap().synced_version(&id), Some(1));
2602 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2603 }
2604
2605 #[tokio::test]
2612 async fn test_ensure_open_path_locks_evicted_after_completion() {
2613 let dir = TempDir::new().unwrap();
2614 let paths: Vec<_> = ["a.rs", "b.rs", "c.rs"]
2615 .iter()
2616 .map(|name| dir.path().join(name))
2617 .collect();
2618 for path in &paths {
2619 std::fs::write(path, "fn f() {}").unwrap();
2620 set_mtime(path, settled_past());
2621 }
2622
2623 let tracker = Arc::new(DocumentTracker::new(
2624 ResourceLimits::default(),
2625 HashMap::new(),
2626 ));
2627 let id = ServerId::from("rust");
2628
2629 let mut handles = Vec::new();
2630 let mut servers = Vec::new();
2631 for path in paths.clone() {
2632 let tracker = Arc::clone(&tracker);
2633 let (client, server) = fake_lsp_client();
2634 servers.push(server);
2635 let id = id.clone();
2636 handles.push(tokio::spawn(async move {
2637 tracker.ensure_open(&path, &id, &client).await
2638 }));
2639 }
2640 for handle in handles {
2641 handle.await.unwrap().unwrap();
2642 }
2643 drop(servers);
2644
2645 assert!(
2646 lock_std(&tracker.path_locks).is_empty(),
2647 "path_locks must be fully evicted once every ensure_open call \
2648 for every path has completed, otherwise the map grows \
2649 unbounded for the lifetime of the process"
2650 );
2651 }
2652}