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 async fn update(&self, path: &Path, content: String) -> Option<i32> {
399 let _path_guard = self.lock_path(path).await;
400 lock_std(&self.documents)
401 .get_mut(path)
402 .map(|state| state.apply_local_edit(content))
403 }
404
405 const fn check_file_size(&self, size: u64) -> Result<()> {
407 if self.limits.max_file_size > 0 && size > self.limits.max_file_size {
408 return Err(Error::FileSizeLimitExceeded {
409 size,
410 max: self.limits.max_file_size,
411 });
412 }
413 Ok(())
414 }
415
416 fn set_disk(&self, path: &Path, snap: DiskSync) {
423 if let Some(st) = lock_std(&self.documents).get_mut(path) {
424 st.set_disk(snap);
425 }
426 }
427
428 pub fn close(&self, path: &Path) -> Option<DocumentState> {
432 lock_std(&self.documents).remove(path)
433 }
434
435 pub fn close_all(&self) -> Vec<DocumentState> {
437 lock_std(&self.documents)
438 .drain()
439 .map(|(_, state)| state)
440 .collect()
441 }
442
443 pub fn open_paths(&self) -> Vec<PathBuf> {
445 lock_std(&self.documents).keys().cloned().collect()
446 }
447
448 pub fn forget_server(&self, server: &ServerId) {
467 *lock_std(&self.generations)
468 .entry(server.clone())
469 .or_insert(0) += 1;
470 for state in lock_std(&self.documents).values_mut() {
471 state.forget_server(server);
472 }
473 }
474
475 fn generation(&self, server: &ServerId) -> u64 {
477 lock_std(&self.generations)
478 .get(server)
479 .copied()
480 .unwrap_or(0)
481 }
482
483 async fn lock_path(&self, path: &Path) -> PathLockGuard<'_> {
498 let arc = {
499 let mut locks = lock_std(&self.path_locks);
500 locks
501 .entry(path.to_path_buf())
502 .or_insert_with(|| Arc::new(AsyncMutex::new(())))
503 .clone()
504 };
505 let guard = Arc::clone(&arc).lock_owned().await;
506 PathLockGuard {
507 path_locks: &self.path_locks,
508 path: path.to_path_buf(),
509 arc,
510 guard: Some(guard),
511 }
512 }
513
514 pub async fn ensure_open(
583 &self,
584 path: &Path,
585 server: &ServerId,
586 lsp_client: &LspClient,
587 ) -> Result<Uri> {
588 let _path_guard = self.lock_path(path).await;
589 let generation = self.generation(server);
590 let decision = self.disk_phase(path).await?;
591 self.sync_phase(path, server, lsp_client, decision, generation)
592 .await
593 }
594
595 async fn disk_phase(&self, path: &Path) -> Result<Decision> {
600 if !lock_std(&self.documents).contains_key(path) {
601 return self.disk_phase_new(path).await;
602 }
603
604 let read_at = SystemTime::now();
605 let meta = fs::metadata(path).await.map_err(|e| Error::FileIo {
606 path: path.to_path_buf(),
607 source: e,
608 })?;
609 let mtime = meta.modified().ok();
610 let size = meta.len();
611
612 let Some((uri, current_version, fast_path)) =
616 lock_std(&self.documents).get(path).map(|st| {
617 let stat_matches = st
618 .disk()
619 .is_some_and(|d| d.mtime == mtime && d.size == size);
620 let fast_path = match st.disk() {
621 Some(d) if stat_matches && d.mtime_settled => true,
622 Some(d)
623 if stat_matches && d.content_checked_at.elapsed() < DISK_CHECK_DEBOUNCE =>
624 {
625 true
626 }
627 _ => false,
628 };
629 (st.uri.clone(), st.version, fast_path)
630 })
631 else {
632 return Err(Error::DocumentNotFound(path.to_path_buf()));
633 };
634 if fast_path {
635 return Ok(Decision::unchanged(uri, current_version));
636 }
637
638 let (fresh, ..) = self.read_to_string_checked(path).await?;
639 let snap = DiskSync {
640 mtime,
641 size,
642 mtime_settled: mtime_settled(mtime, read_at),
643 content_checked_at: Instant::now(),
644 };
645
646 let Some(unchanged) = lock_std(&self.documents)
647 .get(path)
648 .map(|st| fresh == st.content)
649 else {
650 return Err(Error::DocumentNotFound(path.to_path_buf()));
651 };
652
653 if unchanged {
654 self.set_disk(path, snap);
655 return Ok(Decision::unchanged(uri, current_version));
656 }
657
658 Ok(Decision {
659 uri,
660 target_version: current_version.saturating_add(1),
661 fresh_content: Some(fresh),
662 snap: Some(snap),
663 })
664 }
665
666 async fn disk_phase_new(&self, path: &Path) -> Result<Decision> {
670 let read_at = SystemTime::now();
671 let (content, mtime, size) = self.read_to_string_checked(path).await?;
672
673 let uri = self.open(path.to_path_buf(), content)?;
674 self.set_disk(
675 path,
676 DiskSync {
677 mtime,
678 size,
679 mtime_settled: mtime_settled(mtime, read_at),
680 content_checked_at: Instant::now(),
681 },
682 );
683
684 Ok(Decision::unchanged(uri, 1))
685 }
686
687 async fn read_to_string_checked(
699 &self,
700 path: &Path,
701 ) -> Result<(String, Option<SystemTime>, u64)> {
702 let mut file = fs::File::open(path).await.map_err(|e| Error::FileIo {
703 path: path.to_path_buf(),
704 source: e,
705 })?;
706 let meta = file.metadata().await.map_err(|e| Error::FileIo {
707 path: path.to_path_buf(),
708 source: e,
709 })?;
710 self.check_file_size(meta.len())?;
711 let mut content = String::new();
712 file.read_to_string(&mut content)
713 .await
714 .map_err(|e| Error::FileIo {
715 path: path.to_path_buf(),
716 source: e,
717 })?;
718 Ok((content, meta.modified().ok(), meta.len()))
719 }
720
721 async fn sync_phase(
732 &self,
733 path: &Path,
734 server: &ServerId,
735 lsp_client: &LspClient,
736 decision: Decision,
737 generation: u64,
738 ) -> Result<Uri> {
739 let Decision {
740 uri,
741 target_version,
742 fresh_content,
743 snap,
744 } = decision;
745
746 let Some(synced_version) = lock_std(&self.documents)
753 .get(path)
754 .map(|st| st.synced_version(server))
755 else {
756 return Err(Error::DocumentNotFound(path.to_path_buf()));
757 };
758 let up_to_date = synced_version.is_some_and(|v| v >= target_version);
759 let is_first_open = synced_version.is_none();
760
761 if up_to_date {
762 return Ok(uri);
763 }
764
765 let Some((language_id, text)) = lock_std(&self.documents).get(path).map(|st| {
766 let text = fresh_content.clone().unwrap_or_else(|| st.content.clone());
767 (st.language_id.clone(), text)
768 }) else {
769 return Err(Error::DocumentNotFound(path.to_path_buf()));
770 };
771
772 let notify_result = if is_first_open {
773 lsp_client
774 .notify(
775 "textDocument/didOpen",
776 DidOpenTextDocumentParams {
777 text_document: TextDocumentItem {
778 uri: uri.clone(),
779 language_id: language_id.into(),
780 version: target_version,
781 text,
782 },
783 },
784 )
785 .await
786 } else {
787 lsp_client
788 .notify(
789 "textDocument/didChange",
790 DidChangeTextDocumentParams {
791 text_document: VersionedTextDocumentIdentifier {
792 version: target_version,
793 text_document_identifier: lsp_types::TextDocumentIdentifier {
794 uri: uri.clone(),
795 },
796 },
797 content_changes: vec![
798 TextDocumentContentChangeEvent::TextDocumentContentChangeWholeDocument(
799 lsp_types::TextDocumentContentChangeWholeDocument { text },
800 ),
801 ],
802 },
803 )
804 .await
805 };
806
807 if let Err(err) = notify_result {
808 let first_ever_sync = lock_std(&self.documents)
821 .get(path)
822 .is_some_and(DocumentState::has_never_synced);
823 if is_first_open && first_ever_sync {
824 lock_std(&self.documents).remove(path);
825 }
826 return Err(err);
827 }
828
829 let mut documents = lock_std(&self.documents);
832 let Some(st) = documents.get_mut(path) else {
833 return Err(Error::DocumentNotFound(path.to_path_buf()));
834 };
835 if let Some(fresh) = fresh_content {
836 st.commit_reload(target_version, fresh, snap);
837 }
838 if self.generation(server) == generation {
848 st.mark_synced(server.clone(), target_version);
849 }
850 drop(documents);
851
852 Ok(uri)
853 }
854}
855
856struct PathLockGuard<'a> {
865 path_locks: &'a StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
866 path: PathBuf,
867 arc: Arc<AsyncMutex<()>>,
868 guard: Option<OwnedMutexGuard<()>>,
869}
870
871impl Drop for PathLockGuard<'_> {
872 fn drop(&mut self) {
873 self.guard.take();
876
877 let mut locks = lock_std(self.path_locks);
878 if Arc::strong_count(&self.arc) <= 2 {
890 locks.remove(&self.path);
891 }
892 }
893}
894
895struct Decision {
900 uri: Uri,
901 target_version: i32,
902 fresh_content: Option<String>,
903 snap: Option<DiskSync>,
904}
905
906impl Decision {
907 const fn unchanged(uri: Uri, target_version: i32) -> Self {
910 Self {
911 uri,
912 target_version,
913 fresh_content: None,
914 snap: None,
915 }
916 }
917}
918
919pub fn path_to_uri(path: &Path) -> Result<Uri> {
932 try_path_to_uri(path)
933 .ok_or_else(|| Error::InvalidUri(format!("cannot convert path to URI: {}", path.display())))
934}
935
936#[must_use]
942pub fn try_path_to_uri(path: &Path) -> Option<Uri> {
943 let uri_string = encode_rfc3986_path_chars(&file_url(path)?);
944 Some(Uri::from(uri_string))
945}
946
947#[cfg(not(windows))]
948fn file_url(path: &Path) -> Option<Url> {
949 Url::from_file_path(path).ok()
950}
951
952#[cfg(windows)]
953fn file_url(path: &Path) -> Option<Url> {
954 match Url::from_file_path(path) {
955 Ok(file_url) => Some(file_url),
956 Err(()) if path.has_root() => windows_rooted_path_to_file_url(path),
957 Err(()) => None,
958 }
959}
960
961#[cfg(windows)]
962fn windows_rooted_path_to_file_url(path: &Path) -> Option<Url> {
963 let path_str = path.to_string_lossy();
964 let stripped = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
965 let mut file_url = Url::parse("file:///").ok()?;
966 file_url.path_segments_mut().ok()?.clear().extend(
967 stripped
968 .split(['\\', '/'])
969 .filter(|segment| !segment.is_empty()),
970 );
971 Some(file_url)
972}
973
974pub(super) fn encode_rfc3986_path_chars(url: &Url) -> String {
984 let prefix = url[..url::Position::BeforePath].to_owned();
985 let encoded = url[url::Position::BeforePath..]
986 .replace('[', "%5B")
987 .replace(']', "%5D")
988 .replace('^', "%5E")
989 .replace('|', "%7C");
990 format!("{prefix}{encoded}")
991}
992
993#[must_use]
998pub fn uri_to_path(uri: &Uri) -> Option<PathBuf> {
999 let url = Url::parse(uri.as_ref()).ok()?;
1000 if url.scheme() != "file" {
1001 return None;
1002 }
1003 if !url.host_str().unwrap_or("").is_empty() {
1006 return None;
1007 }
1008 url.to_file_path().ok()
1009}
1010
1011#[must_use]
1016pub fn detect_language(path: &Path, extension_map: &HashMap<String, String>) -> String {
1017 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
1018
1019 extension_map
1020 .get(extension)
1021 .cloned()
1022 .unwrap_or_else(|| "plaintext".to_string())
1023}
1024
1025#[cfg(test)]
1026#[allow(clippy::unwrap_used)]
1027mod tests {
1028 use super::*;
1029
1030 #[test]
1031 fn test_detect_language() {
1032 let mut map = HashMap::new();
1033 map.insert("rs".to_string(), "rust".to_string());
1034 map.insert("py".to_string(), "python".to_string());
1035 map.insert("ts".to_string(), "typescript".to_string());
1036
1037 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
1038 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
1039 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
1040 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
1041 }
1042
1043 #[tokio::test]
1044 async fn test_document_tracker() {
1045 let mut map = HashMap::new();
1046 map.insert("rs".to_string(), "rust".to_string());
1047
1048 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1049 let path = PathBuf::from("/test/file.rs");
1050
1051 assert!(!tracker.is_open(&path));
1052
1053 tracker
1054 .open(path.clone(), "fn main() {}".to_string())
1055 .unwrap();
1056 assert!(tracker.is_open(&path));
1057 assert_eq!(tracker.len(), 1);
1058
1059 let state = tracker.get(&path).unwrap();
1060 assert_eq!(state.version(), 1);
1061 assert_eq!(state.language_id(), "rust");
1062
1063 let new_version = tracker
1064 .update(&path, "fn main() { println!() }".to_string())
1065 .await;
1066 assert_eq!(new_version, Some(2));
1067
1068 tracker.close(&path);
1069 assert!(!tracker.is_open(&path));
1070 assert!(tracker.is_empty());
1071 }
1072
1073 #[test]
1079 fn test_forget_server_clears_only_that_servers_synced_version() {
1080 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1081 let path = PathBuf::from("/test/file.rs");
1082 tracker
1083 .open(path.clone(), "fn main() {}".to_string())
1084 .unwrap();
1085
1086 let respawned = ServerId::from("rust-respawned");
1087 let untouched = ServerId::from("rust-diagnostics");
1088 lock_std(&tracker.documents)
1089 .get_mut(&path)
1090 .unwrap()
1091 .synced
1092 .insert(respawned.clone(), 1);
1093 lock_std(&tracker.documents)
1094 .get_mut(&path)
1095 .unwrap()
1096 .synced
1097 .insert(untouched.clone(), 1);
1098
1099 tracker.forget_server(&respawned);
1100
1101 let state = tracker.get(&path).unwrap();
1102 assert!(state.synced_version(&respawned).is_none());
1103 assert!(state.synced_version(&untouched).is_some());
1104 }
1105
1106 #[tokio::test]
1117 async fn test_sync_phase_skips_commit_when_generation_is_stale() {
1118 let dir = TempDir::new().unwrap();
1119 let path = dir.path().join("race.rs");
1120 std::fs::write(&path, "fn main() {}").unwrap();
1121 set_mtime(&path, settled_past());
1122
1123 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1124 let server = ServerId::from("rust");
1125 let generation_before_respawn = 0; tracker.forget_server(&server);
1130
1131 let (stale_client, _guard) = fake_lsp_client();
1132 let decision = tracker.disk_phase(&path).await.unwrap();
1133 tracker
1134 .sync_phase(
1135 &path,
1136 &server,
1137 &stale_client,
1138 decision,
1139 generation_before_respawn,
1140 )
1141 .await
1142 .unwrap();
1143
1144 let state = tracker.get(&path).unwrap();
1145 assert!(
1146 state.synced_version(&server).is_none(),
1147 "a sync_phase call that captured a stale generation must not \
1148 commit `synced`, even though its notify against the \
1149 superseded connection succeeded"
1150 );
1151 }
1152
1153 #[tokio::test]
1158 async fn test_ensure_open_commits_when_generation_is_current() {
1159 let dir = TempDir::new().unwrap();
1160 let path = dir.path().join("no_race.rs");
1161 std::fs::write(&path, "fn main() {}").unwrap();
1162
1163 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1164 let server = ServerId::from("rust");
1165 let (client, _guard) = fake_lsp_client();
1166
1167 tracker.ensure_open(&path, &server, &client).await.unwrap();
1168
1169 let state = tracker.get(&path).unwrap();
1170 assert_eq!(state.synced_version(&server), Some(1));
1171 }
1172
1173 #[test]
1174 fn test_document_limit() {
1175 let limits = ResourceLimits {
1176 max_documents: 2,
1177 max_file_size: 100,
1178 };
1179 let mut map = HashMap::new();
1180 map.insert("rs".to_string(), "rust".to_string());
1181
1182 let tracker = DocumentTracker::new(limits, map);
1183
1184 tracker
1186 .open(PathBuf::from("/test/file1.rs"), "fn test1() {}".to_string())
1187 .unwrap();
1188 tracker
1189 .open(PathBuf::from("/test/file2.rs"), "fn test2() {}".to_string())
1190 .unwrap();
1191
1192 let result = tracker.open(PathBuf::from("/test/file3.rs"), "fn test3() {}".to_string());
1194 assert!(matches!(result, Err(Error::DocumentLimitExceeded { .. })));
1195 }
1196
1197 #[test]
1198 fn test_file_size_limit() {
1199 let limits = ResourceLimits {
1200 max_documents: 10,
1201 max_file_size: 10,
1202 };
1203 let mut map = HashMap::new();
1204 map.insert("rs".to_string(), "rust".to_string());
1205
1206 let tracker = DocumentTracker::new(limits, map);
1207
1208 tracker
1210 .open(PathBuf::from("/test/small.rs"), "fn f(){}".to_string())
1211 .unwrap();
1212
1213 let large_content = "x".repeat(100);
1215 let result = tracker.open(PathBuf::from("/test/large.rs"), large_content);
1216 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
1217 }
1218
1219 #[test]
1220 fn test_resource_limits_default() {
1221 let limits = ResourceLimits::default();
1222 assert_eq!(limits.max_documents, 100);
1223 assert_eq!(limits.max_file_size, 10 * 1024 * 1024);
1224 }
1225
1226 #[test]
1227 fn test_resource_limits_custom() {
1228 let limits = ResourceLimits {
1229 max_documents: 50,
1230 max_file_size: 5 * 1024 * 1024,
1231 };
1232 assert_eq!(limits.max_documents, 50);
1233 assert_eq!(limits.max_file_size, 5 * 1024 * 1024);
1234 }
1235
1236 #[test]
1237 fn test_resource_limits_zero_unlimited() {
1238 let limits = ResourceLimits {
1239 max_documents: 0,
1240 max_file_size: 0,
1241 };
1242 let mut map = HashMap::new();
1243 map.insert("rs".to_string(), "rust".to_string());
1244
1245 let tracker = DocumentTracker::new(limits, map);
1246
1247 for i in 0..200 {
1249 tracker
1250 .open(
1251 PathBuf::from(format!("/test/file{i}.rs")),
1252 "content".to_string(),
1253 )
1254 .unwrap();
1255 }
1256 assert_eq!(tracker.len(), 200);
1257
1258 let huge_content = "x".repeat(100_000_000);
1260 tracker
1261 .open(PathBuf::from("/test/huge.rs"), huge_content)
1262 .unwrap();
1263 }
1264
1265 #[test]
1266 fn test_document_state_clone() {
1267 let state = DocumentState {
1268 uri: Uri::from("file:///test.rs"),
1269 language_id: "rust".to_string(),
1270 version: 5,
1271 content: "fn main() {}".to_string(),
1272 disk: None,
1273 synced: HashMap::new(),
1274 };
1275
1276 #[allow(clippy::redundant_clone)]
1277 let cloned = state.clone();
1278 assert_eq!(cloned.uri(), state.uri());
1279 assert_eq!(cloned.language_id(), state.language_id());
1280 assert_eq!(cloned.version(), 5);
1281 assert_eq!(cloned.content(), state.content());
1282 }
1283
1284 #[tokio::test]
1285 async fn test_update_nonexistent_document() {
1286 let map = HashMap::new();
1287 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1288 let path = PathBuf::from("/test/nonexistent.rs");
1289
1290 let version = tracker.update(&path, "new content".to_string()).await;
1291 assert_eq!(
1292 version, None,
1293 "Updating non-existent document should return None"
1294 );
1295 }
1296
1297 #[test]
1298 fn test_close_nonexistent_document() {
1299 let map = HashMap::new();
1300 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1301 let path = PathBuf::from("/test/nonexistent.rs");
1302
1303 let state = tracker.close(&path);
1304 assert_eq!(
1305 state, None,
1306 "Closing non-existent document should return None"
1307 );
1308 }
1309
1310 #[test]
1311 fn test_close_all_documents() {
1312 let mut map = HashMap::new();
1313 map.insert("rs".to_string(), "rust".to_string());
1314
1315 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1316
1317 tracker
1318 .open(PathBuf::from("/test/file1.rs"), "content1".to_string())
1319 .unwrap();
1320 tracker
1321 .open(PathBuf::from("/test/file2.rs"), "content2".to_string())
1322 .unwrap();
1323 tracker
1324 .open(PathBuf::from("/test/file3.rs"), "content3".to_string())
1325 .unwrap();
1326
1327 assert_eq!(tracker.len(), 3);
1328
1329 let closed = tracker.close_all();
1330 assert_eq!(closed.len(), 3);
1331 assert!(tracker.is_empty());
1332 }
1333
1334 #[test]
1335 fn test_get_nonexistent_document() {
1336 let map = HashMap::new();
1337 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1338 let path = PathBuf::from("/test/nonexistent.rs");
1339
1340 let state = tracker.get(&path);
1341 assert!(
1342 state.is_none(),
1343 "Getting non-existent document should return None"
1344 );
1345 }
1346
1347 #[tokio::test]
1348 async fn test_document_version_increments() {
1349 let mut map = HashMap::new();
1350 map.insert("rs".to_string(), "rust".to_string());
1351
1352 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1353 let path = PathBuf::from("/test/versioned.rs");
1354
1355 tracker.open(path.clone(), "v1".to_string()).unwrap();
1356 assert_eq!(tracker.get(&path).unwrap().version(), 1);
1357
1358 tracker.update(&path, "v2".to_string()).await;
1359 assert_eq!(tracker.get(&path).unwrap().version(), 2);
1360
1361 tracker.update(&path, "v3".to_string()).await;
1362 assert_eq!(tracker.get(&path).unwrap().version(), 3);
1363
1364 tracker.update(&path, "v4".to_string()).await;
1365 assert_eq!(tracker.get(&path).unwrap().version(), 4);
1366 }
1367
1368 #[test]
1369 #[allow(clippy::too_many_lines)]
1370 fn test_detect_language_all_extensions() {
1371 let mut map = HashMap::new();
1372 map.insert("rs".to_string(), "rust".to_string());
1373 map.insert("py".to_string(), "python".to_string());
1374 map.insert("pyw".to_string(), "python".to_string());
1375 map.insert("pyi".to_string(), "python".to_string());
1376 map.insert("js".to_string(), "javascript".to_string());
1377 map.insert("mjs".to_string(), "javascript".to_string());
1378 map.insert("cjs".to_string(), "javascript".to_string());
1379 map.insert("ts".to_string(), "typescript".to_string());
1380 map.insert("mts".to_string(), "typescript".to_string());
1381 map.insert("cts".to_string(), "typescript".to_string());
1382 map.insert("tsx".to_string(), "typescriptreact".to_string());
1383 map.insert("jsx".to_string(), "javascriptreact".to_string());
1384 map.insert("go".to_string(), "go".to_string());
1385 map.insert("c".to_string(), "c".to_string());
1386 map.insert("h".to_string(), "c".to_string());
1387 map.insert("cpp".to_string(), "cpp".to_string());
1388 map.insert("cc".to_string(), "cpp".to_string());
1389 map.insert("cxx".to_string(), "cpp".to_string());
1390 map.insert("hpp".to_string(), "cpp".to_string());
1391 map.insert("hh".to_string(), "cpp".to_string());
1392 map.insert("hxx".to_string(), "cpp".to_string());
1393 map.insert("java".to_string(), "java".to_string());
1394 map.insert("rb".to_string(), "ruby".to_string());
1395 map.insert("php".to_string(), "php".to_string());
1396 map.insert("swift".to_string(), "swift".to_string());
1397 map.insert("kt".to_string(), "kotlin".to_string());
1398 map.insert("kts".to_string(), "kotlin".to_string());
1399 map.insert("scala".to_string(), "scala".to_string());
1400 map.insert("sc".to_string(), "scala".to_string());
1401 map.insert("zig".to_string(), "zig".to_string());
1402 map.insert("lua".to_string(), "lua".to_string());
1403 map.insert("sh".to_string(), "shellscript".to_string());
1404 map.insert("bash".to_string(), "shellscript".to_string());
1405 map.insert("zsh".to_string(), "shellscript".to_string());
1406 map.insert("json".to_string(), "json".to_string());
1407 map.insert("toml".to_string(), "toml".to_string());
1408 map.insert("yaml".to_string(), "yaml".to_string());
1409 map.insert("yml".to_string(), "yaml".to_string());
1410 map.insert("xml".to_string(), "xml".to_string());
1411 map.insert("html".to_string(), "html".to_string());
1412 map.insert("htm".to_string(), "html".to_string());
1413 map.insert("css".to_string(), "css".to_string());
1414 map.insert("scss".to_string(), "scss".to_string());
1415 map.insert("less".to_string(), "less".to_string());
1416 map.insert("md".to_string(), "markdown".to_string());
1417 map.insert("markdown".to_string(), "markdown".to_string());
1418
1419 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
1420 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
1421 assert_eq!(detect_language(Path::new("script.pyw"), &map), "python");
1422 assert_eq!(detect_language(Path::new("script.pyi"), &map), "python");
1423 assert_eq!(detect_language(Path::new("app.js"), &map), "javascript");
1424 assert_eq!(detect_language(Path::new("app.mjs"), &map), "javascript");
1425 assert_eq!(detect_language(Path::new("app.cjs"), &map), "javascript");
1426 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
1427 assert_eq!(detect_language(Path::new("app.mts"), &map), "typescript");
1428 assert_eq!(detect_language(Path::new("app.cts"), &map), "typescript");
1429 assert_eq!(
1430 detect_language(Path::new("component.tsx"), &map),
1431 "typescriptreact"
1432 );
1433 assert_eq!(
1434 detect_language(Path::new("component.jsx"), &map),
1435 "javascriptreact"
1436 );
1437 assert_eq!(detect_language(Path::new("main.go"), &map), "go");
1438 assert_eq!(detect_language(Path::new("main.c"), &map), "c");
1439 assert_eq!(detect_language(Path::new("header.h"), &map), "c");
1440 assert_eq!(detect_language(Path::new("main.cpp"), &map), "cpp");
1441 assert_eq!(detect_language(Path::new("main.cc"), &map), "cpp");
1442 assert_eq!(detect_language(Path::new("main.cxx"), &map), "cpp");
1443 assert_eq!(detect_language(Path::new("header.hpp"), &map), "cpp");
1444 assert_eq!(detect_language(Path::new("header.hh"), &map), "cpp");
1445 assert_eq!(detect_language(Path::new("header.hxx"), &map), "cpp");
1446 assert_eq!(detect_language(Path::new("Main.java"), &map), "java");
1447 assert_eq!(detect_language(Path::new("script.rb"), &map), "ruby");
1448 assert_eq!(detect_language(Path::new("index.php"), &map), "php");
1449 assert_eq!(detect_language(Path::new("App.swift"), &map), "swift");
1450 assert_eq!(detect_language(Path::new("Main.kt"), &map), "kotlin");
1451 assert_eq!(detect_language(Path::new("script.kts"), &map), "kotlin");
1452 assert_eq!(detect_language(Path::new("Main.scala"), &map), "scala");
1453 assert_eq!(detect_language(Path::new("script.sc"), &map), "scala");
1454 assert_eq!(detect_language(Path::new("main.zig"), &map), "zig");
1455 assert_eq!(detect_language(Path::new("script.lua"), &map), "lua");
1456 assert_eq!(detect_language(Path::new("script.sh"), &map), "shellscript");
1457 assert_eq!(
1458 detect_language(Path::new("script.bash"), &map),
1459 "shellscript"
1460 );
1461 assert_eq!(
1462 detect_language(Path::new("script.zsh"), &map),
1463 "shellscript"
1464 );
1465 assert_eq!(detect_language(Path::new("data.json"), &map), "json");
1466 assert_eq!(detect_language(Path::new("config.toml"), &map), "toml");
1467 assert_eq!(detect_language(Path::new("config.yaml"), &map), "yaml");
1468 assert_eq!(detect_language(Path::new("config.yml"), &map), "yaml");
1469 assert_eq!(detect_language(Path::new("data.xml"), &map), "xml");
1470 assert_eq!(detect_language(Path::new("index.html"), &map), "html");
1471 assert_eq!(detect_language(Path::new("index.htm"), &map), "html");
1472 assert_eq!(detect_language(Path::new("styles.css"), &map), "css");
1473 assert_eq!(detect_language(Path::new("styles.scss"), &map), "scss");
1474 assert_eq!(detect_language(Path::new("styles.less"), &map), "less");
1475 assert_eq!(detect_language(Path::new("README.md"), &map), "markdown");
1476 assert_eq!(
1477 detect_language(Path::new("README.markdown"), &map),
1478 "markdown"
1479 );
1480 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
1481 assert_eq!(
1482 detect_language(Path::new("no_extension"), &map),
1483 "plaintext"
1484 );
1485 }
1486
1487 #[test]
1488 fn test_path_to_uri_unix() {
1489 #[cfg(not(windows))]
1490 {
1491 let path = Path::new("/home/user/project/main.rs");
1492 let uri = path_to_uri(path).unwrap();
1493 assert!(
1494 uri.as_ref()
1495 .starts_with("file:///home/user/project/main.rs")
1496 );
1497 }
1498 }
1499
1500 #[test]
1501 fn test_path_to_uri_with_special_chars() {
1502 let path = Path::new("/home/user/project-test/main.rs");
1503 let uri = path_to_uri(path).unwrap();
1504 assert!(uri.as_ref().starts_with("file://"));
1505 assert!(uri.as_ref().contains("project-test"));
1506 }
1507
1508 #[test]
1509 fn test_path_to_uri_percent_encodes_reserved_chars() {
1510 #[cfg(windows)]
1511 let path = Path::new(r"C:\home\user\routes\api\[...]^|.ts");
1512 #[cfg(not(windows))]
1513 let path = Path::new("/home/user/routes/api/[...]^|.ts");
1514
1515 let uri = path_to_uri(path).unwrap();
1516
1517 #[cfg(windows)]
1518 let expected = "file:///C:/home/user/routes/api/%5B...%5D%5E%7C.ts";
1519 #[cfg(not(windows))]
1520 let expected = "file:///home/user/routes/api/%5B...%5D%5E%7C.ts";
1521
1522 assert_eq!(uri.as_ref(), expected);
1523 assert_eq!(
1524 uri_to_path(&uri).as_deref(),
1525 Some(path),
1526 "encoded file URI should round-trip to the original path"
1527 );
1528 }
1529
1530 #[test]
1531 fn test_try_path_to_uri_returns_none_for_relative_path() {
1532 assert_eq!(try_path_to_uri(Path::new("relative/file.ts")), None);
1533 }
1534
1535 #[test]
1539 fn test_path_to_uri_returns_err_for_relative_path() {
1540 let err = path_to_uri(Path::new("relative/file.ts")).unwrap_err();
1541 assert!(matches!(err, Error::InvalidUri(_)));
1542 }
1543
1544 #[cfg(windows)]
1545 #[test]
1546 fn test_try_path_to_uri_encodes_synthetic_windows_root() {
1547 let uri = try_path_to_uri(Path::new("/home/user/#work %23")).unwrap();
1548
1549 assert_eq!(uri.as_ref(), "file:///home/user/%23work%20%2523");
1550 }
1551
1552 #[cfg(windows)]
1560 #[test]
1561 fn test_try_path_to_uri_accepts_rooted_but_not_absolute_windows_path() {
1562 let path = Path::new(r"\foo");
1563 assert!(path.has_root());
1564 assert!(!path.is_absolute());
1565
1566 let uri = try_path_to_uri(path).unwrap();
1567
1568 assert_eq!(uri.as_ref(), "file:///foo");
1569 }
1570
1571 #[test]
1572 fn test_path_to_uri_percent_encodes_reserved_chars_in_short_path() {
1573 #[cfg(windows)]
1575 let path = Path::new(r"C:\[a].ts");
1576 #[cfg(not(windows))]
1577 let path = Path::new("/[a].ts");
1578
1579 let uri = path_to_uri(path).unwrap();
1580
1581 assert!(
1582 uri.as_ref().ends_with("%5Ba%5D.ts"),
1583 "short path should percent-encode reserved chars, got {}",
1584 uri.as_ref()
1585 );
1586 assert_eq!(uri_to_path(&uri).as_deref(), Some(path));
1587 }
1588
1589 #[test]
1590 fn test_path_to_uri_percent_encodes_all_rfc3986_other_reserved_chars() {
1591 #[cfg(windows)]
1595 let path = Path::new(r"C:\home\user\test[]^|{}`.ts");
1596 #[cfg(not(windows))]
1597 let path = Path::new("/home/user/test[]^|{}`.ts");
1598
1599 let uri = try_path_to_uri(path).unwrap();
1600 let uri_str = uri.as_ref();
1601
1602 for (raw, encoded) in [
1603 ('[', "%5B"),
1604 (']', "%5D"),
1605 ('^', "%5E"),
1606 ('|', "%7C"),
1607 ('{', "%7B"),
1608 ('}', "%7D"),
1609 ('`', "%60"),
1610 ] {
1611 assert!(
1612 uri_str.contains(encoded),
1613 "expected {raw:?} to be percent-encoded as {encoded} in {uri_str}"
1614 );
1615 }
1616 assert!(
1617 !uri_str.contains(['[', ']', '^', '|', '{', '}', '`']),
1618 "no raw reserved characters should remain in {uri_str}"
1619 );
1620 }
1621
1622 #[tokio::test]
1623 async fn test_document_tracker_concurrent_operations() {
1624 let mut map = HashMap::new();
1625 map.insert("rs".to_string(), "rust".to_string());
1626
1627 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1628 let path1 = PathBuf::from("/test/file1.rs");
1629 let path2 = PathBuf::from("/test/file2.rs");
1630
1631 tracker.open(path1.clone(), "content1".to_string()).unwrap();
1632 tracker.open(path2.clone(), "content2".to_string()).unwrap();
1633
1634 assert_eq!(tracker.len(), 2);
1635 assert!(tracker.is_open(&path1));
1636 assert!(tracker.is_open(&path2));
1637
1638 tracker.update(&path1, "new content1".to_string()).await;
1639 assert_eq!(tracker.get(&path1).unwrap().content(), "new content1");
1640 assert_eq!(tracker.get(&path2).unwrap().content(), "content2");
1641
1642 tracker.close(&path1);
1643 assert_eq!(tracker.len(), 1);
1644 assert!(!tracker.is_open(&path1));
1645 assert!(tracker.is_open(&path2));
1646 }
1647
1648 #[test]
1649 fn test_empty_content() {
1650 let mut map = HashMap::new();
1651 map.insert("rs".to_string(), "rust".to_string());
1652
1653 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1654 let path = PathBuf::from("/test/empty.rs");
1655
1656 tracker.open(path.clone(), String::new()).unwrap();
1657 assert!(tracker.is_open(&path));
1658 assert_eq!(tracker.get(&path).unwrap().content(), "");
1659 }
1660
1661 #[test]
1662 fn test_unicode_content() {
1663 let mut map = HashMap::new();
1664 map.insert("rs".to_string(), "rust".to_string());
1665
1666 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1667 let path = PathBuf::from("/test/unicode.rs");
1668 let content = "fn テスト() { println!(\"こんにちは\"); }";
1669
1670 tracker.open(path.clone(), content.to_string()).unwrap();
1671 assert_eq!(tracker.get(&path).unwrap().content(), content);
1672 }
1673
1674 #[test]
1675 fn test_document_limit_exact_boundary() {
1676 let limits = ResourceLimits {
1677 max_documents: 5,
1678 max_file_size: 1000,
1679 };
1680 let mut map = HashMap::new();
1681 map.insert("rs".to_string(), "rust".to_string());
1682
1683 let tracker = DocumentTracker::new(limits, map);
1684
1685 for i in 0..5 {
1686 tracker
1687 .open(
1688 PathBuf::from(format!("/test/file{i}.rs")),
1689 "content".to_string(),
1690 )
1691 .unwrap();
1692 }
1693
1694 assert_eq!(tracker.len(), 5);
1695
1696 let result = tracker.open(PathBuf::from("/test/file6.rs"), "content".to_string());
1697 assert!(matches!(result, Err(Error::DocumentLimitExceeded { .. })));
1698 }
1699
1700 #[test]
1701 fn test_file_size_exact_boundary() {
1702 let limits = ResourceLimits {
1703 max_documents: 10,
1704 max_file_size: 100,
1705 };
1706 let mut map = HashMap::new();
1707 map.insert("rs".to_string(), "rust".to_string());
1708
1709 let tracker = DocumentTracker::new(limits, map);
1710
1711 let exact_size_content = "x".repeat(100);
1712 tracker
1713 .open(PathBuf::from("/test/exact.rs"), exact_size_content)
1714 .unwrap();
1715
1716 let over_size_content = "x".repeat(101);
1717 let result = tracker.open(PathBuf::from("/test/over.rs"), over_size_content);
1718 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
1719 }
1720
1721 #[test]
1722 fn test_detect_language_with_custom_extension() {
1723 let mut map = HashMap::new();
1724 map.insert("nu".to_string(), "nushell".to_string());
1725
1726 assert_eq!(detect_language(Path::new("script.nu"), &map), "nushell");
1727
1728 let empty_map = HashMap::new();
1729 assert_eq!(
1730 detect_language(Path::new("script.nu"), &empty_map),
1731 "plaintext"
1732 );
1733 }
1734
1735 #[test]
1736 fn test_detect_language_custom_overrides_default() {
1737 let mut custom_map = HashMap::new();
1738 custom_map.insert("rs".to_string(), "custom-rust".to_string());
1739
1740 assert_eq!(
1741 detect_language(Path::new("main.rs"), &custom_map),
1742 "custom-rust"
1743 );
1744
1745 let mut default_map = HashMap::new();
1746 default_map.insert("rs".to_string(), "rust".to_string());
1747
1748 assert_eq!(detect_language(Path::new("main.rs"), &default_map), "rust");
1749 }
1750
1751 #[test]
1752 fn test_detect_language_fallback_to_plaintext() {
1753 let mut map = HashMap::new();
1754 map.insert("nu".to_string(), "nushell".to_string());
1755
1756 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
1758 }
1759
1760 #[test]
1761 fn test_detect_language_empty_map() {
1762 let map = HashMap::new();
1763 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
1764 }
1765
1766 #[test]
1767 fn test_document_tracker_with_extensions() {
1768 let mut map = HashMap::new();
1769 map.insert("nu".to_string(), "nushell".to_string());
1770
1771 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1772
1773 let path = PathBuf::from("/test/script.nu");
1774 tracker
1775 .open(path.clone(), "# nushell script".to_string())
1776 .unwrap();
1777
1778 let state = tracker.get(&path).unwrap();
1779 assert_eq!(state.language_id(), "nushell");
1780 }
1781
1782 #[test]
1783 fn test_document_tracker_uses_provided_map() {
1784 let mut map = HashMap::new();
1785 map.insert("rs".to_string(), "rust".to_string());
1786
1787 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1788 let path = PathBuf::from("/test/main.rs");
1789 tracker
1790 .open(path.clone(), "fn main() {}".to_string())
1791 .unwrap();
1792
1793 let state = tracker.get(&path).unwrap();
1794 assert_eq!(state.language_id(), "rust");
1795 }
1796
1797 #[test]
1798 fn test_multiple_extensions_same_language() {
1799 let mut map = HashMap::new();
1800 map.insert("cpp".to_string(), "c++".to_string());
1801 map.insert("cc".to_string(), "c++".to_string());
1802 map.insert("cxx".to_string(), "c++".to_string());
1803
1804 assert_eq!(detect_language(Path::new("main.cpp"), &map), "c++");
1805 assert_eq!(detect_language(Path::new("main.cc"), &map), "c++");
1806 assert_eq!(detect_language(Path::new("main.cxx"), &map), "c++");
1807 }
1808
1809 #[test]
1810 fn test_case_sensitive_extensions() {
1811 let mut map = HashMap::new();
1812 map.insert("NU".to_string(), "nushell".to_string());
1813
1814 assert_eq!(detect_language(Path::new("script.nu"), &map), "plaintext");
1816 }
1817
1818 #[cfg(unix)]
1823 #[test]
1824 fn test_uri_to_path_file_scheme() {
1825 let uri: Uri = Uri::from("file:///home/user/main.rs");
1826 let path = uri_to_path(&uri).unwrap();
1827 assert_eq!(path, PathBuf::from("/home/user/main.rs"));
1828 }
1829
1830 #[test]
1831 fn test_uri_to_path_non_file_scheme_returns_none() {
1832 let uri: Uri = Uri::from("https://example.com/file.rs");
1833 assert!(uri_to_path(&uri).is_none());
1834 }
1835
1836 #[test]
1837 fn test_uri_to_path_lsp_diagnostics_scheme_returns_none() {
1838 let uri: Uri = Uri::from("lsp-diagnostics:///home/user/main.rs");
1840 assert!(uri_to_path(&uri).is_none());
1841 }
1842
1843 #[test]
1844 fn test_uri_to_path_with_authority_returns_none() {
1845 let result = uri_to_path(&Uri::from("file://server/share/path.rs"));
1849 assert!(result.is_none());
1850 }
1851
1852 #[test]
1857 fn test_open_paths_empty_tracker() {
1858 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1859 assert_eq!(tracker.open_paths().len(), 0);
1860 }
1861
1862 #[test]
1863 fn test_open_paths_populated_tracker() {
1864 let mut map = HashMap::new();
1865 map.insert("rs".to_string(), "rust".to_string());
1866 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1867 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
1868 tracker.open(PathBuf::from("/b.rs"), String::new()).unwrap();
1869 let mut paths = tracker.open_paths();
1870 paths.sort();
1871 assert_eq!(paths, [PathBuf::from("/a.rs"), PathBuf::from("/b.rs")]);
1872 }
1873
1874 #[test]
1875 fn test_open_paths_after_close() {
1876 let mut map = HashMap::new();
1877 map.insert("rs".to_string(), "rust".to_string());
1878 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1879 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
1880 tracker.close(Path::new("/a.rs"));
1881 assert_eq!(tracker.open_paths().len(), 0);
1882 }
1883
1884 use tempfile::TempDir;
1889 use tokio::io::BufReader;
1890
1891 use crate::test_lsp::{fake_lsp_client, read_framed_message};
1892
1893 fn set_mtime(path: &Path, time: SystemTime) {
1901 let file = std::fs::OpenOptions::new().write(true).open(path).unwrap();
1902 file.set_modified(time).unwrap();
1903 }
1904
1905 fn settled_past() -> SystemTime {
1906 SystemTime::now() - Duration::from_secs(10)
1907 }
1908
1909 #[test]
1910 fn test_mtime_settled_boundary() {
1911 let read_at = SystemTime::now();
1912 assert!(!mtime_settled(None, read_at), "no mtime is never settled");
1913 assert!(
1914 mtime_settled(Some(read_at - Duration::from_secs(3)), read_at),
1915 "3s older than read_at is past the 2s granularity margin"
1916 );
1917 assert!(
1918 !mtime_settled(Some(read_at - Duration::from_secs(1)), read_at),
1919 "1s older than read_at is within the 2s granularity margin"
1920 );
1921 assert!(
1922 !mtime_settled(Some(read_at + Duration::from_secs(10)), read_at),
1923 "an mtime after read_at is never settled"
1924 );
1925 }
1926
1927 #[tokio::test]
1928 async fn test_ensure_open_unchanged_file_is_fast_path() {
1929 let dir = TempDir::new().unwrap();
1930 let path = dir.path().join("a.rs");
1931 std::fs::write(&path, "fn main() {}").unwrap();
1932 set_mtime(&path, settled_past());
1933
1934 let (client, _server) = fake_lsp_client();
1935 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1936
1937 let uri1 = tracker
1938 .ensure_open(&path, &ServerId::from("rust"), &client)
1939 .await
1940 .unwrap();
1941 assert_eq!(tracker.get(&path).unwrap().version(), 1);
1942
1943 let uri2 = tracker
1944 .ensure_open(&path, &ServerId::from("rust"), &client)
1945 .await
1946 .unwrap();
1947 assert_eq!(uri1, uri2);
1948 assert_eq!(tracker.get(&path).unwrap().version(), 1);
1949 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
1950 }
1951
1952 #[tokio::test]
1953 async fn test_ensure_open_resyncs_on_size_change() {
1954 let dir = TempDir::new().unwrap();
1955 let path = dir.path().join("a.rs");
1956 std::fs::write(&path, "fn main() {}").unwrap();
1957 set_mtime(&path, settled_past());
1958
1959 let (client, _server) = fake_lsp_client();
1960 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1961 tracker
1962 .ensure_open(&path, &ServerId::from("rust"), &client)
1963 .await
1964 .unwrap();
1965
1966 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
1967 set_mtime(&path, settled_past());
1968
1969 tracker
1970 .ensure_open(&path, &ServerId::from("rust"), &client)
1971 .await
1972 .unwrap();
1973 let state = tracker.get(&path).unwrap();
1974 assert_eq!(state.version(), 2);
1975 assert_eq!(state.content(), "fn main() { println!(\"hi\"); }");
1976 }
1977
1978 #[tokio::test(start_paused = true)]
1979 async fn test_ensure_open_regression_102_103_racy_same_size_rewrite() {
1980 let dir = TempDir::new().unwrap();
1981 let path = dir.path().join("a.rs");
1982 std::fs::write(&path, "AAAA").unwrap();
1983 let (client, _server) = fake_lsp_client();
1986 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1987 tracker
1988 .ensure_open(&path, &ServerId::from("rust"), &client)
1989 .await
1990 .unwrap();
1991 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
1992
1993 std::fs::write(&path, "BBBB").unwrap();
1996 set_mtime(&path, original_mtime);
1997
1998 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
1999
2000 tracker
2001 .ensure_open(&path, &ServerId::from("rust"), &client)
2002 .await
2003 .unwrap();
2004 let state = tracker.get(&path).unwrap();
2005 assert_eq!(
2006 state.version(),
2007 2,
2008 "must resync despite identical (mtime, size)"
2009 );
2010 assert_eq!(state.content(), "BBBB");
2011 }
2012
2013 #[tokio::test(start_paused = true)]
2014 async fn test_ensure_open_regression_102_103_settled_mtime_is_the_documented_limit() {
2015 let dir = TempDir::new().unwrap();
2016 let path = dir.path().join("a.rs");
2017 std::fs::write(&path, "AAAA").unwrap();
2018 set_mtime(&path, settled_past());
2019
2020 let (client, _server) = fake_lsp_client();
2021 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2022 tracker
2023 .ensure_open(&path, &ServerId::from("rust"), &client)
2024 .await
2025 .unwrap();
2026 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
2027
2028 std::fs::write(&path, "BBBB").unwrap();
2032 set_mtime(&path, original_mtime);
2033
2034 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
2035
2036 tracker
2037 .ensure_open(&path, &ServerId::from("rust"), &client)
2038 .await
2039 .unwrap();
2040 let state = tracker.get(&path).unwrap();
2041 assert_eq!(state.version(), 1, "documented limitation: fast path taken");
2042 assert_eq!(state.content(), "AAAA");
2043 }
2044
2045 #[tokio::test(start_paused = true)]
2046 async fn test_ensure_open_stat_is_never_debounced() {
2047 let dir = TempDir::new().unwrap();
2048 let path = dir.path().join("a.rs");
2049 std::fs::write(&path, "AAAA").unwrap();
2050 set_mtime(&path, settled_past());
2051
2052 let (client, _server) = fake_lsp_client();
2053 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2054 tracker
2055 .ensure_open(&path, &ServerId::from("rust"), &client)
2056 .await
2057 .unwrap();
2058
2059 std::fs::write(&path, "BBBBBBBB").unwrap();
2062 tracker
2063 .ensure_open(&path, &ServerId::from("rust"), &client)
2064 .await
2065 .unwrap();
2066
2067 let state = tracker.get(&path).unwrap();
2068 assert_eq!(state.version(), 2);
2069 assert_eq!(state.content(), "BBBBBBBB");
2070 }
2071
2072 #[tokio::test(start_paused = true)]
2073 async fn test_ensure_open_debounce_gates_reread_only() {
2074 let dir = TempDir::new().unwrap();
2075 let path = dir.path().join("a.rs");
2076 std::fs::write(&path, "AAAA").unwrap();
2077 let (client, _server) = fake_lsp_client();
2080 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2081 tracker
2082 .ensure_open(&path, &ServerId::from("rust"), &client)
2083 .await
2084 .unwrap();
2085 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
2086
2087 std::fs::write(&path, "BBBB").unwrap(); set_mtime(&path, original_mtime); tracker
2092 .ensure_open(&path, &ServerId::from("rust"), &client)
2093 .await
2094 .unwrap();
2095 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2096
2097 tokio::time::advance(Duration::from_millis(300)).await;
2098 tracker
2099 .ensure_open(&path, &ServerId::from("rust"), &client)
2100 .await
2101 .unwrap();
2102 let state = tracker.get(&path).unwrap();
2103 assert_eq!(state.version(), 2);
2104 assert_eq!(state.content(), "BBBB");
2105 }
2106
2107 #[tokio::test]
2108 async fn test_ensure_open_deleted_file_errors_state_untouched() {
2109 let dir = TempDir::new().unwrap();
2110 let path = dir.path().join("a.rs");
2111 std::fs::write(&path, "fn main() {}").unwrap();
2112 set_mtime(&path, settled_past());
2113
2114 let (client, _server) = fake_lsp_client();
2115 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2116 tracker
2117 .ensure_open(&path, &ServerId::from("rust"), &client)
2118 .await
2119 .unwrap();
2120
2121 std::fs::remove_file(&path).unwrap();
2122
2123 let result = tracker
2124 .ensure_open(&path, &ServerId::from("rust"), &client)
2125 .await;
2126 assert!(matches!(result, Err(Error::FileIo { .. })));
2127 assert!(tracker.is_open(&path));
2128 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2129 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
2130 }
2131
2132 #[tokio::test]
2133 async fn test_ensure_open_grows_past_limit_errors_state_intact() {
2134 let dir = TempDir::new().unwrap();
2135 let path = dir.path().join("a.rs");
2136 std::fs::write(&path, "small").unwrap();
2137 set_mtime(&path, settled_past());
2138
2139 let limits = ResourceLimits {
2140 max_documents: 10,
2141 max_file_size: 10,
2142 };
2143 let (client, _server) = fake_lsp_client();
2144 let tracker = DocumentTracker::new(limits, HashMap::new());
2145 tracker
2146 .ensure_open(&path, &ServerId::from("rust"), &client)
2147 .await
2148 .unwrap();
2149
2150 std::fs::write(&path, "x".repeat(100)).unwrap();
2151
2152 let result = tracker
2153 .ensure_open(&path, &ServerId::from("rust"), &client)
2154 .await;
2155 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
2156 assert_eq!(tracker.get(&path).unwrap().content(), "small");
2157 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2158 }
2159
2160 #[tokio::test]
2161 async fn test_ensure_open_resync_at_document_capacity() {
2162 let dir = TempDir::new().unwrap();
2163 let path = dir.path().join("a.rs");
2164 std::fs::write(&path, "AAAA").unwrap();
2165 set_mtime(&path, settled_past());
2166
2167 let limits = ResourceLimits {
2168 max_documents: 1,
2169 max_file_size: 0,
2170 };
2171 let (client, _server) = fake_lsp_client();
2172 let tracker = DocumentTracker::new(limits, HashMap::new());
2173 tracker
2174 .ensure_open(&path, &ServerId::from("rust"), &client)
2175 .await
2176 .unwrap();
2177 assert_eq!(tracker.len(), 1);
2178
2179 std::fs::write(&path, "BBBBBBBB").unwrap();
2180 let result = tracker
2181 .ensure_open(&path, &ServerId::from("rust"), &client)
2182 .await;
2183 assert!(
2184 result.is_ok(),
2185 "resync must not re-run the doc-count check on an already-tracked path"
2186 );
2187 assert_eq!(tracker.len(), 1);
2188 assert_eq!(tracker.get(&path).unwrap().version(), 2);
2189 }
2190
2191 #[tokio::test]
2192 async fn test_update_clears_disk_provenance() {
2193 let dir = TempDir::new().unwrap();
2194 let path = dir.path().join("a.rs");
2195 std::fs::write(&path, "fn main() {}").unwrap();
2196 set_mtime(&path, settled_past());
2197
2198 let (client, _server) = fake_lsp_client();
2199 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2200 tracker
2201 .ensure_open(&path, &ServerId::from("rust"), &client)
2202 .await
2203 .unwrap();
2204 assert!(tracker.get(&path).unwrap().disk.is_some());
2205
2206 tracker
2207 .update(&path, "fn main() { updated(); }".to_string())
2208 .await;
2209 assert!(
2210 tracker.get(&path).unwrap().disk.is_none(),
2211 "update() must clear disk provenance so the next ensure_open re-verifies by content"
2212 );
2213 }
2214
2215 #[tokio::test]
2216 async fn test_first_open_self_heals_when_did_open_notify_fails() {
2217 let dir = TempDir::new().unwrap();
2218 let path = dir.path().join("a.rs");
2219 std::fs::write(&path, "fn main() {}").unwrap();
2220
2221 let (client, _server) = fake_lsp_client();
2222 let notify_will_fail = client.clone();
2228 client.shutdown().await.unwrap();
2229
2230 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2231 let result = tracker
2232 .ensure_open(&path, &ServerId::from("rust"), ¬ify_will_fail)
2233 .await;
2234
2235 assert!(result.is_err(), "notify failure must propagate as an error");
2236 assert!(
2237 !tracker.is_open(&path),
2238 "a failed didOpen must not leave the document tracked, or the server \
2239 and tracker would stay permanently desynced"
2240 );
2241 }
2242
2243 #[tokio::test]
2244 async fn test_resync_sends_didchange_with_full_replacement_over_the_wire() {
2245 let dir = TempDir::new().unwrap();
2246 let path = dir.path().join("a.rs");
2247 std::fs::write(&path, "fn main() {}").unwrap();
2248 set_mtime(&path, settled_past());
2249
2250 let (client, mut server) = fake_lsp_client();
2251 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2252 tracker
2253 .ensure_open(&path, &ServerId::from("rust"), &client)
2254 .await
2255 .unwrap();
2256
2257 let mut wire = BufReader::new(&mut server.write_stdout);
2258 let opened = read_framed_message(&mut wire).await;
2259 assert_eq!(opened["method"], "textDocument/didOpen");
2260
2261 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
2262 set_mtime(&path, settled_past());
2263 tracker
2264 .ensure_open(&path, &ServerId::from("rust"), &client)
2265 .await
2266 .unwrap();
2267
2268 let changed = read_framed_message(&mut wire).await;
2269 assert_eq!(changed["method"], "textDocument/didChange");
2270 let params = &changed["params"];
2271 assert_eq!(params["textDocument"]["version"], 2);
2272 let change = ¶ms["contentChanges"][0];
2273 assert!(
2274 change.get("range").is_none(),
2275 "range must be omitted, not null, for a full-replacement change"
2276 );
2277 assert!(
2278 change.get("rangeLength").is_none(),
2279 "rangeLength must be omitted, not null, for a full-replacement change"
2280 );
2281 assert_eq!(change["text"], "fn main() { println!(\"hi\"); }");
2282 }
2283
2284 #[tokio::test]
2289 async fn test_ensure_open_second_server_gets_didopen_no_disk_change() {
2290 let dir = TempDir::new().unwrap();
2291 let path = dir.path().join("a.rs");
2292 std::fs::write(&path, "fn main() {}").unwrap();
2293 set_mtime(&path, settled_past());
2294
2295 let (client_a, mut server_a) = fake_lsp_client();
2296 let (client_b, mut server_b) = fake_lsp_client();
2297 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2298
2299 let id_a = ServerId::from("server-a");
2300 let id_b = ServerId::from("server-b");
2301
2302 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
2303 let mut wire_a = BufReader::new(&mut server_a.write_stdout);
2304 let opened_a = read_framed_message(&mut wire_a).await;
2305 assert_eq!(opened_a["method"], "textDocument/didOpen");
2306
2307 tracker.ensure_open(&path, &id_b, &client_b).await.unwrap();
2311 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
2312 let opened_b = read_framed_message(&mut wire_b).await;
2313 assert_eq!(opened_b["method"], "textDocument/didOpen");
2314 assert_eq!(opened_b["params"]["textDocument"]["version"], 1);
2315 assert_eq!(opened_b["params"]["textDocument"]["text"], "fn main() {}");
2316 }
2317
2318 #[tokio::test(start_paused = true)]
2322 async fn test_ensure_open_second_server_gets_didopen_unchanged_content_path() {
2323 let dir = TempDir::new().unwrap();
2324 let path = dir.path().join("a.rs");
2325 std::fs::write(&path, "fn main() {}").unwrap();
2326 let (client_a, _server_a) = fake_lsp_client();
2329 let (client_b, mut server_b) = fake_lsp_client();
2330 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2331
2332 tracker
2333 .ensure_open(&path, &ServerId::from("server-a"), &client_a)
2334 .await
2335 .unwrap();
2336
2337 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
2340
2341 tracker
2342 .ensure_open(&path, &ServerId::from("server-b"), &client_b)
2343 .await
2344 .unwrap();
2345 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
2346 let opened_b = read_framed_message(&mut wire_b).await;
2347 assert_eq!(opened_b["method"], "textDocument/didOpen");
2348 }
2349
2350 #[tokio::test]
2357 async fn test_ensure_open_same_server_twice_sends_nothing_second_time() {
2358 let dir = TempDir::new().unwrap();
2359 let path = dir.path().join("a.rs");
2360 std::fs::write(&path, "fn main() {}").unwrap();
2361 set_mtime(&path, settled_past());
2362
2363 let (client, mut server) = fake_lsp_client();
2364 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2365 let id = ServerId::from("rust");
2366
2367 tracker.ensure_open(&path, &id, &client).await.unwrap();
2368 tracker.ensure_open(&path, &id, &client).await.unwrap();
2369
2370 let mut wire = BufReader::new(&mut server.write_stdout);
2371 let opened = read_framed_message(&mut wire).await;
2372 assert_eq!(opened["method"], "textDocument/didOpen");
2373 assert_eq!(
2374 tracker.get(&path).unwrap().synced_version(&id),
2375 Some(1),
2376 "second call for the same server must not re-open or re-change"
2377 );
2378 }
2379
2380 #[tokio::test]
2384 async fn test_sync_phase_failed_didchange_does_not_disturb_other_server() {
2385 let dir = TempDir::new().unwrap();
2386 let path = dir.path().join("a.rs");
2387 std::fs::write(&path, "fn main() {}").unwrap();
2388 set_mtime(&path, settled_past());
2389
2390 let (client_a, _server_a) = fake_lsp_client();
2391 let (client_b, _server_b) = fake_lsp_client();
2392 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2393 let id_a = ServerId::from("server-a");
2394 let id_b = ServerId::from("server-b");
2395
2396 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
2397 tracker.ensure_open(&path, &id_b, &client_b).await.unwrap();
2398
2399 let client_b_will_fail = client_b.clone();
2402 client_b.shutdown().await.unwrap();
2403
2404 std::fs::write(&path, "fn main() { updated(); }").unwrap();
2405 set_mtime(&path, settled_past());
2406
2407 let result = tracker.ensure_open(&path, &id_b, &client_b_will_fail).await;
2408 assert!(result.is_err(), "B's didChange must fail and propagate");
2409
2410 assert!(tracker.is_open(&path));
2416 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
2417 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2418 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_a), Some(1));
2419 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_b), Some(1));
2420
2421 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
2425 assert_eq!(
2426 tracker.get(&path).unwrap().content(),
2427 "fn main() { updated(); }"
2428 );
2429 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_a), Some(2));
2430 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_b), Some(1));
2431 }
2432
2433 #[cfg(unix)]
2449 #[tokio::test]
2450 async fn test_ensure_open_different_paths_do_not_serialize() {
2451 let dir = TempDir::new().unwrap();
2452 let path_a = dir.path().join("a.rs");
2453 let path_b = dir.path().join("b.rs");
2454
2455 std::fs::write(&path_b, "fn b() {}").unwrap();
2456 set_mtime(&path_b, settled_past());
2457
2458 let status = std::process::Command::new("mkfifo")
2459 .arg(&path_a)
2460 .status()
2461 .unwrap();
2462 assert!(status.success(), "mkfifo must succeed to set up this test");
2463
2464 let (client_a, _server_a) = fake_lsp_client();
2465 let (client_b, _server_b) = fake_lsp_client();
2466 let tracker = Arc::new(DocumentTracker::new(
2467 ResourceLimits::default(),
2468 HashMap::new(),
2469 ));
2470
2471 let tracker_for_a = Arc::clone(&tracker);
2474 let path_a_for_task = path_a.clone();
2475 let handle_a = tokio::spawn(async move {
2476 tracker_for_a
2477 .ensure_open(&path_a_for_task, &ServerId::from("server-a"), &client_a)
2478 .await
2479 });
2480
2481 tokio::time::sleep(Duration::from_millis(200)).await;
2484
2485 tokio::time::timeout(
2488 Duration::from_secs(5),
2489 tracker.ensure_open(&path_b, &ServerId::from("server-b"), &client_b),
2490 )
2491 .await
2492 .unwrap()
2493 .unwrap();
2494
2495 let path_a_writer = path_a.clone();
2499 tokio::task::spawn_blocking(move || {
2500 std::fs::write(path_a_writer, "fn a() {}").unwrap();
2501 })
2502 .await
2503 .unwrap();
2504
2505 handle_a.await.unwrap().unwrap();
2506 assert_eq!(tracker.get(&path_a).unwrap().content(), "fn a() {}");
2507 }
2508
2509 #[cfg(unix)]
2522 #[tokio::test]
2523 async fn test_update_serializes_with_concurrent_ensure_open_same_path() {
2524 let dir = TempDir::new().unwrap();
2525 let path = dir.path().join("a.rs");
2526
2527 let status = std::process::Command::new("mkfifo")
2528 .arg(&path)
2529 .status()
2530 .unwrap();
2531 assert!(status.success(), "mkfifo must succeed to set up this test");
2532
2533 let (client, _server) = fake_lsp_client();
2534 let tracker = Arc::new(DocumentTracker::new(
2535 ResourceLimits::default(),
2536 HashMap::new(),
2537 ));
2538
2539 let tracker_for_open = Arc::clone(&tracker);
2542 let path_for_task = path.clone();
2543 let handle_open = tokio::spawn(async move {
2544 tracker_for_open
2545 .ensure_open(&path_for_task, &ServerId::from("rust"), &client)
2546 .await
2547 });
2548
2549 tokio::time::sleep(Duration::from_millis(200)).await;
2553
2554 let update_while_blocked = tokio::time::timeout(
2558 Duration::from_millis(300),
2559 tracker.update(&path, "raced content".to_string()),
2560 )
2561 .await;
2562 assert!(
2563 update_while_blocked.is_err(),
2564 "update() must block while ensure_open holds the per-path lock for the same path"
2565 );
2566
2567 let path_writer = path.clone();
2571 tokio::task::spawn_blocking(move || {
2572 std::fs::write(path_writer, "fn a() {}").unwrap();
2573 })
2574 .await
2575 .unwrap();
2576
2577 handle_open.await.unwrap().unwrap();
2578 assert_eq!(tracker.get(&path).unwrap().content(), "fn a() {}");
2579 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2580
2581 let new_version = tracker
2584 .update(&path, "fn a() { updated(); }".to_string())
2585 .await;
2586 assert_eq!(new_version, Some(2));
2587 assert_eq!(
2588 tracker.get(&path).unwrap().content(),
2589 "fn a() { updated(); }"
2590 );
2591 }
2592
2593 #[tokio::test]
2599 async fn test_ensure_open_concurrent_same_path_single_didopen() {
2600 let dir = TempDir::new().unwrap();
2601 let path = dir.path().join("a.rs");
2602 std::fs::write(&path, "fn main() {}").unwrap();
2603 set_mtime(&path, settled_past());
2604
2605 let (client, mut server) = fake_lsp_client();
2606 let tracker = Arc::new(DocumentTracker::new(
2607 ResourceLimits::default(),
2608 HashMap::new(),
2609 ));
2610 let id = ServerId::from("rust");
2611
2612 let mut handles = Vec::new();
2613 for _ in 0..8 {
2614 let tracker = Arc::clone(&tracker);
2615 let client = client.clone();
2616 let path = path.clone();
2617 let id = id.clone();
2618 handles.push(tokio::spawn(async move {
2619 tracker.ensure_open(&path, &id, &client).await
2620 }));
2621 }
2622 for handle in handles {
2623 handle.await.unwrap().unwrap();
2624 }
2625
2626 let mut wire = BufReader::new(&mut server.write_stdout);
2627 let opened = read_framed_message(&mut wire).await;
2628 assert_eq!(opened["method"], "textDocument/didOpen");
2629
2630 let extra =
2633 tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut wire)).await;
2634 assert!(
2635 extra.is_err(),
2636 "expected no additional notification after the single didOpen"
2637 );
2638
2639 assert_eq!(tracker.get(&path).unwrap().synced_version(&id), Some(1));
2640 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2641 }
2642
2643 #[tokio::test]
2650 async fn test_ensure_open_path_locks_evicted_after_completion() {
2651 let dir = TempDir::new().unwrap();
2652 let paths: Vec<_> = ["a.rs", "b.rs", "c.rs"]
2653 .iter()
2654 .map(|name| dir.path().join(name))
2655 .collect();
2656 for path in &paths {
2657 std::fs::write(path, "fn f() {}").unwrap();
2658 set_mtime(path, settled_past());
2659 }
2660
2661 let tracker = Arc::new(DocumentTracker::new(
2662 ResourceLimits::default(),
2663 HashMap::new(),
2664 ));
2665 let id = ServerId::from("rust");
2666
2667 let mut handles = Vec::new();
2668 let mut servers = Vec::new();
2669 for path in paths.clone() {
2670 let tracker = Arc::clone(&tracker);
2671 let (client, server) = fake_lsp_client();
2672 servers.push(server);
2673 let id = id.clone();
2674 handles.push(tokio::spawn(async move {
2675 tracker.ensure_open(&path, &id, &client).await
2676 }));
2677 }
2678 for handle in handles {
2679 handle.await.unwrap().unwrap();
2680 }
2681 drop(servers);
2682
2683 assert!(
2684 lock_std(&tracker.path_locks).is_empty(),
2685 "path_locks must be fully evicted once every ensure_open call \
2686 for every path has completed, otherwise the map grows \
2687 unbounded for the lifetime of the process"
2688 );
2689 }
2690}