1use std::collections::{HashMap, HashSet};
6use std::num::NonZeroU32;
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::{Arc, Mutex as StdMutex};
10use std::time::{Duration, SystemTime};
11
12use lsp_types::{
13 DidChangeTextDocumentNotification, DidChangeTextDocumentParams,
14 DidCloseTextDocumentNotification, DidCloseTextDocumentParams, DidOpenTextDocumentNotification,
15 DidOpenTextDocumentParams, TextDocumentContentChangeEvent, TextDocumentItem, Uri,
16 VersionedTextDocumentIdentifier,
17};
18use serde::{Deserialize, Serialize};
19use tokio::fs;
20use tokio::io::{AsyncBufReadExt, AsyncReadExt};
21use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard};
22use tokio::time::Instant;
23use url::Url;
24
25use crate::config::{DocumentLimit, LanguageId, LanguageMap, ServerId, SizeLimit};
26use crate::error::{BackgroundTask, Error, Result};
27use crate::lsp::LspClient;
28use crate::util::{
29 BoundedUtf8Error, OpenRegularFileError, RegularFile, SizeExceeded, check_bounded_utf8, lock_std,
30};
31
32const DISK_CHECK_DEBOUNCE: Duration = Duration::from_millis(250);
45
46const MTIME_GRANULARITY: Duration = Duration::from_secs(2);
52
53const INLINE_INDEX_MAX_BYTES: usize = 1024 * 1024;
56
57const LINE_CHECKPOINT_STRIDE: usize = 64;
59
60const fn is_line_terminator(byte: u8) -> bool {
62 matches!(byte, b'\n' | b'\r')
63}
64
65fn line_bounds(content: &str, start: usize) -> (usize, Option<usize>) {
69 let bytes = content.as_bytes();
70 let Some(offset) = bytes
71 .get(start..)
72 .and_then(|rest| rest.iter().position(|&b| is_line_terminator(b)))
73 else {
74 return (content.len(), None);
75 };
76 let end = start.saturating_add(offset);
77 let next = end.saturating_add(1);
78 let crlf = bytes.get(end) == Some(&b'\r') && bytes.get(next) == Some(&b'\n');
79 (end, Some(next.saturating_add(usize::from(crlf))))
80}
81
82#[derive(Clone)]
99pub(super) struct DocumentText {
100 content: String,
101 stride: usize,
102 checkpoints: Box<[usize]>,
104 line_count: NonZeroU32,
107}
108
109impl std::fmt::Debug for DocumentText {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("DocumentText")
112 .field("len", &self.content.len())
113 .field("checkpoints", &self.checkpoints.len())
114 .finish_non_exhaustive()
115 }
116}
117
118impl DocumentText {
119 pub(super) fn new(content: String) -> Self {
121 Self::with_stride(content, LINE_CHECKPOINT_STRIDE)
122 }
123
124 fn with_stride(content: String, stride: usize) -> Self {
127 let stride = stride.max(1);
128 let mut checkpoints = vec![0];
129 let mut start = 0;
130 let mut line = 0usize;
131 while let (_, Some(next)) = line_bounds(&content, start) {
132 start = next;
133 line = line.saturating_add(1);
134 if line.is_multiple_of(stride) {
135 checkpoints.push(start);
136 }
137 }
138 let line_count = u32::try_from(line)
139 .ok()
140 .and_then(|last| last.checked_add(1))
141 .and_then(NonZeroU32::new)
142 .unwrap_or(NonZeroU32::MAX);
143 Self {
144 content,
145 stride,
146 checkpoints: checkpoints.into_boxed_slice(),
147 line_count,
148 }
149 }
150
151 async fn build(content: String) -> Result<Self> {
154 if content.len() <= INLINE_INDEX_MAX_BYTES {
155 return Ok(Self::new(content));
156 }
157 tokio::task::spawn_blocking(move || Self::new(content))
158 .await
159 .map_err(|e| Error::Io(std::io::Error::other(e)))
160 }
161
162 pub(super) fn as_str(&self) -> &str {
164 &self.content
165 }
166
167 pub(super) const fn last_line(&self) -> NonZeroU32 {
169 self.line_count
170 }
171
172 pub(super) fn line(&self, n: u32) -> Option<&str> {
176 let start = self.line_start(n)?;
177 let (end, _) = line_bounds(&self.content, start);
178 Some(&self.content[start..end])
179 }
180
181 fn line_start(&self, n: u32) -> Option<usize> {
182 let n = n as usize;
183 let mut start = *self.checkpoints.get(n.checked_div(self.stride)?)?;
184 for _ in 0..n.checked_rem(self.stride)? {
185 start = line_bounds(&self.content, start).1?;
186 }
187 Some(start)
188 }
189
190 pub(super) fn lines_from(&self, n: u32) -> impl Iterator<Item = &str> {
193 let mut next = self.line_start(n);
194 std::iter::from_fn(move || {
195 let start = next?;
196 let (end, after) = line_bounds(&self.content, start);
197 next = after;
198 self.content.get(start..end)
199 })
200 }
201}
202
203impl PartialEq for DocumentText {
204 fn eq(&self, other: &Self) -> bool {
205 self.content == other.content
206 }
207}
208
209impl Eq for DocumentText {}
210
211fn mtime_settled(mtime: Option<SystemTime>, read_at: SystemTime) -> bool {
218 mtime.is_some_and(|m| {
219 m.checked_add(MTIME_GRANULARITY)
220 .is_some_and(|t| t <= read_at)
221 })
222}
223
224#[derive(Debug, Clone, Copy)]
234pub struct DiskSync {
235 pub mtime: Option<SystemTime>,
240 pub size: u64,
242 pub mtime_settled: bool,
245 pub content_checked_at: Instant,
252}
253
254impl PartialEq for DiskSync {
255 fn eq(&self, other: &Self) -> bool {
256 self.mtime == other.mtime
257 && self.size == other.size
258 && self.mtime_settled == other.mtime_settled
259 }
260}
261
262impl Eq for DiskSync {}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
270#[serde(transparent)]
271pub struct DocumentVersion(i32);
272
273impl DocumentVersion {
274 pub const FIRST: Self = Self(1);
276
277 #[must_use]
279 pub const fn new(version: i32) -> Self {
280 Self(version)
281 }
282
283 #[must_use]
285 pub const fn get(self) -> i32 {
286 self.0
287 }
288
289 #[must_use]
291 pub(crate) const fn next(self) -> Self {
292 Self(self.0.saturating_add(1))
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub struct Opening(u64);
301
302#[derive(Debug, Clone)]
327pub(super) struct DocumentState {
328 uri: Uri,
329 language_id: LanguageId,
330 version: DocumentVersion,
331 text: DocumentText,
332 disk: Option<DiskSync>,
333 synced: HashMap<ServerId, DocumentVersion>,
334 opened: Opening,
335 last_accessed: Instant,
340}
341
342impl PartialEq for DocumentState {
343 fn eq(&self, other: &Self) -> bool {
344 let Self {
349 uri,
350 language_id,
351 version,
352 text,
353 disk,
354 synced,
355 opened: _,
356 last_accessed: _,
357 } = self;
358 *uri == other.uri
359 && *language_id == other.language_id
360 && *version == other.version
361 && *text == other.text
362 && *disk == other.disk
363 && *synced == other.synced
364 }
365}
366
367impl Eq for DocumentState {}
368
369impl DocumentState {
370 fn new(uri: Uri, language_id: LanguageId, text: DocumentText, opened: Opening) -> Self {
373 Self {
374 uri,
375 language_id,
376 version: DocumentVersion::FIRST,
377 text,
378 disk: None,
379 synced: HashMap::new(),
380 opened,
381 last_accessed: Instant::now(),
382 }
383 }
384
385 fn touch(&mut self) {
388 self.last_accessed = Instant::now();
389 }
390
391 #[must_use]
393 #[cfg(test)]
394 pub(crate) const fn uri(&self) -> &Uri {
395 &self.uri
396 }
397
398 #[must_use]
400 #[cfg(test)]
401 pub(crate) const fn language_id(&self) -> &LanguageId {
402 &self.language_id
403 }
404
405 #[must_use]
409 #[cfg(test)]
410 pub(crate) const fn version(&self) -> DocumentVersion {
411 self.version
412 }
413
414 #[must_use]
416 #[cfg(test)]
417 pub(crate) fn content(&self) -> &str {
418 self.text.as_str()
419 }
420
421 const fn disk(&self) -> Option<DiskSync> {
424 self.disk
425 }
426
427 #[must_use]
436 pub(crate) fn synced_version(&self, server: &ServerId) -> Option<DocumentVersion> {
437 self.synced.get(server).copied()
438 }
439
440 fn has_never_synced(&self) -> bool {
442 self.synced.is_empty()
443 }
444
445 fn commit_reload(
451 &mut self,
452 version: DocumentVersion,
453 text: DocumentText,
454 snap: Option<DiskSync>,
455 ) {
456 debug_assert!(
457 version >= self.version,
458 "document version must be monotonically increasing"
459 );
460 self.version = version;
461 self.text = text;
462 self.disk = snap;
463 }
464
465 const fn set_disk(&mut self, snap: DiskSync) {
467 self.disk = Some(snap);
468 }
469
470 fn mark_synced(&mut self, server: ServerId, version: DocumentVersion) {
472 self.synced.insert(server, version);
473 }
474
475 fn forget_server(&mut self, server: &ServerId) {
477 self.synced.remove(server);
478 }
479}
480
481#[derive(Debug, Clone, Copy)]
483pub struct ResourceLimits {
484 pub max_documents: DocumentLimit,
486 pub max_file_size: SizeLimit,
488}
489
490impl Default for ResourceLimits {
491 fn default() -> Self {
492 Self {
493 max_documents: DocumentLimit::DEFAULT,
494 max_file_size: SizeLimit::DEFAULT,
495 }
496 }
497}
498
499pub const OPEN_FAILURE_CHARGE_BYTES: u64 = 4096;
513
514#[derive(Debug, Clone)]
524pub struct LineRead {
525 pub(crate) text: Option<String>,
527 pub(crate) bytes_read: u64,
530}
531
532#[derive(Debug)]
543struct PendingClose {
544 uri: Uri,
545 servers: HashSet<ServerId>,
546}
547
548#[derive(Debug)]
555#[must_use = "dropping the claim releases the path lock; send the closes first"]
556pub struct PendingCloseClaim<'a> {
557 pub(crate) path: PathBuf,
559 pub(crate) uri: Uri,
561 pub(crate) servers: Vec<ServerId>,
563 _path_guard: PathLockGuard<'a>,
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568pub enum LinePresence {
569 Untracked,
571 Present,
573 Beyond {
575 last_line: NonZeroU32,
577 },
578}
579
580#[derive(Debug)]
587pub struct DocumentTracker {
588 documents: StdMutex<HashMap<PathBuf, DocumentState>>,
591 path_locks: StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
600 in_flight: InFlightMap,
605 generations: StdMutex<HashMap<ServerId, u64>>,
612 limits: ResourceLimits,
614 language_map: LanguageMap,
616 pending_closes: StdMutex<HashMap<PathBuf, PendingClose>>,
623 next_opening: AtomicU64,
625}
626
627impl DocumentTracker {
628 #[must_use]
630 pub fn new(limits: ResourceLimits, language_map: impl Into<LanguageMap>) -> Self {
631 Self {
632 documents: StdMutex::new(HashMap::new()),
633 path_locks: StdMutex::new(HashMap::new()),
634 in_flight: Arc::default(),
635 generations: StdMutex::new(HashMap::new()),
636 limits,
637 language_map: language_map.into(),
638 pending_closes: StdMutex::new(HashMap::new()),
639 next_opening: AtomicU64::new(0),
640 }
641 }
642
643 #[must_use]
645 pub(crate) const fn limits(&self) -> ResourceLimits {
646 self.limits
647 }
648
649 pub(crate) fn mark_in_flight(&self, path: &Path) -> InFlightGuard {
656 let mut in_flight = lock_std(&self.in_flight);
657 let count = in_flight.entry(path.to_path_buf()).or_insert(0);
658 *count = count.saturating_add(1);
659 drop(in_flight);
660 InFlightGuard {
661 in_flight: Arc::clone(&self.in_flight),
662 path: path.to_path_buf(),
663 }
664 }
665
666 #[cfg(test)]
668 pub(crate) fn in_flight_count(&self, path: &Path) -> usize {
669 lock_std(&self.in_flight).get(path).copied().unwrap_or(0)
670 }
671
672 #[must_use]
675 pub(crate) fn synced_version(&self, path: &Path, server: &ServerId) -> Option<DocumentVersion> {
676 lock_std(&self.documents).get(path)?.synced_version(server)
677 }
678
679 #[must_use]
681 pub(crate) fn opening(&self, path: &Path) -> Option<Opening> {
682 lock_std(&self.documents)
683 .get(path)
684 .map(|state| state.opened)
685 }
686
687 #[cfg(test)]
690 pub(crate) fn set_synced_version_for_test(&self, path: &Path, server: &ServerId, version: i32) {
691 if let Some(state) = lock_std(&self.documents).get_mut(path) {
692 state.mark_synced(server.clone(), DocumentVersion::new(version));
693 }
694 }
695
696 #[must_use]
698 pub fn is_open(&self, path: &Path) -> bool {
699 lock_std(&self.documents).contains_key(path)
700 }
701
702 #[must_use]
704 #[cfg(test)]
705 pub(super) fn get(&self, path: &Path) -> Option<DocumentState> {
706 lock_std(&self.documents).get(path).cloned()
707 }
708
709 #[must_use]
717 pub fn line_text(&self, path: &Path, line: u32) -> Option<String> {
718 let documents = lock_std(&self.documents);
719 documents.get(path)?.text.line(line).map(str::to_string)
720 }
721
722 #[must_use]
726 pub(crate) fn line_presence(&self, path: &Path, line: u32) -> LinePresence {
727 match lock_std(&self.documents).get(path) {
728 None => LinePresence::Untracked,
729 Some(document) if document.text.line(line).is_some() => LinePresence::Present,
730 Some(document) => LinePresence::Beyond {
731 last_line: document.text.last_line(),
732 },
733 }
734 }
735
736 #[must_use]
742 pub(crate) fn line_window(
743 &self,
744 path: &Path,
745 start_line: u32,
746 max_lines: usize,
747 ) -> Option<String> {
748 let documents = lock_std(&self.documents);
749 let mut lines = documents
750 .get(path)?
751 .text
752 .lines_from(start_line)
753 .take(max_lines);
754 let mut window = lines.next()?.to_string();
755 for line in lines {
756 window.push('\n');
757 window.push_str(line);
758 }
759 drop(documents);
760 Some(window)
761 }
762
763 #[must_use]
765 #[cfg(test)]
766 pub(crate) fn len(&self) -> usize {
767 lock_std(&self.documents).len()
768 }
769
770 #[must_use]
772 #[cfg(test)]
773 pub(crate) fn is_empty(&self) -> bool {
774 lock_std(&self.documents).is_empty()
775 }
776
777 #[cfg(test)]
809 pub(crate) fn open(&self, path: PathBuf, content: String) -> Result<Uri> {
810 self.open_text(path, DocumentText::new(content))
811 }
812
813 fn open_text(&self, path: PathBuf, text: DocumentText) -> Result<Uri> {
816 self.check_file_size(text.as_str().len() as u64)?;
817
818 let uri = path_to_uri(&path)?;
819 let language_id = self.language_map.detect(&path);
820
821 let opened = Opening(self.next_opening.fetch_add(1, Ordering::Relaxed));
822 let state = DocumentState::new(uri.clone(), language_id, text, opened);
823
824 let mut documents = lock_std(&self.documents);
836 if let Some(max) = self.limits.max_documents.get()
837 && documents.len() >= max.get()
838 && !documents.contains_key(&path)
839 {
840 let Some((evicted_path, evicted_state)) =
841 Self::evict_lru(&mut documents, &self.path_locks, &self.in_flight)
842 else {
843 return Err(Error::DocumentLimitExceeded {
844 current: documents.len(),
845 max,
846 });
847 };
848 self.record_pending_close(evicted_path, evicted_state);
849 }
850 documents.insert(path, state);
851 drop(documents);
852 Ok(uri)
853 }
854
855 fn record_pending_close(&self, path: PathBuf, evicted: DocumentState) {
858 if evicted.synced.is_empty() {
859 return;
860 }
861 lock_std(&self.pending_closes)
862 .entry(path)
863 .or_insert_with(|| PendingClose {
864 uri: evicted.uri,
865 servers: HashSet::new(),
866 })
867 .servers
868 .extend(evicted.synced.into_keys());
869 }
870
871 fn evict_lru(
887 documents: &mut HashMap<PathBuf, DocumentState>,
888 path_locks: &StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
889 in_flight: &InFlightMap,
890 ) -> Option<(PathBuf, DocumentState)> {
891 let mut busy = lock_std(path_locks)
892 .keys()
893 .cloned()
894 .collect::<std::collections::HashSet<_>>();
895 busy.extend(lock_std(in_flight).keys().cloned());
896 let lru_path = documents
897 .iter()
898 .filter(|(path, state)| !busy.contains(path.as_path()) && state.disk().is_some())
899 .min_by_key(|(_, state)| state.last_accessed)
900 .map(|(path, _)| path.clone())?;
901 documents.remove(&lru_path).map(|state| (lru_path, state))
902 }
903
904 const fn check_file_size(&self, size: u64) -> Result<()> {
906 match SizeExceeded::check_limit(size, self.limits.max_file_size) {
907 Ok(()) => Ok(()),
908 Err(exceeded) => Err(Error::FileSizeLimitExceeded(exceeded)),
909 }
910 }
911
912 fn set_disk(&self, path: &Path, snap: DiskSync) {
919 if let Some(st) = lock_std(&self.documents).get_mut(path) {
920 st.set_disk(snap);
921 }
922 }
923
924 #[cfg(test)]
928 pub(super) fn close(&self, path: &Path) -> Option<DocumentState> {
929 lock_std(&self.documents).remove(path)
930 }
931
932 pub fn open_paths(&self) -> Vec<PathBuf> {
934 lock_std(&self.documents).keys().cloned().collect()
935 }
936
937 pub fn forget_server(&self, server: &ServerId) {
956 let mut generations = lock_std(&self.generations);
957 let generation = generations.entry(server.clone()).or_insert(0);
958 *generation = generation.saturating_add(1);
959 drop(generations);
960 for state in lock_std(&self.documents).values_mut() {
961 state.forget_server(server);
962 }
963 lock_std(&self.pending_closes).retain(|_, pending| {
965 pending.servers.remove(server);
966 !pending.servers.is_empty()
967 });
968 }
969
970 fn generation(&self, server: &ServerId) -> u64 {
972 lock_std(&self.generations)
973 .get(server)
974 .copied()
975 .unwrap_or(0)
976 }
977
978 async fn lock_path(&self, path: &Path) -> PathLockGuard<'_> {
993 let arc = {
994 let mut locks = lock_std(&self.path_locks);
995 locks
996 .entry(path.to_path_buf())
997 .or_insert_with(|| Arc::new(AsyncMutex::new(())))
998 .clone()
999 };
1000 let guard = Arc::clone(&arc).lock_owned().await;
1001 PathLockGuard {
1002 path_locks: &self.path_locks,
1003 path: path.to_path_buf(),
1004 arc,
1005 guard: Some(guard),
1006 }
1007 }
1008
1009 fn try_lock_path(&self, path: &Path) -> Option<PathLockGuard<'_>> {
1012 let arc = {
1013 let mut locks = lock_std(&self.path_locks);
1014 locks
1015 .entry(path.to_path_buf())
1016 .or_insert_with(|| Arc::new(AsyncMutex::new(())))
1017 .clone()
1018 };
1019 if let Ok(guard) = Arc::clone(&arc).try_lock_owned() {
1020 return Some(PathLockGuard {
1021 path_locks: &self.path_locks,
1022 path: path.to_path_buf(),
1023 arc,
1024 guard: Some(guard),
1025 });
1026 }
1027 let mut locks = lock_std(&self.path_locks);
1030 if Arc::strong_count(&arc) <= 2 {
1031 locks.remove(path);
1032 }
1033 None
1034 }
1035
1036 pub(crate) fn pending_close_paths(&self) -> Vec<PathBuf> {
1039 lock_std(&self.pending_closes).keys().cloned().collect()
1040 }
1041
1042 pub(crate) fn try_claim_pending_close(&self, path: &Path) -> Option<PendingCloseClaim<'_>> {
1055 let path_guard = self.try_lock_path(path)?;
1056 let synced: HashSet<ServerId> = lock_std(&self.documents)
1057 .get(path)
1058 .map_or_default(|st| st.synced.keys().cloned().collect());
1059 let pending = lock_std(&self.pending_closes).remove(path)?;
1060 let servers: Vec<ServerId> = pending.servers.difference(&synced).cloned().collect();
1061 if servers.is_empty() {
1062 return None;
1063 }
1064 Some(PendingCloseClaim {
1065 path: path.to_path_buf(),
1066 uri: pending.uri,
1067 servers,
1068 _path_guard: path_guard,
1069 })
1070 }
1071
1072 fn take_pending_close(&self, path: &Path, server: &ServerId) -> bool {
1075 let mut pending_closes = lock_std(&self.pending_closes);
1076 let Some(pending) = pending_closes.get_mut(path) else {
1077 return false;
1078 };
1079 let owed = pending.servers.remove(server);
1080 if pending.servers.is_empty() {
1081 pending_closes.remove(path);
1082 }
1083 owed
1084 }
1085
1086 fn restore_pending_close(&self, path: &Path, uri: &Uri, server: &ServerId, generation: u64) {
1095 let mut pending_closes = lock_std(&self.pending_closes);
1096 if self.generation(server) != generation {
1097 return;
1098 }
1099 pending_closes
1100 .entry(path.to_path_buf())
1101 .or_insert_with(|| PendingClose {
1102 uri: uri.clone(),
1103 servers: HashSet::new(),
1104 })
1105 .servers
1106 .insert(server.clone());
1107 }
1108
1109 pub async fn ensure_open(
1178 &self,
1179 path: &Path,
1180 server: &ServerId,
1181 lsp_client: &LspClient,
1182 ) -> Result<Uri> {
1183 let _path_guard = self.lock_path(path).await;
1184 let generation = self.generation(server);
1185 let decision = self.disk_phase(path).await?;
1186 self.sync_phase(path, server, lsp_client, decision, generation)
1187 .await
1188 }
1189
1190 async fn disk_phase(&self, path: &Path) -> Result<Decision> {
1195 if !lock_std(&self.documents).contains_key(path) {
1196 return self.disk_phase_new(path).await;
1197 }
1198
1199 let read_at = SystemTime::now();
1200 let meta = fs::metadata(path).await.map_err(|e| Error::FileIo {
1201 path: path.to_path_buf(),
1202 source: e,
1203 })?;
1204 let mtime = meta.modified().ok();
1205 let size = meta.len();
1206
1207 let Some((uri, current_version, fast_path)) =
1215 lock_std(&self.documents).get_mut(path).map(|st| {
1216 st.touch();
1217 let stat_matches = st
1218 .disk()
1219 .is_some_and(|d| d.mtime == mtime && d.size == size);
1220 let fast_path = match st.disk() {
1221 Some(d) if stat_matches && d.mtime_settled => true,
1222 Some(d)
1223 if stat_matches && d.content_checked_at.elapsed() < DISK_CHECK_DEBOUNCE =>
1224 {
1225 true
1226 }
1227 _ => false,
1228 };
1229 (st.uri.clone(), st.version, fast_path)
1230 })
1231 else {
1232 return Err(Error::DocumentNotFound(path.to_path_buf()));
1233 };
1234 if fast_path {
1235 return Ok(Decision::unchanged(uri, current_version));
1236 }
1237
1238 let (fresh, ..) = self.read_to_string_checked(path).await?;
1239 let snap = DiskSync {
1240 mtime,
1241 size,
1242 mtime_settled: mtime_settled(mtime, read_at),
1243 content_checked_at: Instant::now(),
1244 };
1245
1246 let Some(unchanged) = lock_std(&self.documents)
1247 .get(path)
1248 .map(|st| fresh == st.text.as_str())
1249 else {
1250 return Err(Error::DocumentNotFound(path.to_path_buf()));
1251 };
1252
1253 if unchanged {
1254 self.set_disk(path, snap);
1255 return Ok(Decision::unchanged(uri, current_version));
1256 }
1257
1258 Ok(Decision {
1259 uri,
1260 target_version: current_version.next(),
1261 fresh_content: Some(DocumentText::build(fresh).await?),
1262 snap: Some(snap),
1263 })
1264 }
1265
1266 async fn disk_phase_new(&self, path: &Path) -> Result<Decision> {
1270 let read_at = SystemTime::now();
1271 let (content, mtime, size) = self.read_to_string_checked(path).await?;
1272
1273 let uri = self.open_text(path.to_path_buf(), DocumentText::build(content).await?)?;
1274 self.set_disk(
1275 path,
1276 DiskSync {
1277 mtime,
1278 size,
1279 mtime_settled: mtime_settled(mtime, read_at),
1280 content_checked_at: Instant::now(),
1281 },
1282 );
1283
1284 Ok(Decision::unchanged(uri, DocumentVersion::FIRST))
1285 }
1286
1287 async fn open_checked(&self, path: &Path) -> Result<(fs::File, std::fs::Metadata)> {
1297 let owned = path.to_path_buf();
1298 let opened = tokio::task::spawn_blocking(move || RegularFile::open(&owned))
1299 .await
1300 .map_err(|source| Error::TaskFailed {
1301 task: BackgroundTask::FileOpen,
1302 source,
1303 })?;
1304 let (file, meta) = opened
1305 .map_err(|e| match e {
1306 OpenRegularFileError::Io(source) => Error::FileIo {
1307 path: path.to_path_buf(),
1308 source,
1309 },
1310 OpenRegularFileError::NotRegular => Error::NotARegularFile(path.to_path_buf()),
1311 })?
1312 .into_parts();
1313 self.check_file_size(meta.len())?;
1314 Ok((fs::File::from_std(file), meta))
1315 }
1316
1317 async fn read_string_bounded(
1330 &self,
1331 path: &Path,
1332 mut file: fs::File,
1333 size_hint: u64,
1334 ) -> Result<String> {
1335 let max = self.limits.max_file_size;
1336 let cap = max.read_cap();
1337 let mut buf = Vec::with_capacity(usize::try_from(size_hint.min(cap)).unwrap_or(0));
1338 let io_err = |e: std::io::Error| Error::FileIo {
1339 path: path.to_path_buf(),
1340 source: e,
1341 };
1342
1343 (&mut file)
1344 .take(cap)
1345 .read_to_end(&mut buf)
1346 .await
1347 .map_err(io_err)?;
1348 check_bounded_utf8(buf, max).map_err(|e| match e {
1349 BoundedUtf8Error::TooLarge(exceeded) => Error::FileSizeLimitExceeded(exceeded),
1350 BoundedUtf8Error::InvalidUtf8(e) => {
1351 io_err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
1352 }
1353 })
1354 }
1355
1356 async fn read_to_string_checked(
1364 &self,
1365 path: &Path,
1366 ) -> Result<(String, Option<SystemTime>, u64)> {
1367 let (file, meta) = self.open_checked(path).await?;
1368 let mtime = meta.modified().ok();
1369 let size = meta.len();
1370 let content = self.read_string_bounded(path, file, size).await?;
1371 Ok((content, mtime, size))
1372 }
1373
1374 pub(crate) async fn read_line_checked(
1436 &self,
1437 path: &Path,
1438 line: u32,
1439 budget: u64,
1440 ) -> Result<LineRead> {
1441 let Ok((file, _meta)) = self.open_checked(path).await else {
1442 return Ok(LineRead {
1443 text: None,
1444 bytes_read: OPEN_FAILURE_CHARGE_BYTES,
1445 });
1446 };
1447 let max = self.limits.max_file_size;
1448 let cap = max.read_cap().min(budget.saturating_add(1));
1454 let mut reader = tokio::io::BufReader::new(file.take(cap));
1455 let io_err = |e: std::io::Error| Error::FileIo {
1456 path: path.to_path_buf(),
1457 source: e,
1458 };
1459
1460 read_nth_line(&mut reader, line, cap).await.map_err(io_err)
1461 }
1462
1463 async fn sync_phase(
1474 &self,
1475 path: &Path,
1476 server: &ServerId,
1477 lsp_client: &LspClient,
1478 decision: Decision,
1479 generation: u64,
1480 ) -> Result<Uri> {
1481 let Decision {
1482 uri,
1483 target_version,
1484 fresh_content,
1485 snap,
1486 } = decision;
1487
1488 let Some(synced_version) = lock_std(&self.documents)
1495 .get(path)
1496 .map(|st| st.synced_version(server))
1497 else {
1498 return Err(Error::DocumentNotFound(path.to_path_buf()));
1499 };
1500 let up_to_date = synced_version.is_some_and(|v| v >= target_version);
1501 let is_first_open = synced_version.is_none();
1502
1503 if up_to_date {
1504 return Ok(uri);
1505 }
1506
1507 let Some((language_id, text)) = lock_std(&self.documents).get(path).map(|st| {
1508 let text = fresh_content
1509 .as_ref()
1510 .unwrap_or(&st.text)
1511 .as_str()
1512 .to_owned();
1513 (st.language_id.clone(), text)
1514 }) else {
1515 return Err(Error::DocumentNotFound(path.to_path_buf()));
1516 };
1517
1518 let notify_result = if is_first_open {
1519 let closed = if self.take_pending_close(path, server) {
1520 lsp_client
1521 .notify_typed::<DidCloseTextDocumentNotification>(DidCloseTextDocumentParams {
1522 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1523 })
1524 .await
1525 .inspect_err(|_| self.restore_pending_close(path, &uri, server, generation))
1526 } else {
1527 Ok(())
1528 };
1529 match closed {
1530 Ok(()) => {
1531 lsp_client
1532 .notify_typed::<DidOpenTextDocumentNotification>(
1533 DidOpenTextDocumentParams {
1534 text_document: TextDocumentItem {
1535 uri: uri.clone(),
1536 language_id: String::from(language_id).into(),
1537 version: target_version.get(),
1538 text,
1539 },
1540 },
1541 )
1542 .await
1543 }
1544 Err(err) => Err(err),
1545 }
1546 } else {
1547 lsp_client
1548 .notify_typed::<DidChangeTextDocumentNotification>(DidChangeTextDocumentParams {
1549 text_document: VersionedTextDocumentIdentifier {
1550 version: target_version.get(),
1551 text_document_identifier: lsp_types::TextDocumentIdentifier {
1552 uri: uri.clone(),
1553 },
1554 },
1555 content_changes: vec![
1556 TextDocumentContentChangeEvent::TextDocumentContentChangeWholeDocument(
1557 lsp_types::TextDocumentContentChangeWholeDocument { text },
1558 ),
1559 ],
1560 })
1561 .await
1562 };
1563
1564 if let Err(err) = notify_result {
1565 let first_ever_sync = lock_std(&self.documents)
1578 .get(path)
1579 .is_some_and(DocumentState::has_never_synced);
1580 if is_first_open && first_ever_sync {
1581 lock_std(&self.documents).remove(path);
1582 }
1583 return Err(err);
1584 }
1585
1586 let mut documents = lock_std(&self.documents);
1589 let Some(st) = documents.get_mut(path) else {
1590 return Err(Error::DocumentNotFound(path.to_path_buf()));
1591 };
1592 if let Some(fresh) = fresh_content {
1593 st.commit_reload(target_version, fresh, snap);
1594 }
1595 if self.generation(server) == generation {
1605 st.mark_synced(server.clone(), target_version);
1606 }
1607 drop(documents);
1608
1609 Ok(uri)
1610 }
1611}
1612
1613#[allow(
1623 clippy::arithmetic_side_effects,
1624 clippy::indexing_slicing,
1625 reason = "every index derives from `position()` over the same chunk and stays within `chunk.len()`"
1626)]
1627async fn read_nth_line<R>(reader: &mut R, line: u32, cap: u64) -> std::io::Result<LineRead>
1628where
1629 R: tokio::io::AsyncBufRead + Unpin,
1630{
1631 let mut buf = Vec::new();
1632 let mut bytes_read: u64 = 0;
1633 let mut current_line = 0u32;
1634 let mut after_cr = false;
1635 loop {
1636 let chunk = reader.fill_buf().await?;
1637 if chunk.is_empty() {
1638 let reached_line = current_line == line && bytes_read < cap;
1639 return Ok(LineRead {
1640 text: reached_line.then(|| String::from_utf8(buf).ok()).flatten(),
1641 bytes_read,
1642 });
1643 }
1644
1645 let mut i = usize::from(after_cr && chunk[0] == b'\n');
1646 after_cr = false;
1647 let mut finished = false;
1648 while i < chunk.len() {
1649 let rest = &chunk[i..];
1650 let Some(offset) = rest.iter().position(|&b| is_line_terminator(b)) else {
1651 if current_line == line {
1652 buf.extend_from_slice(rest);
1653 }
1654 i = chunk.len();
1655 break;
1656 };
1657 let terminator = i + offset;
1658 if current_line == line {
1659 buf.extend_from_slice(&chunk[i..terminator]);
1660 i = terminator + 1;
1661 finished = true;
1662 break;
1663 }
1664 current_line += 1;
1665 i = terminator + 1;
1666 if chunk[terminator] == b'\r' {
1667 match chunk.get(i) {
1668 Some(b'\n') => i += 1,
1669 Some(_) => {}
1670 None => after_cr = true,
1671 }
1672 }
1673 }
1674
1675 bytes_read += i as u64;
1676 reader.consume(i);
1677 if finished {
1678 return Ok(LineRead {
1679 text: String::from_utf8(buf).ok(),
1680 bytes_read,
1681 });
1682 }
1683 }
1684}
1685
1686type InFlightMap = Arc<StdMutex<HashMap<PathBuf, usize>>>;
1688
1689#[derive(Debug)]
1696#[must_use = "dropping the guard immediately makes the document evictable again"]
1697pub struct InFlightGuard {
1698 in_flight: InFlightMap,
1699 path: PathBuf,
1700}
1701
1702impl Drop for InFlightGuard {
1703 fn drop(&mut self) {
1704 let mut in_flight = lock_std(&self.in_flight);
1705 if let Some(count) = in_flight.get_mut(&self.path) {
1706 *count = count.saturating_sub(1);
1707 if *count == 0 {
1708 in_flight.remove(&self.path);
1709 }
1710 }
1711 }
1712}
1713
1714#[derive(Debug)]
1723struct PathLockGuard<'a> {
1724 path_locks: &'a StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
1725 path: PathBuf,
1726 arc: Arc<AsyncMutex<()>>,
1727 guard: Option<OwnedMutexGuard<()>>,
1728}
1729
1730impl Drop for PathLockGuard<'_> {
1731 fn drop(&mut self) {
1732 self.guard.take();
1735
1736 let mut locks = lock_std(self.path_locks);
1737 if Arc::strong_count(&self.arc) <= 2 {
1749 locks.remove(&self.path);
1750 }
1751 }
1752}
1753
1754struct Decision {
1759 uri: Uri,
1760 target_version: DocumentVersion,
1761 fresh_content: Option<DocumentText>,
1762 snap: Option<DiskSync>,
1763}
1764
1765impl Decision {
1766 const fn unchanged(uri: Uri, target_version: DocumentVersion) -> Self {
1769 Self {
1770 uri,
1771 target_version,
1772 fresh_content: None,
1773 snap: None,
1774 }
1775 }
1776}
1777
1778pub fn path_to_uri(path: &Path) -> Result<Uri> {
1791 try_path_to_uri(path).ok_or_else(|| Error::PathToUri(path.to_path_buf()))
1792}
1793
1794#[must_use]
1800pub fn try_path_to_uri(path: &Path) -> Option<Uri> {
1801 let uri_string = encode_rfc3986_path_chars(&file_url(path)?);
1802 Some(Uri::from(uri_string))
1803}
1804
1805#[cfg(not(windows))]
1806pub(super) fn file_url(path: &Path) -> Option<Url> {
1807 Url::from_file_path(path).ok()
1808}
1809
1810#[cfg(windows)]
1811pub(super) fn file_url(path: &Path) -> Option<Url> {
1812 match Url::from_file_path(path) {
1813 Ok(file_url) => Some(file_url),
1814 Err(()) if path.has_root() => windows_rooted_path_to_file_url(path),
1815 Err(()) => None,
1816 }
1817}
1818
1819#[cfg(windows)]
1820fn windows_rooted_path_to_file_url(path: &Path) -> Option<Url> {
1821 let path_str = path.to_string_lossy();
1822 let stripped = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
1823 let mut file_url = Url::parse("file:///").ok()?;
1824 file_url.path_segments_mut().ok()?.clear().extend(
1825 stripped
1826 .split(['\\', '/'])
1827 .filter(|segment| !segment.is_empty()),
1828 );
1829 Some(file_url)
1830}
1831
1832pub(super) fn encode_rfc3986_path_chars(url: &Url) -> String {
1842 let prefix = url[..url::Position::BeforePath].to_owned();
1843 let encoded = url[url::Position::BeforePath..]
1844 .replace('[', "%5B")
1845 .replace(']', "%5D")
1846 .replace('^', "%5E")
1847 .replace('|', "%7C");
1848 format!("{prefix}{encoded}")
1849}
1850
1851#[must_use]
1856pub fn uri_to_path(uri: &Uri) -> Option<PathBuf> {
1857 let url = Url::parse(uri.as_ref()).ok()?;
1858 if url.scheme() != "file" {
1859 return None;
1860 }
1861 if !url.host_str().unwrap_or("").is_empty() {
1864 return None;
1865 }
1866 url.to_file_path().ok()
1867}
1868
1869#[cfg(test)]
1870mod tests {
1871 use std::assert_matches;
1872
1873 use super::*;
1874 use crate::config::FileExtension;
1875
1876 fn detect_language(
1877 path: &Path,
1878 extension_map: &HashMap<FileExtension, LanguageId>,
1879 ) -> LanguageId {
1880 LanguageMap::from(extension_map.clone()).detect(path)
1881 }
1882
1883 #[test]
1884 fn test_detect_language() {
1885 let mut map = HashMap::new();
1886 map.insert(
1887 FileExtension::from_static("rs"),
1888 LanguageId::from_static("rust"),
1889 );
1890 map.insert(
1891 FileExtension::from_static("py"),
1892 LanguageId::from_static("python"),
1893 );
1894 map.insert(
1895 FileExtension::from_static("ts"),
1896 LanguageId::from_static("typescript"),
1897 );
1898
1899 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
1900 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
1901 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
1902 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
1903 }
1904
1905 #[tokio::test]
1906 async fn test_document_tracker() {
1907 let mut map = HashMap::new();
1908 map.insert(
1909 FileExtension::from_static("rs"),
1910 LanguageId::from_static("rust"),
1911 );
1912
1913 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
1914 let path = PathBuf::from("/test/file.rs");
1915
1916 assert!(!tracker.is_open(&path));
1917
1918 tracker
1919 .open(path.clone(), "fn main() {}".to_string())
1920 .unwrap();
1921 assert!(tracker.is_open(&path));
1922 assert_eq!(tracker.len(), 1);
1923
1924 let state = tracker.get(&path).unwrap();
1925 assert_eq!(state.version().get(), 1);
1926 assert_eq!(state.language_id(), "rust");
1927
1928 tracker.close(&path);
1929 assert!(!tracker.is_open(&path));
1930 assert!(tracker.is_empty());
1931 }
1932
1933 #[test]
1939 fn test_forget_server_clears_only_that_servers_synced_version() {
1940 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1941 let path = PathBuf::from("/test/file.rs");
1942 tracker
1943 .open(path.clone(), "fn main() {}".to_string())
1944 .unwrap();
1945
1946 let respawned = ServerId::from_static("rust-respawned");
1947 let untouched = ServerId::from_static("rust-diagnostics");
1948 lock_std(&tracker.documents)
1949 .get_mut(&path)
1950 .unwrap()
1951 .synced
1952 .insert(respawned.clone(), DocumentVersion::FIRST);
1953 lock_std(&tracker.documents)
1954 .get_mut(&path)
1955 .unwrap()
1956 .synced
1957 .insert(untouched.clone(), DocumentVersion::FIRST);
1958
1959 tracker.forget_server(&respawned);
1960
1961 let state = tracker.get(&path).unwrap();
1962 assert!(state.synced_version(&respawned).is_none());
1963 assert!(state.synced_version(&untouched).is_some());
1964 }
1965
1966 #[tokio::test]
1977 async fn test_sync_phase_skips_commit_when_generation_is_stale() {
1978 let dir = TempDir::new().unwrap();
1979 let path = dir.path().join("race.rs");
1980 std::fs::write(&path, "fn main() {}").unwrap();
1981 set_mtime(&path, settled_past());
1982
1983 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
1984 let server = ServerId::from_static("rust");
1985 let generation_before_respawn = 0; tracker.forget_server(&server);
1990
1991 let (stale_client, _guard) = fake_lsp_client();
1992 let decision = tracker.disk_phase(&path).await.unwrap();
1993 tracker
1994 .sync_phase(
1995 &path,
1996 &server,
1997 &stale_client,
1998 decision,
1999 generation_before_respawn,
2000 )
2001 .await
2002 .unwrap();
2003
2004 let state = tracker.get(&path).unwrap();
2005 assert!(
2006 state.synced_version(&server).is_none(),
2007 "a sync_phase call that captured a stale generation must not \
2008 commit `synced`, even though its notify against the \
2009 superseded connection succeeded"
2010 );
2011 }
2012
2013 #[tokio::test]
2018 async fn test_ensure_open_commits_when_generation_is_current() {
2019 let dir = TempDir::new().unwrap();
2020 let path = dir.path().join("no_race.rs");
2021 std::fs::write(&path, "fn main() {}").unwrap();
2022
2023 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2024 let server = ServerId::from_static("rust");
2025 let (client, _guard) = fake_lsp_client();
2026
2027 tracker.ensure_open(&path, &server, &client).await.unwrap();
2028
2029 let state = tracker.get(&path).unwrap();
2030 assert_eq!(
2031 state.synced_version(&server).map(DocumentVersion::get),
2032 Some(1)
2033 );
2034 }
2035
2036 fn mark_disk_verified(tracker: &DocumentTracker, path: &Path) {
2042 tracker.set_disk(
2043 path,
2044 DiskSync {
2045 mtime: None,
2046 size: 0,
2047 mtime_settled: false,
2048 content_checked_at: Instant::now(),
2049 },
2050 );
2051 }
2052
2053 #[test]
2058 fn test_document_limit_evicts_lru_instead_of_failing() {
2059 let limits = ResourceLimits {
2060 max_documents: DocumentLimit::new(2),
2061 max_file_size: SizeLimit::from_static(100),
2062 };
2063 let mut map = HashMap::new();
2064 map.insert(
2065 FileExtension::from_static("rs"),
2066 LanguageId::from_static("rust"),
2067 );
2068
2069 let tracker = DocumentTracker::new(limits, map);
2070
2071 tracker
2072 .open(PathBuf::from("/test/file1.rs"), "fn test1() {}".to_string())
2073 .unwrap();
2074 mark_disk_verified(&tracker, Path::new("/test/file1.rs"));
2075 tracker
2076 .open(PathBuf::from("/test/file2.rs"), "fn test2() {}".to_string())
2077 .unwrap();
2078 mark_disk_verified(&tracker, Path::new("/test/file2.rs"));
2079
2080 tracker
2081 .open(PathBuf::from("/test/file3.rs"), "fn test3() {}".to_string())
2082 .unwrap();
2083
2084 assert_eq!(tracker.len(), 2);
2085 assert!(!tracker.is_open(Path::new("/test/file1.rs")));
2086 assert!(tracker.is_open(Path::new("/test/file2.rs")));
2087 assert!(tracker.is_open(Path::new("/test/file3.rs")));
2088
2089 assert!(
2090 tracker.pending_close_paths().is_empty(),
2091 "opened directly via `open`, never synced to any server"
2092 );
2093 }
2094
2095 #[test]
2101 fn test_document_limit_falls_back_to_error_when_only_candidate_is_locked() {
2102 let limits = ResourceLimits {
2103 max_documents: DocumentLimit::new(1),
2104 max_file_size: SizeLimit::from_static(100),
2105 };
2106 let tracker = DocumentTracker::new(limits, HashMap::new());
2107
2108 let locked_path = PathBuf::from("/test/locked.rs");
2109 tracker
2110 .open(locked_path.clone(), "fn locked() {}".to_string())
2111 .unwrap();
2112 lock_std(&tracker.path_locks).insert(locked_path.clone(), Arc::new(AsyncMutex::new(())));
2113
2114 let result = tracker.open(PathBuf::from("/test/other.rs"), "fn other() {}".to_string());
2115 assert_matches!(result, Err(Error::DocumentLimitExceeded { .. }));
2116 assert!(
2117 tracker.is_open(&locked_path),
2118 "the locked document must not be evicted"
2119 );
2120 assert!(tracker.pending_close_paths().is_empty());
2121 }
2122
2123 fn pending_servers(tracker: &DocumentTracker, path: &Path) -> HashSet<ServerId> {
2125 lock_std(&tracker.pending_closes)
2126 .get(path)
2127 .map_or_default(|pending| pending.servers.clone())
2128 }
2129
2130 async fn ensure_open_disk_verified(
2133 tracker: &DocumentTracker,
2134 dir: &TempDir,
2135 name: &str,
2136 ) -> PathBuf {
2137 let path = dir.path().join(name);
2138 std::fs::write(&path, name).unwrap();
2139 set_mtime(&path, settled_past());
2140 let (client, _server) = fake_lsp_client();
2141 tracker
2142 .ensure_open(&path, &ServerId::from_static("rust"), &client)
2143 .await
2144 .unwrap();
2145 path
2146 }
2147
2148 #[tokio::test]
2152 async fn test_open_does_not_evict_in_flight_document_until_guard_drops() {
2153 let dir = TempDir::new().unwrap();
2154 let limits = ResourceLimits {
2155 max_documents: DocumentLimit::new(1),
2156 max_file_size: SizeLimit::UNLIMITED,
2157 };
2158 let tracker = DocumentTracker::new(limits, HashMap::new());
2159 let path_a = ensure_open_disk_verified(&tracker, &dir, "a.rs").await;
2160
2161 let guard = tracker.mark_in_flight(&path_a);
2162 let path_b = dir.path().join("b.rs");
2163 std::fs::write(&path_b, "BBBB").unwrap();
2164
2165 let result = tracker.open(path_b.clone(), "BBBB".to_string());
2166 assert_matches!(result, Err(Error::DocumentLimitExceeded { .. }));
2167 assert!(tracker.is_open(&path_a));
2168 assert!(tracker.pending_close_paths().is_empty());
2169
2170 drop(guard);
2171 tracker.open(path_b.clone(), "BBBB".to_string()).unwrap();
2172 assert!(!tracker.is_open(&path_a));
2173 assert!(tracker.is_open(&path_b));
2174 assert_eq!(tracker.pending_close_paths(), vec![path_a.clone()]);
2175 assert_eq!(
2176 pending_servers(&tracker, &path_a),
2177 HashSet::from([ServerId::from_static("rust")])
2178 );
2179 }
2180
2181 #[tokio::test]
2184 async fn test_in_flight_guards_are_refcounted_per_path() {
2185 let dir = TempDir::new().unwrap();
2186 let limits = ResourceLimits {
2187 max_documents: DocumentLimit::new(1),
2188 max_file_size: SizeLimit::UNLIMITED,
2189 };
2190 let tracker = DocumentTracker::new(limits, HashMap::new());
2191 let path_a = ensure_open_disk_verified(&tracker, &dir, "a.rs").await;
2192
2193 let first = tracker.mark_in_flight(&path_a);
2194 let second = tracker.mark_in_flight(&path_a);
2195 drop(first);
2196
2197 let path_b = dir.path().join("b.rs");
2198 let result = tracker.open(path_b.clone(), "BBBB".to_string());
2199 assert_matches!(
2200 result,
2201 Err(Error::DocumentLimitExceeded { .. }),
2202 "one guard still outstanding"
2203 );
2204
2205 drop(second);
2206 assert!(lock_std(&tracker.in_flight).is_empty());
2207 tracker.open(path_b, "BBBB".to_string()).unwrap();
2208 assert!(!tracker.is_open(&path_a));
2209 }
2210
2211 #[tokio::test]
2214 async fn test_in_flight_guard_released_when_holding_future_is_cancelled() {
2215 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2216 let path = PathBuf::from("/test/cancelled.rs");
2217
2218 let held = async {
2219 let _guard = tracker.mark_in_flight(&path);
2220 std::future::pending::<()>().await;
2221 };
2222 let timed_out = tokio::time::timeout(Duration::from_millis(10), held).await;
2223
2224 assert!(timed_out.is_err());
2225 assert_eq!(tracker.in_flight_count(&path), 0);
2226 }
2227
2228 #[test]
2231 fn test_evict_lru_skips_document_without_disk_snapshot() {
2232 let limits = ResourceLimits {
2233 max_documents: DocumentLimit::new(1),
2234 max_file_size: SizeLimit::UNLIMITED,
2235 };
2236 let tracker = DocumentTracker::new(limits, HashMap::new());
2237 let first = PathBuf::from("/test/first.rs");
2238 tracker
2239 .open(first.clone(), "fn first() {}".to_string())
2240 .unwrap();
2241
2242 let result = tracker.open(
2243 PathBuf::from("/test/second.rs"),
2244 "fn second() {}".to_string(),
2245 );
2246 assert_matches!(result, Err(Error::DocumentLimitExceeded { .. }));
2247 assert!(
2248 tracker.is_open(&first),
2249 "the not-disk-verified document must not be evicted"
2250 );
2251 assert!(tracker.pending_close_paths().is_empty());
2252 }
2253
2254 #[tokio::test]
2259 async fn test_ensure_open_touch_changes_lru_eviction_order() {
2260 let dir = TempDir::new().unwrap();
2261 let path_a = dir.path().join("a.rs");
2262 let path_b = dir.path().join("b.rs");
2263 std::fs::write(&path_a, "AAAA").unwrap();
2264 std::fs::write(&path_b, "BBBB").unwrap();
2265 set_mtime(&path_a, settled_past());
2266 set_mtime(&path_b, settled_past());
2267
2268 let limits = ResourceLimits {
2269 max_documents: DocumentLimit::new(2),
2270 max_file_size: SizeLimit::UNLIMITED,
2271 };
2272 let (client, _server) = fake_lsp_client();
2273 let tracker = DocumentTracker::new(limits, HashMap::new());
2274 let server_id = ServerId::from_static("rust");
2275
2276 tracker
2277 .ensure_open(&path_a, &server_id, &client)
2278 .await
2279 .unwrap();
2280 tracker
2281 .ensure_open(&path_b, &server_id, &client)
2282 .await
2283 .unwrap();
2284
2285 tracker
2288 .ensure_open(&path_a, &server_id, &client)
2289 .await
2290 .unwrap();
2291
2292 let path_c = dir.path().join("c.rs");
2293 std::fs::write(&path_c, "CCCC").unwrap();
2294 set_mtime(&path_c, settled_past());
2295 tracker
2296 .ensure_open(&path_c, &server_id, &client)
2297 .await
2298 .unwrap();
2299
2300 assert!(
2301 tracker.is_open(&path_a),
2302 "recently re-accessed, must survive"
2303 );
2304 assert!(
2305 !tracker.is_open(&path_b),
2306 "least-recently-used, must be evicted"
2307 );
2308 assert!(tracker.is_open(&path_c));
2309
2310 assert_eq!(tracker.pending_close_paths(), vec![path_b.clone()]);
2311 assert_eq!(
2312 pending_servers(&tracker, &path_b),
2313 HashSet::from([server_id])
2314 );
2315 }
2316
2317 #[test]
2318 fn test_file_size_limit() {
2319 let limits = ResourceLimits {
2320 max_documents: DocumentLimit::new(10),
2321 max_file_size: SizeLimit::from_static(10),
2322 };
2323 let mut map = HashMap::new();
2324 map.insert(
2325 FileExtension::from_static("rs"),
2326 LanguageId::from_static("rust"),
2327 );
2328
2329 let tracker = DocumentTracker::new(limits, map);
2330
2331 tracker
2333 .open(PathBuf::from("/test/small.rs"), "fn f(){}".to_string())
2334 .unwrap();
2335
2336 let large_content = "x".repeat(100);
2338 let result = tracker.open(PathBuf::from("/test/large.rs"), large_content);
2339 assert_matches!(result, Err(Error::FileSizeLimitExceeded { .. }));
2340 }
2341
2342 #[test]
2343 fn test_resource_limits_default() {
2344 let limits = ResourceLimits::default();
2345 assert_eq!(limits.max_documents, DocumentLimit::new(100));
2346 assert_eq!(
2347 limits.max_file_size,
2348 SizeLimit::from_static(10 * 1024 * 1024)
2349 );
2350 }
2351
2352 #[test]
2353 fn test_resource_limits_custom() {
2354 let limits = ResourceLimits {
2355 max_documents: DocumentLimit::new(50),
2356 max_file_size: SizeLimit::from_static(5 * 1024 * 1024),
2357 };
2358 assert_eq!(limits.max_documents, DocumentLimit::new(50));
2359 assert_eq!(
2360 limits.max_file_size,
2361 SizeLimit::from_static(5 * 1024 * 1024)
2362 );
2363 }
2364
2365 #[test]
2366 fn test_resource_limits_zero_unlimited() {
2367 let limits = ResourceLimits {
2368 max_documents: DocumentLimit::UNLIMITED,
2369 max_file_size: SizeLimit::UNLIMITED,
2370 };
2371 let mut map = HashMap::new();
2372 map.insert(
2373 FileExtension::from_static("rs"),
2374 LanguageId::from_static("rust"),
2375 );
2376
2377 let tracker = DocumentTracker::new(limits, map);
2378
2379 for i in 0..200 {
2381 tracker
2382 .open(
2383 PathBuf::from(format!("/test/file{i}.rs")),
2384 "content".to_string(),
2385 )
2386 .unwrap();
2387 }
2388 assert_eq!(tracker.len(), 200);
2389
2390 let huge_content = "x".repeat(100_000_000);
2392 tracker
2393 .open(PathBuf::from("/test/huge.rs"), huge_content)
2394 .unwrap();
2395 }
2396
2397 #[test]
2398 fn test_document_state_clone() {
2399 let state = DocumentState {
2400 uri: Uri::from("file:///test.rs"),
2401 language_id: LanguageId::from_static("rust"),
2402 version: DocumentVersion::new(5),
2403 text: DocumentText::new("fn main() {}".to_string()),
2404 disk: None,
2405 synced: HashMap::new(),
2406 opened: Opening(0),
2407 last_accessed: Instant::now(),
2408 };
2409
2410 #[allow(clippy::redundant_clone, reason = "the test exercises `Clone` itself")]
2411 let cloned = state.clone();
2412 assert_eq!(cloned.uri(), state.uri());
2413 assert_eq!(cloned.language_id(), state.language_id());
2414 assert_eq!(cloned.version().get(), 5);
2415 assert_eq!(cloned.content(), state.content());
2416 }
2417
2418 #[test]
2419 fn test_line_presence_distinguishes_untracked_present_and_beyond() {
2420 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2421 let path = PathBuf::from("/test/lines.rs");
2422 assert_eq!(tracker.line_presence(&path, 0), LinePresence::Untracked);
2423
2424 tracker.open(path.clone(), "a\r\nb\n".to_string()).unwrap();
2425 assert_eq!(tracker.line_presence(&path, 0), LinePresence::Present);
2426 assert_eq!(tracker.line_presence(&path, 1), LinePresence::Present);
2427 assert_eq!(tracker.line_presence(&path, 2), LinePresence::Present);
2428 let beyond = LinePresence::Beyond {
2429 last_line: NonZeroU32::new(3).unwrap(),
2430 };
2431 assert_eq!(tracker.line_presence(&path, 3), beyond);
2432 assert_eq!(tracker.line_presence(&path, u32::MAX), beyond);
2433
2434 let empty = PathBuf::from("/test/empty.rs");
2435 tracker.open(empty.clone(), String::new()).unwrap();
2436 assert_eq!(tracker.line_presence(&empty, 0), LinePresence::Present);
2437 assert_eq!(
2438 tracker.line_presence(&empty, 1),
2439 LinePresence::Beyond {
2440 last_line: NonZeroU32::MIN
2441 }
2442 );
2443 }
2444
2445 #[test]
2446 fn test_close_nonexistent_document() {
2447 let map = HashMap::new();
2448 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2449 let path = PathBuf::from("/test/nonexistent.rs");
2450
2451 let state = tracker.close(&path);
2452 assert_eq!(
2453 state, None,
2454 "Closing non-existent document should return None"
2455 );
2456 }
2457
2458 #[test]
2459 fn test_get_nonexistent_document() {
2460 let map = HashMap::new();
2461 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2462 let path = PathBuf::from("/test/nonexistent.rs");
2463
2464 let state = tracker.get(&path);
2465 assert!(
2466 state.is_none(),
2467 "Getting non-existent document should return None"
2468 );
2469 }
2470
2471 #[test]
2472 #[allow(
2473 clippy::too_many_lines,
2474 reason = "a table of extension mappings; splitting would scatter it"
2475 )]
2476 fn test_detect_language_all_extensions() {
2477 let mut map = HashMap::new();
2478 map.insert(
2479 FileExtension::from_static("rs"),
2480 LanguageId::from_static("rust"),
2481 );
2482 map.insert(
2483 FileExtension::from_static("py"),
2484 LanguageId::from_static("python"),
2485 );
2486 map.insert(
2487 FileExtension::from_static("pyw"),
2488 LanguageId::from_static("python"),
2489 );
2490 map.insert(
2491 FileExtension::from_static("pyi"),
2492 LanguageId::from_static("python"),
2493 );
2494 map.insert(
2495 FileExtension::from_static("js"),
2496 LanguageId::from_static("javascript"),
2497 );
2498 map.insert(
2499 FileExtension::from_static("mjs"),
2500 LanguageId::from_static("javascript"),
2501 );
2502 map.insert(
2503 FileExtension::from_static("cjs"),
2504 LanguageId::from_static("javascript"),
2505 );
2506 map.insert(
2507 FileExtension::from_static("ts"),
2508 LanguageId::from_static("typescript"),
2509 );
2510 map.insert(
2511 FileExtension::from_static("mts"),
2512 LanguageId::from_static("typescript"),
2513 );
2514 map.insert(
2515 FileExtension::from_static("cts"),
2516 LanguageId::from_static("typescript"),
2517 );
2518 map.insert(
2519 FileExtension::from_static("tsx"),
2520 LanguageId::from_static("typescriptreact"),
2521 );
2522 map.insert(
2523 FileExtension::from_static("jsx"),
2524 LanguageId::from_static("javascriptreact"),
2525 );
2526 map.insert(
2527 FileExtension::from_static("go"),
2528 LanguageId::from_static("go"),
2529 );
2530 map.insert(
2531 FileExtension::from_static("c"),
2532 LanguageId::from_static("c"),
2533 );
2534 map.insert(
2535 FileExtension::from_static("h"),
2536 LanguageId::from_static("c"),
2537 );
2538 map.insert(
2539 FileExtension::from_static("cpp"),
2540 LanguageId::from_static("cpp"),
2541 );
2542 map.insert(
2543 FileExtension::from_static("cc"),
2544 LanguageId::from_static("cpp"),
2545 );
2546 map.insert(
2547 FileExtension::from_static("cxx"),
2548 LanguageId::from_static("cpp"),
2549 );
2550 map.insert(
2551 FileExtension::from_static("hpp"),
2552 LanguageId::from_static("cpp"),
2553 );
2554 map.insert(
2555 FileExtension::from_static("hh"),
2556 LanguageId::from_static("cpp"),
2557 );
2558 map.insert(
2559 FileExtension::from_static("hxx"),
2560 LanguageId::from_static("cpp"),
2561 );
2562 map.insert(
2563 FileExtension::from_static("java"),
2564 LanguageId::from_static("java"),
2565 );
2566 map.insert(
2567 FileExtension::from_static("rb"),
2568 LanguageId::from_static("ruby"),
2569 );
2570 map.insert(
2571 FileExtension::from_static("php"),
2572 LanguageId::from_static("php"),
2573 );
2574 map.insert(
2575 FileExtension::from_static("swift"),
2576 LanguageId::from_static("swift"),
2577 );
2578 map.insert(
2579 FileExtension::from_static("kt"),
2580 LanguageId::from_static("kotlin"),
2581 );
2582 map.insert(
2583 FileExtension::from_static("kts"),
2584 LanguageId::from_static("kotlin"),
2585 );
2586 map.insert(
2587 FileExtension::from_static("scala"),
2588 LanguageId::from_static("scala"),
2589 );
2590 map.insert(
2591 FileExtension::from_static("sc"),
2592 LanguageId::from_static("scala"),
2593 );
2594 map.insert(
2595 FileExtension::from_static("zig"),
2596 LanguageId::from_static("zig"),
2597 );
2598 map.insert(
2599 FileExtension::from_static("lua"),
2600 LanguageId::from_static("lua"),
2601 );
2602 map.insert(
2603 FileExtension::from_static("sh"),
2604 LanguageId::from_static("shellscript"),
2605 );
2606 map.insert(
2607 FileExtension::from_static("bash"),
2608 LanguageId::from_static("shellscript"),
2609 );
2610 map.insert(
2611 FileExtension::from_static("zsh"),
2612 LanguageId::from_static("shellscript"),
2613 );
2614 map.insert(
2615 FileExtension::from_static("json"),
2616 LanguageId::from_static("json"),
2617 );
2618 map.insert(
2619 FileExtension::from_static("toml"),
2620 LanguageId::from_static("toml"),
2621 );
2622 map.insert(
2623 FileExtension::from_static("yaml"),
2624 LanguageId::from_static("yaml"),
2625 );
2626 map.insert(
2627 FileExtension::from_static("yml"),
2628 LanguageId::from_static("yaml"),
2629 );
2630 map.insert(
2631 FileExtension::from_static("xml"),
2632 LanguageId::from_static("xml"),
2633 );
2634 map.insert(
2635 FileExtension::from_static("html"),
2636 LanguageId::from_static("html"),
2637 );
2638 map.insert(
2639 FileExtension::from_static("htm"),
2640 LanguageId::from_static("html"),
2641 );
2642 map.insert(
2643 FileExtension::from_static("css"),
2644 LanguageId::from_static("css"),
2645 );
2646 map.insert(
2647 FileExtension::from_static("scss"),
2648 LanguageId::from_static("scss"),
2649 );
2650 map.insert(
2651 FileExtension::from_static("less"),
2652 LanguageId::from_static("less"),
2653 );
2654 map.insert(
2655 FileExtension::from_static("md"),
2656 LanguageId::from_static("markdown"),
2657 );
2658 map.insert(
2659 FileExtension::from_static("markdown"),
2660 LanguageId::from_static("markdown"),
2661 );
2662
2663 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
2664 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
2665 assert_eq!(detect_language(Path::new("script.pyw"), &map), "python");
2666 assert_eq!(detect_language(Path::new("script.pyi"), &map), "python");
2667 assert_eq!(detect_language(Path::new("app.js"), &map), "javascript");
2668 assert_eq!(detect_language(Path::new("app.mjs"), &map), "javascript");
2669 assert_eq!(detect_language(Path::new("app.cjs"), &map), "javascript");
2670 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
2671 assert_eq!(detect_language(Path::new("app.mts"), &map), "typescript");
2672 assert_eq!(detect_language(Path::new("app.cts"), &map), "typescript");
2673 assert_eq!(
2674 detect_language(Path::new("component.tsx"), &map),
2675 "typescriptreact"
2676 );
2677 assert_eq!(
2678 detect_language(Path::new("component.jsx"), &map),
2679 "javascriptreact"
2680 );
2681 assert_eq!(detect_language(Path::new("main.go"), &map), "go");
2682 assert_eq!(detect_language(Path::new("main.c"), &map), "c");
2683 assert_eq!(detect_language(Path::new("header.h"), &map), "c");
2684 assert_eq!(detect_language(Path::new("main.cpp"), &map), "cpp");
2685 assert_eq!(detect_language(Path::new("main.cc"), &map), "cpp");
2686 assert_eq!(detect_language(Path::new("main.cxx"), &map), "cpp");
2687 assert_eq!(detect_language(Path::new("header.hpp"), &map), "cpp");
2688 assert_eq!(detect_language(Path::new("header.hh"), &map), "cpp");
2689 assert_eq!(detect_language(Path::new("header.hxx"), &map), "cpp");
2690 assert_eq!(detect_language(Path::new("Main.java"), &map), "java");
2691 assert_eq!(detect_language(Path::new("script.rb"), &map), "ruby");
2692 assert_eq!(detect_language(Path::new("index.php"), &map), "php");
2693 assert_eq!(detect_language(Path::new("App.swift"), &map), "swift");
2694 assert_eq!(detect_language(Path::new("Main.kt"), &map), "kotlin");
2695 assert_eq!(detect_language(Path::new("script.kts"), &map), "kotlin");
2696 assert_eq!(detect_language(Path::new("Main.scala"), &map), "scala");
2697 assert_eq!(detect_language(Path::new("script.sc"), &map), "scala");
2698 assert_eq!(detect_language(Path::new("main.zig"), &map), "zig");
2699 assert_eq!(detect_language(Path::new("script.lua"), &map), "lua");
2700 assert_eq!(detect_language(Path::new("script.sh"), &map), "shellscript");
2701 assert_eq!(
2702 detect_language(Path::new("script.bash"), &map),
2703 "shellscript"
2704 );
2705 assert_eq!(
2706 detect_language(Path::new("script.zsh"), &map),
2707 "shellscript"
2708 );
2709 assert_eq!(detect_language(Path::new("data.json"), &map), "json");
2710 assert_eq!(detect_language(Path::new("config.toml"), &map), "toml");
2711 assert_eq!(detect_language(Path::new("config.yaml"), &map), "yaml");
2712 assert_eq!(detect_language(Path::new("config.yml"), &map), "yaml");
2713 assert_eq!(detect_language(Path::new("data.xml"), &map), "xml");
2714 assert_eq!(detect_language(Path::new("index.html"), &map), "html");
2715 assert_eq!(detect_language(Path::new("index.htm"), &map), "html");
2716 assert_eq!(detect_language(Path::new("styles.css"), &map), "css");
2717 assert_eq!(detect_language(Path::new("styles.scss"), &map), "scss");
2718 assert_eq!(detect_language(Path::new("styles.less"), &map), "less");
2719 assert_eq!(detect_language(Path::new("README.md"), &map), "markdown");
2720 assert_eq!(
2721 detect_language(Path::new("README.markdown"), &map),
2722 "markdown"
2723 );
2724 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
2725 assert_eq!(
2726 detect_language(Path::new("no_extension"), &map),
2727 "plaintext"
2728 );
2729 }
2730
2731 #[test]
2732 fn test_path_to_uri_unix() {
2733 #[cfg(not(windows))]
2734 {
2735 let path = Path::new("/home/user/project/main.rs");
2736 let uri = path_to_uri(path).unwrap();
2737 assert!(
2738 uri.as_ref()
2739 .starts_with("file:///home/user/project/main.rs")
2740 );
2741 }
2742 }
2743
2744 #[test]
2745 fn test_path_to_uri_with_special_chars() {
2746 let path = Path::new("/home/user/project-test/main.rs");
2747 let uri = path_to_uri(path).unwrap();
2748 assert!(uri.as_ref().starts_with("file://"));
2749 assert!(uri.as_ref().contains("project-test"));
2750 }
2751
2752 #[test]
2753 fn test_path_to_uri_percent_encodes_reserved_chars() {
2754 #[cfg(windows)]
2755 let path = Path::new(r"C:\home\user\routes\api\[...]^|.ts");
2756 #[cfg(not(windows))]
2757 let path = Path::new("/home/user/routes/api/[...]^|.ts");
2758
2759 let uri = path_to_uri(path).unwrap();
2760
2761 #[cfg(windows)]
2762 let expected = "file:///C:/home/user/routes/api/%5B...%5D%5E%7C.ts";
2763 #[cfg(not(windows))]
2764 let expected = "file:///home/user/routes/api/%5B...%5D%5E%7C.ts";
2765
2766 assert_eq!(uri.as_ref(), expected);
2767 assert_eq!(
2768 uri_to_path(&uri).as_deref(),
2769 Some(path),
2770 "encoded file URI should round-trip to the original path"
2771 );
2772 }
2773
2774 #[test]
2775 fn test_try_path_to_uri_returns_none_for_relative_path() {
2776 assert_eq!(try_path_to_uri(Path::new("relative/file.ts")), None);
2777 }
2778
2779 #[test]
2783 fn test_path_to_uri_returns_err_for_relative_path() {
2784 let err = path_to_uri(Path::new("relative/file.ts")).unwrap_err();
2785 assert_matches!(err, Error::PathToUri(_));
2786 }
2787
2788 #[cfg(windows)]
2789 #[test]
2790 fn test_try_path_to_uri_encodes_synthetic_windows_root() {
2791 let uri = try_path_to_uri(Path::new("/home/user/#work %23")).unwrap();
2792
2793 assert_eq!(uri.as_ref(), "file:///home/user/%23work%20%2523");
2794 }
2795
2796 #[cfg(windows)]
2804 #[test]
2805 fn test_try_path_to_uri_accepts_rooted_but_not_absolute_windows_path() {
2806 let path = Path::new(r"\foo");
2807 assert!(path.has_root());
2808 assert!(!path.is_absolute());
2809
2810 let uri = try_path_to_uri(path).unwrap();
2811
2812 assert_eq!(uri.as_ref(), "file:///foo");
2813 }
2814
2815 #[test]
2816 fn test_path_to_uri_percent_encodes_reserved_chars_in_short_path() {
2817 #[cfg(windows)]
2819 let path = Path::new(r"C:\[a].ts");
2820 #[cfg(not(windows))]
2821 let path = Path::new("/[a].ts");
2822
2823 let uri = path_to_uri(path).unwrap();
2824
2825 assert!(
2826 uri.as_ref().ends_with("%5Ba%5D.ts"),
2827 "short path should percent-encode reserved chars, got {}",
2828 uri.as_ref()
2829 );
2830 assert_eq!(uri_to_path(&uri).as_deref(), Some(path));
2831 }
2832
2833 #[test]
2834 fn test_path_to_uri_percent_encodes_all_rfc3986_other_reserved_chars() {
2835 #[cfg(windows)]
2839 let path = Path::new(r"C:\home\user\test[]^|{}`.ts");
2840 #[cfg(not(windows))]
2841 let path = Path::new("/home/user/test[]^|{}`.ts");
2842
2843 let uri = try_path_to_uri(path).unwrap();
2844 let uri_str = uri.as_ref();
2845
2846 for (raw, encoded) in [
2847 ('[', "%5B"),
2848 (']', "%5D"),
2849 ('^', "%5E"),
2850 ('|', "%7C"),
2851 ('{', "%7B"),
2852 ('}', "%7D"),
2853 ('`', "%60"),
2854 ] {
2855 assert!(
2856 uri_str.contains(encoded),
2857 "expected {raw:?} to be percent-encoded as {encoded} in {uri_str}"
2858 );
2859 }
2860 assert!(
2861 !uri_str.contains(['[', ']', '^', '|', '{', '}', '`']),
2862 "no raw reserved characters should remain in {uri_str}"
2863 );
2864 }
2865
2866 #[tokio::test]
2867 async fn test_document_tracker_concurrent_operations() {
2868 let mut map = HashMap::new();
2869 map.insert(
2870 FileExtension::from_static("rs"),
2871 LanguageId::from_static("rust"),
2872 );
2873
2874 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2875 let path1 = PathBuf::from("/test/file1.rs");
2876 let path2 = PathBuf::from("/test/file2.rs");
2877
2878 tracker.open(path1.clone(), "content1".to_string()).unwrap();
2879 tracker.open(path2.clone(), "content2".to_string()).unwrap();
2880
2881 assert_eq!(tracker.len(), 2);
2882 assert!(tracker.is_open(&path1));
2883 assert!(tracker.is_open(&path2));
2884
2885 assert_eq!(tracker.get(&path1).unwrap().content(), "content1");
2886 assert_eq!(tracker.get(&path2).unwrap().content(), "content2");
2887
2888 tracker.close(&path1);
2889 assert_eq!(tracker.len(), 1);
2890 assert!(!tracker.is_open(&path1));
2891 assert!(tracker.is_open(&path2));
2892 }
2893
2894 #[test]
2895 fn test_empty_content() {
2896 let mut map = HashMap::new();
2897 map.insert(
2898 FileExtension::from_static("rs"),
2899 LanguageId::from_static("rust"),
2900 );
2901
2902 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2903 let path = PathBuf::from("/test/empty.rs");
2904
2905 tracker.open(path.clone(), String::new()).unwrap();
2906 assert!(tracker.is_open(&path));
2907 assert_eq!(tracker.get(&path).unwrap().content(), "");
2908 }
2909
2910 #[test]
2911 fn test_unicode_content() {
2912 let mut map = HashMap::new();
2913 map.insert(
2914 FileExtension::from_static("rs"),
2915 LanguageId::from_static("rust"),
2916 );
2917
2918 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2919 let path = PathBuf::from("/test/unicode.rs");
2920 let content = "fn テスト() { println!(\"こんにちは\"); }";
2921
2922 tracker.open(path.clone(), content.to_string()).unwrap();
2923 assert_eq!(tracker.get(&path).unwrap().content(), content);
2924 }
2925
2926 #[test]
2930 fn test_document_limit_exact_boundary() {
2931 let limits = ResourceLimits {
2932 max_documents: DocumentLimit::new(5),
2933 max_file_size: SizeLimit::from_static(1000),
2934 };
2935 let mut map = HashMap::new();
2936 map.insert(
2937 FileExtension::from_static("rs"),
2938 LanguageId::from_static("rust"),
2939 );
2940
2941 let tracker = DocumentTracker::new(limits, map);
2942
2943 for i in 0..5 {
2944 let path = PathBuf::from(format!("/test/file{i}.rs"));
2945 tracker.open(path.clone(), "content".to_string()).unwrap();
2946 mark_disk_verified(&tracker, &path);
2947 }
2948
2949 assert_eq!(tracker.len(), 5);
2950
2951 tracker
2952 .open(PathBuf::from("/test/file6.rs"), "content".to_string())
2953 .unwrap();
2954
2955 assert_eq!(tracker.len(), 5);
2956 assert!(!tracker.is_open(Path::new("/test/file0.rs")));
2957 assert!(tracker.is_open(Path::new("/test/file6.rs")));
2958 }
2959
2960 #[test]
2961 fn test_file_size_exact_boundary() {
2962 let limits = ResourceLimits {
2963 max_documents: DocumentLimit::new(10),
2964 max_file_size: SizeLimit::from_static(100),
2965 };
2966 let mut map = HashMap::new();
2967 map.insert(
2968 FileExtension::from_static("rs"),
2969 LanguageId::from_static("rust"),
2970 );
2971
2972 let tracker = DocumentTracker::new(limits, map);
2973
2974 let exact_size_content = "x".repeat(100);
2975 tracker
2976 .open(PathBuf::from("/test/exact.rs"), exact_size_content)
2977 .unwrap();
2978
2979 let over_size_content = "x".repeat(101);
2980 let result = tracker.open(PathBuf::from("/test/over.rs"), over_size_content);
2981 assert_matches!(result, Err(Error::FileSizeLimitExceeded { .. }));
2982 }
2983
2984 #[test]
2985 fn test_detect_language_with_custom_extension() {
2986 let mut map = HashMap::new();
2987 map.insert(
2988 FileExtension::from_static("nu"),
2989 LanguageId::from_static("nushell"),
2990 );
2991
2992 assert_eq!(detect_language(Path::new("script.nu"), &map), "nushell");
2993
2994 let empty_map = HashMap::new();
2995 assert_eq!(
2996 detect_language(Path::new("script.nu"), &empty_map),
2997 "plaintext"
2998 );
2999 }
3000
3001 #[test]
3002 fn test_detect_language_custom_overrides_default() {
3003 let mut custom_map = HashMap::new();
3004 custom_map.insert(
3005 FileExtension::from_static("rs"),
3006 LanguageId::from_static("custom-rust"),
3007 );
3008
3009 assert_eq!(
3010 detect_language(Path::new("main.rs"), &custom_map),
3011 "custom-rust"
3012 );
3013
3014 let mut default_map = HashMap::new();
3015 default_map.insert(
3016 FileExtension::from_static("rs"),
3017 LanguageId::from_static("rust"),
3018 );
3019
3020 assert_eq!(detect_language(Path::new("main.rs"), &default_map), "rust");
3021 }
3022
3023 #[test]
3024 fn test_detect_language_fallback_to_plaintext() {
3025 let mut map = HashMap::new();
3026 map.insert(
3027 FileExtension::from_static("nu"),
3028 LanguageId::from_static("nushell"),
3029 );
3030
3031 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
3033 }
3034
3035 #[test]
3036 fn test_detect_language_empty_map() {
3037 let map = HashMap::new();
3038 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
3039 }
3040
3041 #[test]
3042 fn test_document_tracker_with_extensions() {
3043 let mut map = HashMap::new();
3044 map.insert(
3045 FileExtension::from_static("nu"),
3046 LanguageId::from_static("nushell"),
3047 );
3048
3049 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3050
3051 let path = PathBuf::from("/test/script.nu");
3052 tracker
3053 .open(path.clone(), "# nushell script".to_string())
3054 .unwrap();
3055
3056 let state = tracker.get(&path).unwrap();
3057 assert_eq!(state.language_id(), "nushell");
3058 }
3059
3060 #[test]
3061 fn test_document_tracker_uses_provided_map() {
3062 let mut map = HashMap::new();
3063 map.insert(
3064 FileExtension::from_static("rs"),
3065 LanguageId::from_static("rust"),
3066 );
3067
3068 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3069 let path = PathBuf::from("/test/main.rs");
3070 tracker
3071 .open(path.clone(), "fn main() {}".to_string())
3072 .unwrap();
3073
3074 let state = tracker.get(&path).unwrap();
3075 assert_eq!(state.language_id(), "rust");
3076 }
3077
3078 #[test]
3079 fn test_multiple_extensions_same_language() {
3080 let mut map = HashMap::new();
3081 map.insert(
3082 FileExtension::from_static("cpp"),
3083 LanguageId::from_static("c++"),
3084 );
3085 map.insert(
3086 FileExtension::from_static("cc"),
3087 LanguageId::from_static("c++"),
3088 );
3089 map.insert(
3090 FileExtension::from_static("cxx"),
3091 LanguageId::from_static("c++"),
3092 );
3093
3094 assert_eq!(detect_language(Path::new("main.cpp"), &map), "c++");
3095 assert_eq!(detect_language(Path::new("main.cc"), &map), "c++");
3096 assert_eq!(detect_language(Path::new("main.cxx"), &map), "c++");
3097 }
3098
3099 #[test]
3100 fn test_case_sensitive_extensions() {
3101 let mut map = HashMap::new();
3102 map.insert(
3103 FileExtension::from_static("NU"),
3104 LanguageId::from_static("nushell"),
3105 );
3106
3107 assert_eq!(detect_language(Path::new("script.nu"), &map), "plaintext");
3109 }
3110
3111 #[cfg(unix)]
3116 #[test]
3117 fn test_uri_to_path_file_scheme() {
3118 let uri: Uri = Uri::from("file:///home/user/main.rs");
3119 let path = uri_to_path(&uri).unwrap();
3120 assert_eq!(path, PathBuf::from("/home/user/main.rs"));
3121 }
3122
3123 #[test]
3124 fn test_uri_to_path_non_file_scheme_returns_none() {
3125 let uri: Uri = Uri::from("https://example.com/file.rs");
3126 assert!(uri_to_path(&uri).is_none());
3127 }
3128
3129 #[test]
3130 fn test_uri_to_path_lsp_diagnostics_scheme_returns_none() {
3131 let uri: Uri = Uri::from("lsp-diagnostics:///home/user/main.rs");
3133 assert!(uri_to_path(&uri).is_none());
3134 }
3135
3136 #[test]
3137 fn test_uri_to_path_with_authority_returns_none() {
3138 let result = uri_to_path(&Uri::from("file://server/share/path.rs"));
3142 assert!(result.is_none());
3143 }
3144
3145 #[test]
3150 fn test_open_paths_empty_tracker() {
3151 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3152 assert_eq!(tracker.open_paths().len(), 0);
3153 }
3154
3155 #[test]
3156 fn test_open_paths_populated_tracker() {
3157 let mut map = HashMap::new();
3158 map.insert(
3159 FileExtension::from_static("rs"),
3160 LanguageId::from_static("rust"),
3161 );
3162 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3163 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
3164 tracker.open(PathBuf::from("/b.rs"), String::new()).unwrap();
3165 let mut paths = tracker.open_paths();
3166 paths.sort();
3167 assert_eq!(paths, [PathBuf::from("/a.rs"), PathBuf::from("/b.rs")]);
3168 }
3169
3170 #[test]
3171 fn test_open_paths_after_close() {
3172 let mut map = HashMap::new();
3173 map.insert(
3174 FileExtension::from_static("rs"),
3175 LanguageId::from_static("rust"),
3176 );
3177 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3178 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
3179 tracker.close(Path::new("/a.rs"));
3180 assert_eq!(tracker.open_paths().len(), 0);
3181 }
3182
3183 use tempfile::TempDir;
3188 use tokio::io::BufReader;
3189
3190 use crate::test_lsp::{fake_lsp_client, read_framed_message};
3191
3192 fn set_mtime(path: &Path, time: SystemTime) {
3200 let file = std::fs::OpenOptions::new().write(true).open(path).unwrap();
3201 file.set_modified(time).unwrap();
3202 }
3203
3204 fn settled_past() -> SystemTime {
3205 SystemTime::now() - Duration::from_secs(10)
3206 }
3207
3208 #[test]
3209 fn test_mtime_settled_boundary() {
3210 let read_at = SystemTime::now();
3211 assert!(!mtime_settled(None, read_at), "no mtime is never settled");
3212 assert!(
3213 mtime_settled(Some(read_at - Duration::from_secs(3)), read_at),
3214 "3s older than read_at is past the 2s granularity margin"
3215 );
3216 assert!(
3217 !mtime_settled(Some(read_at - Duration::from_secs(1)), read_at),
3218 "1s older than read_at is within the 2s granularity margin"
3219 );
3220 assert!(
3221 !mtime_settled(Some(read_at + Duration::from_secs(10)), read_at),
3222 "an mtime after read_at is never settled"
3223 );
3224 }
3225
3226 #[tokio::test]
3227 async fn test_ensure_open_unchanged_file_is_fast_path() {
3228 let dir = TempDir::new().unwrap();
3229 let path = dir.path().join("a.rs");
3230 std::fs::write(&path, "fn main() {}").unwrap();
3231 set_mtime(&path, settled_past());
3232
3233 let (client, _server) = fake_lsp_client();
3234 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3235
3236 let uri1 = tracker
3237 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3238 .await
3239 .unwrap();
3240 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
3241
3242 let uri2 = tracker
3243 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3244 .await
3245 .unwrap();
3246 assert_eq!(uri1, uri2);
3247 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
3248 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
3249 }
3250
3251 #[tokio::test]
3252 async fn test_ensure_open_resyncs_on_size_change() {
3253 let dir = TempDir::new().unwrap();
3254 let path = dir.path().join("a.rs");
3255 std::fs::write(&path, "fn main() {}").unwrap();
3256 set_mtime(&path, settled_past());
3257
3258 let (client, _server) = fake_lsp_client();
3259 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3260 tracker
3261 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3262 .await
3263 .unwrap();
3264
3265 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
3266 set_mtime(&path, settled_past());
3267
3268 tracker
3269 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3270 .await
3271 .unwrap();
3272 let state = tracker.get(&path).unwrap();
3273 assert_eq!(state.version().get(), 2);
3274 assert_eq!(state.content(), "fn main() { println!(\"hi\"); }");
3275 }
3276
3277 #[tokio::test(start_paused = true)]
3278 async fn test_ensure_open_regression_102_103_racy_same_size_rewrite() {
3279 let dir = TempDir::new().unwrap();
3280 let path = dir.path().join("a.rs");
3281 std::fs::write(&path, "AAAA").unwrap();
3282 let (client, _server) = fake_lsp_client();
3285 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3286 tracker
3287 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3288 .await
3289 .unwrap();
3290 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
3291
3292 std::fs::write(&path, "BBBB").unwrap();
3295 set_mtime(&path, original_mtime);
3296
3297 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
3298
3299 tracker
3300 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3301 .await
3302 .unwrap();
3303 let state = tracker.get(&path).unwrap();
3304 assert_eq!(
3305 state.version().get(),
3306 2,
3307 "must resync despite identical (mtime, size)"
3308 );
3309 assert_eq!(state.content(), "BBBB");
3310 }
3311
3312 #[tokio::test(start_paused = true)]
3313 async fn test_ensure_open_regression_102_103_settled_mtime_is_the_documented_limit() {
3314 let dir = TempDir::new().unwrap();
3315 let path = dir.path().join("a.rs");
3316 std::fs::write(&path, "AAAA").unwrap();
3317 set_mtime(&path, settled_past());
3318
3319 let (client, _server) = fake_lsp_client();
3320 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3321 tracker
3322 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3323 .await
3324 .unwrap();
3325 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
3326
3327 std::fs::write(&path, "BBBB").unwrap();
3331 set_mtime(&path, original_mtime);
3332
3333 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
3334
3335 tracker
3336 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3337 .await
3338 .unwrap();
3339 let state = tracker.get(&path).unwrap();
3340 assert_eq!(
3341 state.version().get(),
3342 1,
3343 "documented limitation: fast path taken"
3344 );
3345 assert_eq!(state.content(), "AAAA");
3346 }
3347
3348 #[tokio::test(start_paused = true)]
3349 async fn test_ensure_open_stat_is_never_debounced() {
3350 let dir = TempDir::new().unwrap();
3351 let path = dir.path().join("a.rs");
3352 std::fs::write(&path, "AAAA").unwrap();
3353 set_mtime(&path, settled_past());
3354
3355 let (client, _server) = fake_lsp_client();
3356 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3357 tracker
3358 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3359 .await
3360 .unwrap();
3361
3362 std::fs::write(&path, "BBBBBBBB").unwrap();
3365 tracker
3366 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3367 .await
3368 .unwrap();
3369
3370 let state = tracker.get(&path).unwrap();
3371 assert_eq!(state.version().get(), 2);
3372 assert_eq!(state.content(), "BBBBBBBB");
3373 }
3374
3375 #[tokio::test(start_paused = true)]
3376 async fn test_ensure_open_debounce_gates_reread_only() {
3377 let dir = TempDir::new().unwrap();
3378 let path = dir.path().join("a.rs");
3379 std::fs::write(&path, "AAAA").unwrap();
3380 let (client, _server) = fake_lsp_client();
3383 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3384 tracker
3385 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3386 .await
3387 .unwrap();
3388 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
3389
3390 std::fs::write(&path, "BBBB").unwrap(); set_mtime(&path, original_mtime); tracker
3395 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3396 .await
3397 .unwrap();
3398 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
3399
3400 tokio::time::advance(Duration::from_millis(300)).await;
3401 tracker
3402 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3403 .await
3404 .unwrap();
3405 let state = tracker.get(&path).unwrap();
3406 assert_eq!(state.version().get(), 2);
3407 assert_eq!(state.content(), "BBBB");
3408 }
3409
3410 #[tokio::test]
3411 async fn test_ensure_open_deleted_file_errors_state_untouched() {
3412 let dir = TempDir::new().unwrap();
3413 let path = dir.path().join("a.rs");
3414 std::fs::write(&path, "fn main() {}").unwrap();
3415 set_mtime(&path, settled_past());
3416
3417 let (client, _server) = fake_lsp_client();
3418 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3419 tracker
3420 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3421 .await
3422 .unwrap();
3423
3424 std::fs::remove_file(&path).unwrap();
3425
3426 let result = tracker
3427 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3428 .await;
3429 assert_matches!(result, Err(Error::FileIo { .. }));
3430 assert!(tracker.is_open(&path));
3431 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
3432 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
3433 }
3434
3435 #[tokio::test]
3436 async fn test_ensure_open_grows_past_limit_errors_state_intact() {
3437 let dir = TempDir::new().unwrap();
3438 let path = dir.path().join("a.rs");
3439 std::fs::write(&path, "small").unwrap();
3440 set_mtime(&path, settled_past());
3441
3442 let limits = ResourceLimits {
3443 max_documents: DocumentLimit::new(10),
3444 max_file_size: SizeLimit::from_static(10),
3445 };
3446 let (client, _server) = fake_lsp_client();
3447 let tracker = DocumentTracker::new(limits, HashMap::new());
3448 tracker
3449 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3450 .await
3451 .unwrap();
3452
3453 std::fs::write(&path, "x".repeat(100)).unwrap();
3454
3455 let result = tracker
3456 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3457 .await;
3458 assert_matches!(result, Err(Error::FileSizeLimitExceeded { .. }));
3459 assert_eq!(tracker.get(&path).unwrap().content(), "small");
3460 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
3461 }
3462
3463 #[tokio::test]
3464 async fn test_ensure_open_resync_at_document_capacity() {
3465 let dir = TempDir::new().unwrap();
3466 let path = dir.path().join("a.rs");
3467 std::fs::write(&path, "AAAA").unwrap();
3468 set_mtime(&path, settled_past());
3469
3470 let limits = ResourceLimits {
3471 max_documents: DocumentLimit::new(1),
3472 max_file_size: SizeLimit::UNLIMITED,
3473 };
3474 let (client, _server) = fake_lsp_client();
3475 let tracker = DocumentTracker::new(limits, HashMap::new());
3476 tracker
3477 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3478 .await
3479 .unwrap();
3480 assert_eq!(tracker.len(), 1);
3481
3482 std::fs::write(&path, "BBBBBBBB").unwrap();
3483 let result = tracker
3484 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3485 .await;
3486 assert!(
3487 result.is_ok(),
3488 "resync must not re-run the doc-count check on an already-tracked path"
3489 );
3490 assert_eq!(tracker.len(), 1);
3491 assert_eq!(tracker.get(&path).unwrap().version().get(), 2);
3492 }
3493
3494 #[tokio::test]
3495 async fn test_first_open_self_heals_when_did_open_notify_fails() {
3496 let dir = TempDir::new().unwrap();
3497 let path = dir.path().join("a.rs");
3498 std::fs::write(&path, "fn main() {}").unwrap();
3499
3500 let (client, _server) = fake_lsp_client();
3501 let notify_will_fail = client.clone();
3507 client.shutdown().await.unwrap();
3508
3509 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3510 let result = tracker
3511 .ensure_open(&path, &ServerId::from_static("rust"), ¬ify_will_fail)
3512 .await;
3513
3514 assert!(result.is_err(), "notify failure must propagate as an error");
3515 assert!(
3516 !tracker.is_open(&path),
3517 "a failed didOpen must not leave the document tracked, or the server \
3518 and tracker would stay permanently desynced"
3519 );
3520 }
3521
3522 #[tokio::test]
3523 async fn test_resync_sends_didchange_with_full_replacement_over_the_wire() {
3524 let dir = TempDir::new().unwrap();
3525 let path = dir.path().join("a.rs");
3526 std::fs::write(&path, "fn main() {}").unwrap();
3527 set_mtime(&path, settled_past());
3528
3529 let (client, mut server) = fake_lsp_client();
3530 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3531 tracker
3532 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3533 .await
3534 .unwrap();
3535
3536 let mut wire = BufReader::new(&mut server.write_stdout);
3537 let opened = read_framed_message(&mut wire).await;
3538 assert_eq!(opened["method"], "textDocument/didOpen");
3539
3540 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
3541 set_mtime(&path, settled_past());
3542 tracker
3543 .ensure_open(&path, &ServerId::from_static("rust"), &client)
3544 .await
3545 .unwrap();
3546
3547 let changed = read_framed_message(&mut wire).await;
3548 assert_eq!(changed["method"], "textDocument/didChange");
3549 let params = &changed["params"];
3550 assert_eq!(params["textDocument"]["version"], 2);
3551 let change = ¶ms["contentChanges"][0];
3552 assert!(
3553 change.get("range").is_none(),
3554 "range must be omitted, not null, for a full-replacement change"
3555 );
3556 assert!(
3557 change.get("rangeLength").is_none(),
3558 "rangeLength must be omitted, not null, for a full-replacement change"
3559 );
3560 assert_eq!(change["text"], "fn main() { println!(\"hi\"); }");
3561 }
3562
3563 #[tokio::test]
3568 async fn test_ensure_open_second_server_gets_didopen_no_disk_change() {
3569 let dir = TempDir::new().unwrap();
3570 let path = dir.path().join("a.rs");
3571 std::fs::write(&path, "fn main() {}").unwrap();
3572 set_mtime(&path, settled_past());
3573
3574 let (client_a, mut server_a) = fake_lsp_client();
3575 let (client_b, mut server_b) = fake_lsp_client();
3576 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3577
3578 let id_a = ServerId::from_static("server-a");
3579 let id_b = ServerId::from_static("server-b");
3580
3581 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
3582 let mut wire_a = BufReader::new(&mut server_a.write_stdout);
3583 let opened_a = read_framed_message(&mut wire_a).await;
3584 assert_eq!(opened_a["method"], "textDocument/didOpen");
3585
3586 tracker.ensure_open(&path, &id_b, &client_b).await.unwrap();
3590 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
3591 let opened_b = read_framed_message(&mut wire_b).await;
3592 assert_eq!(opened_b["method"], "textDocument/didOpen");
3593 assert_eq!(opened_b["params"]["textDocument"]["version"], 1);
3594 assert_eq!(opened_b["params"]["textDocument"]["text"], "fn main() {}");
3595 }
3596
3597 #[tokio::test(start_paused = true)]
3601 async fn test_ensure_open_second_server_gets_didopen_unchanged_content_path() {
3602 let dir = TempDir::new().unwrap();
3603 let path = dir.path().join("a.rs");
3604 std::fs::write(&path, "fn main() {}").unwrap();
3605 let (client_a, _server_a) = fake_lsp_client();
3608 let (client_b, mut server_b) = fake_lsp_client();
3609 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3610
3611 tracker
3612 .ensure_open(&path, &ServerId::from_static("server-a"), &client_a)
3613 .await
3614 .unwrap();
3615
3616 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
3619
3620 tracker
3621 .ensure_open(&path, &ServerId::from_static("server-b"), &client_b)
3622 .await
3623 .unwrap();
3624 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
3625 let opened_b = read_framed_message(&mut wire_b).await;
3626 assert_eq!(opened_b["method"], "textDocument/didOpen");
3627 }
3628
3629 #[tokio::test]
3636 async fn test_ensure_open_same_server_twice_sends_nothing_second_time() {
3637 let dir = TempDir::new().unwrap();
3638 let path = dir.path().join("a.rs");
3639 std::fs::write(&path, "fn main() {}").unwrap();
3640 set_mtime(&path, settled_past());
3641
3642 let (client, mut server) = fake_lsp_client();
3643 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3644 let id = ServerId::from_static("rust");
3645
3646 tracker.ensure_open(&path, &id, &client).await.unwrap();
3647 tracker.ensure_open(&path, &id, &client).await.unwrap();
3648
3649 let mut wire = BufReader::new(&mut server.write_stdout);
3650 let opened = read_framed_message(&mut wire).await;
3651 assert_eq!(opened["method"], "textDocument/didOpen");
3652 assert_eq!(
3653 tracker
3654 .get(&path)
3655 .unwrap()
3656 .synced_version(&id)
3657 .map(DocumentVersion::get),
3658 Some(1),
3659 "second call for the same server must not re-open or re-change"
3660 );
3661 }
3662
3663 #[tokio::test]
3667 async fn test_sync_phase_failed_didchange_does_not_disturb_other_server() {
3668 let dir = TempDir::new().unwrap();
3669 let path = dir.path().join("a.rs");
3670 std::fs::write(&path, "fn main() {}").unwrap();
3671 set_mtime(&path, settled_past());
3672
3673 let (client_a, _server_a) = fake_lsp_client();
3674 let (client_b, _server_b) = fake_lsp_client();
3675 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3676 let id_a = ServerId::from_static("server-a");
3677 let id_b = ServerId::from_static("server-b");
3678
3679 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
3680 tracker.ensure_open(&path, &id_b, &client_b).await.unwrap();
3681
3682 let client_b_will_fail = client_b.clone();
3685 client_b.shutdown().await.unwrap();
3686
3687 std::fs::write(&path, "fn main() { updated(); }").unwrap();
3688 set_mtime(&path, settled_past());
3689
3690 let result = tracker.ensure_open(&path, &id_b, &client_b_will_fail).await;
3691 assert!(result.is_err(), "B's didChange must fail and propagate");
3692
3693 assert!(tracker.is_open(&path));
3699 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
3700 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
3701 assert_eq!(
3702 tracker
3703 .get(&path)
3704 .unwrap()
3705 .synced_version(&id_a)
3706 .map(DocumentVersion::get),
3707 Some(1)
3708 );
3709 assert_eq!(
3710 tracker
3711 .get(&path)
3712 .unwrap()
3713 .synced_version(&id_b)
3714 .map(DocumentVersion::get),
3715 Some(1)
3716 );
3717
3718 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
3722 assert_eq!(
3723 tracker.get(&path).unwrap().content(),
3724 "fn main() { updated(); }"
3725 );
3726 assert_eq!(
3727 tracker
3728 .get(&path)
3729 .unwrap()
3730 .synced_version(&id_a)
3731 .map(DocumentVersion::get),
3732 Some(2)
3733 );
3734 assert_eq!(
3735 tracker
3736 .get(&path)
3737 .unwrap()
3738 .synced_version(&id_b)
3739 .map(DocumentVersion::get),
3740 Some(1)
3741 );
3742 }
3743
3744 #[tokio::test]
3767 async fn test_ensure_open_different_paths_do_not_serialize() {
3768 let dir = TempDir::new().unwrap();
3769 let path_a = dir.path().join("a.rs");
3770 let path_b = dir.path().join("b.rs");
3771
3772 std::fs::write(&path_a, "fn a() {}").unwrap();
3773 std::fs::write(&path_b, "fn b() {}").unwrap();
3774 set_mtime(&path_a, settled_past());
3775 set_mtime(&path_b, settled_past());
3776
3777 let (client_a, _server_a) = fake_lsp_client();
3778 let (client_b, _server_b) = fake_lsp_client();
3779 let tracker = Arc::new(DocumentTracker::new(
3780 ResourceLimits::default(),
3781 HashMap::new(),
3782 ));
3783
3784 let path_a_guard = tracker.lock_path(&path_a).await;
3785
3786 let tracker_for_a = Arc::clone(&tracker);
3790 let path_a_for_task = path_a.clone();
3791 let handle_a = tokio::spawn(async move {
3792 tracker_for_a
3793 .ensure_open(
3794 &path_a_for_task,
3795 &ServerId::from_static("server-a"),
3796 &client_a,
3797 )
3798 .await
3799 });
3800
3801 tokio::time::sleep(Duration::from_millis(200)).await;
3804
3805 tokio::time::timeout(
3808 Duration::from_secs(5),
3809 tracker.ensure_open(&path_b, &ServerId::from_static("server-b"), &client_b),
3810 )
3811 .await
3812 .unwrap()
3813 .unwrap();
3814
3815 drop(path_a_guard);
3816
3817 handle_a.await.unwrap().unwrap();
3818 assert_eq!(tracker.get(&path_a).unwrap().content(), "fn a() {}");
3819 }
3820
3821 #[tokio::test]
3827 async fn test_ensure_open_concurrent_same_path_single_didopen() {
3828 let dir = TempDir::new().unwrap();
3829 let path = dir.path().join("a.rs");
3830 std::fs::write(&path, "fn main() {}").unwrap();
3831 set_mtime(&path, settled_past());
3832
3833 let (client, mut server) = fake_lsp_client();
3834 let tracker = Arc::new(DocumentTracker::new(
3835 ResourceLimits::default(),
3836 HashMap::new(),
3837 ));
3838 let id = ServerId::from_static("rust");
3839
3840 let mut handles = Vec::new();
3841 for _ in 0..8 {
3842 let tracker = Arc::clone(&tracker);
3843 let client = client.clone();
3844 let path = path.clone();
3845 let id = id.clone();
3846 handles.push(tokio::spawn(async move {
3847 tracker.ensure_open(&path, &id, &client).await
3848 }));
3849 }
3850 for handle in handles {
3851 handle.await.unwrap().unwrap();
3852 }
3853
3854 let mut wire = BufReader::new(&mut server.write_stdout);
3855 let opened = read_framed_message(&mut wire).await;
3856 assert_eq!(opened["method"], "textDocument/didOpen");
3857
3858 let extra =
3861 tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut wire)).await;
3862 assert!(
3863 extra.is_err(),
3864 "expected no additional notification after the single didOpen"
3865 );
3866
3867 assert_eq!(
3868 tracker
3869 .get(&path)
3870 .unwrap()
3871 .synced_version(&id)
3872 .map(DocumentVersion::get),
3873 Some(1)
3874 );
3875 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
3876 }
3877
3878 #[tokio::test]
3885 async fn test_ensure_open_path_locks_evicted_after_completion() {
3886 let dir = TempDir::new().unwrap();
3887 let paths: Vec<_> = ["a.rs", "b.rs", "c.rs"]
3888 .iter()
3889 .map(|name| dir.path().join(name))
3890 .collect();
3891 for path in &paths {
3892 std::fs::write(path, "fn f() {}").unwrap();
3893 set_mtime(path, settled_past());
3894 }
3895
3896 let tracker = Arc::new(DocumentTracker::new(
3897 ResourceLimits::default(),
3898 HashMap::new(),
3899 ));
3900 let id = ServerId::from_static("rust");
3901
3902 let mut handles = Vec::new();
3903 let mut servers = Vec::new();
3904 for path in paths.clone() {
3905 let tracker = Arc::clone(&tracker);
3906 let (client, server) = fake_lsp_client();
3907 servers.push(server);
3908 let id = id.clone();
3909 handles.push(tokio::spawn(async move {
3910 tracker.ensure_open(&path, &id, &client).await
3911 }));
3912 }
3913 for handle in handles {
3914 handle.await.unwrap().unwrap();
3915 }
3916 drop(servers);
3917
3918 assert!(
3919 lock_std(&tracker.path_locks).is_empty(),
3920 "path_locks must be fully evicted once every ensure_open call \
3921 for every path has completed, otherwise the map grows \
3922 unbounded for the lifetime of the process"
3923 );
3924 }
3925
3926 #[cfg(unix)]
3938 #[tokio::test]
3939 async fn test_read_to_string_checked_rejects_fifo() {
3940 let dir = TempDir::new().unwrap();
3941 let path = dir.path().join("fifo");
3942 crate::test_lsp::make_fifo(&path);
3943
3944 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3945 let result = tokio::time::timeout(
3948 Duration::from_secs(5),
3949 tracker.read_to_string_checked(&path),
3950 )
3951 .await
3952 .unwrap();
3953
3954 assert_matches!(result, Err(Error::NotARegularFile(_)));
3955 }
3956
3957 #[cfg(windows)]
3966 #[tokio::test]
3967 async fn test_read_to_string_checked_rejects_nul_device() {
3968 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3969 let path = PathBuf::from("NUL");
3970 let result = tokio::time::timeout(
3971 Duration::from_secs(5),
3972 tracker.read_to_string_checked(&path),
3973 )
3974 .await
3975 .unwrap();
3976
3977 assert_matches!(result, Err(Error::NotARegularFile(_)));
3978 }
3979
3980 #[tokio::test]
3986 async fn test_read_to_string_checked_size_boundary() {
3987 let dir = TempDir::new().unwrap();
3988 let path = dir.path().join("boundary.rs");
3989 let tracker = DocumentTracker::new(
3990 ResourceLimits {
3991 max_documents: DocumentLimit::new(100),
3992 max_file_size: SizeLimit::from_static(10),
3993 },
3994 HashMap::new(),
3995 );
3996
3997 std::fs::write(&path, "a".repeat(10)).unwrap();
3998 let (content, ..) = tracker.read_to_string_checked(&path).await.unwrap();
3999 assert_eq!(content.len(), 10);
4000
4001 std::fs::write(&path, "a".repeat(11)).unwrap();
4002 let result = tracker.read_to_string_checked(&path).await;
4003 assert_matches!(
4004 result,
4005 Err(Error::FileSizeLimitExceeded(SizeExceeded { size: 11, max })) if max.get() == 10
4006 );
4007 }
4008
4009 #[tokio::test]
4015 async fn test_read_line_checked_does_not_read_past_target_line() {
4016 let dir = TempDir::new().unwrap();
4017 let path = dir.path().join("partial.rs");
4018 let mut content = b"hello\n".to_vec();
4019 content.extend_from_slice(&[0xFF, 0xFE]);
4020 content.push(b'\n');
4021 std::fs::write(&path, &content).unwrap();
4022
4023 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4024 let line = tracker.read_line_checked(&path, 0, u64::MAX).await.unwrap();
4025 assert_eq!(line.text.as_deref(), Some("hello"));
4026 }
4027
4028 #[tokio::test]
4032 async fn test_read_line_checked_returns_requested_non_zero_line() {
4033 let dir = TempDir::new().unwrap();
4034 let path = dir.path().join("multi.rs");
4035 std::fs::write(&path, "first\nsecond\nthird\nfourth\n").unwrap();
4036
4037 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4038 assert_eq!(
4039 tracker
4040 .read_line_checked(&path, 2, u64::MAX)
4041 .await
4042 .unwrap()
4043 .text
4044 .as_deref(),
4045 Some("third")
4046 );
4047 }
4048
4049 #[tokio::test]
4053 async fn test_read_line_checked_returns_none_past_last_line() {
4054 let dir = TempDir::new().unwrap();
4055 let path = dir.path().join("short.rs");
4056 std::fs::write(&path, "only one line").unwrap();
4057
4058 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4059 assert_eq!(
4060 tracker
4061 .read_line_checked(&path, 5, u64::MAX)
4062 .await
4063 .unwrap()
4064 .text,
4065 None
4066 );
4067 }
4068
4069 #[tokio::test]
4074 async fn test_read_line_checked_reads_last_line_without_trailing_newline() {
4075 let dir = TempDir::new().unwrap();
4076 let path = dir.path().join("no_newline.rs");
4077 std::fs::write(&path, "only one line").unwrap();
4078
4079 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4080 assert_eq!(
4081 tracker
4082 .read_line_checked(&path, 0, u64::MAX)
4083 .await
4084 .unwrap()
4085 .text
4086 .as_deref(),
4087 Some("only one line")
4088 );
4089 }
4090
4091 #[tokio::test]
4092 async fn test_read_line_checked_empty_file_line_zero_is_empty_line() {
4093 let dir = TempDir::new().unwrap();
4094 let path = dir.path().join("empty.rs");
4095 std::fs::write(&path, "").unwrap();
4096
4097 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4098 assert_eq!(
4099 tracker
4100 .read_line_checked(&path, 0, u64::MAX)
4101 .await
4102 .unwrap()
4103 .text
4104 .as_deref(),
4105 Some("")
4106 );
4107 assert_eq!(
4108 tracker
4109 .read_line_checked(&path, 1, u64::MAX)
4110 .await
4111 .unwrap()
4112 .text,
4113 None
4114 );
4115 }
4116
4117 #[tokio::test]
4118 async fn test_read_line_checked_trailing_empty_line_after_final_newline() {
4119 let dir = TempDir::new().unwrap();
4120 let path = dir.path().join("trailing.rs");
4121 std::fs::write(&path, "a\n").unwrap();
4122
4123 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4124 let read = |line| tracker.read_line_checked(&path, line, u64::MAX);
4125 assert_eq!(read(1).await.unwrap().text.as_deref(), Some(""));
4126 assert_eq!(read(2).await.unwrap().text, None);
4127 }
4128
4129 #[tokio::test]
4130 async fn test_read_line_checked_cap_at_line_boundary_is_not_empty_line() {
4131 let dir = TempDir::new().unwrap();
4132 let path = dir.path().join("capped.rs");
4133 std::fs::write(&path, "abc\nrest\n").unwrap();
4134
4135 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4136 assert_eq!(
4138 tracker.read_line_checked(&path, 1, 3).await.unwrap().text,
4139 None
4140 );
4141 }
4142
4143 #[tokio::test]
4145 async fn test_read_line_checked_strips_crlf_line_ending() {
4146 let dir = TempDir::new().unwrap();
4147 let path = dir.path().join("crlf.rs");
4148 std::fs::write(&path, "first\r\nsecond\r\n").unwrap();
4149
4150 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4151 assert_eq!(
4152 tracker
4153 .read_line_checked(&path, 0, u64::MAX)
4154 .await
4155 .unwrap()
4156 .text
4157 .as_deref(),
4158 Some("first")
4159 );
4160 assert_eq!(
4161 tracker
4162 .read_line_checked(&path, 1, u64::MAX)
4163 .await
4164 .unwrap()
4165 .text
4166 .as_deref(),
4167 Some("second")
4168 );
4169 }
4170
4171 #[tokio::test]
4176 async fn test_line_text_and_read_line_checked_agree_on_line_rule() {
4177 let dir = TempDir::new().unwrap();
4178 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4179
4180 let contents = [
4181 "",
4182 "abc",
4183 "abc\n",
4184 "abc\r\n",
4185 "abc\r\r\n",
4186 "abc\r",
4187 "a\n\nb",
4188 "a\r\nb\r\n",
4189 "a\rb",
4190 "a\r\r\nb",
4191 "\n\r",
4192 "\r\n\r\n",
4193 ];
4194 for (i, content) in contents.iter().enumerate() {
4195 let path = dir.path().join(format!("parity_{i}.rs"));
4196 std::fs::write(&path, content).unwrap();
4197 tracker.open(path.clone(), (*content).to_string()).unwrap();
4198 for line in 0..6 {
4199 let disk = tracker
4200 .read_line_checked(&path, line, u64::MAX)
4201 .await
4202 .unwrap()
4203 .text;
4204 assert_eq!(
4205 tracker.line_text(&path, line),
4206 disk,
4207 "content {content:?}, line {line}"
4208 );
4209 }
4210 }
4211 }
4212
4213 #[test]
4214 fn test_line_text_trailing_empty_line_and_lone_cr_terminator() {
4215 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4216 let path = PathBuf::from("/test/lines.rs");
4217 tracker
4218 .open(path.clone(), "ab\r\ncd\r\r\n".to_string())
4219 .unwrap();
4220
4221 assert_eq!(tracker.line_text(&path, 0).as_deref(), Some("ab"));
4222 assert_eq!(tracker.line_text(&path, 1).as_deref(), Some("cd"));
4223 assert_eq!(tracker.line_text(&path, 2).as_deref(), Some(""));
4224 assert_eq!(tracker.line_text(&path, 3).as_deref(), Some(""));
4225 assert_eq!(tracker.line_text(&path, 4), None);
4226
4227 let empty = PathBuf::from("/test/empty.rs");
4228 tracker.open(empty.clone(), String::new()).unwrap();
4229 assert_eq!(tracker.line_text(&empty, 0).as_deref(), Some(""));
4230 assert_eq!(tracker.line_text(&empty, 1), None);
4231 }
4232
4233 #[tokio::test]
4239 async fn test_read_line_checked_exact_max_file_size_reads_to_eof_without_error() {
4240 let dir = TempDir::new().unwrap();
4241 let path = dir.path().join("exact.rs");
4242 let content = "a".repeat(20);
4243 std::fs::write(&path, &content).unwrap();
4244
4245 let limits = ResourceLimits {
4246 max_documents: DocumentLimit::new(100),
4247 max_file_size: SizeLimit::from_static(20),
4248 };
4249 let tracker = DocumentTracker::new(limits, HashMap::new());
4250
4251 assert_eq!(
4252 tracker
4253 .read_line_checked(&path, 0, u64::MAX)
4254 .await
4255 .unwrap()
4256 .text
4257 .as_deref(),
4258 Some(content.as_str())
4259 );
4260 assert_eq!(
4261 tracker
4262 .read_line_checked(&path, 1, u64::MAX)
4263 .await
4264 .unwrap()
4265 .text,
4266 None,
4267 "a line past an exact-max_file_size file's only line must read to EOF cleanly, not \
4268 be misreported as truncated"
4269 );
4270 }
4271
4272 #[tokio::test]
4279 async fn test_read_line_checked_bounds_read_by_budget_not_just_max_file_size() {
4280 let dir = TempDir::new().unwrap();
4281 let path = dir.path().join("budget.rs");
4282 std::fs::write(&path, "a".repeat(1000)).unwrap();
4283
4284 let limits = ResourceLimits {
4285 max_documents: DocumentLimit::new(100),
4286 max_file_size: SizeLimit::from_static(1000),
4287 };
4288 let tracker = DocumentTracker::new(limits, HashMap::new());
4289
4290 let read = tracker.read_line_checked(&path, 0, 10).await.unwrap();
4291 assert_eq!(
4292 read.text, None,
4293 "a single line far longer than the budget must not be returned as if complete"
4294 );
4295 assert_eq!(
4296 read.bytes_read, 11,
4297 "the read must stop at exactly the budget's +1 slack (see the correctness-gate fix \
4298 below), not at max_file_size"
4299 );
4300 }
4301
4302 #[tokio::test]
4310 async fn test_read_line_checked_exact_budget_match_on_unterminated_line_not_truncated() {
4311 let dir = TempDir::new().unwrap();
4312 let path = dir.path().join("exact_budget.rs");
4313 let content = "twelve chars";
4314 std::fs::write(&path, content).unwrap();
4315
4316 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4317 let read = tracker
4318 .read_line_checked(&path, 0, content.len() as u64)
4319 .await
4320 .unwrap();
4321 assert_eq!(
4322 read.text.as_deref(),
4323 Some(content),
4324 "budget exactly matching the line's byte length must not be misreported as truncated"
4325 );
4326 assert_eq!(read.bytes_read, content.len() as u64);
4327 }
4328
4329 #[tokio::test]
4336 async fn test_read_line_checked_reports_bytes_read_for_invalid_utf8_line() {
4337 let dir = TempDir::new().unwrap();
4338 let path = dir.path().join("invalid_utf8.rs");
4339 let mut content = vec![0xFFu8, 0xFE, 0xFD];
4340 content.push(b'\n');
4341 std::fs::write(&path, &content).unwrap();
4342
4343 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4344 let read = tracker.read_line_checked(&path, 0, u64::MAX).await.unwrap();
4345 assert_eq!(read.text, None);
4346 assert_eq!(
4347 read.bytes_read,
4348 content.len() as u64,
4349 "bytes scanned must be reported even though the line wasn't valid UTF-8"
4350 );
4351 }
4352
4353 #[tokio::test]
4362 async fn test_read_line_checked_charges_nominal_amount_for_nonexistent_path() {
4363 let dir = TempDir::new().unwrap();
4364 let path = dir.path().join("does_not_exist.rs");
4365
4366 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4367 let read = tracker.read_line_checked(&path, 0, u64::MAX).await.unwrap();
4369 assert_eq!(read.text, None);
4370 assert_eq!(read.bytes_read, OPEN_FAILURE_CHARGE_BYTES);
4371 }
4372
4373 fn naive_lines(content: &str) -> Vec<&str> {
4376 let bytes = content.as_bytes();
4377 let mut lines = Vec::new();
4378 let (mut start, mut i) = (0, 0);
4379 while i < bytes.len() {
4380 match bytes[i] {
4381 b'\n' => {
4382 lines.push(&content[start..i]);
4383 i += 1;
4384 start = i;
4385 }
4386 b'\r' => {
4387 lines.push(&content[start..i]);
4388 i += 1;
4389 if bytes.get(i) == Some(&b'\n') {
4390 i += 1;
4391 }
4392 start = i;
4393 }
4394 _ => i += 1,
4395 }
4396 }
4397 lines.push(&content[start..]);
4398 lines
4399 }
4400
4401 fn all_strings(max_len: usize) -> Vec<String> {
4403 let alphabet = ['a', 'é', '\r', '\n'];
4404 let mut all = vec![String::new()];
4405 let mut frontier = vec![String::new()];
4406 for _ in 0..max_len {
4407 frontier = frontier
4408 .iter()
4409 .flat_map(|s| {
4410 alphabet.iter().map(move |c| {
4411 let mut next = s.clone();
4412 next.push(*c);
4413 next
4414 })
4415 })
4416 .collect();
4417 all.extend(frontier.iter().cloned());
4418 }
4419 all
4420 }
4421
4422 #[test]
4426 fn test_document_text_line_matches_naive_splitter_exhaustively() {
4427 for content in all_strings(8) {
4428 let expected = naive_lines(&content);
4429 for stride in 1..=3 {
4430 let text = DocumentText::with_stride(content.clone(), stride);
4431 for (n, want) in expected.iter().enumerate() {
4432 assert_eq!(
4433 text.line(u32::try_from(n).unwrap()),
4434 Some(*want),
4435 "{content:?} stride {stride} line {n}"
4436 );
4437 }
4438 assert_eq!(
4439 text.line(u32::try_from(expected.len()).unwrap()),
4440 None,
4441 "{content:?} stride {stride} past the last line"
4442 );
4443 }
4444 }
4445 }
4446
4447 #[test]
4449 fn test_document_text_lines_from_matches_line_exhaustively() {
4450 for content in all_strings(7) {
4451 let expected = naive_lines(&content);
4452 for stride in 1..=3 {
4453 let text = DocumentText::with_stride(content.clone(), stride);
4454 for start in 0..=expected.len() {
4455 let got: Vec<&str> = text.lines_from(u32::try_from(start).unwrap()).collect();
4456 assert_eq!(
4457 got,
4458 expected[start.min(expected.len())..],
4459 "{content:?} {stride} {start}"
4460 );
4461 }
4462 }
4463 }
4464 }
4465
4466 #[tokio::test]
4469 async fn test_read_nth_line_matches_naive_splitter_exhaustively() {
4470 for content in all_strings(7) {
4471 let expected = naive_lines(&content);
4472 for capacity in [1, 2, 8 * 1024] {
4473 for n in 0..=expected.len() {
4474 let mut reader =
4475 tokio::io::BufReader::with_capacity(capacity, content.as_bytes());
4476 let read = read_nth_line(&mut reader, u32::try_from(n).unwrap(), u64::MAX)
4477 .await
4478 .unwrap();
4479 assert_eq!(
4480 read.text.as_deref(),
4481 expected.get(n).copied(),
4482 "{content:?} capacity {capacity} line {n}"
4483 );
4484 }
4485 }
4486 }
4487 }
4488
4489 #[test]
4492 fn test_document_text_checkpoint_at_eof_after_full_stride() {
4493 for terminator in ["\n", "\r\n", "\r"] {
4494 let content = format!("x{terminator}").repeat(LINE_CHECKPOINT_STRIDE);
4495 let text = DocumentText::new(content);
4496 let stride = u32::try_from(LINE_CHECKPOINT_STRIDE).unwrap();
4497 assert_eq!(text.line(stride - 1), Some("x"));
4498 assert_eq!(text.line(stride), Some(""), "terminator {terminator:?}");
4499 assert_eq!(text.line(stride + 1), None);
4500 }
4501 }
4502
4503 #[test]
4506 fn test_document_text_lines_across_default_stride_boundaries() {
4507 let content: String = (0..200)
4508 .map(|i| format!("line{i}\r\n"))
4509 .collect::<Vec<_>>()
4510 .concat();
4511 let text = DocumentText::new(content);
4512 for i in [0u32, 1, 63, 64, 65, 127, 128, 199] {
4513 assert_eq!(text.line(i), Some(format!("line{i}").as_str()), "line {i}");
4514 }
4515 assert_eq!(text.line(200), Some(""));
4516 assert_eq!(text.line(201), None);
4517 }
4518
4519 #[tokio::test]
4522 async fn test_document_text_build_above_inline_threshold_matches_inline() {
4523 let content = "ab\r\n".repeat(INLINE_INDEX_MAX_BYTES / 4 + 10);
4524 assert!(content.len() > INLINE_INDEX_MAX_BYTES);
4525 let built = DocumentText::build(content.clone()).await.unwrap();
4526 let inline = DocumentText::new(content);
4527 assert_eq!(built.checkpoints, inline.checkpoints);
4528 assert_eq!(built.line(1000), Some("ab"));
4529 }
4530
4531 #[test]
4532 fn test_document_text_checkpoint_memory_is_sparse() {
4533 let text = DocumentText::new("\n".repeat(6400));
4534 assert_eq!(text.checkpoints.len(), 6400 / LINE_CHECKPOINT_STRIDE + 1);
4535 }
4536
4537 #[tokio::test]
4540 async fn test_read_nth_line_crlf_split_across_fills_is_one_terminator() {
4541 let mut reader = tokio::io::BufReader::with_capacity(1, b"a\r\nb".as_slice());
4542 let read = read_nth_line(&mut reader, 1, u64::MAX).await.unwrap();
4543 assert_eq!(read.text.as_deref(), Some("b"));
4544 }
4545
4546 #[tokio::test]
4549 async fn test_read_nth_line_cap_exhausted_mid_line_is_none() {
4550 let mut reader =
4551 tokio::io::BufReader::new(tokio::io::AsyncReadExt::take(b"abcdef\n".as_slice(), 3));
4552 let read = read_nth_line(&mut reader, 0, 3).await.unwrap();
4553 assert_eq!(read.text, None);
4554 assert_eq!(read.bytes_read, 3);
4555 }
4556
4557 async fn open_for_servers(
4560 tracker: &DocumentTracker,
4561 dir: &TempDir,
4562 name: &str,
4563 servers: &[&str],
4564 ) -> (
4565 PathBuf,
4566 Vec<(ServerId, LspClient, crate::test_lsp::FakeServer)>,
4567 ) {
4568 let path = dir.path().join(name);
4569 std::fs::write(&path, name).unwrap();
4570 set_mtime(&path, settled_past());
4571 let mut out = Vec::new();
4572 for server in servers {
4573 let (client, fake) = fake_lsp_client();
4574 let id = ServerId::new(*server).unwrap();
4575 tracker.ensure_open(&path, &id, &client).await.unwrap();
4576 out.push((id, client, fake));
4577 }
4578 (path, out)
4579 }
4580
4581 async fn evict_by_opening(tracker: &DocumentTracker, dir: &TempDir, other: &str) {
4583 let path = dir.path().join(other);
4584 std::fs::write(&path, other).unwrap();
4585 set_mtime(&path, settled_past());
4586 let (client, _fake) = fake_lsp_client();
4587 tracker
4588 .ensure_open(&path, &ServerId::from_static("other"), &client)
4589 .await
4590 .unwrap();
4591 }
4592
4593 fn one_document_tracker() -> DocumentTracker {
4594 DocumentTracker::new(
4595 ResourceLimits {
4596 max_documents: DocumentLimit::new(1),
4597 max_file_size: SizeLimit::UNLIMITED,
4598 },
4599 HashMap::new(),
4600 )
4601 }
4602
4603 #[tokio::test]
4606 async fn test_reopen_through_one_server_settles_only_that_servers_close() {
4607 let dir = TempDir::new().unwrap();
4608 let tracker = one_document_tracker();
4609 let (path, mut servers) = open_for_servers(&tracker, &dir, "p.rs", &["a", "d"]).await;
4610 evict_by_opening(&tracker, &dir, "q.rs").await;
4611 assert_eq!(
4612 pending_servers(&tracker, &path),
4613 HashSet::from([ServerId::from_static("a"), ServerId::from_static("d")])
4614 );
4615
4616 let (id_a, client_a, mut fake_a) = servers.remove(0);
4617 tracker.ensure_open(&path, &id_a, &client_a).await.unwrap();
4618
4619 let mut wire = BufReader::new(&mut fake_a.write_stdout);
4620 assert_eq!(
4621 read_framed_message(&mut wire).await["method"],
4622 "textDocument/didOpen"
4623 );
4624 assert_eq!(
4625 read_framed_message(&mut wire).await["method"],
4626 "textDocument/didClose"
4627 );
4628 assert_eq!(
4629 read_framed_message(&mut wire).await["method"],
4630 "textDocument/didOpen"
4631 );
4632
4633 assert_eq!(
4634 pending_servers(&tracker, &path),
4635 HashSet::from([ServerId::from_static("d")])
4636 );
4637 let claim = tracker.try_claim_pending_close(&path).unwrap();
4638 assert_eq!(claim.servers, vec![ServerId::from_static("d")]);
4639 }
4640
4641 #[tokio::test]
4643 async fn test_claim_returns_every_owed_server_once() {
4644 let dir = TempDir::new().unwrap();
4645 let tracker = one_document_tracker();
4646 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a", "d"]).await;
4647 evict_by_opening(&tracker, &dir, "q.rs").await;
4648
4649 assert_eq!(tracker.pending_close_paths(), vec![path.clone()]);
4650 let claim = tracker.try_claim_pending_close(&path).unwrap();
4651 assert_eq!(claim.path, path);
4652 let mut claimed = claim.servers.clone();
4653 claimed.sort_by_key(ToString::to_string);
4654 assert_eq!(
4655 claimed,
4656 vec![ServerId::from_static("a"), ServerId::from_static("d")]
4657 );
4658 drop(claim);
4659
4660 assert!(tracker.pending_close_paths().is_empty());
4661 assert!(tracker.try_claim_pending_close(&path).is_none());
4662 }
4663
4664 #[tokio::test]
4666 async fn test_claim_defers_busy_path_without_waiting() {
4667 let dir = TempDir::new().unwrap();
4668 let tracker = one_document_tracker();
4669 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a"]).await;
4670 evict_by_opening(&tracker, &dir, "q.rs").await;
4671
4672 let busy = tracker.lock_path(&path).await;
4673 assert!(tracker.try_claim_pending_close(&path).is_none());
4674 assert_eq!(
4675 pending_servers(&tracker, &path),
4676 HashSet::from([ServerId::from_static("a")]),
4677 "a deferred claim must leave the debt pending"
4678 );
4679 drop(busy);
4680 assert!(tracker.try_claim_pending_close(&path).is_some());
4681 assert!(
4682 lock_std(&tracker.path_locks).is_empty(),
4683 "failed and successful claims must not leak path-lock entries"
4684 );
4685 }
4686
4687 #[tokio::test]
4690 async fn test_claim_holds_path_lock_until_dropped() {
4691 let dir = TempDir::new().unwrap();
4692 let tracker = Arc::new(one_document_tracker());
4693 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a"]).await;
4694 evict_by_opening(&tracker, &dir, "q.rs").await;
4695
4696 let claim = tracker.try_claim_pending_close(&path).unwrap();
4697 let (client, _fake) = fake_lsp_client();
4698 let reopen = {
4699 let tracker = Arc::clone(&tracker);
4700 let path = path.clone();
4701 tokio::spawn(async move {
4702 tracker
4703 .ensure_open(&path, &ServerId::from_static("b"), &client)
4704 .await
4705 })
4706 };
4707 tokio::time::sleep(Duration::from_millis(50)).await;
4708 assert!(!reopen.is_finished(), "ensure_open must wait for the claim");
4709 drop(claim);
4710 reopen.await.unwrap().unwrap();
4711 }
4712
4713 #[tokio::test]
4715 async fn test_repeated_eviction_merges_pending_closes() {
4716 let dir = TempDir::new().unwrap();
4717 let tracker = one_document_tracker();
4718 let (path, mut servers) = open_for_servers(&tracker, &dir, "p.rs", &["a", "d"]).await;
4719 evict_by_opening(&tracker, &dir, "q.rs").await;
4720
4721 let (id_d, client_d, _fake_d) = servers.remove(1);
4722 tracker.ensure_open(&path, &id_d, &client_d).await.unwrap();
4723 evict_by_opening(&tracker, &dir, "r.rs").await;
4724
4725 assert_eq!(
4726 pending_servers(&tracker, &path),
4727 HashSet::from([ServerId::from_static("a"), ServerId::from_static("d")])
4728 );
4729 assert_eq!(tracker.pending_close_paths().len(), 2);
4730 }
4731
4732 #[tokio::test]
4735 async fn test_restore_pending_close_skips_forgotten_server() {
4736 let dir = TempDir::new().unwrap();
4737 let tracker = one_document_tracker();
4738 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a"]).await;
4739 evict_by_opening(&tracker, &dir, "q.rs").await;
4740 let server = ServerId::from_static("a");
4741 let uri = path_to_uri(&path).unwrap();
4742
4743 let generation = tracker.generation(&server);
4744 assert!(tracker.take_pending_close(&path, &server));
4745 tracker.forget_server(&server);
4746 tracker.restore_pending_close(&path, &uri, &server, generation);
4747 assert!(pending_servers(&tracker, &path).is_empty());
4748
4749 tracker.restore_pending_close(&path, &uri, &server, tracker.generation(&server));
4750 assert_eq!(pending_servers(&tracker, &path), HashSet::from([server]));
4751 }
4752
4753 #[tokio::test]
4756 async fn test_forget_server_purges_its_pending_closes() {
4757 let dir = TempDir::new().unwrap();
4758 let tracker = one_document_tracker();
4759 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a", "d"]).await;
4760 evict_by_opening(&tracker, &dir, "q.rs").await;
4761
4762 tracker.forget_server(&ServerId::from_static("a"));
4763 assert_eq!(
4764 pending_servers(&tracker, &path),
4765 HashSet::from([ServerId::from_static("d")])
4766 );
4767 tracker.forget_server(&ServerId::from_static("d"));
4768 assert!(!lock_std(&tracker.pending_closes).contains_key(&path));
4769 }
4770
4771 #[tokio::test]
4773 async fn test_failed_close_before_reopen_restores_the_debt() {
4774 let dir = TempDir::new().unwrap();
4775 let tracker = one_document_tracker();
4776 let (path, mut servers) = open_for_servers(&tracker, &dir, "p.rs", &["a"]).await;
4777 evict_by_opening(&tracker, &dir, "q.rs").await;
4778
4779 let (id_a, client_a, _fake_a) = servers.remove(0);
4780 let will_fail = client_a.clone();
4781 client_a.shutdown().await.unwrap();
4782 assert!(tracker.ensure_open(&path, &id_a, &will_fail).await.is_err());
4783 assert_eq!(
4784 pending_servers(&tracker, &path),
4785 HashSet::from([ServerId::from_static("a")])
4786 );
4787 }
4788}