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 DidChangeTextDocumentNotification, DidChangeTextDocumentParams,
12 DidOpenTextDocumentNotification, DidOpenTextDocumentParams, TextDocumentContentChangeEvent,
13 TextDocumentItem, Uri, VersionedTextDocumentIdentifier,
14};
15use tokio::fs;
16use tokio::io::{AsyncBufReadExt, AsyncReadExt};
17use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard};
18use tokio::time::Instant;
19use url::Url;
20
21use super::lock_std;
22use crate::config::ServerId;
23use crate::error::{Error, Result};
24use crate::lsp::LspClient;
25use crate::util::{BoundedReadOutcome, bounded_read_cap, check_bounded_utf8};
26
27const DISK_CHECK_DEBOUNCE: Duration = Duration::from_millis(250);
40
41const MTIME_GRANULARITY: Duration = Duration::from_secs(2);
47
48fn mtime_settled(mtime: Option<SystemTime>, read_at: SystemTime) -> bool {
55 mtime.is_some_and(|m| {
56 m.checked_add(MTIME_GRANULARITY)
57 .is_some_and(|t| t <= read_at)
58 })
59}
60
61#[cfg(windows)]
69fn check_disk_file_type(file: &fs::File, path: &Path) -> Result<()> {
70 use std::os::windows::io::AsRawHandle;
71
72 use windows_sys::Win32::Storage::FileSystem::{FILE_TYPE_DISK, GetFileType};
73
74 #[allow(unsafe_code)]
75 let file_type = unsafe { GetFileType(file.as_raw_handle().cast()) };
77
78 if file_type != FILE_TYPE_DISK {
79 return Err(Error::NotARegularFile(path.to_path_buf()));
80 }
81 Ok(())
82}
83
84#[derive(Debug, Clone, Copy)]
94pub struct DiskSync {
95 pub mtime: Option<SystemTime>,
100 pub size: u64,
102 pub mtime_settled: bool,
105 pub content_checked_at: Instant,
112}
113
114impl PartialEq for DiskSync {
115 fn eq(&self, other: &Self) -> bool {
116 self.mtime == other.mtime
117 && self.size == other.size
118 && self.mtime_settled == other.mtime_settled
119 }
120}
121
122impl Eq for DiskSync {}
123
124#[derive(Debug, Clone)]
149pub struct DocumentState {
150 uri: Uri,
151 language_id: String,
152 version: i32,
153 content: String,
154 disk: Option<DiskSync>,
155 synced: HashMap<ServerId, i32>,
156 last_accessed: Instant,
161}
162
163impl PartialEq for DocumentState {
164 fn eq(&self, other: &Self) -> bool {
165 let Self {
170 uri,
171 language_id,
172 version,
173 content,
174 disk,
175 synced,
176 last_accessed: _,
177 } = self;
178 *uri == other.uri
179 && *language_id == other.language_id
180 && *version == other.version
181 && *content == other.content
182 && *disk == other.disk
183 && *synced == other.synced
184 }
185}
186
187impl Eq for DocumentState {}
188
189impl DocumentState {
190 fn new(uri: Uri, language_id: String, content: String) -> Self {
193 Self {
194 uri,
195 language_id,
196 version: 1,
197 content,
198 disk: None,
199 synced: HashMap::new(),
200 last_accessed: Instant::now(),
201 }
202 }
203
204 fn touch(&mut self) {
207 self.last_accessed = Instant::now();
208 }
209
210 #[must_use]
212 pub const fn uri(&self) -> &Uri {
213 &self.uri
214 }
215
216 #[must_use]
218 pub fn language_id(&self) -> &str {
219 &self.language_id
220 }
221
222 #[must_use]
226 pub const fn version(&self) -> i32 {
227 self.version
228 }
229
230 #[must_use]
232 pub fn content(&self) -> &str {
233 &self.content
234 }
235
236 const fn disk(&self) -> Option<DiskSync> {
239 self.disk
240 }
241
242 #[must_use]
251 pub fn synced_version(&self, server: &ServerId) -> Option<i32> {
252 self.synced.get(server).copied()
253 }
254
255 fn has_never_synced(&self) -> bool {
257 self.synced.is_empty()
258 }
259
260 fn apply_local_edit(&mut self, content: String) -> i32 {
264 self.version += 1;
265 self.content = content;
266 self.disk = None;
267 self.version
268 }
269
270 fn commit_reload(&mut self, version: i32, content: String, snap: Option<DiskSync>) {
276 debug_assert!(
277 version >= self.version,
278 "document version must be monotonically increasing"
279 );
280 self.version = version;
281 self.content = content;
282 self.disk = snap;
283 }
284
285 const fn set_disk(&mut self, snap: DiskSync) {
287 self.disk = Some(snap);
288 }
289
290 fn mark_synced(&mut self, server: ServerId, version: i32) {
292 self.synced.insert(server, version);
293 }
294
295 fn forget_server(&mut self, server: &ServerId) {
297 self.synced.remove(server);
298 }
299}
300
301pub const DEFAULT_MAX_DOCUMENTS: usize = 100;
304
305pub const DEFAULT_MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
308
309#[derive(Debug, Clone, Copy)]
311pub struct ResourceLimits {
312 pub max_documents: usize,
314 pub max_file_size: u64,
316}
317
318impl Default for ResourceLimits {
319 fn default() -> Self {
320 Self {
321 max_documents: DEFAULT_MAX_DOCUMENTS,
322 max_file_size: DEFAULT_MAX_FILE_SIZE,
323 }
324 }
325}
326
327pub const OPEN_FAILURE_CHARGE_BYTES: u64 = 4096;
341
342#[derive(Debug, Clone)]
352pub struct LineRead {
353 pub(crate) text: Option<String>,
355 pub(crate) bytes_read: u64,
358}
359
360#[derive(Debug, Clone)]
372pub struct EvictedDocument {
373 pub path: PathBuf,
375 pub uri: Uri,
377 pub synced_servers: Vec<ServerId>,
380}
381
382#[derive(Debug)]
389pub struct DocumentTracker {
390 documents: StdMutex<HashMap<PathBuf, DocumentState>>,
393 path_locks: StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
404 generations: StdMutex<HashMap<ServerId, u64>>,
411 limits: ResourceLimits,
413 extension_map: HashMap<String, String>,
415 evicted: StdMutex<Vec<EvictedDocument>>,
419}
420
421impl DocumentTracker {
422 #[must_use]
424 pub fn new(limits: ResourceLimits, extension_map: HashMap<String, String>) -> Self {
425 Self {
426 documents: StdMutex::new(HashMap::new()),
427 path_locks: StdMutex::new(HashMap::new()),
428 generations: StdMutex::new(HashMap::new()),
429 limits,
430 extension_map,
431 evicted: StdMutex::new(Vec::new()),
432 }
433 }
434
435 pub fn take_evicted(&self) -> Vec<EvictedDocument> {
442 std::mem::take(&mut lock_std(&self.evicted))
443 }
444
445 #[must_use]
447 pub fn is_open(&self, path: &Path) -> bool {
448 lock_std(&self.documents).contains_key(path)
449 }
450
451 #[must_use]
453 pub fn get(&self, path: &Path) -> Option<DocumentState> {
454 lock_std(&self.documents).get(path).cloned()
455 }
456
457 #[must_use]
465 pub fn line_text(&self, path: &Path, line: u32) -> Option<String> {
466 lock_std(&self.documents)
467 .get(path)?
468 .content
469 .lines()
470 .nth(line as usize)
471 .map(str::to_string)
472 }
473
474 #[must_use]
476 pub fn len(&self) -> usize {
477 lock_std(&self.documents).len()
478 }
479
480 #[must_use]
482 pub fn is_empty(&self) -> bool {
483 lock_std(&self.documents).is_empty()
484 }
485
486 pub fn open(&self, path: PathBuf, content: String) -> Result<Uri> {
523 self.check_file_size(content.len() as u64)?;
524
525 let uri = path_to_uri(&path)?;
526 let language_id = detect_language(&path, &self.extension_map);
527
528 let state = DocumentState::new(uri.clone(), language_id, content);
529
530 let mut documents = lock_std(&self.documents);
542 if self.limits.max_documents > 0
543 && documents.len() >= self.limits.max_documents
544 && !documents.contains_key(&path)
545 {
546 let Some((evicted_path, evicted_state)) =
547 Self::evict_lru(&mut documents, &self.path_locks)
548 else {
549 return Err(Error::DocumentLimitExceeded {
550 current: documents.len(),
551 max: self.limits.max_documents,
552 });
553 };
554 lock_std(&self.evicted).push(EvictedDocument {
555 path: evicted_path,
556 uri: evicted_state.uri,
557 synced_servers: evicted_state.synced.into_keys().collect(),
558 });
559 }
560 documents.insert(path, state);
561 drop(documents);
562 Ok(uri)
563 }
564
565 fn evict_lru(
587 documents: &mut HashMap<PathBuf, DocumentState>,
588 path_locks: &StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
589 ) -> Option<(PathBuf, DocumentState)> {
590 let locked = lock_std(path_locks)
591 .keys()
592 .cloned()
593 .collect::<std::collections::HashSet<_>>();
594 let lru_path = documents
595 .iter()
596 .filter(|(path, state)| !locked.contains(path.as_path()) && state.disk().is_some())
597 .min_by_key(|(_, state)| state.last_accessed)
598 .map(|(path, _)| path.clone())?;
599 documents.remove(&lru_path).map(|state| (lru_path, state))
600 }
601
602 pub async fn update(&self, path: &Path, content: String) -> Option<i32> {
623 let _path_guard = self.lock_path(path).await;
624 lock_std(&self.documents).get_mut(path).map(|state| {
625 state.touch();
626 state.apply_local_edit(content)
627 })
628 }
629
630 const fn check_file_size(&self, size: u64) -> Result<()> {
632 if self.limits.max_file_size > 0 && size > self.limits.max_file_size {
633 return Err(Error::FileSizeLimitExceeded {
634 size,
635 max: self.limits.max_file_size,
636 });
637 }
638 Ok(())
639 }
640
641 fn set_disk(&self, path: &Path, snap: DiskSync) {
648 if let Some(st) = lock_std(&self.documents).get_mut(path) {
649 st.set_disk(snap);
650 }
651 }
652
653 pub fn close(&self, path: &Path) -> Option<DocumentState> {
657 lock_std(&self.documents).remove(path)
658 }
659
660 pub fn close_all(&self) -> Vec<DocumentState> {
662 lock_std(&self.documents)
663 .drain()
664 .map(|(_, state)| state)
665 .collect()
666 }
667
668 pub fn open_paths(&self) -> Vec<PathBuf> {
670 lock_std(&self.documents).keys().cloned().collect()
671 }
672
673 pub fn forget_server(&self, server: &ServerId) {
692 *lock_std(&self.generations)
693 .entry(server.clone())
694 .or_insert(0) += 1;
695 for state in lock_std(&self.documents).values_mut() {
696 state.forget_server(server);
697 }
698 }
699
700 fn generation(&self, server: &ServerId) -> u64 {
702 lock_std(&self.generations)
703 .get(server)
704 .copied()
705 .unwrap_or(0)
706 }
707
708 async fn lock_path(&self, path: &Path) -> PathLockGuard<'_> {
723 let arc = {
724 let mut locks = lock_std(&self.path_locks);
725 locks
726 .entry(path.to_path_buf())
727 .or_insert_with(|| Arc::new(AsyncMutex::new(())))
728 .clone()
729 };
730 let guard = Arc::clone(&arc).lock_owned().await;
731 PathLockGuard {
732 path_locks: &self.path_locks,
733 path: path.to_path_buf(),
734 arc,
735 guard: Some(guard),
736 }
737 }
738
739 pub async fn ensure_open(
808 &self,
809 path: &Path,
810 server: &ServerId,
811 lsp_client: &LspClient,
812 ) -> Result<Uri> {
813 let _path_guard = self.lock_path(path).await;
814 let generation = self.generation(server);
815 let decision = self.disk_phase(path).await?;
816 self.sync_phase(path, server, lsp_client, decision, generation)
817 .await
818 }
819
820 async fn disk_phase(&self, path: &Path) -> Result<Decision> {
825 if !lock_std(&self.documents).contains_key(path) {
826 return self.disk_phase_new(path).await;
827 }
828
829 let read_at = SystemTime::now();
830 let meta = fs::metadata(path).await.map_err(|e| Error::FileIo {
831 path: path.to_path_buf(),
832 source: e,
833 })?;
834 let mtime = meta.modified().ok();
835 let size = meta.len();
836
837 let Some((uri, current_version, fast_path)) =
845 lock_std(&self.documents).get_mut(path).map(|st| {
846 st.touch();
847 let stat_matches = st
848 .disk()
849 .is_some_and(|d| d.mtime == mtime && d.size == size);
850 let fast_path = match st.disk() {
851 Some(d) if stat_matches && d.mtime_settled => true,
852 Some(d)
853 if stat_matches && d.content_checked_at.elapsed() < DISK_CHECK_DEBOUNCE =>
854 {
855 true
856 }
857 _ => false,
858 };
859 (st.uri.clone(), st.version, fast_path)
860 })
861 else {
862 return Err(Error::DocumentNotFound(path.to_path_buf()));
863 };
864 if fast_path {
865 return Ok(Decision::unchanged(uri, current_version));
866 }
867
868 let (fresh, ..) = self.read_to_string_checked(path).await?;
869 let snap = DiskSync {
870 mtime,
871 size,
872 mtime_settled: mtime_settled(mtime, read_at),
873 content_checked_at: Instant::now(),
874 };
875
876 let Some(unchanged) = lock_std(&self.documents)
877 .get(path)
878 .map(|st| fresh == st.content)
879 else {
880 return Err(Error::DocumentNotFound(path.to_path_buf()));
881 };
882
883 if unchanged {
884 self.set_disk(path, snap);
885 return Ok(Decision::unchanged(uri, current_version));
886 }
887
888 Ok(Decision {
889 uri,
890 target_version: current_version.saturating_add(1),
891 fresh_content: Some(fresh),
892 snap: Some(snap),
893 })
894 }
895
896 async fn disk_phase_new(&self, path: &Path) -> Result<Decision> {
900 let read_at = SystemTime::now();
901 let (content, mtime, size) = self.read_to_string_checked(path).await?;
902
903 let uri = self.open(path.to_path_buf(), content)?;
904 self.set_disk(
905 path,
906 DiskSync {
907 mtime,
908 size,
909 mtime_settled: mtime_settled(mtime, read_at),
910 content_checked_at: Instant::now(),
911 },
912 );
913
914 Ok(Decision::unchanged(uri, 1))
915 }
916
917 async fn open_checked(&self, path: &Path) -> Result<(fs::File, std::fs::Metadata)> {
944 #[cfg(unix)]
945 let opened = fs::OpenOptions::new()
946 .read(true)
947 .custom_flags(libc::O_NONBLOCK)
948 .open(path)
949 .await;
950 #[cfg(not(unix))]
951 let opened = fs::File::open(path).await;
952
953 let file = opened.map_err(|e| Error::FileIo {
954 path: path.to_path_buf(),
955 source: e,
956 })?;
957 #[cfg(windows)]
959 check_disk_file_type(&file, path)?;
960 let meta = file.metadata().await.map_err(|e| Error::FileIo {
961 path: path.to_path_buf(),
962 source: e,
963 })?;
964 if !meta.is_file() {
965 return Err(Error::NotARegularFile(path.to_path_buf()));
966 }
967 self.check_file_size(meta.len())?;
968 Ok((file, meta))
969 }
970
971 async fn read_string_bounded(
984 &self,
985 path: &Path,
986 mut file: fs::File,
987 size_hint: u64,
988 ) -> Result<String> {
989 let max = self.limits.max_file_size;
990 let cap = bounded_read_cap(max);
991 let mut buf = Vec::with_capacity(usize::try_from(size_hint.min(cap)).unwrap_or(0));
992 let io_err = |e: std::io::Error| Error::FileIo {
993 path: path.to_path_buf(),
994 source: e,
995 };
996
997 (&mut file)
998 .take(cap)
999 .read_to_end(&mut buf)
1000 .await
1001 .map_err(io_err)?;
1002 match check_bounded_utf8(buf, max) {
1003 BoundedReadOutcome::Ok(s) => Ok(s),
1004 BoundedReadOutcome::TooLarge { size } => {
1005 Err(Error::FileSizeLimitExceeded { size, max })
1006 }
1007 BoundedReadOutcome::InvalidUtf8(e) => Err(io_err(std::io::Error::new(
1008 std::io::ErrorKind::InvalidData,
1009 e,
1010 ))),
1011 }
1012 }
1013
1014 async fn read_to_string_checked(
1022 &self,
1023 path: &Path,
1024 ) -> Result<(String, Option<SystemTime>, u64)> {
1025 let (file, meta) = self.open_checked(path).await?;
1026 let mtime = meta.modified().ok();
1027 let size = meta.len();
1028 let content = self.read_string_bounded(path, file, size).await?;
1029 Ok((content, mtime, size))
1030 }
1031
1032 pub(crate) async fn read_line_checked(
1096 &self,
1097 path: &Path,
1098 line: u32,
1099 budget: u64,
1100 ) -> Result<LineRead> {
1101 let Ok((file, _meta)) = self.open_checked(path).await else {
1102 return Ok(LineRead {
1103 text: None,
1104 bytes_read: OPEN_FAILURE_CHARGE_BYTES,
1105 });
1106 };
1107 let max = self.limits.max_file_size;
1108 let cap = bounded_read_cap(max).min(budget.saturating_add(1));
1114 let mut reader = tokio::io::BufReader::new(file.take(cap));
1115 let io_err = |e: std::io::Error| Error::FileIo {
1116 path: path.to_path_buf(),
1117 source: e,
1118 };
1119
1120 let mut buf = Vec::new();
1121 let mut bytes_read: u64 = 0;
1122 let mut current_line = 0u32;
1123 loop {
1124 buf.clear();
1125 let n = reader.read_until(b'\n', &mut buf).await.map_err(io_err)?;
1126 bytes_read += n as u64;
1127 if n == 0 {
1128 return Ok(LineRead {
1131 text: None,
1132 bytes_read,
1133 });
1134 }
1135 if current_line == line {
1136 let truncated_by_cap = bytes_read >= cap && buf.last() != Some(&b'\n');
1137 if truncated_by_cap {
1138 return Ok(LineRead {
1139 text: None,
1140 bytes_read,
1141 });
1142 }
1143 if buf.last() == Some(&b'\n') {
1144 buf.pop();
1145 if buf.last() == Some(&b'\r') {
1146 buf.pop();
1147 }
1148 }
1149 return Ok(LineRead {
1150 text: String::from_utf8(buf).ok(),
1151 bytes_read,
1152 });
1153 }
1154 current_line += 1;
1155 }
1156 }
1157
1158 async fn sync_phase(
1169 &self,
1170 path: &Path,
1171 server: &ServerId,
1172 lsp_client: &LspClient,
1173 decision: Decision,
1174 generation: u64,
1175 ) -> Result<Uri> {
1176 let Decision {
1177 uri,
1178 target_version,
1179 fresh_content,
1180 snap,
1181 } = decision;
1182
1183 let Some(synced_version) = lock_std(&self.documents)
1190 .get(path)
1191 .map(|st| st.synced_version(server))
1192 else {
1193 return Err(Error::DocumentNotFound(path.to_path_buf()));
1194 };
1195 let up_to_date = synced_version.is_some_and(|v| v >= target_version);
1196 let is_first_open = synced_version.is_none();
1197
1198 if up_to_date {
1199 return Ok(uri);
1200 }
1201
1202 let Some((language_id, text)) = lock_std(&self.documents).get(path).map(|st| {
1203 let text = fresh_content.clone().unwrap_or_else(|| st.content.clone());
1204 (st.language_id.clone(), text)
1205 }) else {
1206 return Err(Error::DocumentNotFound(path.to_path_buf()));
1207 };
1208
1209 let notify_result = if is_first_open {
1210 lsp_client
1211 .notify_typed::<DidOpenTextDocumentNotification>(DidOpenTextDocumentParams {
1212 text_document: TextDocumentItem {
1213 uri: uri.clone(),
1214 language_id: language_id.into(),
1215 version: target_version,
1216 text,
1217 },
1218 })
1219 .await
1220 } else {
1221 lsp_client
1222 .notify_typed::<DidChangeTextDocumentNotification>(DidChangeTextDocumentParams {
1223 text_document: VersionedTextDocumentIdentifier {
1224 version: target_version,
1225 text_document_identifier: lsp_types::TextDocumentIdentifier {
1226 uri: uri.clone(),
1227 },
1228 },
1229 content_changes: vec![
1230 TextDocumentContentChangeEvent::TextDocumentContentChangeWholeDocument(
1231 lsp_types::TextDocumentContentChangeWholeDocument { text },
1232 ),
1233 ],
1234 })
1235 .await
1236 };
1237
1238 if let Err(err) = notify_result {
1239 let first_ever_sync = lock_std(&self.documents)
1252 .get(path)
1253 .is_some_and(DocumentState::has_never_synced);
1254 if is_first_open && first_ever_sync {
1255 lock_std(&self.documents).remove(path);
1256 }
1257 return Err(err);
1258 }
1259
1260 let mut documents = lock_std(&self.documents);
1263 let Some(st) = documents.get_mut(path) else {
1264 return Err(Error::DocumentNotFound(path.to_path_buf()));
1265 };
1266 if let Some(fresh) = fresh_content {
1267 st.commit_reload(target_version, fresh, snap);
1268 }
1269 if self.generation(server) == generation {
1279 st.mark_synced(server.clone(), target_version);
1280 }
1281 drop(documents);
1282
1283 Ok(uri)
1284 }
1285}
1286
1287struct PathLockGuard<'a> {
1296 path_locks: &'a StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
1297 path: PathBuf,
1298 arc: Arc<AsyncMutex<()>>,
1299 guard: Option<OwnedMutexGuard<()>>,
1300}
1301
1302impl Drop for PathLockGuard<'_> {
1303 fn drop(&mut self) {
1304 self.guard.take();
1307
1308 let mut locks = lock_std(self.path_locks);
1309 if Arc::strong_count(&self.arc) <= 2 {
1321 locks.remove(&self.path);
1322 }
1323 }
1324}
1325
1326struct Decision {
1331 uri: Uri,
1332 target_version: i32,
1333 fresh_content: Option<String>,
1334 snap: Option<DiskSync>,
1335}
1336
1337impl Decision {
1338 const fn unchanged(uri: Uri, target_version: i32) -> Self {
1341 Self {
1342 uri,
1343 target_version,
1344 fresh_content: None,
1345 snap: None,
1346 }
1347 }
1348}
1349
1350pub fn path_to_uri(path: &Path) -> Result<Uri> {
1363 try_path_to_uri(path)
1364 .ok_or_else(|| Error::InvalidUri(format!("cannot convert path to URI: {}", path.display())))
1365}
1366
1367#[must_use]
1373pub fn try_path_to_uri(path: &Path) -> Option<Uri> {
1374 let uri_string = encode_rfc3986_path_chars(&file_url(path)?);
1375 Some(Uri::from(uri_string))
1376}
1377
1378#[cfg(not(windows))]
1379fn file_url(path: &Path) -> Option<Url> {
1380 Url::from_file_path(path).ok()
1381}
1382
1383#[cfg(windows)]
1384fn file_url(path: &Path) -> Option<Url> {
1385 match Url::from_file_path(path) {
1386 Ok(file_url) => Some(file_url),
1387 Err(()) if path.has_root() => windows_rooted_path_to_file_url(path),
1388 Err(()) => None,
1389 }
1390}
1391
1392#[cfg(windows)]
1393fn windows_rooted_path_to_file_url(path: &Path) -> Option<Url> {
1394 let path_str = path.to_string_lossy();
1395 let stripped = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
1396 let mut file_url = Url::parse("file:///").ok()?;
1397 file_url.path_segments_mut().ok()?.clear().extend(
1398 stripped
1399 .split(['\\', '/'])
1400 .filter(|segment| !segment.is_empty()),
1401 );
1402 Some(file_url)
1403}
1404
1405pub(super) fn encode_rfc3986_path_chars(url: &Url) -> String {
1415 let prefix = url[..url::Position::BeforePath].to_owned();
1416 let encoded = url[url::Position::BeforePath..]
1417 .replace('[', "%5B")
1418 .replace(']', "%5D")
1419 .replace('^', "%5E")
1420 .replace('|', "%7C");
1421 format!("{prefix}{encoded}")
1422}
1423
1424#[must_use]
1429pub fn uri_to_path(uri: &Uri) -> Option<PathBuf> {
1430 let url = Url::parse(uri.as_ref()).ok()?;
1431 if url.scheme() != "file" {
1432 return None;
1433 }
1434 if !url.host_str().unwrap_or("").is_empty() {
1437 return None;
1438 }
1439 url.to_file_path().ok()
1440}
1441
1442#[must_use]
1447pub fn detect_language(path: &Path, extension_map: &HashMap<String, String>) -> String {
1448 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
1449
1450 extension_map
1451 .get(extension)
1452 .cloned()
1453 .unwrap_or_else(|| "plaintext".to_string())
1454}
1455
1456#[cfg(test)]
1457#[allow(clippy::unwrap_used)]
1458mod tests {
1459 use super::*;
1460
1461 #[test]
1462 fn test_detect_language() {
1463 let mut map = HashMap::new();
1464 map.insert("rs".to_string(), "rust".to_string());
1465 map.insert("py".to_string(), "python".to_string());
1466 map.insert("ts".to_string(), "typescript".to_string());
1467
1468 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
1469 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
1470 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
1471 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
1472 }
1473
1474 #[tokio::test]
1475 async fn test_document_tracker() {
1476 let mut map = HashMap::new();
1477 map.insert("rs".to_string(), "rust".to_string());
1478
1479 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1480 let path = PathBuf::from("/test/file.rs");
1481
1482 assert!(!tracker.is_open(&path));
1483
1484 tracker
1485 .open(path.clone(), "fn main() {}".to_string())
1486 .unwrap();
1487 assert!(tracker.is_open(&path));
1488 assert_eq!(tracker.len(), 1);
1489
1490 let state = tracker.get(&path).unwrap();
1491 assert_eq!(state.version(), 1);
1492 assert_eq!(state.language_id(), "rust");
1493
1494 let new_version = tracker
1495 .update(&path, "fn main() { println!() }".to_string())
1496 .await;
1497 assert_eq!(new_version, Some(2));
1498
1499 tracker.close(&path);
1500 assert!(!tracker.is_open(&path));
1501 assert!(tracker.is_empty());
1502 }
1503
1504 #[test]
1510 fn test_forget_server_clears_only_that_servers_synced_version() {
1511 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1512 let path = PathBuf::from("/test/file.rs");
1513 tracker
1514 .open(path.clone(), "fn main() {}".to_string())
1515 .unwrap();
1516
1517 let respawned = ServerId::from("rust-respawned");
1518 let untouched = ServerId::from("rust-diagnostics");
1519 lock_std(&tracker.documents)
1520 .get_mut(&path)
1521 .unwrap()
1522 .synced
1523 .insert(respawned.clone(), 1);
1524 lock_std(&tracker.documents)
1525 .get_mut(&path)
1526 .unwrap()
1527 .synced
1528 .insert(untouched.clone(), 1);
1529
1530 tracker.forget_server(&respawned);
1531
1532 let state = tracker.get(&path).unwrap();
1533 assert!(state.synced_version(&respawned).is_none());
1534 assert!(state.synced_version(&untouched).is_some());
1535 }
1536
1537 #[tokio::test]
1548 async fn test_sync_phase_skips_commit_when_generation_is_stale() {
1549 let dir = TempDir::new().unwrap();
1550 let path = dir.path().join("race.rs");
1551 std::fs::write(&path, "fn main() {}").unwrap();
1552 set_mtime(&path, settled_past());
1553
1554 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1555 let server = ServerId::from("rust");
1556 let generation_before_respawn = 0; tracker.forget_server(&server);
1561
1562 let (stale_client, _guard) = fake_lsp_client();
1563 let decision = tracker.disk_phase(&path).await.unwrap();
1564 tracker
1565 .sync_phase(
1566 &path,
1567 &server,
1568 &stale_client,
1569 decision,
1570 generation_before_respawn,
1571 )
1572 .await
1573 .unwrap();
1574
1575 let state = tracker.get(&path).unwrap();
1576 assert!(
1577 state.synced_version(&server).is_none(),
1578 "a sync_phase call that captured a stale generation must not \
1579 commit `synced`, even though its notify against the \
1580 superseded connection succeeded"
1581 );
1582 }
1583
1584 #[tokio::test]
1589 async fn test_ensure_open_commits_when_generation_is_current() {
1590 let dir = TempDir::new().unwrap();
1591 let path = dir.path().join("no_race.rs");
1592 std::fs::write(&path, "fn main() {}").unwrap();
1593
1594 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1595 let server = ServerId::from("rust");
1596 let (client, _guard) = fake_lsp_client();
1597
1598 tracker.ensure_open(&path, &server, &client).await.unwrap();
1599
1600 let state = tracker.get(&path).unwrap();
1601 assert_eq!(state.synced_version(&server), Some(1));
1602 }
1603
1604 fn mark_disk_verified(tracker: &DocumentTracker, path: &Path) {
1610 tracker.set_disk(
1611 path,
1612 DiskSync {
1613 mtime: None,
1614 size: 0,
1615 mtime_settled: false,
1616 content_checked_at: Instant::now(),
1617 },
1618 );
1619 }
1620
1621 #[test]
1626 fn test_document_limit_evicts_lru_instead_of_failing() {
1627 let limits = ResourceLimits {
1628 max_documents: 2,
1629 max_file_size: 100,
1630 };
1631 let mut map = HashMap::new();
1632 map.insert("rs".to_string(), "rust".to_string());
1633
1634 let tracker = DocumentTracker::new(limits, map);
1635
1636 tracker
1637 .open(PathBuf::from("/test/file1.rs"), "fn test1() {}".to_string())
1638 .unwrap();
1639 mark_disk_verified(&tracker, Path::new("/test/file1.rs"));
1640 tracker
1641 .open(PathBuf::from("/test/file2.rs"), "fn test2() {}".to_string())
1642 .unwrap();
1643 mark_disk_verified(&tracker, Path::new("/test/file2.rs"));
1644
1645 tracker
1646 .open(PathBuf::from("/test/file3.rs"), "fn test3() {}".to_string())
1647 .unwrap();
1648
1649 assert_eq!(tracker.len(), 2);
1650 assert!(!tracker.is_open(Path::new("/test/file1.rs")));
1651 assert!(tracker.is_open(Path::new("/test/file2.rs")));
1652 assert!(tracker.is_open(Path::new("/test/file3.rs")));
1653
1654 let evicted = tracker.take_evicted();
1655 assert_eq!(evicted.len(), 1);
1656 assert_eq!(evicted[0].path, PathBuf::from("/test/file1.rs"));
1657 assert!(
1658 evicted[0].synced_servers.is_empty(),
1659 "opened directly via `open`, never synced to any server"
1660 );
1661 }
1662
1663 #[test]
1669 fn test_document_limit_falls_back_to_error_when_only_candidate_is_locked() {
1670 let limits = ResourceLimits {
1671 max_documents: 1,
1672 max_file_size: 100,
1673 };
1674 let tracker = DocumentTracker::new(limits, HashMap::new());
1675
1676 let locked_path = PathBuf::from("/test/locked.rs");
1677 tracker
1678 .open(locked_path.clone(), "fn locked() {}".to_string())
1679 .unwrap();
1680 lock_std(&tracker.path_locks).insert(locked_path.clone(), Arc::new(AsyncMutex::new(())));
1681
1682 let result = tracker.open(PathBuf::from("/test/other.rs"), "fn other() {}".to_string());
1683 assert!(matches!(result, Err(Error::DocumentLimitExceeded { .. })));
1684 assert!(
1685 tracker.is_open(&locked_path),
1686 "the locked document must not be evicted"
1687 );
1688 assert!(tracker.take_evicted().is_empty());
1689 }
1690
1691 #[tokio::test]
1698 async fn test_evict_lru_skips_document_with_diverged_unsaved_content() {
1699 let dir = TempDir::new().unwrap();
1700 let path_a = dir.path().join("a.rs");
1701 std::fs::write(&path_a, "AAAA").unwrap();
1702 set_mtime(&path_a, settled_past());
1703
1704 let limits = ResourceLimits {
1705 max_documents: 1,
1706 max_file_size: 0,
1707 };
1708 let (client, _server) = fake_lsp_client();
1709 let tracker = DocumentTracker::new(limits, HashMap::new());
1710 let server_id = ServerId::from("rust");
1711
1712 tracker
1713 .ensure_open(&path_a, &server_id, &client)
1714 .await
1715 .unwrap();
1716 tracker
1718 .update(&path_a, "AAAA-edited".to_string())
1719 .await
1720 .unwrap();
1721
1722 let path_b = dir.path().join("b.rs");
1723 std::fs::write(&path_b, "BBBB").unwrap();
1724
1725 let result = tracker.open(path_b, "BBBB".to_string());
1726 assert!(matches!(result, Err(Error::DocumentLimitExceeded { .. })));
1727 assert!(
1728 tracker.is_open(&path_a),
1729 "the diverged, not-disk-verified document must not be evicted"
1730 );
1731 assert_eq!(tracker.get(&path_a).unwrap().content(), "AAAA-edited");
1732 assert!(tracker.take_evicted().is_empty());
1733 }
1734
1735 #[tokio::test]
1740 async fn test_ensure_open_touch_changes_lru_eviction_order() {
1741 let dir = TempDir::new().unwrap();
1742 let path_a = dir.path().join("a.rs");
1743 let path_b = dir.path().join("b.rs");
1744 std::fs::write(&path_a, "AAAA").unwrap();
1745 std::fs::write(&path_b, "BBBB").unwrap();
1746 set_mtime(&path_a, settled_past());
1747 set_mtime(&path_b, settled_past());
1748
1749 let limits = ResourceLimits {
1750 max_documents: 2,
1751 max_file_size: 0,
1752 };
1753 let (client, _server) = fake_lsp_client();
1754 let tracker = DocumentTracker::new(limits, HashMap::new());
1755 let server_id = ServerId::from("rust");
1756
1757 tracker
1758 .ensure_open(&path_a, &server_id, &client)
1759 .await
1760 .unwrap();
1761 tracker
1762 .ensure_open(&path_b, &server_id, &client)
1763 .await
1764 .unwrap();
1765
1766 tracker
1769 .ensure_open(&path_a, &server_id, &client)
1770 .await
1771 .unwrap();
1772
1773 let path_c = dir.path().join("c.rs");
1774 std::fs::write(&path_c, "CCCC").unwrap();
1775 set_mtime(&path_c, settled_past());
1776 tracker
1777 .ensure_open(&path_c, &server_id, &client)
1778 .await
1779 .unwrap();
1780
1781 assert!(
1782 tracker.is_open(&path_a),
1783 "recently re-accessed, must survive"
1784 );
1785 assert!(
1786 !tracker.is_open(&path_b),
1787 "least-recently-used, must be evicted"
1788 );
1789 assert!(tracker.is_open(&path_c));
1790
1791 let evicted = tracker.take_evicted();
1792 assert_eq!(evicted.len(), 1);
1793 assert_eq!(evicted[0].path, path_b);
1794 assert_eq!(evicted[0].synced_servers, vec![server_id]);
1795 }
1796
1797 #[test]
1798 fn test_file_size_limit() {
1799 let limits = ResourceLimits {
1800 max_documents: 10,
1801 max_file_size: 10,
1802 };
1803 let mut map = HashMap::new();
1804 map.insert("rs".to_string(), "rust".to_string());
1805
1806 let tracker = DocumentTracker::new(limits, map);
1807
1808 tracker
1810 .open(PathBuf::from("/test/small.rs"), "fn f(){}".to_string())
1811 .unwrap();
1812
1813 let large_content = "x".repeat(100);
1815 let result = tracker.open(PathBuf::from("/test/large.rs"), large_content);
1816 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
1817 }
1818
1819 #[test]
1820 fn test_resource_limits_default() {
1821 let limits = ResourceLimits::default();
1822 assert_eq!(limits.max_documents, 100);
1823 assert_eq!(limits.max_file_size, 10 * 1024 * 1024);
1824 }
1825
1826 #[test]
1827 fn test_resource_limits_custom() {
1828 let limits = ResourceLimits {
1829 max_documents: 50,
1830 max_file_size: 5 * 1024 * 1024,
1831 };
1832 assert_eq!(limits.max_documents, 50);
1833 assert_eq!(limits.max_file_size, 5 * 1024 * 1024);
1834 }
1835
1836 #[test]
1837 fn test_resource_limits_zero_unlimited() {
1838 let limits = ResourceLimits {
1839 max_documents: 0,
1840 max_file_size: 0,
1841 };
1842 let mut map = HashMap::new();
1843 map.insert("rs".to_string(), "rust".to_string());
1844
1845 let tracker = DocumentTracker::new(limits, map);
1846
1847 for i in 0..200 {
1849 tracker
1850 .open(
1851 PathBuf::from(format!("/test/file{i}.rs")),
1852 "content".to_string(),
1853 )
1854 .unwrap();
1855 }
1856 assert_eq!(tracker.len(), 200);
1857
1858 let huge_content = "x".repeat(100_000_000);
1860 tracker
1861 .open(PathBuf::from("/test/huge.rs"), huge_content)
1862 .unwrap();
1863 }
1864
1865 #[test]
1866 fn test_document_state_clone() {
1867 let state = DocumentState {
1868 uri: Uri::from("file:///test.rs"),
1869 language_id: "rust".to_string(),
1870 version: 5,
1871 content: "fn main() {}".to_string(),
1872 disk: None,
1873 synced: HashMap::new(),
1874 last_accessed: Instant::now(),
1875 };
1876
1877 #[allow(clippy::redundant_clone)]
1878 let cloned = state.clone();
1879 assert_eq!(cloned.uri(), state.uri());
1880 assert_eq!(cloned.language_id(), state.language_id());
1881 assert_eq!(cloned.version(), 5);
1882 assert_eq!(cloned.content(), state.content());
1883 }
1884
1885 #[tokio::test]
1886 async fn test_update_nonexistent_document() {
1887 let map = HashMap::new();
1888 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1889 let path = PathBuf::from("/test/nonexistent.rs");
1890
1891 let version = tracker.update(&path, "new content".to_string()).await;
1892 assert_eq!(
1893 version, None,
1894 "Updating non-existent document should return None"
1895 );
1896 }
1897
1898 #[test]
1899 fn test_close_nonexistent_document() {
1900 let map = HashMap::new();
1901 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1902 let path = PathBuf::from("/test/nonexistent.rs");
1903
1904 let state = tracker.close(&path);
1905 assert_eq!(
1906 state, None,
1907 "Closing non-existent document should return None"
1908 );
1909 }
1910
1911 #[test]
1912 fn test_close_all_documents() {
1913 let mut map = HashMap::new();
1914 map.insert("rs".to_string(), "rust".to_string());
1915
1916 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1917
1918 tracker
1919 .open(PathBuf::from("/test/file1.rs"), "content1".to_string())
1920 .unwrap();
1921 tracker
1922 .open(PathBuf::from("/test/file2.rs"), "content2".to_string())
1923 .unwrap();
1924 tracker
1925 .open(PathBuf::from("/test/file3.rs"), "content3".to_string())
1926 .unwrap();
1927
1928 assert_eq!(tracker.len(), 3);
1929
1930 let closed = tracker.close_all();
1931 assert_eq!(closed.len(), 3);
1932 assert!(tracker.is_empty());
1933 }
1934
1935 #[test]
1936 fn test_get_nonexistent_document() {
1937 let map = HashMap::new();
1938 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1939 let path = PathBuf::from("/test/nonexistent.rs");
1940
1941 let state = tracker.get(&path);
1942 assert!(
1943 state.is_none(),
1944 "Getting non-existent document should return None"
1945 );
1946 }
1947
1948 #[tokio::test]
1949 async fn test_document_version_increments() {
1950 let mut map = HashMap::new();
1951 map.insert("rs".to_string(), "rust".to_string());
1952
1953 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1954 let path = PathBuf::from("/test/versioned.rs");
1955
1956 tracker.open(path.clone(), "v1".to_string()).unwrap();
1957 assert_eq!(tracker.get(&path).unwrap().version(), 1);
1958
1959 tracker.update(&path, "v2".to_string()).await;
1960 assert_eq!(tracker.get(&path).unwrap().version(), 2);
1961
1962 tracker.update(&path, "v3".to_string()).await;
1963 assert_eq!(tracker.get(&path).unwrap().version(), 3);
1964
1965 tracker.update(&path, "v4".to_string()).await;
1966 assert_eq!(tracker.get(&path).unwrap().version(), 4);
1967 }
1968
1969 #[test]
1970 #[allow(clippy::too_many_lines)]
1971 fn test_detect_language_all_extensions() {
1972 let mut map = HashMap::new();
1973 map.insert("rs".to_string(), "rust".to_string());
1974 map.insert("py".to_string(), "python".to_string());
1975 map.insert("pyw".to_string(), "python".to_string());
1976 map.insert("pyi".to_string(), "python".to_string());
1977 map.insert("js".to_string(), "javascript".to_string());
1978 map.insert("mjs".to_string(), "javascript".to_string());
1979 map.insert("cjs".to_string(), "javascript".to_string());
1980 map.insert("ts".to_string(), "typescript".to_string());
1981 map.insert("mts".to_string(), "typescript".to_string());
1982 map.insert("cts".to_string(), "typescript".to_string());
1983 map.insert("tsx".to_string(), "typescriptreact".to_string());
1984 map.insert("jsx".to_string(), "javascriptreact".to_string());
1985 map.insert("go".to_string(), "go".to_string());
1986 map.insert("c".to_string(), "c".to_string());
1987 map.insert("h".to_string(), "c".to_string());
1988 map.insert("cpp".to_string(), "cpp".to_string());
1989 map.insert("cc".to_string(), "cpp".to_string());
1990 map.insert("cxx".to_string(), "cpp".to_string());
1991 map.insert("hpp".to_string(), "cpp".to_string());
1992 map.insert("hh".to_string(), "cpp".to_string());
1993 map.insert("hxx".to_string(), "cpp".to_string());
1994 map.insert("java".to_string(), "java".to_string());
1995 map.insert("rb".to_string(), "ruby".to_string());
1996 map.insert("php".to_string(), "php".to_string());
1997 map.insert("swift".to_string(), "swift".to_string());
1998 map.insert("kt".to_string(), "kotlin".to_string());
1999 map.insert("kts".to_string(), "kotlin".to_string());
2000 map.insert("scala".to_string(), "scala".to_string());
2001 map.insert("sc".to_string(), "scala".to_string());
2002 map.insert("zig".to_string(), "zig".to_string());
2003 map.insert("lua".to_string(), "lua".to_string());
2004 map.insert("sh".to_string(), "shellscript".to_string());
2005 map.insert("bash".to_string(), "shellscript".to_string());
2006 map.insert("zsh".to_string(), "shellscript".to_string());
2007 map.insert("json".to_string(), "json".to_string());
2008 map.insert("toml".to_string(), "toml".to_string());
2009 map.insert("yaml".to_string(), "yaml".to_string());
2010 map.insert("yml".to_string(), "yaml".to_string());
2011 map.insert("xml".to_string(), "xml".to_string());
2012 map.insert("html".to_string(), "html".to_string());
2013 map.insert("htm".to_string(), "html".to_string());
2014 map.insert("css".to_string(), "css".to_string());
2015 map.insert("scss".to_string(), "scss".to_string());
2016 map.insert("less".to_string(), "less".to_string());
2017 map.insert("md".to_string(), "markdown".to_string());
2018 map.insert("markdown".to_string(), "markdown".to_string());
2019
2020 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
2021 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
2022 assert_eq!(detect_language(Path::new("script.pyw"), &map), "python");
2023 assert_eq!(detect_language(Path::new("script.pyi"), &map), "python");
2024 assert_eq!(detect_language(Path::new("app.js"), &map), "javascript");
2025 assert_eq!(detect_language(Path::new("app.mjs"), &map), "javascript");
2026 assert_eq!(detect_language(Path::new("app.cjs"), &map), "javascript");
2027 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
2028 assert_eq!(detect_language(Path::new("app.mts"), &map), "typescript");
2029 assert_eq!(detect_language(Path::new("app.cts"), &map), "typescript");
2030 assert_eq!(
2031 detect_language(Path::new("component.tsx"), &map),
2032 "typescriptreact"
2033 );
2034 assert_eq!(
2035 detect_language(Path::new("component.jsx"), &map),
2036 "javascriptreact"
2037 );
2038 assert_eq!(detect_language(Path::new("main.go"), &map), "go");
2039 assert_eq!(detect_language(Path::new("main.c"), &map), "c");
2040 assert_eq!(detect_language(Path::new("header.h"), &map), "c");
2041 assert_eq!(detect_language(Path::new("main.cpp"), &map), "cpp");
2042 assert_eq!(detect_language(Path::new("main.cc"), &map), "cpp");
2043 assert_eq!(detect_language(Path::new("main.cxx"), &map), "cpp");
2044 assert_eq!(detect_language(Path::new("header.hpp"), &map), "cpp");
2045 assert_eq!(detect_language(Path::new("header.hh"), &map), "cpp");
2046 assert_eq!(detect_language(Path::new("header.hxx"), &map), "cpp");
2047 assert_eq!(detect_language(Path::new("Main.java"), &map), "java");
2048 assert_eq!(detect_language(Path::new("script.rb"), &map), "ruby");
2049 assert_eq!(detect_language(Path::new("index.php"), &map), "php");
2050 assert_eq!(detect_language(Path::new("App.swift"), &map), "swift");
2051 assert_eq!(detect_language(Path::new("Main.kt"), &map), "kotlin");
2052 assert_eq!(detect_language(Path::new("script.kts"), &map), "kotlin");
2053 assert_eq!(detect_language(Path::new("Main.scala"), &map), "scala");
2054 assert_eq!(detect_language(Path::new("script.sc"), &map), "scala");
2055 assert_eq!(detect_language(Path::new("main.zig"), &map), "zig");
2056 assert_eq!(detect_language(Path::new("script.lua"), &map), "lua");
2057 assert_eq!(detect_language(Path::new("script.sh"), &map), "shellscript");
2058 assert_eq!(
2059 detect_language(Path::new("script.bash"), &map),
2060 "shellscript"
2061 );
2062 assert_eq!(
2063 detect_language(Path::new("script.zsh"), &map),
2064 "shellscript"
2065 );
2066 assert_eq!(detect_language(Path::new("data.json"), &map), "json");
2067 assert_eq!(detect_language(Path::new("config.toml"), &map), "toml");
2068 assert_eq!(detect_language(Path::new("config.yaml"), &map), "yaml");
2069 assert_eq!(detect_language(Path::new("config.yml"), &map), "yaml");
2070 assert_eq!(detect_language(Path::new("data.xml"), &map), "xml");
2071 assert_eq!(detect_language(Path::new("index.html"), &map), "html");
2072 assert_eq!(detect_language(Path::new("index.htm"), &map), "html");
2073 assert_eq!(detect_language(Path::new("styles.css"), &map), "css");
2074 assert_eq!(detect_language(Path::new("styles.scss"), &map), "scss");
2075 assert_eq!(detect_language(Path::new("styles.less"), &map), "less");
2076 assert_eq!(detect_language(Path::new("README.md"), &map), "markdown");
2077 assert_eq!(
2078 detect_language(Path::new("README.markdown"), &map),
2079 "markdown"
2080 );
2081 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
2082 assert_eq!(
2083 detect_language(Path::new("no_extension"), &map),
2084 "plaintext"
2085 );
2086 }
2087
2088 #[test]
2089 fn test_path_to_uri_unix() {
2090 #[cfg(not(windows))]
2091 {
2092 let path = Path::new("/home/user/project/main.rs");
2093 let uri = path_to_uri(path).unwrap();
2094 assert!(
2095 uri.as_ref()
2096 .starts_with("file:///home/user/project/main.rs")
2097 );
2098 }
2099 }
2100
2101 #[test]
2102 fn test_path_to_uri_with_special_chars() {
2103 let path = Path::new("/home/user/project-test/main.rs");
2104 let uri = path_to_uri(path).unwrap();
2105 assert!(uri.as_ref().starts_with("file://"));
2106 assert!(uri.as_ref().contains("project-test"));
2107 }
2108
2109 #[test]
2110 fn test_path_to_uri_percent_encodes_reserved_chars() {
2111 #[cfg(windows)]
2112 let path = Path::new(r"C:\home\user\routes\api\[...]^|.ts");
2113 #[cfg(not(windows))]
2114 let path = Path::new("/home/user/routes/api/[...]^|.ts");
2115
2116 let uri = path_to_uri(path).unwrap();
2117
2118 #[cfg(windows)]
2119 let expected = "file:///C:/home/user/routes/api/%5B...%5D%5E%7C.ts";
2120 #[cfg(not(windows))]
2121 let expected = "file:///home/user/routes/api/%5B...%5D%5E%7C.ts";
2122
2123 assert_eq!(uri.as_ref(), expected);
2124 assert_eq!(
2125 uri_to_path(&uri).as_deref(),
2126 Some(path),
2127 "encoded file URI should round-trip to the original path"
2128 );
2129 }
2130
2131 #[test]
2132 fn test_try_path_to_uri_returns_none_for_relative_path() {
2133 assert_eq!(try_path_to_uri(Path::new("relative/file.ts")), None);
2134 }
2135
2136 #[test]
2140 fn test_path_to_uri_returns_err_for_relative_path() {
2141 let err = path_to_uri(Path::new("relative/file.ts")).unwrap_err();
2142 assert!(matches!(err, Error::InvalidUri(_)));
2143 }
2144
2145 #[cfg(windows)]
2146 #[test]
2147 fn test_try_path_to_uri_encodes_synthetic_windows_root() {
2148 let uri = try_path_to_uri(Path::new("/home/user/#work %23")).unwrap();
2149
2150 assert_eq!(uri.as_ref(), "file:///home/user/%23work%20%2523");
2151 }
2152
2153 #[cfg(windows)]
2161 #[test]
2162 fn test_try_path_to_uri_accepts_rooted_but_not_absolute_windows_path() {
2163 let path = Path::new(r"\foo");
2164 assert!(path.has_root());
2165 assert!(!path.is_absolute());
2166
2167 let uri = try_path_to_uri(path).unwrap();
2168
2169 assert_eq!(uri.as_ref(), "file:///foo");
2170 }
2171
2172 #[test]
2173 fn test_path_to_uri_percent_encodes_reserved_chars_in_short_path() {
2174 #[cfg(windows)]
2176 let path = Path::new(r"C:\[a].ts");
2177 #[cfg(not(windows))]
2178 let path = Path::new("/[a].ts");
2179
2180 let uri = path_to_uri(path).unwrap();
2181
2182 assert!(
2183 uri.as_ref().ends_with("%5Ba%5D.ts"),
2184 "short path should percent-encode reserved chars, got {}",
2185 uri.as_ref()
2186 );
2187 assert_eq!(uri_to_path(&uri).as_deref(), Some(path));
2188 }
2189
2190 #[test]
2191 fn test_path_to_uri_percent_encodes_all_rfc3986_other_reserved_chars() {
2192 #[cfg(windows)]
2196 let path = Path::new(r"C:\home\user\test[]^|{}`.ts");
2197 #[cfg(not(windows))]
2198 let path = Path::new("/home/user/test[]^|{}`.ts");
2199
2200 let uri = try_path_to_uri(path).unwrap();
2201 let uri_str = uri.as_ref();
2202
2203 for (raw, encoded) in [
2204 ('[', "%5B"),
2205 (']', "%5D"),
2206 ('^', "%5E"),
2207 ('|', "%7C"),
2208 ('{', "%7B"),
2209 ('}', "%7D"),
2210 ('`', "%60"),
2211 ] {
2212 assert!(
2213 uri_str.contains(encoded),
2214 "expected {raw:?} to be percent-encoded as {encoded} in {uri_str}"
2215 );
2216 }
2217 assert!(
2218 !uri_str.contains(['[', ']', '^', '|', '{', '}', '`']),
2219 "no raw reserved characters should remain in {uri_str}"
2220 );
2221 }
2222
2223 #[tokio::test]
2224 async fn test_document_tracker_concurrent_operations() {
2225 let mut map = HashMap::new();
2226 map.insert("rs".to_string(), "rust".to_string());
2227
2228 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2229 let path1 = PathBuf::from("/test/file1.rs");
2230 let path2 = PathBuf::from("/test/file2.rs");
2231
2232 tracker.open(path1.clone(), "content1".to_string()).unwrap();
2233 tracker.open(path2.clone(), "content2".to_string()).unwrap();
2234
2235 assert_eq!(tracker.len(), 2);
2236 assert!(tracker.is_open(&path1));
2237 assert!(tracker.is_open(&path2));
2238
2239 tracker.update(&path1, "new content1".to_string()).await;
2240 assert_eq!(tracker.get(&path1).unwrap().content(), "new content1");
2241 assert_eq!(tracker.get(&path2).unwrap().content(), "content2");
2242
2243 tracker.close(&path1);
2244 assert_eq!(tracker.len(), 1);
2245 assert!(!tracker.is_open(&path1));
2246 assert!(tracker.is_open(&path2));
2247 }
2248
2249 #[test]
2250 fn test_empty_content() {
2251 let mut map = HashMap::new();
2252 map.insert("rs".to_string(), "rust".to_string());
2253
2254 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2255 let path = PathBuf::from("/test/empty.rs");
2256
2257 tracker.open(path.clone(), String::new()).unwrap();
2258 assert!(tracker.is_open(&path));
2259 assert_eq!(tracker.get(&path).unwrap().content(), "");
2260 }
2261
2262 #[test]
2263 fn test_unicode_content() {
2264 let mut map = HashMap::new();
2265 map.insert("rs".to_string(), "rust".to_string());
2266
2267 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2268 let path = PathBuf::from("/test/unicode.rs");
2269 let content = "fn テスト() { println!(\"こんにちは\"); }";
2270
2271 tracker.open(path.clone(), content.to_string()).unwrap();
2272 assert_eq!(tracker.get(&path).unwrap().content(), content);
2273 }
2274
2275 #[test]
2279 fn test_document_limit_exact_boundary() {
2280 let limits = ResourceLimits {
2281 max_documents: 5,
2282 max_file_size: 1000,
2283 };
2284 let mut map = HashMap::new();
2285 map.insert("rs".to_string(), "rust".to_string());
2286
2287 let tracker = DocumentTracker::new(limits, map);
2288
2289 for i in 0..5 {
2290 let path = PathBuf::from(format!("/test/file{i}.rs"));
2291 tracker.open(path.clone(), "content".to_string()).unwrap();
2292 mark_disk_verified(&tracker, &path);
2293 }
2294
2295 assert_eq!(tracker.len(), 5);
2296
2297 tracker
2298 .open(PathBuf::from("/test/file6.rs"), "content".to_string())
2299 .unwrap();
2300
2301 assert_eq!(tracker.len(), 5);
2302 assert!(!tracker.is_open(Path::new("/test/file0.rs")));
2303 assert!(tracker.is_open(Path::new("/test/file6.rs")));
2304 }
2305
2306 #[test]
2307 fn test_file_size_exact_boundary() {
2308 let limits = ResourceLimits {
2309 max_documents: 10,
2310 max_file_size: 100,
2311 };
2312 let mut map = HashMap::new();
2313 map.insert("rs".to_string(), "rust".to_string());
2314
2315 let tracker = DocumentTracker::new(limits, map);
2316
2317 let exact_size_content = "x".repeat(100);
2318 tracker
2319 .open(PathBuf::from("/test/exact.rs"), exact_size_content)
2320 .unwrap();
2321
2322 let over_size_content = "x".repeat(101);
2323 let result = tracker.open(PathBuf::from("/test/over.rs"), over_size_content);
2324 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
2325 }
2326
2327 #[test]
2328 fn test_detect_language_with_custom_extension() {
2329 let mut map = HashMap::new();
2330 map.insert("nu".to_string(), "nushell".to_string());
2331
2332 assert_eq!(detect_language(Path::new("script.nu"), &map), "nushell");
2333
2334 let empty_map = HashMap::new();
2335 assert_eq!(
2336 detect_language(Path::new("script.nu"), &empty_map),
2337 "plaintext"
2338 );
2339 }
2340
2341 #[test]
2342 fn test_detect_language_custom_overrides_default() {
2343 let mut custom_map = HashMap::new();
2344 custom_map.insert("rs".to_string(), "custom-rust".to_string());
2345
2346 assert_eq!(
2347 detect_language(Path::new("main.rs"), &custom_map),
2348 "custom-rust"
2349 );
2350
2351 let mut default_map = HashMap::new();
2352 default_map.insert("rs".to_string(), "rust".to_string());
2353
2354 assert_eq!(detect_language(Path::new("main.rs"), &default_map), "rust");
2355 }
2356
2357 #[test]
2358 fn test_detect_language_fallback_to_plaintext() {
2359 let mut map = HashMap::new();
2360 map.insert("nu".to_string(), "nushell".to_string());
2361
2362 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
2364 }
2365
2366 #[test]
2367 fn test_detect_language_empty_map() {
2368 let map = HashMap::new();
2369 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
2370 }
2371
2372 #[test]
2373 fn test_document_tracker_with_extensions() {
2374 let mut map = HashMap::new();
2375 map.insert("nu".to_string(), "nushell".to_string());
2376
2377 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2378
2379 let path = PathBuf::from("/test/script.nu");
2380 tracker
2381 .open(path.clone(), "# nushell script".to_string())
2382 .unwrap();
2383
2384 let state = tracker.get(&path).unwrap();
2385 assert_eq!(state.language_id(), "nushell");
2386 }
2387
2388 #[test]
2389 fn test_document_tracker_uses_provided_map() {
2390 let mut map = HashMap::new();
2391 map.insert("rs".to_string(), "rust".to_string());
2392
2393 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2394 let path = PathBuf::from("/test/main.rs");
2395 tracker
2396 .open(path.clone(), "fn main() {}".to_string())
2397 .unwrap();
2398
2399 let state = tracker.get(&path).unwrap();
2400 assert_eq!(state.language_id(), "rust");
2401 }
2402
2403 #[test]
2404 fn test_multiple_extensions_same_language() {
2405 let mut map = HashMap::new();
2406 map.insert("cpp".to_string(), "c++".to_string());
2407 map.insert("cc".to_string(), "c++".to_string());
2408 map.insert("cxx".to_string(), "c++".to_string());
2409
2410 assert_eq!(detect_language(Path::new("main.cpp"), &map), "c++");
2411 assert_eq!(detect_language(Path::new("main.cc"), &map), "c++");
2412 assert_eq!(detect_language(Path::new("main.cxx"), &map), "c++");
2413 }
2414
2415 #[test]
2416 fn test_case_sensitive_extensions() {
2417 let mut map = HashMap::new();
2418 map.insert("NU".to_string(), "nushell".to_string());
2419
2420 assert_eq!(detect_language(Path::new("script.nu"), &map), "plaintext");
2422 }
2423
2424 #[cfg(unix)]
2429 #[test]
2430 fn test_uri_to_path_file_scheme() {
2431 let uri: Uri = Uri::from("file:///home/user/main.rs");
2432 let path = uri_to_path(&uri).unwrap();
2433 assert_eq!(path, PathBuf::from("/home/user/main.rs"));
2434 }
2435
2436 #[test]
2437 fn test_uri_to_path_non_file_scheme_returns_none() {
2438 let uri: Uri = Uri::from("https://example.com/file.rs");
2439 assert!(uri_to_path(&uri).is_none());
2440 }
2441
2442 #[test]
2443 fn test_uri_to_path_lsp_diagnostics_scheme_returns_none() {
2444 let uri: Uri = Uri::from("lsp-diagnostics:///home/user/main.rs");
2446 assert!(uri_to_path(&uri).is_none());
2447 }
2448
2449 #[test]
2450 fn test_uri_to_path_with_authority_returns_none() {
2451 let result = uri_to_path(&Uri::from("file://server/share/path.rs"));
2455 assert!(result.is_none());
2456 }
2457
2458 #[test]
2463 fn test_open_paths_empty_tracker() {
2464 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2465 assert_eq!(tracker.open_paths().len(), 0);
2466 }
2467
2468 #[test]
2469 fn test_open_paths_populated_tracker() {
2470 let mut map = HashMap::new();
2471 map.insert("rs".to_string(), "rust".to_string());
2472 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2473 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
2474 tracker.open(PathBuf::from("/b.rs"), String::new()).unwrap();
2475 let mut paths = tracker.open_paths();
2476 paths.sort();
2477 assert_eq!(paths, [PathBuf::from("/a.rs"), PathBuf::from("/b.rs")]);
2478 }
2479
2480 #[test]
2481 fn test_open_paths_after_close() {
2482 let mut map = HashMap::new();
2483 map.insert("rs".to_string(), "rust".to_string());
2484 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2485 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
2486 tracker.close(Path::new("/a.rs"));
2487 assert_eq!(tracker.open_paths().len(), 0);
2488 }
2489
2490 use tempfile::TempDir;
2495 use tokio::io::BufReader;
2496
2497 use crate::test_lsp::{fake_lsp_client, read_framed_message};
2498
2499 fn set_mtime(path: &Path, time: SystemTime) {
2507 let file = std::fs::OpenOptions::new().write(true).open(path).unwrap();
2508 file.set_modified(time).unwrap();
2509 }
2510
2511 fn settled_past() -> SystemTime {
2512 SystemTime::now() - Duration::from_secs(10)
2513 }
2514
2515 #[test]
2516 fn test_mtime_settled_boundary() {
2517 let read_at = SystemTime::now();
2518 assert!(!mtime_settled(None, read_at), "no mtime is never settled");
2519 assert!(
2520 mtime_settled(Some(read_at - Duration::from_secs(3)), read_at),
2521 "3s older than read_at is past the 2s granularity margin"
2522 );
2523 assert!(
2524 !mtime_settled(Some(read_at - Duration::from_secs(1)), read_at),
2525 "1s older than read_at is within the 2s granularity margin"
2526 );
2527 assert!(
2528 !mtime_settled(Some(read_at + Duration::from_secs(10)), read_at),
2529 "an mtime after read_at is never settled"
2530 );
2531 }
2532
2533 #[tokio::test]
2534 async fn test_ensure_open_unchanged_file_is_fast_path() {
2535 let dir = TempDir::new().unwrap();
2536 let path = dir.path().join("a.rs");
2537 std::fs::write(&path, "fn main() {}").unwrap();
2538 set_mtime(&path, settled_past());
2539
2540 let (client, _server) = fake_lsp_client();
2541 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2542
2543 let uri1 = tracker
2544 .ensure_open(&path, &ServerId::from("rust"), &client)
2545 .await
2546 .unwrap();
2547 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2548
2549 let uri2 = tracker
2550 .ensure_open(&path, &ServerId::from("rust"), &client)
2551 .await
2552 .unwrap();
2553 assert_eq!(uri1, uri2);
2554 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2555 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
2556 }
2557
2558 #[tokio::test]
2559 async fn test_ensure_open_resyncs_on_size_change() {
2560 let dir = TempDir::new().unwrap();
2561 let path = dir.path().join("a.rs");
2562 std::fs::write(&path, "fn main() {}").unwrap();
2563 set_mtime(&path, settled_past());
2564
2565 let (client, _server) = fake_lsp_client();
2566 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2567 tracker
2568 .ensure_open(&path, &ServerId::from("rust"), &client)
2569 .await
2570 .unwrap();
2571
2572 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
2573 set_mtime(&path, settled_past());
2574
2575 tracker
2576 .ensure_open(&path, &ServerId::from("rust"), &client)
2577 .await
2578 .unwrap();
2579 let state = tracker.get(&path).unwrap();
2580 assert_eq!(state.version(), 2);
2581 assert_eq!(state.content(), "fn main() { println!(\"hi\"); }");
2582 }
2583
2584 #[tokio::test(start_paused = true)]
2585 async fn test_ensure_open_regression_102_103_racy_same_size_rewrite() {
2586 let dir = TempDir::new().unwrap();
2587 let path = dir.path().join("a.rs");
2588 std::fs::write(&path, "AAAA").unwrap();
2589 let (client, _server) = fake_lsp_client();
2592 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2593 tracker
2594 .ensure_open(&path, &ServerId::from("rust"), &client)
2595 .await
2596 .unwrap();
2597 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
2598
2599 std::fs::write(&path, "BBBB").unwrap();
2602 set_mtime(&path, original_mtime);
2603
2604 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
2605
2606 tracker
2607 .ensure_open(&path, &ServerId::from("rust"), &client)
2608 .await
2609 .unwrap();
2610 let state = tracker.get(&path).unwrap();
2611 assert_eq!(
2612 state.version(),
2613 2,
2614 "must resync despite identical (mtime, size)"
2615 );
2616 assert_eq!(state.content(), "BBBB");
2617 }
2618
2619 #[tokio::test(start_paused = true)]
2620 async fn test_ensure_open_regression_102_103_settled_mtime_is_the_documented_limit() {
2621 let dir = TempDir::new().unwrap();
2622 let path = dir.path().join("a.rs");
2623 std::fs::write(&path, "AAAA").unwrap();
2624 set_mtime(&path, settled_past());
2625
2626 let (client, _server) = fake_lsp_client();
2627 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2628 tracker
2629 .ensure_open(&path, &ServerId::from("rust"), &client)
2630 .await
2631 .unwrap();
2632 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
2633
2634 std::fs::write(&path, "BBBB").unwrap();
2638 set_mtime(&path, original_mtime);
2639
2640 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
2641
2642 tracker
2643 .ensure_open(&path, &ServerId::from("rust"), &client)
2644 .await
2645 .unwrap();
2646 let state = tracker.get(&path).unwrap();
2647 assert_eq!(state.version(), 1, "documented limitation: fast path taken");
2648 assert_eq!(state.content(), "AAAA");
2649 }
2650
2651 #[tokio::test(start_paused = true)]
2652 async fn test_ensure_open_stat_is_never_debounced() {
2653 let dir = TempDir::new().unwrap();
2654 let path = dir.path().join("a.rs");
2655 std::fs::write(&path, "AAAA").unwrap();
2656 set_mtime(&path, settled_past());
2657
2658 let (client, _server) = fake_lsp_client();
2659 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2660 tracker
2661 .ensure_open(&path, &ServerId::from("rust"), &client)
2662 .await
2663 .unwrap();
2664
2665 std::fs::write(&path, "BBBBBBBB").unwrap();
2668 tracker
2669 .ensure_open(&path, &ServerId::from("rust"), &client)
2670 .await
2671 .unwrap();
2672
2673 let state = tracker.get(&path).unwrap();
2674 assert_eq!(state.version(), 2);
2675 assert_eq!(state.content(), "BBBBBBBB");
2676 }
2677
2678 #[tokio::test(start_paused = true)]
2679 async fn test_ensure_open_debounce_gates_reread_only() {
2680 let dir = TempDir::new().unwrap();
2681 let path = dir.path().join("a.rs");
2682 std::fs::write(&path, "AAAA").unwrap();
2683 let (client, _server) = fake_lsp_client();
2686 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2687 tracker
2688 .ensure_open(&path, &ServerId::from("rust"), &client)
2689 .await
2690 .unwrap();
2691 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
2692
2693 std::fs::write(&path, "BBBB").unwrap(); set_mtime(&path, original_mtime); tracker
2698 .ensure_open(&path, &ServerId::from("rust"), &client)
2699 .await
2700 .unwrap();
2701 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2702
2703 tokio::time::advance(Duration::from_millis(300)).await;
2704 tracker
2705 .ensure_open(&path, &ServerId::from("rust"), &client)
2706 .await
2707 .unwrap();
2708 let state = tracker.get(&path).unwrap();
2709 assert_eq!(state.version(), 2);
2710 assert_eq!(state.content(), "BBBB");
2711 }
2712
2713 #[tokio::test]
2714 async fn test_ensure_open_deleted_file_errors_state_untouched() {
2715 let dir = TempDir::new().unwrap();
2716 let path = dir.path().join("a.rs");
2717 std::fs::write(&path, "fn main() {}").unwrap();
2718 set_mtime(&path, settled_past());
2719
2720 let (client, _server) = fake_lsp_client();
2721 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2722 tracker
2723 .ensure_open(&path, &ServerId::from("rust"), &client)
2724 .await
2725 .unwrap();
2726
2727 std::fs::remove_file(&path).unwrap();
2728
2729 let result = tracker
2730 .ensure_open(&path, &ServerId::from("rust"), &client)
2731 .await;
2732 assert!(matches!(result, Err(Error::FileIo { .. })));
2733 assert!(tracker.is_open(&path));
2734 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2735 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
2736 }
2737
2738 #[tokio::test]
2739 async fn test_ensure_open_grows_past_limit_errors_state_intact() {
2740 let dir = TempDir::new().unwrap();
2741 let path = dir.path().join("a.rs");
2742 std::fs::write(&path, "small").unwrap();
2743 set_mtime(&path, settled_past());
2744
2745 let limits = ResourceLimits {
2746 max_documents: 10,
2747 max_file_size: 10,
2748 };
2749 let (client, _server) = fake_lsp_client();
2750 let tracker = DocumentTracker::new(limits, HashMap::new());
2751 tracker
2752 .ensure_open(&path, &ServerId::from("rust"), &client)
2753 .await
2754 .unwrap();
2755
2756 std::fs::write(&path, "x".repeat(100)).unwrap();
2757
2758 let result = tracker
2759 .ensure_open(&path, &ServerId::from("rust"), &client)
2760 .await;
2761 assert!(matches!(result, Err(Error::FileSizeLimitExceeded { .. })));
2762 assert_eq!(tracker.get(&path).unwrap().content(), "small");
2763 assert_eq!(tracker.get(&path).unwrap().version(), 1);
2764 }
2765
2766 #[tokio::test]
2767 async fn test_ensure_open_resync_at_document_capacity() {
2768 let dir = TempDir::new().unwrap();
2769 let path = dir.path().join("a.rs");
2770 std::fs::write(&path, "AAAA").unwrap();
2771 set_mtime(&path, settled_past());
2772
2773 let limits = ResourceLimits {
2774 max_documents: 1,
2775 max_file_size: 0,
2776 };
2777 let (client, _server) = fake_lsp_client();
2778 let tracker = DocumentTracker::new(limits, HashMap::new());
2779 tracker
2780 .ensure_open(&path, &ServerId::from("rust"), &client)
2781 .await
2782 .unwrap();
2783 assert_eq!(tracker.len(), 1);
2784
2785 std::fs::write(&path, "BBBBBBBB").unwrap();
2786 let result = tracker
2787 .ensure_open(&path, &ServerId::from("rust"), &client)
2788 .await;
2789 assert!(
2790 result.is_ok(),
2791 "resync must not re-run the doc-count check on an already-tracked path"
2792 );
2793 assert_eq!(tracker.len(), 1);
2794 assert_eq!(tracker.get(&path).unwrap().version(), 2);
2795 }
2796
2797 #[tokio::test]
2798 async fn test_update_clears_disk_provenance() {
2799 let dir = TempDir::new().unwrap();
2800 let path = dir.path().join("a.rs");
2801 std::fs::write(&path, "fn main() {}").unwrap();
2802 set_mtime(&path, settled_past());
2803
2804 let (client, _server) = fake_lsp_client();
2805 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2806 tracker
2807 .ensure_open(&path, &ServerId::from("rust"), &client)
2808 .await
2809 .unwrap();
2810 assert!(tracker.get(&path).unwrap().disk.is_some());
2811
2812 tracker
2813 .update(&path, "fn main() { updated(); }".to_string())
2814 .await;
2815 assert!(
2816 tracker.get(&path).unwrap().disk.is_none(),
2817 "update() must clear disk provenance so the next ensure_open re-verifies by content"
2818 );
2819 }
2820
2821 #[tokio::test]
2822 async fn test_first_open_self_heals_when_did_open_notify_fails() {
2823 let dir = TempDir::new().unwrap();
2824 let path = dir.path().join("a.rs");
2825 std::fs::write(&path, "fn main() {}").unwrap();
2826
2827 let (client, _server) = fake_lsp_client();
2828 let notify_will_fail = client.clone();
2834 client.shutdown().await.unwrap();
2835
2836 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2837 let result = tracker
2838 .ensure_open(&path, &ServerId::from("rust"), ¬ify_will_fail)
2839 .await;
2840
2841 assert!(result.is_err(), "notify failure must propagate as an error");
2842 assert!(
2843 !tracker.is_open(&path),
2844 "a failed didOpen must not leave the document tracked, or the server \
2845 and tracker would stay permanently desynced"
2846 );
2847 }
2848
2849 #[tokio::test]
2850 async fn test_resync_sends_didchange_with_full_replacement_over_the_wire() {
2851 let dir = TempDir::new().unwrap();
2852 let path = dir.path().join("a.rs");
2853 std::fs::write(&path, "fn main() {}").unwrap();
2854 set_mtime(&path, settled_past());
2855
2856 let (client, mut server) = fake_lsp_client();
2857 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2858 tracker
2859 .ensure_open(&path, &ServerId::from("rust"), &client)
2860 .await
2861 .unwrap();
2862
2863 let mut wire = BufReader::new(&mut server.write_stdout);
2864 let opened = read_framed_message(&mut wire).await;
2865 assert_eq!(opened["method"], "textDocument/didOpen");
2866
2867 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
2868 set_mtime(&path, settled_past());
2869 tracker
2870 .ensure_open(&path, &ServerId::from("rust"), &client)
2871 .await
2872 .unwrap();
2873
2874 let changed = read_framed_message(&mut wire).await;
2875 assert_eq!(changed["method"], "textDocument/didChange");
2876 let params = &changed["params"];
2877 assert_eq!(params["textDocument"]["version"], 2);
2878 let change = ¶ms["contentChanges"][0];
2879 assert!(
2880 change.get("range").is_none(),
2881 "range must be omitted, not null, for a full-replacement change"
2882 );
2883 assert!(
2884 change.get("rangeLength").is_none(),
2885 "rangeLength must be omitted, not null, for a full-replacement change"
2886 );
2887 assert_eq!(change["text"], "fn main() { println!(\"hi\"); }");
2888 }
2889
2890 #[tokio::test]
2895 async fn test_ensure_open_second_server_gets_didopen_no_disk_change() {
2896 let dir = TempDir::new().unwrap();
2897 let path = dir.path().join("a.rs");
2898 std::fs::write(&path, "fn main() {}").unwrap();
2899 set_mtime(&path, settled_past());
2900
2901 let (client_a, mut server_a) = fake_lsp_client();
2902 let (client_b, mut server_b) = fake_lsp_client();
2903 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2904
2905 let id_a = ServerId::from("server-a");
2906 let id_b = ServerId::from("server-b");
2907
2908 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
2909 let mut wire_a = BufReader::new(&mut server_a.write_stdout);
2910 let opened_a = read_framed_message(&mut wire_a).await;
2911 assert_eq!(opened_a["method"], "textDocument/didOpen");
2912
2913 tracker.ensure_open(&path, &id_b, &client_b).await.unwrap();
2917 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
2918 let opened_b = read_framed_message(&mut wire_b).await;
2919 assert_eq!(opened_b["method"], "textDocument/didOpen");
2920 assert_eq!(opened_b["params"]["textDocument"]["version"], 1);
2921 assert_eq!(opened_b["params"]["textDocument"]["text"], "fn main() {}");
2922 }
2923
2924 #[tokio::test(start_paused = true)]
2928 async fn test_ensure_open_second_server_gets_didopen_unchanged_content_path() {
2929 let dir = TempDir::new().unwrap();
2930 let path = dir.path().join("a.rs");
2931 std::fs::write(&path, "fn main() {}").unwrap();
2932 let (client_a, _server_a) = fake_lsp_client();
2935 let (client_b, mut server_b) = fake_lsp_client();
2936 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2937
2938 tracker
2939 .ensure_open(&path, &ServerId::from("server-a"), &client_a)
2940 .await
2941 .unwrap();
2942
2943 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
2946
2947 tracker
2948 .ensure_open(&path, &ServerId::from("server-b"), &client_b)
2949 .await
2950 .unwrap();
2951 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
2952 let opened_b = read_framed_message(&mut wire_b).await;
2953 assert_eq!(opened_b["method"], "textDocument/didOpen");
2954 }
2955
2956 #[tokio::test]
2963 async fn test_ensure_open_same_server_twice_sends_nothing_second_time() {
2964 let dir = TempDir::new().unwrap();
2965 let path = dir.path().join("a.rs");
2966 std::fs::write(&path, "fn main() {}").unwrap();
2967 set_mtime(&path, settled_past());
2968
2969 let (client, mut server) = fake_lsp_client();
2970 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2971 let id = ServerId::from("rust");
2972
2973 tracker.ensure_open(&path, &id, &client).await.unwrap();
2974 tracker.ensure_open(&path, &id, &client).await.unwrap();
2975
2976 let mut wire = BufReader::new(&mut server.write_stdout);
2977 let opened = read_framed_message(&mut wire).await;
2978 assert_eq!(opened["method"], "textDocument/didOpen");
2979 assert_eq!(
2980 tracker.get(&path).unwrap().synced_version(&id),
2981 Some(1),
2982 "second call for the same server must not re-open or re-change"
2983 );
2984 }
2985
2986 #[tokio::test]
2990 async fn test_sync_phase_failed_didchange_does_not_disturb_other_server() {
2991 let dir = TempDir::new().unwrap();
2992 let path = dir.path().join("a.rs");
2993 std::fs::write(&path, "fn main() {}").unwrap();
2994 set_mtime(&path, settled_past());
2995
2996 let (client_a, _server_a) = fake_lsp_client();
2997 let (client_b, _server_b) = fake_lsp_client();
2998 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2999 let id_a = ServerId::from("server-a");
3000 let id_b = ServerId::from("server-b");
3001
3002 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
3003 tracker.ensure_open(&path, &id_b, &client_b).await.unwrap();
3004
3005 let client_b_will_fail = client_b.clone();
3008 client_b.shutdown().await.unwrap();
3009
3010 std::fs::write(&path, "fn main() { updated(); }").unwrap();
3011 set_mtime(&path, settled_past());
3012
3013 let result = tracker.ensure_open(&path, &id_b, &client_b_will_fail).await;
3014 assert!(result.is_err(), "B's didChange must fail and propagate");
3015
3016 assert!(tracker.is_open(&path));
3022 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
3023 assert_eq!(tracker.get(&path).unwrap().version(), 1);
3024 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_a), Some(1));
3025 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_b), Some(1));
3026
3027 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
3031 assert_eq!(
3032 tracker.get(&path).unwrap().content(),
3033 "fn main() { updated(); }"
3034 );
3035 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_a), Some(2));
3036 assert_eq!(tracker.get(&path).unwrap().synced_version(&id_b), Some(1));
3037 }
3038
3039 #[tokio::test]
3062 async fn test_ensure_open_different_paths_do_not_serialize() {
3063 let dir = TempDir::new().unwrap();
3064 let path_a = dir.path().join("a.rs");
3065 let path_b = dir.path().join("b.rs");
3066
3067 std::fs::write(&path_a, "fn a() {}").unwrap();
3068 std::fs::write(&path_b, "fn b() {}").unwrap();
3069 set_mtime(&path_a, settled_past());
3070 set_mtime(&path_b, settled_past());
3071
3072 let (client_a, _server_a) = fake_lsp_client();
3073 let (client_b, _server_b) = fake_lsp_client();
3074 let tracker = Arc::new(DocumentTracker::new(
3075 ResourceLimits::default(),
3076 HashMap::new(),
3077 ));
3078
3079 let path_a_guard = tracker.lock_path(&path_a).await;
3080
3081 let tracker_for_a = Arc::clone(&tracker);
3085 let path_a_for_task = path_a.clone();
3086 let handle_a = tokio::spawn(async move {
3087 tracker_for_a
3088 .ensure_open(&path_a_for_task, &ServerId::from("server-a"), &client_a)
3089 .await
3090 });
3091
3092 tokio::time::sleep(Duration::from_millis(200)).await;
3095
3096 tokio::time::timeout(
3099 Duration::from_secs(5),
3100 tracker.ensure_open(&path_b, &ServerId::from("server-b"), &client_b),
3101 )
3102 .await
3103 .unwrap()
3104 .unwrap();
3105
3106 drop(path_a_guard);
3107
3108 handle_a.await.unwrap().unwrap();
3109 assert_eq!(tracker.get(&path_a).unwrap().content(), "fn a() {}");
3110 }
3111
3112 #[tokio::test]
3125 async fn test_update_serializes_with_concurrent_ensure_open_same_path() {
3126 let dir = TempDir::new().unwrap();
3127 let path = dir.path().join("a.rs");
3128 std::fs::write(&path, "fn a() {}").unwrap();
3129 set_mtime(&path, settled_past());
3130
3131 let (client, _server) = fake_lsp_client();
3132 let tracker = Arc::new(DocumentTracker::new(
3133 ResourceLimits::default(),
3134 HashMap::new(),
3135 ));
3136
3137 let path_guard = tracker.lock_path(&path).await;
3138
3139 let tracker_for_open = Arc::clone(&tracker);
3143 let path_for_task = path.clone();
3144 let handle_open = tokio::spawn(async move {
3145 tracker_for_open
3146 .ensure_open(&path_for_task, &ServerId::from("rust"), &client)
3147 .await
3148 });
3149
3150 tokio::time::sleep(Duration::from_millis(200)).await;
3153
3154 let update_while_blocked = tokio::time::timeout(
3158 Duration::from_millis(300),
3159 tracker.update(&path, "raced content".to_string()),
3160 )
3161 .await;
3162 assert!(
3163 update_while_blocked.is_err(),
3164 "update() must block while ensure_open holds the per-path lock for the same path"
3165 );
3166
3167 drop(path_guard);
3168
3169 handle_open.await.unwrap().unwrap();
3170 assert_eq!(tracker.get(&path).unwrap().content(), "fn a() {}");
3171 assert_eq!(tracker.get(&path).unwrap().version(), 1);
3172
3173 let new_version = tracker
3176 .update(&path, "fn a() { updated(); }".to_string())
3177 .await;
3178 assert_eq!(new_version, Some(2));
3179 assert_eq!(
3180 tracker.get(&path).unwrap().content(),
3181 "fn a() { updated(); }"
3182 );
3183 }
3184
3185 #[tokio::test]
3191 async fn test_ensure_open_concurrent_same_path_single_didopen() {
3192 let dir = TempDir::new().unwrap();
3193 let path = dir.path().join("a.rs");
3194 std::fs::write(&path, "fn main() {}").unwrap();
3195 set_mtime(&path, settled_past());
3196
3197 let (client, mut server) = fake_lsp_client();
3198 let tracker = Arc::new(DocumentTracker::new(
3199 ResourceLimits::default(),
3200 HashMap::new(),
3201 ));
3202 let id = ServerId::from("rust");
3203
3204 let mut handles = Vec::new();
3205 for _ in 0..8 {
3206 let tracker = Arc::clone(&tracker);
3207 let client = client.clone();
3208 let path = path.clone();
3209 let id = id.clone();
3210 handles.push(tokio::spawn(async move {
3211 tracker.ensure_open(&path, &id, &client).await
3212 }));
3213 }
3214 for handle in handles {
3215 handle.await.unwrap().unwrap();
3216 }
3217
3218 let mut wire = BufReader::new(&mut server.write_stdout);
3219 let opened = read_framed_message(&mut wire).await;
3220 assert_eq!(opened["method"], "textDocument/didOpen");
3221
3222 let extra =
3225 tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut wire)).await;
3226 assert!(
3227 extra.is_err(),
3228 "expected no additional notification after the single didOpen"
3229 );
3230
3231 assert_eq!(tracker.get(&path).unwrap().synced_version(&id), Some(1));
3232 assert_eq!(tracker.get(&path).unwrap().version(), 1);
3233 }
3234
3235 #[tokio::test]
3242 async fn test_ensure_open_path_locks_evicted_after_completion() {
3243 let dir = TempDir::new().unwrap();
3244 let paths: Vec<_> = ["a.rs", "b.rs", "c.rs"]
3245 .iter()
3246 .map(|name| dir.path().join(name))
3247 .collect();
3248 for path in &paths {
3249 std::fs::write(path, "fn f() {}").unwrap();
3250 set_mtime(path, settled_past());
3251 }
3252
3253 let tracker = Arc::new(DocumentTracker::new(
3254 ResourceLimits::default(),
3255 HashMap::new(),
3256 ));
3257 let id = ServerId::from("rust");
3258
3259 let mut handles = Vec::new();
3260 let mut servers = Vec::new();
3261 for path in paths.clone() {
3262 let tracker = Arc::clone(&tracker);
3263 let (client, server) = fake_lsp_client();
3264 servers.push(server);
3265 let id = id.clone();
3266 handles.push(tokio::spawn(async move {
3267 tracker.ensure_open(&path, &id, &client).await
3268 }));
3269 }
3270 for handle in handles {
3271 handle.await.unwrap().unwrap();
3272 }
3273 drop(servers);
3274
3275 assert!(
3276 lock_std(&tracker.path_locks).is_empty(),
3277 "path_locks must be fully evicted once every ensure_open call \
3278 for every path has completed, otherwise the map grows \
3279 unbounded for the lifetime of the process"
3280 );
3281 }
3282
3283 #[cfg(unix)]
3295 #[tokio::test]
3296 async fn test_read_to_string_checked_rejects_fifo() {
3297 let dir = TempDir::new().unwrap();
3298 let path = dir.path().join("fifo");
3299 let status = std::process::Command::new("mkfifo")
3300 .arg(&path)
3301 .status()
3302 .unwrap();
3303 assert!(status.success(), "mkfifo must succeed to set up this test");
3304
3305 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3306 let result = tokio::time::timeout(
3309 Duration::from_secs(5),
3310 tracker.read_to_string_checked(&path),
3311 )
3312 .await
3313 .unwrap();
3314
3315 assert!(matches!(result, Err(Error::NotARegularFile(_))));
3316 }
3317
3318 #[cfg(windows)]
3325 #[tokio::test]
3326 async fn test_check_disk_file_type_accepts_regular_rejects_nul() {
3327 let dir = TempDir::new().unwrap();
3328 let path = dir.path().join("regular.txt");
3329 std::fs::write(&path, "hello").unwrap();
3330
3331 let regular = fs::File::open(&path).await.unwrap();
3332 assert!(check_disk_file_type(®ular, &path).is_ok());
3333
3334 let nul_path = PathBuf::from("NUL");
3335 let nul = fs::File::open(&nul_path).await.unwrap();
3336 assert!(matches!(
3337 check_disk_file_type(&nul, &nul_path),
3338 Err(Error::NotARegularFile(_))
3339 ));
3340 }
3341
3342 #[cfg(windows)]
3351 #[tokio::test]
3352 async fn test_read_to_string_checked_rejects_nul_device() {
3353 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3354 let path = PathBuf::from("NUL");
3355 let result = tokio::time::timeout(
3356 Duration::from_secs(5),
3357 tracker.read_to_string_checked(&path),
3358 )
3359 .await
3360 .unwrap();
3361
3362 assert!(matches!(result, Err(Error::NotARegularFile(_))));
3363 }
3364
3365 #[tokio::test]
3371 async fn test_read_to_string_checked_size_boundary() {
3372 let dir = TempDir::new().unwrap();
3373 let path = dir.path().join("boundary.rs");
3374 let tracker = DocumentTracker::new(
3375 ResourceLimits {
3376 max_documents: 100,
3377 max_file_size: 10,
3378 },
3379 HashMap::new(),
3380 );
3381
3382 std::fs::write(&path, "a".repeat(10)).unwrap();
3383 let (content, ..) = tracker.read_to_string_checked(&path).await.unwrap();
3384 assert_eq!(content.len(), 10);
3385
3386 std::fs::write(&path, "a".repeat(11)).unwrap();
3387 let result = tracker.read_to_string_checked(&path).await;
3388 assert!(matches!(
3389 result,
3390 Err(Error::FileSizeLimitExceeded { size: 11, max: 10 })
3391 ));
3392 }
3393
3394 #[tokio::test]
3400 async fn test_read_line_checked_does_not_read_past_target_line() {
3401 let dir = TempDir::new().unwrap();
3402 let path = dir.path().join("partial.rs");
3403 let mut content = b"hello\n".to_vec();
3404 content.extend_from_slice(&[0xFF, 0xFE]);
3405 content.push(b'\n');
3406 std::fs::write(&path, &content).unwrap();
3407
3408 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3409 let line = tracker.read_line_checked(&path, 0, u64::MAX).await.unwrap();
3410 assert_eq!(line.text.as_deref(), Some("hello"));
3411 }
3412
3413 #[tokio::test]
3417 async fn test_read_line_checked_returns_requested_non_zero_line() {
3418 let dir = TempDir::new().unwrap();
3419 let path = dir.path().join("multi.rs");
3420 std::fs::write(&path, "first\nsecond\nthird\nfourth\n").unwrap();
3421
3422 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3423 assert_eq!(
3424 tracker
3425 .read_line_checked(&path, 2, u64::MAX)
3426 .await
3427 .unwrap()
3428 .text
3429 .as_deref(),
3430 Some("third")
3431 );
3432 }
3433
3434 #[tokio::test]
3438 async fn test_read_line_checked_returns_none_past_last_line() {
3439 let dir = TempDir::new().unwrap();
3440 let path = dir.path().join("short.rs");
3441 std::fs::write(&path, "only one line").unwrap();
3442
3443 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3444 assert_eq!(
3445 tracker
3446 .read_line_checked(&path, 5, u64::MAX)
3447 .await
3448 .unwrap()
3449 .text,
3450 None
3451 );
3452 }
3453
3454 #[tokio::test]
3459 async fn test_read_line_checked_reads_last_line_without_trailing_newline() {
3460 let dir = TempDir::new().unwrap();
3461 let path = dir.path().join("no_newline.rs");
3462 std::fs::write(&path, "only one line").unwrap();
3463
3464 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3465 assert_eq!(
3466 tracker
3467 .read_line_checked(&path, 0, u64::MAX)
3468 .await
3469 .unwrap()
3470 .text
3471 .as_deref(),
3472 Some("only one line")
3473 );
3474 }
3475
3476 #[tokio::test]
3477 async fn test_read_line_checked_empty_file_returns_none() {
3478 let dir = TempDir::new().unwrap();
3479 let path = dir.path().join("empty.rs");
3480 std::fs::write(&path, "").unwrap();
3481
3482 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3483 assert_eq!(
3484 tracker
3485 .read_line_checked(&path, 0, u64::MAX)
3486 .await
3487 .unwrap()
3488 .text,
3489 None
3490 );
3491 }
3492
3493 #[tokio::test]
3498 async fn test_read_line_checked_strips_crlf_line_ending() {
3499 let dir = TempDir::new().unwrap();
3500 let path = dir.path().join("crlf.rs");
3501 std::fs::write(&path, "first\r\nsecond\r\n").unwrap();
3502
3503 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3504 assert_eq!(
3505 tracker
3506 .read_line_checked(&path, 0, u64::MAX)
3507 .await
3508 .unwrap()
3509 .text
3510 .as_deref(),
3511 Some("first")
3512 );
3513 assert_eq!(
3514 tracker
3515 .read_line_checked(&path, 1, u64::MAX)
3516 .await
3517 .unwrap()
3518 .text
3519 .as_deref(),
3520 Some("second")
3521 );
3522 }
3523
3524 #[tokio::test]
3531 async fn test_read_line_checked_matches_str_lines_crlf_semantics() {
3532 let dir = TempDir::new().unwrap();
3533 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3534
3535 let double_cr = "abc\r\r\n";
3536 let path_a = dir.path().join("double_cr.rs");
3537 std::fs::write(&path_a, double_cr).unwrap();
3538 assert_eq!(
3539 tracker
3540 .read_line_checked(&path_a, 0, u64::MAX)
3541 .await
3542 .unwrap()
3543 .text
3544 .as_deref(),
3545 double_cr.lines().next()
3546 );
3547
3548 let trailing_cr_no_newline = "abc\r";
3549 let path_b = dir.path().join("trailing_cr_no_newline.rs");
3550 std::fs::write(&path_b, trailing_cr_no_newline).unwrap();
3551 assert_eq!(
3552 tracker
3553 .read_line_checked(&path_b, 0, u64::MAX)
3554 .await
3555 .unwrap()
3556 .text
3557 .as_deref(),
3558 trailing_cr_no_newline.lines().next()
3559 );
3560 }
3561
3562 #[tokio::test]
3568 async fn test_read_line_checked_exact_max_file_size_reads_to_eof_without_error() {
3569 let dir = TempDir::new().unwrap();
3570 let path = dir.path().join("exact.rs");
3571 let content = "a".repeat(20);
3572 std::fs::write(&path, &content).unwrap();
3573
3574 let limits = ResourceLimits {
3575 max_documents: 100,
3576 max_file_size: 20,
3577 };
3578 let tracker = DocumentTracker::new(limits, HashMap::new());
3579
3580 assert_eq!(
3581 tracker
3582 .read_line_checked(&path, 0, u64::MAX)
3583 .await
3584 .unwrap()
3585 .text
3586 .as_deref(),
3587 Some(content.as_str())
3588 );
3589 assert_eq!(
3590 tracker
3591 .read_line_checked(&path, 1, u64::MAX)
3592 .await
3593 .unwrap()
3594 .text,
3595 None,
3596 "a line past an exact-max_file_size file's only line must read to EOF cleanly, not \
3597 be misreported as truncated"
3598 );
3599 }
3600
3601 #[tokio::test]
3608 async fn test_read_line_checked_bounds_read_by_budget_not_just_max_file_size() {
3609 let dir = TempDir::new().unwrap();
3610 let path = dir.path().join("budget.rs");
3611 std::fs::write(&path, "a".repeat(1000)).unwrap();
3612
3613 let limits = ResourceLimits {
3614 max_documents: 100,
3615 max_file_size: 1000,
3616 };
3617 let tracker = DocumentTracker::new(limits, HashMap::new());
3618
3619 let read = tracker.read_line_checked(&path, 0, 10).await.unwrap();
3620 assert_eq!(
3621 read.text, None,
3622 "a single line far longer than the budget must not be returned as if complete"
3623 );
3624 assert_eq!(
3625 read.bytes_read, 11,
3626 "the read must stop at exactly the budget's +1 slack (see the correctness-gate fix \
3627 below), not at max_file_size"
3628 );
3629 }
3630
3631 #[tokio::test]
3639 async fn test_read_line_checked_exact_budget_match_on_unterminated_line_not_truncated() {
3640 let dir = TempDir::new().unwrap();
3641 let path = dir.path().join("exact_budget.rs");
3642 let content = "twelve chars";
3643 std::fs::write(&path, content).unwrap();
3644
3645 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3646 let read = tracker
3647 .read_line_checked(&path, 0, content.len() as u64)
3648 .await
3649 .unwrap();
3650 assert_eq!(
3651 read.text.as_deref(),
3652 Some(content),
3653 "budget exactly matching the line's byte length must not be misreported as truncated"
3654 );
3655 assert_eq!(read.bytes_read, content.len() as u64);
3656 }
3657
3658 #[tokio::test]
3665 async fn test_read_line_checked_reports_bytes_read_for_invalid_utf8_line() {
3666 let dir = TempDir::new().unwrap();
3667 let path = dir.path().join("invalid_utf8.rs");
3668 let mut content = vec![0xFFu8, 0xFE, 0xFD];
3669 content.push(b'\n');
3670 std::fs::write(&path, &content).unwrap();
3671
3672 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3673 let read = tracker.read_line_checked(&path, 0, u64::MAX).await.unwrap();
3674 assert_eq!(read.text, None);
3675 assert_eq!(
3676 read.bytes_read,
3677 content.len() as u64,
3678 "bytes scanned must be reported even though the line wasn't valid UTF-8"
3679 );
3680 }
3681
3682 #[tokio::test]
3691 async fn test_read_line_checked_charges_nominal_amount_for_nonexistent_path() {
3692 let dir = TempDir::new().unwrap();
3693 let path = dir.path().join("does_not_exist.rs");
3694
3695 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3696 let read = tracker.read_line_checked(&path, 0, u64::MAX).await.unwrap();
3698 assert_eq!(read.text, None);
3699 assert_eq!(read.bytes_read, OPEN_FAILURE_CHARGE_BYTES);
3700 }
3701}