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 super::notifications::DocumentStamp;
26use super::{WorkspacePath, WorkspaceRoots};
27use crate::config::{DocumentLimit, LanguageId, LanguageMap, ServerId, SizeLimit};
28use crate::error::{BackgroundTask, Error, Result};
29use crate::lsp::LspClient;
30use crate::util::{BoundedUtf8Error, RegularFile, SizeExceeded, check_bounded_utf8, lock_std};
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 digest: ContentDigest,
102 stride: usize,
103 checkpoints: Box<[usize]>,
105 line_count: NonZeroU32,
108}
109
110impl std::fmt::Debug for DocumentText {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("DocumentText")
113 .field("len", &self.content.len())
114 .field("checkpoints", &self.checkpoints.len())
115 .finish_non_exhaustive()
116 }
117}
118
119impl DocumentText {
120 pub(super) fn new(content: String) -> Self {
122 Self::with_stride(content, LINE_CHECKPOINT_STRIDE)
123 }
124
125 fn with_stride(content: String, stride: usize) -> Self {
128 let stride = stride.max(1);
129 let mut checkpoints = vec![0];
130 let mut start = 0;
131 let mut line = 0usize;
132 while let (_, Some(next)) = line_bounds(&content, start) {
133 start = next;
134 line = line.saturating_add(1);
135 if line.is_multiple_of(stride) {
136 checkpoints.push(start);
137 }
138 }
139 let line_count = u32::try_from(line)
140 .ok()
141 .and_then(|last| last.checked_add(1))
142 .and_then(NonZeroU32::new)
143 .unwrap_or(NonZeroU32::MAX);
144 Self {
145 digest: ContentDigest::of(&content),
146 content,
147 stride,
148 checkpoints: checkpoints.into_boxed_slice(),
149 line_count,
150 }
151 }
152
153 pub(super) const fn digest(&self) -> ContentDigest {
155 self.digest
156 }
157
158 async fn build(content: String) -> Result<Self> {
161 if content.len() <= INLINE_INDEX_MAX_BYTES {
162 return Ok(Self::new(content));
163 }
164 tokio::task::spawn_blocking(move || Self::new(content))
165 .await
166 .map_err(|e| Error::Io(std::io::Error::other(e)))
167 }
168
169 pub(super) fn as_str(&self) -> &str {
171 &self.content
172 }
173
174 pub(super) const fn last_line(&self) -> NonZeroU32 {
176 self.line_count
177 }
178
179 pub(super) fn line(&self, n: u32) -> Option<&str> {
183 let start = self.line_start(n)?;
184 let (end, _) = line_bounds(&self.content, start);
185 Some(&self.content[start..end])
186 }
187
188 fn line_start(&self, n: u32) -> Option<usize> {
189 let n = n as usize;
190 let mut start = *self.checkpoints.get(n.checked_div(self.stride)?)?;
191 for _ in 0..n.checked_rem(self.stride)? {
192 start = line_bounds(&self.content, start).1?;
193 }
194 Some(start)
195 }
196
197 pub(super) fn lines_from(&self, n: u32) -> impl Iterator<Item = &str> {
200 let mut next = self.line_start(n);
201 std::iter::from_fn(move || {
202 let start = next?;
203 let (end, after) = line_bounds(&self.content, start);
204 next = after;
205 self.content.get(start..end)
206 })
207 }
208}
209
210impl PartialEq for DocumentText {
211 fn eq(&self, other: &Self) -> bool {
212 self.content == other.content
213 }
214}
215
216impl Eq for DocumentText {}
217
218fn mtime_settled(mtime: Option<SystemTime>, read_at: SystemTime) -> bool {
225 mtime.is_some_and(|m| {
226 m.checked_add(MTIME_GRANULARITY)
227 .is_some_and(|t| t <= read_at)
228 })
229}
230
231#[derive(Debug, Clone, Copy)]
241pub struct DiskSync {
242 pub mtime: Option<SystemTime>,
247 pub size: u64,
249 pub mtime_settled: bool,
252 pub content_checked_at: Instant,
259}
260
261impl PartialEq for DiskSync {
262 fn eq(&self, other: &Self) -> bool {
263 self.mtime == other.mtime
264 && self.size == other.size
265 && self.mtime_settled == other.mtime_settled
266 }
267}
268
269impl Eq for DiskSync {}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
277#[serde(transparent)]
278pub struct DocumentVersion(i32);
279
280impl DocumentVersion {
281 pub const FIRST: Self = Self(1);
283
284 #[must_use]
286 pub const fn new(version: i32) -> Self {
287 Self(version)
288 }
289
290 #[must_use]
292 pub const fn get(self) -> i32 {
293 self.0
294 }
295
296 #[must_use]
298 pub(crate) const fn next(self) -> Self {
299 Self(self.0.saturating_add(1))
300 }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub struct ContentDigest(u64);
310
311impl ContentDigest {
312 pub(crate) fn of(text: &str) -> Self {
313 use std::hash::{DefaultHasher, Hash as _, Hasher as _};
314
315 let mut hasher = DefaultHasher::new();
316 text.hash(&mut hasher);
317 Self(hasher.finish())
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub enum SweepState {
325 Current {
327 stamp: DocumentStamp,
329 digest: ContentDigest,
331 },
332 Lagging {
334 stamp: DocumentStamp,
336 },
337}
338
339impl SweepState {
340 pub(crate) const fn stamp(self) -> DocumentStamp {
342 match self {
343 Self::Current { stamp, .. } | Self::Lagging { stamp } => stamp,
344 }
345 }
346
347 pub(crate) fn same_text(self, other: Self) -> bool {
351 matches!(
352 (self, other),
353 (Self::Current { digest: a, .. }, Self::Current { digest: b, .. }) if a == b
354 )
355 }
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub struct Opening(u64);
363
364#[derive(Debug, Clone)]
389pub(super) struct DocumentState {
390 uri: Uri,
391 language_id: LanguageId,
392 version: DocumentVersion,
393 text: DocumentText,
394 disk: Option<DiskSync>,
395 synced: HashMap<ServerId, DocumentVersion>,
396 opened: Opening,
397 last_accessed: Instant,
402}
403
404impl PartialEq for DocumentState {
405 fn eq(&self, other: &Self) -> bool {
406 let Self {
411 uri,
412 language_id,
413 version,
414 text,
415 disk,
416 synced,
417 opened: _,
418 last_accessed: _,
419 } = self;
420 *uri == other.uri
421 && *language_id == other.language_id
422 && *version == other.version
423 && *text == other.text
424 && *disk == other.disk
425 && *synced == other.synced
426 }
427}
428
429impl Eq for DocumentState {}
430
431impl DocumentState {
432 fn new(uri: Uri, language_id: LanguageId, text: DocumentText, opened: Opening) -> Self {
435 Self {
436 uri,
437 language_id,
438 version: DocumentVersion::FIRST,
439 text,
440 disk: None,
441 synced: HashMap::new(),
442 opened,
443 last_accessed: Instant::now(),
444 }
445 }
446
447 fn touch(&mut self) {
450 self.last_accessed = Instant::now();
451 }
452
453 #[must_use]
455 #[cfg(test)]
456 pub(crate) const fn uri(&self) -> &Uri {
457 &self.uri
458 }
459
460 #[must_use]
462 #[cfg(test)]
463 pub(crate) const fn language_id(&self) -> &LanguageId {
464 &self.language_id
465 }
466
467 #[must_use]
471 #[cfg(test)]
472 pub(crate) const fn version(&self) -> DocumentVersion {
473 self.version
474 }
475
476 #[must_use]
478 #[cfg(test)]
479 pub(crate) fn content(&self) -> &str {
480 self.text.as_str()
481 }
482
483 const fn disk(&self) -> Option<DiskSync> {
486 self.disk
487 }
488
489 #[must_use]
498 pub(crate) fn synced_version(&self, server: &ServerId) -> Option<DocumentVersion> {
499 self.synced.get(server).copied()
500 }
501
502 fn has_never_synced(&self) -> bool {
504 self.synced.is_empty()
505 }
506
507 fn commit_reload(
513 &mut self,
514 version: DocumentVersion,
515 text: DocumentText,
516 snap: Option<DiskSync>,
517 ) {
518 debug_assert!(
519 version >= self.version,
520 "document version must be monotonically increasing"
521 );
522 self.version = version;
523 self.text = text;
524 self.disk = snap;
525 }
526
527 const fn set_disk(&mut self, snap: DiskSync) {
529 self.disk = Some(snap);
530 }
531
532 fn mark_synced(&mut self, server: ServerId, version: DocumentVersion) {
534 self.synced.insert(server, version);
535 }
536
537 fn forget_server(&mut self, server: &ServerId) {
539 self.synced.remove(server);
540 }
541}
542
543struct OpenedDocument {
545 uri: Uri,
546 language_id: LanguageId,
547 text: String,
548 version: DocumentVersion,
549}
550
551#[derive(Debug, Clone, Copy)]
553pub struct ResourceLimits {
554 pub max_documents: DocumentLimit,
556 pub max_file_size: SizeLimit,
558}
559
560impl Default for ResourceLimits {
561 fn default() -> Self {
562 Self {
563 max_documents: DocumentLimit::DEFAULT,
564 max_file_size: SizeLimit::DEFAULT,
565 }
566 }
567}
568
569pub const OPEN_FAILURE_CHARGE_BYTES: u64 = 4096;
583
584#[derive(Debug, Clone)]
594pub struct LineRead {
595 pub(crate) text: Option<String>,
597 pub(crate) bytes_read: u64,
600}
601
602#[derive(Debug)]
613struct PendingClose {
614 uri: Uri,
615 servers: HashSet<ServerId>,
616}
617
618#[derive(Debug)]
629#[must_use = "dropping the claim releases the path lock; send the closes first"]
630pub struct PendingCloseClaim<'a> {
631 pub(crate) path: PathBuf,
633 pub(crate) uri: Uri,
635 servers: Vec<ServerId>,
637 tracker: &'a DocumentTracker,
638 unsettled: Vec<(ServerId, SyncGeneration)>,
640 _path_guard: PathLockGuard<'a>,
641}
642
643impl PendingCloseClaim<'_> {
644 pub(crate) fn servers(&self) -> impl Iterator<Item = &ServerId> {
646 self.servers.iter()
647 }
648
649 pub(crate) fn settle(&mut self, server: &ServerId) {
651 self.unsettled.retain(|(unsettled, _)| unsettled != server);
652 }
653}
654
655impl Drop for PendingCloseClaim<'_> {
656 fn drop(&mut self) {
657 for (server, generation) in self.unsettled.drain(..) {
658 self.tracker
659 .restore_pending_close(&self.path, &self.uri, &server, generation);
660 }
661 }
662}
663
664#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
667struct SyncGeneration(u64);
668
669impl SyncGeneration {
670 const fn next(self) -> Self {
671 Self(self.0.saturating_add(1))
672 }
673}
674
675#[derive(Debug, Clone, Copy, PartialEq, Eq)]
677pub enum LinePresence {
678 Untracked,
680 Present,
682 Beyond {
684 last_line: NonZeroU32,
686 },
687}
688
689#[derive(Debug, Clone, Copy, PartialEq, Eq)]
703pub enum DocumentOpenState {
704 Open,
706 NotOpen,
708}
709
710#[derive(Debug)]
717pub struct DocumentTracker {
718 documents: StdMutex<HashMap<PathBuf, DocumentState>>,
721 path_locks: StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
730 in_flight: InFlightMap,
735 generations: StdMutex<HashMap<ServerId, SyncGeneration>>,
742 limits: ResourceLimits,
744 language_map: LanguageMap,
746 pending_closes: StdMutex<HashMap<PathBuf, PendingClose>>,
753 next_opening: AtomicU64,
755}
756
757impl DocumentTracker {
758 #[must_use]
760 pub fn new(limits: ResourceLimits, language_map: impl Into<LanguageMap>) -> Self {
761 Self {
762 documents: StdMutex::new(HashMap::new()),
763 path_locks: StdMutex::new(HashMap::new()),
764 in_flight: Arc::default(),
765 generations: StdMutex::new(HashMap::new()),
766 limits,
767 language_map: language_map.into(),
768 pending_closes: StdMutex::new(HashMap::new()),
769 next_opening: AtomicU64::new(0),
770 }
771 }
772
773 #[must_use]
775 pub(crate) const fn limits(&self) -> ResourceLimits {
776 self.limits
777 }
778
779 pub(crate) fn mark_in_flight(&self, path: &Path) -> InFlightGuard {
786 let mut in_flight = lock_std(&self.in_flight);
787 let count = in_flight.entry(path.to_path_buf()).or_insert(0);
788 *count = count.saturating_add(1);
789 drop(in_flight);
790 InFlightGuard {
791 in_flight: Arc::clone(&self.in_flight),
792 path: path.to_path_buf(),
793 }
794 }
795
796 #[cfg(test)]
798 pub(crate) fn in_flight_count(&self, path: &Path) -> usize {
799 lock_std(&self.in_flight).get(path).copied().unwrap_or(0)
800 }
801
802 #[must_use]
805 pub(crate) fn synced_version(&self, path: &Path, server: &ServerId) -> Option<DocumentVersion> {
806 lock_std(&self.documents).get(path)?.synced_version(server)
807 }
808
809 #[must_use]
813 pub(crate) fn stamp(&self, path: &Path, server: &ServerId) -> Option<DocumentStamp> {
814 let documents = lock_std(&self.documents);
815 let stamp = documents.get(path).and_then(|state| {
816 Some(DocumentStamp::new(
817 Some(state.opened),
818 state.synced_version(server)?,
819 ))
820 });
821 drop(documents);
822 stamp
823 }
824
825 #[must_use]
828 pub(crate) fn sweep_state(&self, path: &Path, server: &ServerId) -> Option<SweepState> {
829 let documents = lock_std(&self.documents);
830 let sweep_state = documents.get(path).and_then(|state| {
831 let synced = state.synced_version(server)?;
832 let stamp = DocumentStamp::new(Some(state.opened), synced);
833 Some(if synced == state.version {
834 SweepState::Current {
835 stamp,
836 digest: state.text.digest(),
837 }
838 } else {
839 SweepState::Lagging { stamp }
840 })
841 });
842 drop(documents);
843 sweep_state
844 }
845
846 #[must_use]
848 pub(crate) fn opening(&self, path: &Path) -> Option<Opening> {
849 lock_std(&self.documents)
850 .get(path)
851 .map(|state| state.opened)
852 }
853
854 #[cfg(test)]
857 pub(crate) fn set_synced_version_for_test(&self, path: &Path, server: &ServerId, version: i32) {
858 if let Some(state) = lock_std(&self.documents).get_mut(path) {
859 state.mark_synced(server.clone(), DocumentVersion::new(version));
860 }
861 }
862
863 #[must_use]
865 pub fn open_state(&self, path: &Path) -> DocumentOpenState {
866 if lock_std(&self.documents).contains_key(path) {
867 DocumentOpenState::Open
868 } else {
869 DocumentOpenState::NotOpen
870 }
871 }
872
873 #[must_use]
875 #[cfg(test)]
876 pub(super) fn get(&self, path: &Path) -> Option<DocumentState> {
877 lock_std(&self.documents).get(path).cloned()
878 }
879
880 #[must_use]
888 pub fn line_text(&self, path: &Path, line: u32) -> Option<String> {
889 let documents = lock_std(&self.documents);
890 documents.get(path)?.text.line(line).map(str::to_string)
891 }
892
893 #[must_use]
896 pub(crate) fn text(&self, path: &Path) -> Option<String> {
897 lock_std(&self.documents)
898 .get(path)
899 .map(|state| state.text.as_str().to_owned())
900 }
901
902 #[must_use]
906 pub(crate) fn line_presence(&self, path: &Path, line: u32) -> LinePresence {
907 match lock_std(&self.documents).get(path) {
908 None => LinePresence::Untracked,
909 Some(document) if document.text.line(line).is_some() => LinePresence::Present,
910 Some(document) => LinePresence::Beyond {
911 last_line: document.text.last_line(),
912 },
913 }
914 }
915
916 #[must_use]
922 pub(crate) fn line_window(
923 &self,
924 path: &Path,
925 start_line: u32,
926 max_lines: usize,
927 ) -> Option<String> {
928 let documents = lock_std(&self.documents);
929 let mut lines = documents
930 .get(path)?
931 .text
932 .lines_from(start_line)
933 .take(max_lines);
934 let mut window = lines.next()?.to_string();
935 for line in lines {
936 window.push('\n');
937 window.push_str(line);
938 }
939 drop(documents);
940 Some(window)
941 }
942
943 #[must_use]
945 #[cfg(test)]
946 pub(crate) fn len(&self) -> usize {
947 lock_std(&self.documents).len()
948 }
949
950 #[must_use]
952 #[cfg(test)]
953 pub(crate) fn is_empty(&self) -> bool {
954 lock_std(&self.documents).is_empty()
955 }
956
957 #[cfg(test)]
989 pub(crate) fn open(&self, path: PathBuf, content: String) -> Result<Uri> {
990 self.open_text(path, DocumentText::new(content))
991 }
992
993 fn open_text(&self, path: PathBuf, text: DocumentText) -> Result<Uri> {
996 self.check_file_size(text.as_str().len() as u64)?;
997
998 let uri = path_to_uri(&path)?;
999 let language_id = self.language_map.detect(&path);
1000
1001 let opened = Opening(self.next_opening.fetch_add(1, Ordering::Relaxed));
1002 let state = DocumentState::new(uri.clone(), language_id, text, opened);
1003
1004 let mut documents = lock_std(&self.documents);
1016 if let Some(max) = self.limits.max_documents.get()
1017 && documents.len() >= max.get()
1018 && !documents.contains_key(&path)
1019 {
1020 let Some((evicted_path, evicted_state)) =
1021 Self::evict_lru(&mut documents, &self.path_locks, &self.in_flight)
1022 else {
1023 return Err(Error::DocumentLimitExceeded {
1024 current: documents.len(),
1025 max,
1026 });
1027 };
1028 self.record_pending_close(evicted_path, evicted_state);
1029 }
1030 documents.insert(path, state);
1031 drop(documents);
1032 Ok(uri)
1033 }
1034
1035 fn record_pending_close(&self, path: PathBuf, evicted: DocumentState) {
1038 if evicted.synced.is_empty() {
1039 return;
1040 }
1041 lock_std(&self.pending_closes)
1042 .entry(path)
1043 .or_insert_with(|| PendingClose {
1044 uri: evicted.uri,
1045 servers: HashSet::new(),
1046 })
1047 .servers
1048 .extend(evicted.synced.into_keys());
1049 }
1050
1051 fn evict_lru(
1067 documents: &mut HashMap<PathBuf, DocumentState>,
1068 path_locks: &StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
1069 in_flight: &InFlightMap,
1070 ) -> Option<(PathBuf, DocumentState)> {
1071 let mut busy = lock_std(path_locks)
1072 .keys()
1073 .cloned()
1074 .collect::<std::collections::HashSet<_>>();
1075 busy.extend(lock_std(in_flight).keys().cloned());
1076 let lru_path = documents
1077 .iter()
1078 .filter(|(path, state)| !busy.contains(path.as_path()) && state.disk().is_some())
1079 .min_by_key(|(_, state)| state.last_accessed)
1080 .map(|(path, _)| path.clone())?;
1081 documents.remove(&lru_path).map(|state| (lru_path, state))
1082 }
1083
1084 const fn check_file_size(&self, size: u64) -> Result<()> {
1086 match SizeExceeded::check_limit(size, self.limits.max_file_size) {
1087 Ok(()) => Ok(()),
1088 Err(exceeded) => Err(Error::FileSizeLimitExceeded(exceeded)),
1089 }
1090 }
1091
1092 fn set_disk(&self, path: &Path, snap: DiskSync) {
1099 if let Some(st) = lock_std(&self.documents).get_mut(path) {
1100 st.set_disk(snap);
1101 }
1102 }
1103
1104 #[cfg(test)]
1108 pub(super) fn close(&self, path: &Path) -> Option<DocumentState> {
1109 lock_std(&self.documents).remove(path)
1110 }
1111
1112 pub fn open_paths(&self) -> Vec<PathBuf> {
1114 lock_std(&self.documents).keys().cloned().collect()
1115 }
1116
1117 pub(crate) fn open_paths_by_recency(&self) -> Vec<PathBuf> {
1120 let mut paths: Vec<(PathBuf, Instant)> = lock_std(&self.documents)
1121 .iter()
1122 .map(|(path, state)| (path.clone(), state.last_accessed))
1123 .collect();
1124 paths.sort_unstable_by(|(_, a), (_, b)| b.cmp(a));
1125 paths.into_iter().map(|(path, _)| path).collect()
1126 }
1127
1128 pub fn forget_server(&self, server: &ServerId) {
1147 let mut generations = lock_std(&self.generations);
1148 let generation = generations.entry(server.clone()).or_default();
1149 *generation = generation.next();
1150 drop(generations);
1151 for state in lock_std(&self.documents).values_mut() {
1152 state.forget_server(server);
1153 }
1154 lock_std(&self.pending_closes).retain(|_, pending| {
1156 pending.servers.remove(server);
1157 !pending.servers.is_empty()
1158 });
1159 }
1160
1161 pub(crate) fn synced_document_count(&self, server: &ServerId) -> usize {
1163 lock_std(&self.documents)
1164 .values()
1165 .filter(|state| state.synced_version(server).is_some())
1166 .count()
1167 }
1168
1169 pub(crate) fn sync_history_forgotten(&self, server: &ServerId) -> bool {
1171 self.generation(server) != SyncGeneration::default()
1172 }
1173
1174 fn generation(&self, server: &ServerId) -> SyncGeneration {
1176 lock_std(&self.generations)
1177 .get(server)
1178 .copied()
1179 .unwrap_or_default()
1180 }
1181
1182 async fn lock_path(&self, path: &Path) -> PathLockGuard<'_> {
1197 let arc = {
1198 let mut locks = lock_std(&self.path_locks);
1199 locks
1200 .entry(path.to_path_buf())
1201 .or_insert_with(|| Arc::new(AsyncMutex::new(())))
1202 .clone()
1203 };
1204 let guard = Arc::clone(&arc).lock_owned().await;
1205 PathLockGuard {
1206 path_locks: &self.path_locks,
1207 path: path.to_path_buf(),
1208 arc,
1209 guard: Some(guard),
1210 }
1211 }
1212
1213 fn try_lock_path(&self, path: &Path) -> Option<PathLockGuard<'_>> {
1216 let arc = {
1217 let mut locks = lock_std(&self.path_locks);
1218 locks
1219 .entry(path.to_path_buf())
1220 .or_insert_with(|| Arc::new(AsyncMutex::new(())))
1221 .clone()
1222 };
1223 if let Ok(guard) = Arc::clone(&arc).try_lock_owned() {
1224 return Some(PathLockGuard {
1225 path_locks: &self.path_locks,
1226 path: path.to_path_buf(),
1227 arc,
1228 guard: Some(guard),
1229 });
1230 }
1231 let mut locks = lock_std(&self.path_locks);
1234 if Arc::strong_count(&arc) <= 2 {
1235 locks.remove(path);
1236 }
1237 None
1238 }
1239
1240 pub(crate) fn pending_close_paths(&self) -> Vec<PathBuf> {
1243 lock_std(&self.pending_closes).keys().cloned().collect()
1244 }
1245
1246 pub(crate) fn try_claim_pending_close(&self, path: &Path) -> Option<PendingCloseClaim<'_>> {
1259 let path_guard = self.try_lock_path(path)?;
1260 let synced: HashSet<ServerId> = lock_std(&self.documents)
1261 .get(path)
1262 .map_or_default(|st| st.synced.keys().cloned().collect());
1263 let pending = lock_std(&self.pending_closes).remove(path)?;
1264 let servers: Vec<ServerId> = pending.servers.difference(&synced).cloned().collect();
1265 if servers.is_empty() {
1266 return None;
1267 }
1268 let unsettled = servers
1269 .iter()
1270 .map(|server| (server.clone(), self.generation(server)))
1271 .collect();
1272 Some(PendingCloseClaim {
1273 path: path.to_path_buf(),
1274 uri: pending.uri,
1275 servers,
1276 tracker: self,
1277 unsettled,
1278 _path_guard: path_guard,
1279 })
1280 }
1281
1282 fn owes_pending_close(&self, path: &Path, server: &ServerId) -> bool {
1285 lock_std(&self.pending_closes)
1286 .get(path)
1287 .is_some_and(|pending| pending.servers.contains(server))
1288 }
1289
1290 fn take_pending_close(&self, path: &Path, server: &ServerId) -> bool {
1293 let mut pending_closes = lock_std(&self.pending_closes);
1294 let Some(pending) = pending_closes.get_mut(path) else {
1295 return false;
1296 };
1297 let owed = pending.servers.remove(server);
1298 if pending.servers.is_empty() {
1299 pending_closes.remove(path);
1300 }
1301 owed
1302 }
1303
1304 fn restore_pending_close(
1313 &self,
1314 path: &Path,
1315 uri: &Uri,
1316 server: &ServerId,
1317 generation: SyncGeneration,
1318 ) {
1319 let mut pending_closes = lock_std(&self.pending_closes);
1320 if self.generation(server) != generation {
1321 return;
1322 }
1323 pending_closes
1324 .entry(path.to_path_buf())
1325 .or_insert_with(|| PendingClose {
1326 uri: uri.clone(),
1327 servers: HashSet::new(),
1328 })
1329 .servers
1330 .insert(server.clone());
1331 }
1332
1333 pub async fn ensure_open(&self, request: OpenRequest<'_>) -> Result<Uri> {
1406 let path = request.path.as_path();
1407 let _path_guard = self.lock_path(path).await;
1408 let generation = self.generation(request.server);
1409 let decision = self.disk_phase(request.gate(), request.access).await?;
1410 self.sync_phase(path, request.server, request.client, decision, generation)
1411 .await
1412 }
1413
1414 async fn disk_phase(&self, gate: OpenGate<'_>, access: Access) -> Result<Decision> {
1419 let path = gate.path();
1420 if !lock_std(&self.documents).contains_key(path) {
1421 if access == Access::Probe {
1422 return Err(Error::DocumentNotFound(path.to_path_buf()));
1423 }
1424 return self.disk_phase_new(gate).await;
1425 }
1426
1427 let read_at = SystemTime::now();
1428 let meta = fs::metadata(path).await.map_err(|e| Error::FileIo {
1429 path: path.to_path_buf(),
1430 source: e,
1431 })?;
1432 let mtime = meta.modified().ok();
1433 let size = meta.len();
1434
1435 let Some((uri, current_version, fast_path)) =
1443 lock_std(&self.documents).get_mut(path).map(|st| {
1444 if access == Access::Use {
1445 st.touch();
1446 }
1447 let stat_matches = st
1448 .disk()
1449 .is_some_and(|d| d.mtime == mtime && d.size == size);
1450 let fast_path = match st.disk() {
1451 Some(d) if stat_matches && d.mtime_settled => true,
1452 Some(d)
1453 if stat_matches && d.content_checked_at.elapsed() < DISK_CHECK_DEBOUNCE =>
1454 {
1455 true
1456 }
1457 _ => false,
1458 };
1459 (st.uri.clone(), st.version, fast_path)
1460 })
1461 else {
1462 return Err(Error::DocumentNotFound(path.to_path_buf()));
1463 };
1464 if fast_path {
1465 return Ok(Decision::unchanged(uri, current_version));
1466 }
1467
1468 let (fresh, ..) = self.read_to_string_checked(gate).await?;
1469 let snap = DiskSync {
1470 mtime,
1471 size,
1472 mtime_settled: mtime_settled(mtime, read_at),
1473 content_checked_at: Instant::now(),
1474 };
1475
1476 let Some(unchanged) = lock_std(&self.documents)
1477 .get(path)
1478 .map(|st| fresh == st.text.as_str())
1479 else {
1480 return Err(Error::DocumentNotFound(path.to_path_buf()));
1481 };
1482
1483 if unchanged {
1484 self.set_disk(path, snap);
1485 return Ok(Decision::unchanged(uri, current_version));
1486 }
1487
1488 Ok(Decision {
1489 uri,
1490 target_version: current_version.next(),
1491 fresh_content: Some(DocumentText::build(fresh).await?),
1492 snap: Some(snap),
1493 })
1494 }
1495
1496 async fn disk_phase_new(&self, gate: OpenGate<'_>) -> Result<Decision> {
1500 let path = gate.path();
1501 let read_at = SystemTime::now();
1502 let (content, mtime, size) = self.read_to_string_checked(gate).await?;
1503
1504 let uri = self.open_text(path.to_path_buf(), DocumentText::build(content).await?)?;
1505 self.set_disk(
1506 path,
1507 DiskSync {
1508 mtime,
1509 size,
1510 mtime_settled: mtime_settled(mtime, read_at),
1511 content_checked_at: Instant::now(),
1512 },
1513 );
1514
1515 Ok(Decision::unchanged(uri, DocumentVersion::FIRST))
1516 }
1517
1518 async fn open_checked(&self, gate: OpenGate<'_>) -> Result<(fs::File, std::fs::Metadata)> {
1530 let opened = match gate {
1531 OpenGate::Contained { roots, path } => {
1532 let (roots, path) = (roots.clone(), path.clone());
1533 tokio::task::spawn_blocking(move || roots.open_contained(&path))
1534 }
1535 OpenGate::Ungated(path) => {
1536 let owned = path.to_path_buf();
1537 tokio::task::spawn_blocking(move || {
1538 RegularFile::open(&owned).map_err(|error| error.into_error(&owned))
1539 })
1540 }
1541 }
1542 .await
1543 .map_err(|source| Error::TaskFailed {
1544 task: BackgroundTask::FileOpen,
1545 source,
1546 })?;
1547 let (file, meta) = opened?.into_parts();
1548 self.check_file_size(meta.len())?;
1549 Ok((fs::File::from_std(file), meta))
1550 }
1551
1552 async fn read_string_bounded(
1565 &self,
1566 path: &Path,
1567 mut file: fs::File,
1568 size_hint: u64,
1569 ) -> Result<String> {
1570 let max = self.limits.max_file_size;
1571 let cap = max.read_cap();
1572 let mut buf = Vec::with_capacity(usize::try_from(size_hint.min(cap)).unwrap_or(0));
1573 let io_err = |e: std::io::Error| Error::FileIo {
1574 path: path.to_path_buf(),
1575 source: e,
1576 };
1577
1578 (&mut file)
1579 .take(cap)
1580 .read_to_end(&mut buf)
1581 .await
1582 .map_err(io_err)?;
1583 check_bounded_utf8(buf, max).map_err(|e| match e {
1584 BoundedUtf8Error::TooLarge(exceeded) => Error::FileSizeLimitExceeded(exceeded),
1585 BoundedUtf8Error::InvalidUtf8(e) => {
1586 io_err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
1587 }
1588 })
1589 }
1590
1591 async fn read_to_string_checked(
1599 &self,
1600 gate: OpenGate<'_>,
1601 ) -> Result<(String, Option<SystemTime>, u64)> {
1602 let path = gate.path();
1603 let (file, meta) = self.open_checked(gate).await?;
1604 let mtime = meta.modified().ok();
1605 let size = meta.len();
1606 let content = self.read_string_bounded(path, file, size).await?;
1607 Ok((content, mtime, size))
1608 }
1609
1610 pub(crate) async fn read_line_checked(
1672 &self,
1673 path: &Path,
1674 line: u32,
1675 budget: u64,
1676 ) -> Result<LineRead> {
1677 let Ok((file, _meta)) = self.open_checked(OpenGate::Ungated(path)).await else {
1678 return Ok(LineRead {
1679 text: None,
1680 bytes_read: OPEN_FAILURE_CHARGE_BYTES,
1681 });
1682 };
1683 let max = self.limits.max_file_size;
1684 let cap = max.read_cap().min(budget.saturating_add(1));
1690 let mut reader = tokio::io::BufReader::new(file.take(cap));
1691 let io_err = |e: std::io::Error| Error::FileIo {
1692 path: path.to_path_buf(),
1693 source: e,
1694 };
1695
1696 read_nth_line(&mut reader, line, cap).await.map_err(io_err)
1697 }
1698
1699 async fn send_open(
1708 &self,
1709 path: &Path,
1710 server: &ServerId,
1711 lsp_client: &LspClient,
1712 document: OpenedDocument,
1713 ) -> Result<()> {
1714 let OpenedDocument {
1715 uri,
1716 language_id,
1717 text,
1718 version,
1719 } = document;
1720 let open = DidOpenTextDocumentParams {
1721 text_document: TextDocumentItem {
1722 uri: uri.clone(),
1723 language_id: String::from(language_id).into(),
1724 version: version.get(),
1725 text,
1726 },
1727 };
1728 if !self.owes_pending_close(path, server) {
1729 return lsp_client
1730 .notify_typed::<DidOpenTextDocumentNotification>(open)
1731 .await;
1732 }
1733 let open = LspClient::outbound_notification::<DidOpenTextDocumentNotification>(open)?;
1734 let close = LspClient::outbound_notification::<DidCloseTextDocumentNotification>(
1735 DidCloseTextDocumentParams {
1736 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1737 },
1738 )?;
1739 let (close_permit, open_permit) = lsp_client.reserve_notification_pair().await?;
1740 if self.take_pending_close(path, server) {
1741 close_permit.send(close);
1742 }
1743 open_permit.send(open);
1744 Ok(())
1745 }
1746
1747 async fn send_change(
1750 lsp_client: &LspClient,
1751 uri: &Uri,
1752 text: String,
1753 version: DocumentVersion,
1754 ) -> Result<()> {
1755 lsp_client
1756 .notify_typed::<DidChangeTextDocumentNotification>(DidChangeTextDocumentParams {
1757 text_document: VersionedTextDocumentIdentifier {
1758 version: version.get(),
1759 text_document_identifier: lsp_types::TextDocumentIdentifier {
1760 uri: uri.clone(),
1761 },
1762 },
1763 content_changes: vec![
1764 TextDocumentContentChangeEvent::TextDocumentContentChangeWholeDocument(
1765 lsp_types::TextDocumentContentChangeWholeDocument { text },
1766 ),
1767 ],
1768 })
1769 .await
1770 }
1771
1772 async fn sync_phase(
1783 &self,
1784 path: &Path,
1785 server: &ServerId,
1786 lsp_client: &LspClient,
1787 decision: Decision,
1788 generation: SyncGeneration,
1789 ) -> Result<Uri> {
1790 let Decision {
1791 uri,
1792 target_version,
1793 fresh_content,
1794 snap,
1795 } = decision;
1796
1797 let Some(synced_version) = lock_std(&self.documents)
1804 .get(path)
1805 .map(|st| st.synced_version(server))
1806 else {
1807 return Err(Error::DocumentNotFound(path.to_path_buf()));
1808 };
1809 let up_to_date = synced_version.is_some_and(|v| v >= target_version);
1810 let is_first_open = synced_version.is_none();
1811
1812 if up_to_date {
1813 return Ok(uri);
1814 }
1815
1816 let Some((language_id, text)) = lock_std(&self.documents).get(path).map(|st| {
1817 let text = fresh_content
1818 .as_ref()
1819 .unwrap_or(&st.text)
1820 .as_str()
1821 .to_owned();
1822 (st.language_id.clone(), text)
1823 }) else {
1824 return Err(Error::DocumentNotFound(path.to_path_buf()));
1825 };
1826
1827 let notify_result = if is_first_open {
1828 let document = OpenedDocument {
1829 uri: uri.clone(),
1830 language_id,
1831 text,
1832 version: target_version,
1833 };
1834 self.send_open(path, server, lsp_client, document).await
1835 } else {
1836 Self::send_change(lsp_client, &uri, text, target_version).await
1837 };
1838
1839 if let Err(err) = notify_result {
1840 let first_ever_sync = lock_std(&self.documents)
1853 .get(path)
1854 .is_some_and(DocumentState::has_never_synced);
1855 if is_first_open && first_ever_sync {
1856 lock_std(&self.documents).remove(path);
1857 }
1858 return Err(err);
1859 }
1860
1861 let mut documents = lock_std(&self.documents);
1864 let Some(st) = documents.get_mut(path) else {
1865 return Err(Error::DocumentNotFound(path.to_path_buf()));
1866 };
1867 if let Some(fresh) = fresh_content {
1868 st.commit_reload(target_version, fresh, snap);
1869 }
1870 if self.generation(server) == generation {
1880 st.mark_synced(server.clone(), target_version);
1881 }
1882 drop(documents);
1883
1884 Ok(uri)
1885 }
1886}
1887
1888#[allow(
1898 clippy::arithmetic_side_effects,
1899 clippy::indexing_slicing,
1900 reason = "every index derives from `position()` over the same chunk and stays within `chunk.len()`"
1901)]
1902async fn read_nth_line<R>(reader: &mut R, line: u32, cap: u64) -> std::io::Result<LineRead>
1903where
1904 R: tokio::io::AsyncBufRead + Unpin,
1905{
1906 let mut buf = Vec::new();
1907 let mut bytes_read: u64 = 0;
1908 let mut current_line = 0u32;
1909 let mut after_cr = false;
1910 loop {
1911 let chunk = reader.fill_buf().await?;
1912 if chunk.is_empty() {
1913 let reached_line = current_line == line && bytes_read < cap;
1914 return Ok(LineRead {
1915 text: reached_line.then(|| String::from_utf8(buf).ok()).flatten(),
1916 bytes_read,
1917 });
1918 }
1919
1920 let mut i = usize::from(after_cr && chunk[0] == b'\n');
1921 after_cr = false;
1922 let mut finished = false;
1923 while i < chunk.len() {
1924 let rest = &chunk[i..];
1925 let Some(offset) = rest.iter().position(|&b| is_line_terminator(b)) else {
1926 if current_line == line {
1927 buf.extend_from_slice(rest);
1928 }
1929 i = chunk.len();
1930 break;
1931 };
1932 let terminator = i + offset;
1933 if current_line == line {
1934 buf.extend_from_slice(&chunk[i..terminator]);
1935 i = terminator + 1;
1936 finished = true;
1937 break;
1938 }
1939 current_line += 1;
1940 i = terminator + 1;
1941 if chunk[terminator] == b'\r' {
1942 match chunk.get(i) {
1943 Some(b'\n') => i += 1,
1944 Some(_) => {}
1945 None => after_cr = true,
1946 }
1947 }
1948 }
1949
1950 bytes_read += i as u64;
1951 reader.consume(i);
1952 if finished {
1953 return Ok(LineRead {
1954 text: String::from_utf8(buf).ok(),
1955 bytes_read,
1956 });
1957 }
1958 }
1959}
1960
1961type InFlightMap = Arc<StdMutex<HashMap<PathBuf, usize>>>;
1963
1964#[derive(Debug)]
1971#[must_use = "dropping the guard immediately makes the document evictable again"]
1972pub struct InFlightGuard {
1973 in_flight: InFlightMap,
1974 path: PathBuf,
1975}
1976
1977impl Drop for InFlightGuard {
1978 fn drop(&mut self) {
1979 let mut in_flight = lock_std(&self.in_flight);
1980 if let Some(count) = in_flight.get_mut(&self.path) {
1981 *count = count.saturating_sub(1);
1982 if *count == 0 {
1983 in_flight.remove(&self.path);
1984 }
1985 }
1986 }
1987}
1988
1989#[derive(Debug)]
1998struct PathLockGuard<'a> {
1999 path_locks: &'a StdMutex<HashMap<PathBuf, Arc<AsyncMutex<()>>>>,
2000 path: PathBuf,
2001 arc: Arc<AsyncMutex<()>>,
2002 guard: Option<OwnedMutexGuard<()>>,
2003}
2004
2005impl Drop for PathLockGuard<'_> {
2006 fn drop(&mut self) {
2007 self.guard.take();
2010
2011 let mut locks = lock_std(self.path_locks);
2012 if Arc::strong_count(&self.arc) <= 2 {
2024 locks.remove(&self.path);
2025 }
2026 }
2027}
2028
2029#[cfg(test)]
2030impl DocumentTracker {
2031 pub(crate) async fn ensure_open_for_test(
2034 &self,
2035 path: &Path,
2036 server: &ServerId,
2037 client: &LspClient,
2038 ) -> Result<Uri> {
2039 self.ensure_open_as_for_test(path, server, client, Access::Use)
2040 .await
2041 }
2042
2043 pub(crate) async fn ensure_open_as_for_test(
2045 &self,
2046 path: &Path,
2047 server: &ServerId,
2048 client: &LspClient,
2049 access: Access,
2050 ) -> Result<Uri> {
2051 let parent = path.parent().unwrap_or(path);
2052 let mut roots = vec![parent.to_path_buf()];
2053 if let Ok(canonical) = dunce::canonicalize(parent)
2057 && canonical != parent
2058 {
2059 roots.push(canonical);
2060 }
2061 self.ensure_open(OpenRequest::new(
2062 &WorkspacePath::for_test(path.to_path_buf()),
2063 &WorkspaceRoots::for_test(roots, Vec::new()),
2064 server,
2065 client,
2066 access,
2067 ))
2068 .await
2069 }
2070}
2071
2072#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2074pub enum Access {
2075 Use,
2078 Probe,
2081}
2082
2083#[derive(Debug, Clone, Copy)]
2090pub struct OpenRequest<'a> {
2091 path: &'a WorkspacePath,
2092 roots: &'a WorkspaceRoots,
2093 server: &'a ServerId,
2094 client: &'a LspClient,
2095 access: Access,
2096}
2097
2098impl<'a> OpenRequest<'a> {
2099 #[must_use]
2103 pub const fn new(
2104 path: &'a WorkspacePath,
2105 roots: &'a WorkspaceRoots,
2106 server: &'a ServerId,
2107 client: &'a LspClient,
2108 access: Access,
2109 ) -> Self {
2110 Self {
2111 path,
2112 roots,
2113 server,
2114 client,
2115 access,
2116 }
2117 }
2118
2119 const fn gate(&self) -> OpenGate<'a> {
2120 OpenGate::Contained {
2121 roots: self.roots,
2122 path: self.path,
2123 }
2124 }
2125}
2126
2127#[derive(Clone, Copy)]
2129enum OpenGate<'a> {
2130 Contained {
2132 roots: &'a WorkspaceRoots,
2133 path: &'a WorkspacePath,
2134 },
2135 Ungated(&'a Path),
2138}
2139
2140impl<'a> OpenGate<'a> {
2141 fn path(self) -> &'a Path {
2142 match self {
2143 Self::Contained { path, .. } => path.as_path(),
2144 Self::Ungated(path) => path,
2145 }
2146 }
2147}
2148
2149struct Decision {
2154 uri: Uri,
2155 target_version: DocumentVersion,
2156 fresh_content: Option<DocumentText>,
2157 snap: Option<DiskSync>,
2158}
2159
2160impl Decision {
2161 const fn unchanged(uri: Uri, target_version: DocumentVersion) -> Self {
2164 Self {
2165 uri,
2166 target_version,
2167 fresh_content: None,
2168 snap: None,
2169 }
2170 }
2171}
2172
2173pub fn path_to_uri(path: &Path) -> Result<Uri> {
2186 try_path_to_uri(path).ok_or_else(|| Error::PathToUri(path.to_path_buf()))
2187}
2188
2189#[must_use]
2195pub fn try_path_to_uri(path: &Path) -> Option<Uri> {
2196 let uri_string = encode_rfc3986_path_chars(&file_url(path)?);
2197 Some(Uri::from(uri_string))
2198}
2199
2200#[cfg(not(windows))]
2201pub(super) fn file_url(path: &Path) -> Option<Url> {
2202 Url::from_file_path(path).ok()
2203}
2204
2205#[cfg(windows)]
2206pub(super) fn file_url(path: &Path) -> Option<Url> {
2207 match Url::from_file_path(path) {
2208 Ok(file_url) => Some(file_url),
2209 Err(()) if path.has_root() => windows_rooted_path_to_file_url(path),
2210 Err(()) => None,
2211 }
2212}
2213
2214#[cfg(windows)]
2215fn windows_rooted_path_to_file_url(path: &Path) -> Option<Url> {
2216 let path_str = path.to_string_lossy();
2217 let stripped = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
2218 let mut file_url = Url::parse("file:///").ok()?;
2219 file_url.path_segments_mut().ok()?.clear().extend(
2220 stripped
2221 .split(['\\', '/'])
2222 .filter(|segment| !segment.is_empty()),
2223 );
2224 Some(file_url)
2225}
2226
2227pub(super) fn encode_rfc3986_path_chars(url: &Url) -> String {
2237 let prefix = url[..url::Position::BeforePath].to_owned();
2238 let encoded = url[url::Position::BeforePath..]
2239 .replace('[', "%5B")
2240 .replace(']', "%5D")
2241 .replace('^', "%5E")
2242 .replace('|', "%7C");
2243 format!("{prefix}{encoded}")
2244}
2245
2246#[must_use]
2251pub fn uri_to_path(uri: &Uri) -> Option<PathBuf> {
2252 let url = Url::parse(uri.as_ref()).ok()?;
2253 if url.scheme() != "file" {
2254 return None;
2255 }
2256 if !url.host_str().unwrap_or("").is_empty() {
2259 return None;
2260 }
2261 url.to_file_path().ok()
2262}
2263
2264#[cfg(test)]
2265mod tests {
2266 use std::assert_matches;
2267
2268 use super::*;
2269 use crate::config::FileExtension;
2270
2271 fn detect_language(
2272 path: &Path,
2273 extension_map: &HashMap<FileExtension, LanguageId>,
2274 ) -> LanguageId {
2275 LanguageMap::from(extension_map.clone()).detect(path)
2276 }
2277
2278 #[test]
2279 fn test_detect_language() {
2280 let mut map = HashMap::new();
2281 map.insert(
2282 FileExtension::from_static("rs"),
2283 LanguageId::from_static("rust"),
2284 );
2285 map.insert(
2286 FileExtension::from_static("py"),
2287 LanguageId::from_static("python"),
2288 );
2289 map.insert(
2290 FileExtension::from_static("ts"),
2291 LanguageId::from_static("typescript"),
2292 );
2293
2294 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
2295 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
2296 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
2297 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
2298 }
2299
2300 #[tokio::test]
2301 async fn test_document_tracker() {
2302 let mut map = HashMap::new();
2303 map.insert(
2304 FileExtension::from_static("rs"),
2305 LanguageId::from_static("rust"),
2306 );
2307
2308 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
2309 let path = PathBuf::from("/test/file.rs");
2310
2311 assert_eq!(tracker.open_state(&path), DocumentOpenState::NotOpen);
2312
2313 tracker
2314 .open(path.clone(), "fn main() {}".to_string())
2315 .unwrap();
2316 assert_eq!(tracker.open_state(&path), DocumentOpenState::Open);
2317 assert_eq!(tracker.len(), 1);
2318
2319 let state = tracker.get(&path).unwrap();
2320 assert_eq!(state.version().get(), 1);
2321 assert_eq!(state.language_id(), "rust");
2322
2323 tracker.close(&path);
2324 assert_eq!(tracker.open_state(&path), DocumentOpenState::NotOpen);
2325 assert!(tracker.is_empty());
2326 }
2327
2328 #[test]
2332 fn test_stamp_and_sweep_state_read_one_opening_and_version() {
2333 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2334 let server = ServerId::from_static("rust");
2335 let path = PathBuf::from("/test/stamp.rs");
2336 tracker.open(path.clone(), "fn a() {}".to_string()).unwrap();
2337 assert_eq!(tracker.stamp(&path, &server), None);
2338 assert_eq!(tracker.sweep_state(&path, &server), None);
2339
2340 tracker.set_synced_version_for_test(&path, &server, 1);
2341 let first = tracker.stamp(&path, &server).unwrap();
2342 assert_eq!(first.opening(), tracker.opening(&path));
2343 assert_eq!(first.version(), DocumentVersion::new(1));
2344 assert_matches!(
2345 tracker.sweep_state(&path, &server),
2346 Some(SweepState::Current { stamp, .. }) if stamp == first
2347 );
2348
2349 tracker.set_synced_version_for_test(&path, &server, 0);
2350 assert_matches!(
2351 tracker.sweep_state(&path, &server),
2352 Some(SweepState::Lagging { .. })
2353 );
2354
2355 tracker.close(&path);
2356 tracker.open(path.clone(), "fn a() {}".to_string()).unwrap();
2357 tracker.set_synced_version_for_test(&path, &server, 1);
2358 let second = tracker.stamp(&path, &server).unwrap();
2359 assert_ne!(second.opening(), first.opening());
2360 assert_eq!(second.opening(), tracker.opening(&path));
2361 }
2362
2363 #[test]
2369 fn test_forget_server_clears_only_that_servers_synced_version() {
2370 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2371 let path = PathBuf::from("/test/file.rs");
2372 tracker
2373 .open(path.clone(), "fn main() {}".to_string())
2374 .unwrap();
2375
2376 let respawned = ServerId::from_static("rust-respawned");
2377 let untouched = ServerId::from_static("rust-diagnostics");
2378 lock_std(&tracker.documents)
2379 .get_mut(&path)
2380 .unwrap()
2381 .synced
2382 .insert(respawned.clone(), DocumentVersion::FIRST);
2383 lock_std(&tracker.documents)
2384 .get_mut(&path)
2385 .unwrap()
2386 .synced
2387 .insert(untouched.clone(), DocumentVersion::FIRST);
2388
2389 tracker.forget_server(&respawned);
2390
2391 let state = tracker.get(&path).unwrap();
2392 assert!(state.synced_version(&respawned).is_none());
2393 assert!(state.synced_version(&untouched).is_some());
2394 }
2395
2396 #[tokio::test]
2407 async fn test_sync_phase_skips_commit_when_generation_is_stale() {
2408 let dir = TempDir::new().unwrap();
2409 let path = dir.path().join("race.rs");
2410 std::fs::write(&path, "fn main() {}").unwrap();
2411 set_mtime(&path, settled_past());
2412
2413 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2414 let server = ServerId::from_static("rust");
2415 let generation_before_respawn = SyncGeneration::default();
2416
2417 tracker.forget_server(&server);
2420
2421 let (stale_client, _guard) = fake_lsp_client();
2422 let decision = tracker
2423 .disk_phase(OpenGate::Ungated(&path), Access::Use)
2424 .await
2425 .unwrap();
2426 tracker
2427 .sync_phase(
2428 &path,
2429 &server,
2430 &stale_client,
2431 decision,
2432 generation_before_respawn,
2433 )
2434 .await
2435 .unwrap();
2436
2437 let state = tracker.get(&path).unwrap();
2438 assert!(
2439 state.synced_version(&server).is_none(),
2440 "a sync_phase call that captured a stale generation must not \
2441 commit `synced`, even though its notify against the \
2442 superseded connection succeeded"
2443 );
2444 }
2445
2446 #[tokio::test]
2451 async fn test_ensure_open_commits_when_generation_is_current() {
2452 let dir = TempDir::new().unwrap();
2453 let path = dir.path().join("no_race.rs");
2454 std::fs::write(&path, "fn main() {}").unwrap();
2455
2456 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2457 let server = ServerId::from_static("rust");
2458 let (client, _guard) = fake_lsp_client();
2459
2460 tracker
2461 .ensure_open_for_test(&path, &server, &client)
2462 .await
2463 .unwrap();
2464
2465 let state = tracker.get(&path).unwrap();
2466 assert_eq!(
2467 state.synced_version(&server).map(DocumentVersion::get),
2468 Some(1)
2469 );
2470 }
2471
2472 #[tokio::test]
2475 async fn test_synced_document_count_and_forgotten_history() {
2476 let dir = TempDir::new().unwrap();
2477 let path = dir.path().join("cover.rs");
2478 std::fs::write(&path, "fn main() {}").unwrap();
2479 set_mtime(&path, settled_past());
2480
2481 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2482 let server = ServerId::from_static("rust");
2483 let other = ServerId::from_static("other");
2484 let (client, _guard) = fake_lsp_client();
2485
2486 assert_eq!(tracker.synced_document_count(&server), 0);
2487 assert!(!tracker.sync_history_forgotten(&server));
2488
2489 tracker
2490 .ensure_open_for_test(&path, &server, &client)
2491 .await
2492 .unwrap();
2493 assert_eq!(tracker.synced_document_count(&server), 1);
2494 assert_eq!(tracker.synced_document_count(&other), 0);
2495
2496 tracker.forget_server(&server);
2497 assert_eq!(tracker.synced_document_count(&server), 0);
2498 assert!(tracker.sync_history_forgotten(&server));
2499 assert!(!tracker.sync_history_forgotten(&other));
2500 }
2501
2502 fn mark_disk_verified(tracker: &DocumentTracker, path: &Path) {
2508 tracker.set_disk(
2509 path,
2510 DiskSync {
2511 mtime: None,
2512 size: 0,
2513 mtime_settled: false,
2514 content_checked_at: Instant::now(),
2515 },
2516 );
2517 }
2518
2519 #[test]
2524 fn test_document_limit_evicts_lru_instead_of_failing() {
2525 let limits = ResourceLimits {
2526 max_documents: DocumentLimit::new(2),
2527 max_file_size: SizeLimit::from_static(100),
2528 };
2529 let mut map = HashMap::new();
2530 map.insert(
2531 FileExtension::from_static("rs"),
2532 LanguageId::from_static("rust"),
2533 );
2534
2535 let tracker = DocumentTracker::new(limits, map);
2536
2537 tracker
2538 .open(PathBuf::from("/test/file1.rs"), "fn test1() {}".to_string())
2539 .unwrap();
2540 mark_disk_verified(&tracker, Path::new("/test/file1.rs"));
2541 tracker
2542 .open(PathBuf::from("/test/file2.rs"), "fn test2() {}".to_string())
2543 .unwrap();
2544 mark_disk_verified(&tracker, Path::new("/test/file2.rs"));
2545
2546 tracker
2547 .open(PathBuf::from("/test/file3.rs"), "fn test3() {}".to_string())
2548 .unwrap();
2549
2550 assert_eq!(tracker.len(), 2);
2551 assert_eq!(
2552 tracker.open_state(Path::new("/test/file1.rs")),
2553 DocumentOpenState::NotOpen
2554 );
2555 assert_eq!(
2556 tracker.open_state(Path::new("/test/file2.rs")),
2557 DocumentOpenState::Open
2558 );
2559 assert_eq!(
2560 tracker.open_state(Path::new("/test/file3.rs")),
2561 DocumentOpenState::Open
2562 );
2563
2564 assert!(
2565 tracker.pending_close_paths().is_empty(),
2566 "opened directly via `open`, never synced to any server"
2567 );
2568 }
2569
2570 #[test]
2576 fn test_document_limit_falls_back_to_error_when_only_candidate_is_locked() {
2577 let limits = ResourceLimits {
2578 max_documents: DocumentLimit::new(1),
2579 max_file_size: SizeLimit::from_static(100),
2580 };
2581 let tracker = DocumentTracker::new(limits, HashMap::new());
2582
2583 let locked_path = PathBuf::from("/test/locked.rs");
2584 tracker
2585 .open(locked_path.clone(), "fn locked() {}".to_string())
2586 .unwrap();
2587 lock_std(&tracker.path_locks).insert(locked_path.clone(), Arc::new(AsyncMutex::new(())));
2588
2589 let result = tracker.open(PathBuf::from("/test/other.rs"), "fn other() {}".to_string());
2590 assert_matches!(result, Err(Error::DocumentLimitExceeded { .. }));
2591 assert!(
2592 tracker.open_state(&locked_path) == DocumentOpenState::Open,
2593 "the locked document must not be evicted"
2594 );
2595 assert_eq!(tracker.pending_close_paths().len(), 0);
2596 }
2597
2598 fn pending_servers(tracker: &DocumentTracker, path: &Path) -> HashSet<ServerId> {
2600 lock_std(&tracker.pending_closes)
2601 .get(path)
2602 .map_or_default(|pending| pending.servers.clone())
2603 }
2604
2605 async fn ensure_open_disk_verified(
2608 tracker: &DocumentTracker,
2609 dir: &TempDir,
2610 name: &str,
2611 ) -> PathBuf {
2612 let path = dir.path().join(name);
2613 std::fs::write(&path, name).unwrap();
2614 set_mtime(&path, settled_past());
2615 let (client, _server) = fake_lsp_client();
2616 tracker
2617 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
2618 .await
2619 .unwrap();
2620 path
2621 }
2622
2623 #[cfg(unix)]
2627 #[tokio::test]
2628 async fn test_ensure_open_refuses_a_path_swapped_out_of_the_roots_after_validation() {
2629 let dir = TempDir::new().unwrap();
2630 let base = dunce::canonicalize(dir.path()).unwrap();
2631 let root = base.join("ws");
2632 std::fs::create_dir_all(root.join("sub")).unwrap();
2633 std::fs::create_dir_all(base.join("out")).unwrap();
2634 let roots = WorkspaceRoots::from_paths(&[&root]).unwrap();
2635 let file = root.join("sub/a.rs");
2636 std::fs::write(&file, "inside").unwrap();
2637 let validated = roots
2638 .validate_blocking(&crate::bridge::ClientPath::try_from(file).unwrap())
2639 .unwrap();
2640 std::fs::write(base.join("out/a.rs"), "secret").unwrap();
2641 std::fs::remove_dir_all(root.join("sub")).unwrap();
2642 std::os::unix::fs::symlink(base.join("out"), root.join("sub")).unwrap();
2643 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2644 let server = ServerId::from_static("rust");
2645 let (client, _guard) = fake_lsp_client();
2646
2647 let result = tracker
2648 .ensure_open(OpenRequest::new(
2649 &validated,
2650 &roots,
2651 &server,
2652 &client,
2653 Access::Use,
2654 ))
2655 .await;
2656
2657 assert_matches!(result, Err(Error::PathOutsideWorkspace(_)));
2658 assert_eq!(tracker.open_paths().len(), 0);
2659 }
2660
2661 #[tokio::test]
2664 async fn test_probe_of_an_untracked_path_opens_nothing() {
2665 let dir = TempDir::new().unwrap();
2666 let path = dir.path().join("a.rs");
2667 std::fs::write(&path, "fn a() {}").unwrap();
2668 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2669 let (client, _guard) = fake_lsp_client();
2670
2671 let result = tracker
2672 .ensure_open_as_for_test(
2673 &path,
2674 &ServerId::from_static("rust"),
2675 &client,
2676 Access::Probe,
2677 )
2678 .await;
2679
2680 assert_matches!(result, Err(Error::DocumentNotFound(_)));
2681 assert_eq!(tracker.open_paths().len(), 0);
2682 }
2683
2684 #[tokio::test]
2687 async fn test_probe_leaves_the_recency_order_and_use_changes_it() {
2688 let dir = TempDir::new().unwrap();
2689 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2690 let server = ServerId::from_static("rust");
2691 let (client, _guard) = fake_lsp_client();
2692 let (a, b) = (dir.path().join("a.rs"), dir.path().join("b.rs"));
2693 for path in [&a, &b] {
2694 std::fs::write(path, "fn f() {}").unwrap();
2695 tracker
2696 .ensure_open_for_test(path, &server, &client)
2697 .await
2698 .unwrap();
2699 tokio::time::sleep(Duration::from_millis(10)).await;
2700 }
2701 assert_eq!(tracker.open_paths_by_recency(), [b.clone(), a.clone()]);
2702
2703 tracker
2704 .ensure_open_as_for_test(&a, &server, &client, Access::Probe)
2705 .await
2706 .unwrap();
2707 assert_eq!(tracker.open_paths_by_recency(), [b.clone(), a.clone()]);
2708
2709 tracker
2710 .ensure_open_for_test(&a, &server, &client)
2711 .await
2712 .unwrap();
2713 assert_eq!(tracker.open_paths_by_recency(), [a, b]);
2714 }
2715
2716 #[tokio::test]
2720 async fn test_open_does_not_evict_in_flight_document_until_guard_drops() {
2721 let dir = TempDir::new().unwrap();
2722 let limits = ResourceLimits {
2723 max_documents: DocumentLimit::new(1),
2724 max_file_size: SizeLimit::UNLIMITED,
2725 };
2726 let tracker = DocumentTracker::new(limits, HashMap::new());
2727 let path_a = ensure_open_disk_verified(&tracker, &dir, "a.rs").await;
2728
2729 let guard = tracker.mark_in_flight(&path_a);
2730 let path_b = dir.path().join("b.rs");
2731 std::fs::write(&path_b, "BBBB").unwrap();
2732
2733 let result = tracker.open(path_b.clone(), "BBBB".to_string());
2734 assert_matches!(result, Err(Error::DocumentLimitExceeded { .. }));
2735 assert_eq!(tracker.open_state(&path_a), DocumentOpenState::Open);
2736 assert_eq!(tracker.pending_close_paths().len(), 0);
2737
2738 drop(guard);
2739 tracker.open(path_b.clone(), "BBBB".to_string()).unwrap();
2740 assert_eq!(tracker.open_state(&path_a), DocumentOpenState::NotOpen);
2741 assert_eq!(tracker.open_state(&path_b), DocumentOpenState::Open);
2742 assert_eq!(tracker.pending_close_paths(), vec![path_a.clone()]);
2743 assert_eq!(
2744 pending_servers(&tracker, &path_a),
2745 HashSet::from([ServerId::from_static("rust")])
2746 );
2747 }
2748
2749 #[tokio::test]
2752 async fn test_in_flight_guards_are_refcounted_per_path() {
2753 let dir = TempDir::new().unwrap();
2754 let limits = ResourceLimits {
2755 max_documents: DocumentLimit::new(1),
2756 max_file_size: SizeLimit::UNLIMITED,
2757 };
2758 let tracker = DocumentTracker::new(limits, HashMap::new());
2759 let path_a = ensure_open_disk_verified(&tracker, &dir, "a.rs").await;
2760
2761 let first = tracker.mark_in_flight(&path_a);
2762 let second = tracker.mark_in_flight(&path_a);
2763 drop(first);
2764
2765 let path_b = dir.path().join("b.rs");
2766 let result = tracker.open(path_b.clone(), "BBBB".to_string());
2767 assert_matches!(
2768 result,
2769 Err(Error::DocumentLimitExceeded { .. }),
2770 "one guard still outstanding"
2771 );
2772
2773 drop(second);
2774 assert!(lock_std(&tracker.in_flight).is_empty());
2775 tracker.open(path_b, "BBBB".to_string()).unwrap();
2776 assert_eq!(tracker.open_state(&path_a), DocumentOpenState::NotOpen);
2777 }
2778
2779 #[tokio::test]
2782 async fn test_in_flight_guard_released_when_holding_future_is_cancelled() {
2783 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2784 let path = PathBuf::from("/test/cancelled.rs");
2785
2786 let held = async {
2787 let _guard = tracker.mark_in_flight(&path);
2788 std::future::pending::<()>().await;
2789 };
2790 let timed_out = tokio::time::timeout(Duration::from_millis(10), held).await;
2791
2792 assert!(timed_out.is_err());
2793 assert_eq!(tracker.in_flight_count(&path), 0);
2794 }
2795
2796 #[test]
2799 fn test_evict_lru_skips_document_without_disk_snapshot() {
2800 let limits = ResourceLimits {
2801 max_documents: DocumentLimit::new(1),
2802 max_file_size: SizeLimit::UNLIMITED,
2803 };
2804 let tracker = DocumentTracker::new(limits, HashMap::new());
2805 let first = PathBuf::from("/test/first.rs");
2806 tracker
2807 .open(first.clone(), "fn first() {}".to_string())
2808 .unwrap();
2809
2810 let result = tracker.open(
2811 PathBuf::from("/test/second.rs"),
2812 "fn second() {}".to_string(),
2813 );
2814 assert_matches!(result, Err(Error::DocumentLimitExceeded { .. }));
2815 assert!(
2816 tracker.open_state(&first) == DocumentOpenState::Open,
2817 "the not-disk-verified document must not be evicted"
2818 );
2819 assert_eq!(tracker.pending_close_paths().len(), 0);
2820 }
2821
2822 #[tokio::test]
2827 async fn test_ensure_open_touch_changes_lru_eviction_order() {
2828 let dir = TempDir::new().unwrap();
2829 let path_a = dir.path().join("a.rs");
2830 let path_b = dir.path().join("b.rs");
2831 std::fs::write(&path_a, "AAAA").unwrap();
2832 std::fs::write(&path_b, "BBBB").unwrap();
2833 set_mtime(&path_a, settled_past());
2834 set_mtime(&path_b, settled_past());
2835
2836 let limits = ResourceLimits {
2837 max_documents: DocumentLimit::new(2),
2838 max_file_size: SizeLimit::UNLIMITED,
2839 };
2840 let (client, _server) = fake_lsp_client();
2841 let tracker = DocumentTracker::new(limits, HashMap::new());
2842 let server_id = ServerId::from_static("rust");
2843
2844 tracker
2845 .ensure_open_for_test(&path_a, &server_id, &client)
2846 .await
2847 .unwrap();
2848 tracker
2849 .ensure_open_for_test(&path_b, &server_id, &client)
2850 .await
2851 .unwrap();
2852
2853 tracker
2856 .ensure_open_for_test(&path_a, &server_id, &client)
2857 .await
2858 .unwrap();
2859
2860 let path_c = dir.path().join("c.rs");
2861 std::fs::write(&path_c, "CCCC").unwrap();
2862 set_mtime(&path_c, settled_past());
2863 tracker
2864 .ensure_open_for_test(&path_c, &server_id, &client)
2865 .await
2866 .unwrap();
2867
2868 assert!(
2869 tracker.open_state(&path_a) == DocumentOpenState::Open,
2870 "recently re-accessed, must survive"
2871 );
2872 assert!(
2873 tracker.open_state(&path_b) == DocumentOpenState::NotOpen,
2874 "least-recently-used, must be evicted"
2875 );
2876 assert_eq!(tracker.open_state(&path_c), DocumentOpenState::Open);
2877
2878 assert_eq!(tracker.pending_close_paths(), vec![path_b.clone()]);
2879 assert_eq!(
2880 pending_servers(&tracker, &path_b),
2881 HashSet::from([server_id])
2882 );
2883 }
2884
2885 #[test]
2886 fn test_file_size_limit() {
2887 let limits = ResourceLimits {
2888 max_documents: DocumentLimit::new(10),
2889 max_file_size: SizeLimit::from_static(10),
2890 };
2891 let mut map = HashMap::new();
2892 map.insert(
2893 FileExtension::from_static("rs"),
2894 LanguageId::from_static("rust"),
2895 );
2896
2897 let tracker = DocumentTracker::new(limits, map);
2898
2899 tracker
2901 .open(PathBuf::from("/test/small.rs"), "fn f(){}".to_string())
2902 .unwrap();
2903
2904 let large_content = "x".repeat(100);
2906 let result = tracker.open(PathBuf::from("/test/large.rs"), large_content);
2907 assert_matches!(result, Err(Error::FileSizeLimitExceeded { .. }));
2908 }
2909
2910 #[test]
2911 fn test_resource_limits_default() {
2912 let limits = ResourceLimits::default();
2913 assert_eq!(limits.max_documents, DocumentLimit::new(100));
2914 assert_eq!(
2915 limits.max_file_size,
2916 SizeLimit::from_static(10 * 1024 * 1024)
2917 );
2918 }
2919
2920 #[test]
2921 fn test_resource_limits_custom() {
2922 let limits = ResourceLimits {
2923 max_documents: DocumentLimit::new(50),
2924 max_file_size: SizeLimit::from_static(5 * 1024 * 1024),
2925 };
2926 assert_eq!(limits.max_documents, DocumentLimit::new(50));
2927 assert_eq!(
2928 limits.max_file_size,
2929 SizeLimit::from_static(5 * 1024 * 1024)
2930 );
2931 }
2932
2933 #[test]
2934 fn test_resource_limits_zero_unlimited() {
2935 let limits = ResourceLimits {
2936 max_documents: DocumentLimit::UNLIMITED,
2937 max_file_size: SizeLimit::UNLIMITED,
2938 };
2939 let mut map = HashMap::new();
2940 map.insert(
2941 FileExtension::from_static("rs"),
2942 LanguageId::from_static("rust"),
2943 );
2944
2945 let tracker = DocumentTracker::new(limits, map);
2946
2947 for i in 0..200 {
2949 tracker
2950 .open(
2951 PathBuf::from(format!("/test/file{i}.rs")),
2952 "content".to_string(),
2953 )
2954 .unwrap();
2955 }
2956 assert_eq!(tracker.len(), 200);
2957
2958 let huge_content = "x".repeat(100_000_000);
2960 tracker
2961 .open(PathBuf::from("/test/huge.rs"), huge_content)
2962 .unwrap();
2963 }
2964
2965 #[test]
2966 fn test_document_state_clone() {
2967 let state = DocumentState {
2968 uri: Uri::from("file:///test.rs"),
2969 language_id: LanguageId::from_static("rust"),
2970 version: DocumentVersion::new(5),
2971 text: DocumentText::new("fn main() {}".to_string()),
2972 disk: None,
2973 synced: HashMap::new(),
2974 opened: Opening(0),
2975 last_accessed: Instant::now(),
2976 };
2977
2978 #[allow(clippy::redundant_clone, reason = "the test exercises `Clone` itself")]
2979 let cloned = state.clone();
2980 assert_eq!(cloned.uri(), state.uri());
2981 assert_eq!(cloned.language_id(), state.language_id());
2982 assert_eq!(cloned.version().get(), 5);
2983 assert_eq!(cloned.content(), state.content());
2984 }
2985
2986 #[test]
2987 fn test_line_presence_distinguishes_untracked_present_and_beyond() {
2988 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
2989 let path = PathBuf::from("/test/lines.rs");
2990 assert_eq!(tracker.line_presence(&path, 0), LinePresence::Untracked);
2991
2992 tracker.open(path.clone(), "a\r\nb\n".to_string()).unwrap();
2993 assert_eq!(tracker.line_presence(&path, 0), LinePresence::Present);
2994 assert_eq!(tracker.line_presence(&path, 1), LinePresence::Present);
2995 assert_eq!(tracker.line_presence(&path, 2), LinePresence::Present);
2996 let beyond = LinePresence::Beyond {
2997 last_line: NonZeroU32::new(3).unwrap(),
2998 };
2999 assert_eq!(tracker.line_presence(&path, 3), beyond);
3000 assert_eq!(tracker.line_presence(&path, u32::MAX), beyond);
3001
3002 let empty = PathBuf::from("/test/empty.rs");
3003 tracker.open(empty.clone(), String::new()).unwrap();
3004 assert_eq!(tracker.line_presence(&empty, 0), LinePresence::Present);
3005 assert_eq!(
3006 tracker.line_presence(&empty, 1),
3007 LinePresence::Beyond {
3008 last_line: NonZeroU32::MIN
3009 }
3010 );
3011 }
3012
3013 #[test]
3014 fn test_close_nonexistent_document() {
3015 let map = HashMap::new();
3016 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3017 let path = PathBuf::from("/test/nonexistent.rs");
3018
3019 let state = tracker.close(&path);
3020 assert_eq!(
3021 state, None,
3022 "Closing non-existent document should return None"
3023 );
3024 }
3025
3026 #[test]
3027 fn test_get_nonexistent_document() {
3028 let map = HashMap::new();
3029 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3030 let path = PathBuf::from("/test/nonexistent.rs");
3031
3032 let state = tracker.get(&path);
3033 assert!(
3034 state.is_none(),
3035 "Getting non-existent document should return None"
3036 );
3037 }
3038
3039 #[test]
3040 #[allow(
3041 clippy::too_many_lines,
3042 reason = "a table of extension mappings; splitting would scatter it"
3043 )]
3044 fn test_detect_language_all_extensions() {
3045 let mut map = HashMap::new();
3046 map.insert(
3047 FileExtension::from_static("rs"),
3048 LanguageId::from_static("rust"),
3049 );
3050 map.insert(
3051 FileExtension::from_static("py"),
3052 LanguageId::from_static("python"),
3053 );
3054 map.insert(
3055 FileExtension::from_static("pyw"),
3056 LanguageId::from_static("python"),
3057 );
3058 map.insert(
3059 FileExtension::from_static("pyi"),
3060 LanguageId::from_static("python"),
3061 );
3062 map.insert(
3063 FileExtension::from_static("js"),
3064 LanguageId::from_static("javascript"),
3065 );
3066 map.insert(
3067 FileExtension::from_static("mjs"),
3068 LanguageId::from_static("javascript"),
3069 );
3070 map.insert(
3071 FileExtension::from_static("cjs"),
3072 LanguageId::from_static("javascript"),
3073 );
3074 map.insert(
3075 FileExtension::from_static("ts"),
3076 LanguageId::from_static("typescript"),
3077 );
3078 map.insert(
3079 FileExtension::from_static("mts"),
3080 LanguageId::from_static("typescript"),
3081 );
3082 map.insert(
3083 FileExtension::from_static("cts"),
3084 LanguageId::from_static("typescript"),
3085 );
3086 map.insert(
3087 FileExtension::from_static("tsx"),
3088 LanguageId::from_static("typescriptreact"),
3089 );
3090 map.insert(
3091 FileExtension::from_static("jsx"),
3092 LanguageId::from_static("javascriptreact"),
3093 );
3094 map.insert(
3095 FileExtension::from_static("go"),
3096 LanguageId::from_static("go"),
3097 );
3098 map.insert(
3099 FileExtension::from_static("c"),
3100 LanguageId::from_static("c"),
3101 );
3102 map.insert(
3103 FileExtension::from_static("h"),
3104 LanguageId::from_static("c"),
3105 );
3106 map.insert(
3107 FileExtension::from_static("cpp"),
3108 LanguageId::from_static("cpp"),
3109 );
3110 map.insert(
3111 FileExtension::from_static("cc"),
3112 LanguageId::from_static("cpp"),
3113 );
3114 map.insert(
3115 FileExtension::from_static("cxx"),
3116 LanguageId::from_static("cpp"),
3117 );
3118 map.insert(
3119 FileExtension::from_static("hpp"),
3120 LanguageId::from_static("cpp"),
3121 );
3122 map.insert(
3123 FileExtension::from_static("hh"),
3124 LanguageId::from_static("cpp"),
3125 );
3126 map.insert(
3127 FileExtension::from_static("hxx"),
3128 LanguageId::from_static("cpp"),
3129 );
3130 map.insert(
3131 FileExtension::from_static("java"),
3132 LanguageId::from_static("java"),
3133 );
3134 map.insert(
3135 FileExtension::from_static("rb"),
3136 LanguageId::from_static("ruby"),
3137 );
3138 map.insert(
3139 FileExtension::from_static("php"),
3140 LanguageId::from_static("php"),
3141 );
3142 map.insert(
3143 FileExtension::from_static("swift"),
3144 LanguageId::from_static("swift"),
3145 );
3146 map.insert(
3147 FileExtension::from_static("kt"),
3148 LanguageId::from_static("kotlin"),
3149 );
3150 map.insert(
3151 FileExtension::from_static("kts"),
3152 LanguageId::from_static("kotlin"),
3153 );
3154 map.insert(
3155 FileExtension::from_static("scala"),
3156 LanguageId::from_static("scala"),
3157 );
3158 map.insert(
3159 FileExtension::from_static("sc"),
3160 LanguageId::from_static("scala"),
3161 );
3162 map.insert(
3163 FileExtension::from_static("zig"),
3164 LanguageId::from_static("zig"),
3165 );
3166 map.insert(
3167 FileExtension::from_static("lua"),
3168 LanguageId::from_static("lua"),
3169 );
3170 map.insert(
3171 FileExtension::from_static("sh"),
3172 LanguageId::from_static("shellscript"),
3173 );
3174 map.insert(
3175 FileExtension::from_static("bash"),
3176 LanguageId::from_static("shellscript"),
3177 );
3178 map.insert(
3179 FileExtension::from_static("zsh"),
3180 LanguageId::from_static("shellscript"),
3181 );
3182 map.insert(
3183 FileExtension::from_static("json"),
3184 LanguageId::from_static("json"),
3185 );
3186 map.insert(
3187 FileExtension::from_static("toml"),
3188 LanguageId::from_static("toml"),
3189 );
3190 map.insert(
3191 FileExtension::from_static("yaml"),
3192 LanguageId::from_static("yaml"),
3193 );
3194 map.insert(
3195 FileExtension::from_static("yml"),
3196 LanguageId::from_static("yaml"),
3197 );
3198 map.insert(
3199 FileExtension::from_static("xml"),
3200 LanguageId::from_static("xml"),
3201 );
3202 map.insert(
3203 FileExtension::from_static("html"),
3204 LanguageId::from_static("html"),
3205 );
3206 map.insert(
3207 FileExtension::from_static("htm"),
3208 LanguageId::from_static("html"),
3209 );
3210 map.insert(
3211 FileExtension::from_static("css"),
3212 LanguageId::from_static("css"),
3213 );
3214 map.insert(
3215 FileExtension::from_static("scss"),
3216 LanguageId::from_static("scss"),
3217 );
3218 map.insert(
3219 FileExtension::from_static("less"),
3220 LanguageId::from_static("less"),
3221 );
3222 map.insert(
3223 FileExtension::from_static("md"),
3224 LanguageId::from_static("markdown"),
3225 );
3226 map.insert(
3227 FileExtension::from_static("markdown"),
3228 LanguageId::from_static("markdown"),
3229 );
3230
3231 assert_eq!(detect_language(Path::new("main.rs"), &map), "rust");
3232 assert_eq!(detect_language(Path::new("script.py"), &map), "python");
3233 assert_eq!(detect_language(Path::new("script.pyw"), &map), "python");
3234 assert_eq!(detect_language(Path::new("script.pyi"), &map), "python");
3235 assert_eq!(detect_language(Path::new("app.js"), &map), "javascript");
3236 assert_eq!(detect_language(Path::new("app.mjs"), &map), "javascript");
3237 assert_eq!(detect_language(Path::new("app.cjs"), &map), "javascript");
3238 assert_eq!(detect_language(Path::new("app.ts"), &map), "typescript");
3239 assert_eq!(detect_language(Path::new("app.mts"), &map), "typescript");
3240 assert_eq!(detect_language(Path::new("app.cts"), &map), "typescript");
3241 assert_eq!(
3242 detect_language(Path::new("component.tsx"), &map),
3243 "typescriptreact"
3244 );
3245 assert_eq!(
3246 detect_language(Path::new("component.jsx"), &map),
3247 "javascriptreact"
3248 );
3249 assert_eq!(detect_language(Path::new("main.go"), &map), "go");
3250 assert_eq!(detect_language(Path::new("main.c"), &map), "c");
3251 assert_eq!(detect_language(Path::new("header.h"), &map), "c");
3252 assert_eq!(detect_language(Path::new("main.cpp"), &map), "cpp");
3253 assert_eq!(detect_language(Path::new("main.cc"), &map), "cpp");
3254 assert_eq!(detect_language(Path::new("main.cxx"), &map), "cpp");
3255 assert_eq!(detect_language(Path::new("header.hpp"), &map), "cpp");
3256 assert_eq!(detect_language(Path::new("header.hh"), &map), "cpp");
3257 assert_eq!(detect_language(Path::new("header.hxx"), &map), "cpp");
3258 assert_eq!(detect_language(Path::new("Main.java"), &map), "java");
3259 assert_eq!(detect_language(Path::new("script.rb"), &map), "ruby");
3260 assert_eq!(detect_language(Path::new("index.php"), &map), "php");
3261 assert_eq!(detect_language(Path::new("App.swift"), &map), "swift");
3262 assert_eq!(detect_language(Path::new("Main.kt"), &map), "kotlin");
3263 assert_eq!(detect_language(Path::new("script.kts"), &map), "kotlin");
3264 assert_eq!(detect_language(Path::new("Main.scala"), &map), "scala");
3265 assert_eq!(detect_language(Path::new("script.sc"), &map), "scala");
3266 assert_eq!(detect_language(Path::new("main.zig"), &map), "zig");
3267 assert_eq!(detect_language(Path::new("script.lua"), &map), "lua");
3268 assert_eq!(detect_language(Path::new("script.sh"), &map), "shellscript");
3269 assert_eq!(
3270 detect_language(Path::new("script.bash"), &map),
3271 "shellscript"
3272 );
3273 assert_eq!(
3274 detect_language(Path::new("script.zsh"), &map),
3275 "shellscript"
3276 );
3277 assert_eq!(detect_language(Path::new("data.json"), &map), "json");
3278 assert_eq!(detect_language(Path::new("config.toml"), &map), "toml");
3279 assert_eq!(detect_language(Path::new("config.yaml"), &map), "yaml");
3280 assert_eq!(detect_language(Path::new("config.yml"), &map), "yaml");
3281 assert_eq!(detect_language(Path::new("data.xml"), &map), "xml");
3282 assert_eq!(detect_language(Path::new("index.html"), &map), "html");
3283 assert_eq!(detect_language(Path::new("index.htm"), &map), "html");
3284 assert_eq!(detect_language(Path::new("styles.css"), &map), "css");
3285 assert_eq!(detect_language(Path::new("styles.scss"), &map), "scss");
3286 assert_eq!(detect_language(Path::new("styles.less"), &map), "less");
3287 assert_eq!(detect_language(Path::new("README.md"), &map), "markdown");
3288 assert_eq!(
3289 detect_language(Path::new("README.markdown"), &map),
3290 "markdown"
3291 );
3292 assert_eq!(detect_language(Path::new("unknown.xyz"), &map), "plaintext");
3293 assert_eq!(
3294 detect_language(Path::new("no_extension"), &map),
3295 "plaintext"
3296 );
3297 }
3298
3299 #[test]
3300 fn test_path_to_uri_unix() {
3301 #[cfg(not(windows))]
3302 {
3303 let path = Path::new("/home/user/project/main.rs");
3304 let uri = path_to_uri(path).unwrap();
3305 assert!(
3306 uri.as_ref()
3307 .starts_with("file:///home/user/project/main.rs")
3308 );
3309 }
3310 }
3311
3312 #[test]
3313 fn test_path_to_uri_with_special_chars() {
3314 let path = Path::new("/home/user/project-test/main.rs");
3315 let uri = path_to_uri(path).unwrap();
3316 assert!(uri.as_ref().starts_with("file://"));
3317 assert!(uri.as_ref().contains("project-test"));
3318 }
3319
3320 #[test]
3321 fn test_path_to_uri_percent_encodes_reserved_chars() {
3322 #[cfg(windows)]
3323 let path = Path::new(r"C:\home\user\routes\api\[...]^|.ts");
3324 #[cfg(not(windows))]
3325 let path = Path::new("/home/user/routes/api/[...]^|.ts");
3326
3327 let uri = path_to_uri(path).unwrap();
3328
3329 #[cfg(windows)]
3330 let expected = "file:///C:/home/user/routes/api/%5B...%5D%5E%7C.ts";
3331 #[cfg(not(windows))]
3332 let expected = "file:///home/user/routes/api/%5B...%5D%5E%7C.ts";
3333
3334 assert_eq!(uri.as_ref(), expected);
3335 assert_eq!(
3336 uri_to_path(&uri).as_deref(),
3337 Some(path),
3338 "encoded file URI should round-trip to the original path"
3339 );
3340 }
3341
3342 #[test]
3343 fn test_try_path_to_uri_returns_none_for_relative_path() {
3344 assert_eq!(try_path_to_uri(Path::new("relative/file.ts")), None);
3345 }
3346
3347 #[test]
3351 fn test_path_to_uri_returns_err_for_relative_path() {
3352 let err = path_to_uri(Path::new("relative/file.ts")).unwrap_err();
3353 assert_matches!(err, Error::PathToUri(_));
3354 }
3355
3356 #[cfg(windows)]
3357 #[test]
3358 fn test_try_path_to_uri_encodes_synthetic_windows_root() {
3359 let uri = try_path_to_uri(Path::new("/home/user/#work %23")).unwrap();
3360
3361 assert_eq!(uri.as_ref(), "file:///home/user/%23work%20%2523");
3362 }
3363
3364 #[cfg(windows)]
3372 #[test]
3373 fn test_try_path_to_uri_accepts_rooted_but_not_absolute_windows_path() {
3374 let path = Path::new(r"\foo");
3375 assert!(path.has_root());
3376 assert!(!path.is_absolute());
3377
3378 let uri = try_path_to_uri(path).unwrap();
3379
3380 assert_eq!(uri.as_ref(), "file:///foo");
3381 }
3382
3383 #[test]
3384 fn test_path_to_uri_percent_encodes_reserved_chars_in_short_path() {
3385 #[cfg(windows)]
3387 let path = Path::new(r"C:\[a].ts");
3388 #[cfg(not(windows))]
3389 let path = Path::new("/[a].ts");
3390
3391 let uri = path_to_uri(path).unwrap();
3392
3393 assert!(
3394 uri.as_ref().ends_with("%5Ba%5D.ts"),
3395 "short path should percent-encode reserved chars, got {}",
3396 uri.as_ref()
3397 );
3398 assert_eq!(uri_to_path(&uri).as_deref(), Some(path));
3399 }
3400
3401 #[test]
3402 fn test_path_to_uri_percent_encodes_all_rfc3986_other_reserved_chars() {
3403 #[cfg(windows)]
3407 let path = Path::new(r"C:\home\user\test[]^|{}`.ts");
3408 #[cfg(not(windows))]
3409 let path = Path::new("/home/user/test[]^|{}`.ts");
3410
3411 let uri = try_path_to_uri(path).unwrap();
3412 let uri_str = uri.as_ref();
3413
3414 for (raw, encoded) in [
3415 ('[', "%5B"),
3416 (']', "%5D"),
3417 ('^', "%5E"),
3418 ('|', "%7C"),
3419 ('{', "%7B"),
3420 ('}', "%7D"),
3421 ('`', "%60"),
3422 ] {
3423 assert!(
3424 uri_str.contains(encoded),
3425 "expected {raw:?} to be percent-encoded as {encoded} in {uri_str}"
3426 );
3427 }
3428 assert!(
3429 !uri_str.contains(['[', ']', '^', '|', '{', '}', '`']),
3430 "no raw reserved characters should remain in {uri_str}"
3431 );
3432 }
3433
3434 #[tokio::test]
3435 async fn test_document_tracker_concurrent_operations() {
3436 let mut map = HashMap::new();
3437 map.insert(
3438 FileExtension::from_static("rs"),
3439 LanguageId::from_static("rust"),
3440 );
3441
3442 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3443 let path1 = PathBuf::from("/test/file1.rs");
3444 let path2 = PathBuf::from("/test/file2.rs");
3445
3446 tracker.open(path1.clone(), "content1".to_string()).unwrap();
3447 tracker.open(path2.clone(), "content2".to_string()).unwrap();
3448
3449 assert_eq!(tracker.len(), 2);
3450 assert_eq!(tracker.open_state(&path1), DocumentOpenState::Open);
3451 assert_eq!(tracker.open_state(&path2), DocumentOpenState::Open);
3452
3453 assert_eq!(tracker.get(&path1).unwrap().content(), "content1");
3454 assert_eq!(tracker.get(&path2).unwrap().content(), "content2");
3455
3456 tracker.close(&path1);
3457 assert_eq!(tracker.len(), 1);
3458 assert_eq!(tracker.open_state(&path1), DocumentOpenState::NotOpen);
3459 assert_eq!(tracker.open_state(&path2), DocumentOpenState::Open);
3460 }
3461
3462 #[test]
3463 fn test_empty_content() {
3464 let mut map = HashMap::new();
3465 map.insert(
3466 FileExtension::from_static("rs"),
3467 LanguageId::from_static("rust"),
3468 );
3469
3470 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3471 let path = PathBuf::from("/test/empty.rs");
3472
3473 tracker.open(path.clone(), String::new()).unwrap();
3474 assert_eq!(tracker.open_state(&path), DocumentOpenState::Open);
3475 assert_eq!(tracker.get(&path).unwrap().content(), "");
3476 }
3477
3478 #[test]
3479 fn test_unicode_content() {
3480 let mut map = HashMap::new();
3481 map.insert(
3482 FileExtension::from_static("rs"),
3483 LanguageId::from_static("rust"),
3484 );
3485
3486 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3487 let path = PathBuf::from("/test/unicode.rs");
3488 let content = "fn テスト() { println!(\"こんにちは\"); }";
3489
3490 tracker.open(path.clone(), content.to_string()).unwrap();
3491 assert_eq!(tracker.get(&path).unwrap().content(), content);
3492 }
3493
3494 #[test]
3498 fn test_document_limit_exact_boundary() {
3499 let limits = ResourceLimits {
3500 max_documents: DocumentLimit::new(5),
3501 max_file_size: SizeLimit::from_static(1000),
3502 };
3503 let mut map = HashMap::new();
3504 map.insert(
3505 FileExtension::from_static("rs"),
3506 LanguageId::from_static("rust"),
3507 );
3508
3509 let tracker = DocumentTracker::new(limits, map);
3510
3511 for i in 0..5 {
3512 let path = PathBuf::from(format!("/test/file{i}.rs"));
3513 tracker.open(path.clone(), "content".to_string()).unwrap();
3514 mark_disk_verified(&tracker, &path);
3515 }
3516
3517 assert_eq!(tracker.len(), 5);
3518
3519 tracker
3520 .open(PathBuf::from("/test/file6.rs"), "content".to_string())
3521 .unwrap();
3522
3523 assert_eq!(tracker.len(), 5);
3524 assert_eq!(
3525 tracker.open_state(Path::new("/test/file0.rs")),
3526 DocumentOpenState::NotOpen
3527 );
3528 assert_eq!(
3529 tracker.open_state(Path::new("/test/file6.rs")),
3530 DocumentOpenState::Open
3531 );
3532 }
3533
3534 #[test]
3535 fn test_file_size_exact_boundary() {
3536 let limits = ResourceLimits {
3537 max_documents: DocumentLimit::new(10),
3538 max_file_size: SizeLimit::from_static(100),
3539 };
3540 let mut map = HashMap::new();
3541 map.insert(
3542 FileExtension::from_static("rs"),
3543 LanguageId::from_static("rust"),
3544 );
3545
3546 let tracker = DocumentTracker::new(limits, map);
3547
3548 let exact_size_content = "x".repeat(100);
3549 tracker
3550 .open(PathBuf::from("/test/exact.rs"), exact_size_content)
3551 .unwrap();
3552
3553 let over_size_content = "x".repeat(101);
3554 let result = tracker.open(PathBuf::from("/test/over.rs"), over_size_content);
3555 assert_matches!(result, Err(Error::FileSizeLimitExceeded { .. }));
3556 }
3557
3558 #[test]
3559 fn test_detect_language_with_custom_extension() {
3560 let mut map = HashMap::new();
3561 map.insert(
3562 FileExtension::from_static("nu"),
3563 LanguageId::from_static("nushell"),
3564 );
3565
3566 assert_eq!(detect_language(Path::new("script.nu"), &map), "nushell");
3567
3568 let empty_map = HashMap::new();
3569 assert_eq!(
3570 detect_language(Path::new("script.nu"), &empty_map),
3571 "plaintext"
3572 );
3573 }
3574
3575 #[test]
3576 fn test_detect_language_custom_overrides_default() {
3577 let mut custom_map = HashMap::new();
3578 custom_map.insert(
3579 FileExtension::from_static("rs"),
3580 LanguageId::from_static("custom-rust"),
3581 );
3582
3583 assert_eq!(
3584 detect_language(Path::new("main.rs"), &custom_map),
3585 "custom-rust"
3586 );
3587
3588 let mut default_map = HashMap::new();
3589 default_map.insert(
3590 FileExtension::from_static("rs"),
3591 LanguageId::from_static("rust"),
3592 );
3593
3594 assert_eq!(detect_language(Path::new("main.rs"), &default_map), "rust");
3595 }
3596
3597 #[test]
3598 fn test_detect_language_fallback_to_plaintext() {
3599 let mut map = HashMap::new();
3600 map.insert(
3601 FileExtension::from_static("nu"),
3602 LanguageId::from_static("nushell"),
3603 );
3604
3605 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
3607 }
3608
3609 #[test]
3610 fn test_detect_language_empty_map() {
3611 let map = HashMap::new();
3612 assert_eq!(detect_language(Path::new("main.rs"), &map), "plaintext");
3613 }
3614
3615 #[test]
3616 fn test_document_tracker_with_extensions() {
3617 let mut map = HashMap::new();
3618 map.insert(
3619 FileExtension::from_static("nu"),
3620 LanguageId::from_static("nushell"),
3621 );
3622
3623 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3624
3625 let path = PathBuf::from("/test/script.nu");
3626 tracker
3627 .open(path.clone(), "# nushell script".to_string())
3628 .unwrap();
3629
3630 let state = tracker.get(&path).unwrap();
3631 assert_eq!(state.language_id(), "nushell");
3632 }
3633
3634 #[test]
3635 fn test_document_tracker_uses_provided_map() {
3636 let mut map = HashMap::new();
3637 map.insert(
3638 FileExtension::from_static("rs"),
3639 LanguageId::from_static("rust"),
3640 );
3641
3642 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3643 let path = PathBuf::from("/test/main.rs");
3644 tracker
3645 .open(path.clone(), "fn main() {}".to_string())
3646 .unwrap();
3647
3648 let state = tracker.get(&path).unwrap();
3649 assert_eq!(state.language_id(), "rust");
3650 }
3651
3652 #[test]
3653 fn test_multiple_extensions_same_language() {
3654 let mut map = HashMap::new();
3655 map.insert(
3656 FileExtension::from_static("cpp"),
3657 LanguageId::from_static("c++"),
3658 );
3659 map.insert(
3660 FileExtension::from_static("cc"),
3661 LanguageId::from_static("c++"),
3662 );
3663 map.insert(
3664 FileExtension::from_static("cxx"),
3665 LanguageId::from_static("c++"),
3666 );
3667
3668 assert_eq!(detect_language(Path::new("main.cpp"), &map), "c++");
3669 assert_eq!(detect_language(Path::new("main.cc"), &map), "c++");
3670 assert_eq!(detect_language(Path::new("main.cxx"), &map), "c++");
3671 }
3672
3673 #[test]
3674 fn test_case_sensitive_extensions() {
3675 let mut map = HashMap::new();
3676 map.insert(
3677 FileExtension::from_static("NU"),
3678 LanguageId::from_static("nushell"),
3679 );
3680
3681 assert_eq!(detect_language(Path::new("script.nu"), &map), "plaintext");
3683 }
3684
3685 #[cfg(unix)]
3690 #[test]
3691 fn test_uri_to_path_file_scheme() {
3692 let uri: Uri = Uri::from("file:///home/user/main.rs");
3693 let path = uri_to_path(&uri).unwrap();
3694 assert_eq!(path, PathBuf::from("/home/user/main.rs"));
3695 }
3696
3697 #[test]
3698 fn test_uri_to_path_non_file_scheme_returns_none() {
3699 let uri: Uri = Uri::from("https://example.com/file.rs");
3700 assert!(uri_to_path(&uri).is_none());
3701 }
3702
3703 #[test]
3704 fn test_uri_to_path_lsp_diagnostics_scheme_returns_none() {
3705 let uri: Uri = Uri::from("lsp-diagnostics:///home/user/main.rs");
3707 assert!(uri_to_path(&uri).is_none());
3708 }
3709
3710 #[test]
3711 fn test_uri_to_path_with_authority_returns_none() {
3712 let result = uri_to_path(&Uri::from("file://server/share/path.rs"));
3716 assert!(result.is_none());
3717 }
3718
3719 #[test]
3724 fn test_open_paths_empty_tracker() {
3725 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3726 assert_eq!(tracker.open_paths().len(), 0);
3727 }
3728
3729 #[test]
3730 fn test_open_paths_populated_tracker() {
3731 let mut map = HashMap::new();
3732 map.insert(
3733 FileExtension::from_static("rs"),
3734 LanguageId::from_static("rust"),
3735 );
3736 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3737 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
3738 tracker.open(PathBuf::from("/b.rs"), String::new()).unwrap();
3739 let mut paths = tracker.open_paths();
3740 paths.sort();
3741 assert_eq!(paths, [PathBuf::from("/a.rs"), PathBuf::from("/b.rs")]);
3742 }
3743
3744 #[test]
3745 fn test_open_paths_after_close() {
3746 let mut map = HashMap::new();
3747 map.insert(
3748 FileExtension::from_static("rs"),
3749 LanguageId::from_static("rust"),
3750 );
3751 let tracker = DocumentTracker::new(ResourceLimits::default(), map);
3752 tracker.open(PathBuf::from("/a.rs"), String::new()).unwrap();
3753 tracker.close(Path::new("/a.rs"));
3754 assert_eq!(tracker.open_paths().len(), 0);
3755 }
3756
3757 use tempfile::TempDir;
3762 use tokio::io::BufReader;
3763
3764 use crate::test_lsp::{fake_lsp_client, read_framed_message};
3765
3766 fn set_mtime(path: &Path, time: SystemTime) {
3774 let file = std::fs::OpenOptions::new().write(true).open(path).unwrap();
3775 file.set_modified(time).unwrap();
3776 }
3777
3778 fn settled_past() -> SystemTime {
3779 SystemTime::now() - Duration::from_secs(10)
3780 }
3781
3782 #[test]
3783 fn test_mtime_settled_boundary() {
3784 let read_at = SystemTime::now();
3785 assert!(!mtime_settled(None, read_at), "no mtime is never settled");
3786 assert!(
3787 mtime_settled(Some(read_at - Duration::from_secs(3)), read_at),
3788 "3s older than read_at is past the 2s granularity margin"
3789 );
3790 assert!(
3791 !mtime_settled(Some(read_at - Duration::from_secs(1)), read_at),
3792 "1s older than read_at is within the 2s granularity margin"
3793 );
3794 assert!(
3795 !mtime_settled(Some(read_at + Duration::from_secs(10)), read_at),
3796 "an mtime after read_at is never settled"
3797 );
3798 }
3799
3800 #[tokio::test]
3801 async fn test_ensure_open_unchanged_file_is_fast_path() {
3802 let dir = TempDir::new().unwrap();
3803 let path = dir.path().join("a.rs");
3804 std::fs::write(&path, "fn main() {}").unwrap();
3805 set_mtime(&path, settled_past());
3806
3807 let (client, _server) = fake_lsp_client();
3808 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3809
3810 let uri1 = tracker
3811 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3812 .await
3813 .unwrap();
3814 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
3815
3816 let uri2 = tracker
3817 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3818 .await
3819 .unwrap();
3820 assert_eq!(uri1, uri2);
3821 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
3822 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
3823 }
3824
3825 #[tokio::test]
3826 async fn test_ensure_open_resyncs_on_size_change() {
3827 let dir = TempDir::new().unwrap();
3828 let path = dir.path().join("a.rs");
3829 std::fs::write(&path, "fn main() {}").unwrap();
3830 set_mtime(&path, settled_past());
3831
3832 let (client, _server) = fake_lsp_client();
3833 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3834 tracker
3835 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3836 .await
3837 .unwrap();
3838
3839 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
3840 set_mtime(&path, settled_past());
3841
3842 tracker
3843 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3844 .await
3845 .unwrap();
3846 let state = tracker.get(&path).unwrap();
3847 assert_eq!(state.version().get(), 2);
3848 assert_eq!(state.content(), "fn main() { println!(\"hi\"); }");
3849 }
3850
3851 #[tokio::test(start_paused = true)]
3852 async fn test_ensure_open_regression_102_103_racy_same_size_rewrite() {
3853 let dir = TempDir::new().unwrap();
3854 let path = dir.path().join("a.rs");
3855 std::fs::write(&path, "AAAA").unwrap();
3856 let (client, _server) = fake_lsp_client();
3859 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3860 tracker
3861 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3862 .await
3863 .unwrap();
3864 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
3865
3866 std::fs::write(&path, "BBBB").unwrap();
3869 set_mtime(&path, original_mtime);
3870
3871 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
3872
3873 tracker
3874 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3875 .await
3876 .unwrap();
3877 let state = tracker.get(&path).unwrap();
3878 assert_eq!(
3879 state.version().get(),
3880 2,
3881 "must resync despite identical (mtime, size)"
3882 );
3883 assert_eq!(state.content(), "BBBB");
3884 }
3885
3886 #[tokio::test(start_paused = true)]
3887 async fn test_ensure_open_regression_102_103_settled_mtime_is_the_documented_limit() {
3888 let dir = TempDir::new().unwrap();
3889 let path = dir.path().join("a.rs");
3890 std::fs::write(&path, "AAAA").unwrap();
3891 set_mtime(&path, settled_past());
3892
3893 let (client, _server) = fake_lsp_client();
3894 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3895 tracker
3896 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3897 .await
3898 .unwrap();
3899 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
3900
3901 std::fs::write(&path, "BBBB").unwrap();
3905 set_mtime(&path, original_mtime);
3906
3907 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
3908
3909 tracker
3910 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3911 .await
3912 .unwrap();
3913 let state = tracker.get(&path).unwrap();
3914 assert_eq!(
3915 state.version().get(),
3916 1,
3917 "documented limitation: fast path taken"
3918 );
3919 assert_eq!(state.content(), "AAAA");
3920 }
3921
3922 #[tokio::test(start_paused = true)]
3923 async fn test_ensure_open_stat_is_never_debounced() {
3924 let dir = TempDir::new().unwrap();
3925 let path = dir.path().join("a.rs");
3926 std::fs::write(&path, "AAAA").unwrap();
3927 set_mtime(&path, settled_past());
3928
3929 let (client, _server) = fake_lsp_client();
3930 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3931 tracker
3932 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3933 .await
3934 .unwrap();
3935
3936 std::fs::write(&path, "BBBBBBBB").unwrap();
3939 tracker
3940 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3941 .await
3942 .unwrap();
3943
3944 let state = tracker.get(&path).unwrap();
3945 assert_eq!(state.version().get(), 2);
3946 assert_eq!(state.content(), "BBBBBBBB");
3947 }
3948
3949 #[tokio::test(start_paused = true)]
3950 async fn test_ensure_open_debounce_gates_reread_only() {
3951 let dir = TempDir::new().unwrap();
3952 let path = dir.path().join("a.rs");
3953 std::fs::write(&path, "AAAA").unwrap();
3954 let (client, _server) = fake_lsp_client();
3957 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3958 tracker
3959 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3960 .await
3961 .unwrap();
3962 let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
3963
3964 std::fs::write(&path, "BBBB").unwrap(); set_mtime(&path, original_mtime); tracker
3969 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3970 .await
3971 .unwrap();
3972 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
3973
3974 tokio::time::advance(Duration::from_millis(300)).await;
3975 tracker
3976 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3977 .await
3978 .unwrap();
3979 let state = tracker.get(&path).unwrap();
3980 assert_eq!(state.version().get(), 2);
3981 assert_eq!(state.content(), "BBBB");
3982 }
3983
3984 #[tokio::test]
3985 async fn test_ensure_open_deleted_file_errors_state_untouched() {
3986 let dir = TempDir::new().unwrap();
3987 let path = dir.path().join("a.rs");
3988 std::fs::write(&path, "fn main() {}").unwrap();
3989 set_mtime(&path, settled_past());
3990
3991 let (client, _server) = fake_lsp_client();
3992 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
3993 tracker
3994 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
3995 .await
3996 .unwrap();
3997
3998 std::fs::remove_file(&path).unwrap();
3999
4000 let result = tracker
4001 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
4002 .await;
4003 assert_matches!(result, Err(Error::FileIo { .. }));
4004 assert_eq!(tracker.open_state(&path), DocumentOpenState::Open);
4005 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
4006 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
4007 }
4008
4009 #[tokio::test]
4010 async fn test_ensure_open_grows_past_limit_errors_state_intact() {
4011 let dir = TempDir::new().unwrap();
4012 let path = dir.path().join("a.rs");
4013 std::fs::write(&path, "small").unwrap();
4014 set_mtime(&path, settled_past());
4015
4016 let limits = ResourceLimits {
4017 max_documents: DocumentLimit::new(10),
4018 max_file_size: SizeLimit::from_static(10),
4019 };
4020 let (client, _server) = fake_lsp_client();
4021 let tracker = DocumentTracker::new(limits, HashMap::new());
4022 tracker
4023 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
4024 .await
4025 .unwrap();
4026
4027 std::fs::write(&path, "x".repeat(100)).unwrap();
4028
4029 let result = tracker
4030 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
4031 .await;
4032 assert_matches!(result, Err(Error::FileSizeLimitExceeded { .. }));
4033 assert_eq!(tracker.get(&path).unwrap().content(), "small");
4034 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
4035 }
4036
4037 #[tokio::test]
4038 async fn test_ensure_open_resync_at_document_capacity() {
4039 let dir = TempDir::new().unwrap();
4040 let path = dir.path().join("a.rs");
4041 std::fs::write(&path, "AAAA").unwrap();
4042 set_mtime(&path, settled_past());
4043
4044 let limits = ResourceLimits {
4045 max_documents: DocumentLimit::new(1),
4046 max_file_size: SizeLimit::UNLIMITED,
4047 };
4048 let (client, _server) = fake_lsp_client();
4049 let tracker = DocumentTracker::new(limits, HashMap::new());
4050 tracker
4051 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
4052 .await
4053 .unwrap();
4054 assert_eq!(tracker.len(), 1);
4055
4056 std::fs::write(&path, "BBBBBBBB").unwrap();
4057 let result = tracker
4058 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
4059 .await;
4060 assert!(
4061 result.is_ok(),
4062 "resync must not re-run the doc-count check on an already-tracked path"
4063 );
4064 assert_eq!(tracker.len(), 1);
4065 assert_eq!(tracker.get(&path).unwrap().version().get(), 2);
4066 }
4067
4068 #[tokio::test]
4069 async fn test_first_open_self_heals_when_did_open_notify_fails() {
4070 let dir = TempDir::new().unwrap();
4071 let path = dir.path().join("a.rs");
4072 std::fs::write(&path, "fn main() {}").unwrap();
4073
4074 let (client, _server) = fake_lsp_client();
4075 let notify_will_fail = client.clone();
4081 client.shutdown().await.unwrap();
4082
4083 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4084 let result = tracker
4085 .ensure_open_for_test(&path, &ServerId::from_static("rust"), ¬ify_will_fail)
4086 .await;
4087
4088 assert!(result.is_err(), "notify failure must propagate as an error");
4089 assert!(
4090 tracker.open_state(&path) == DocumentOpenState::NotOpen,
4091 "a failed didOpen must not leave the document tracked, or the server \
4092 and tracker would stay permanently desynced"
4093 );
4094 }
4095
4096 #[tokio::test]
4097 async fn test_resync_sends_didchange_with_full_replacement_over_the_wire() {
4098 let dir = TempDir::new().unwrap();
4099 let path = dir.path().join("a.rs");
4100 std::fs::write(&path, "fn main() {}").unwrap();
4101 set_mtime(&path, settled_past());
4102
4103 let (client, mut server) = fake_lsp_client();
4104 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4105 tracker
4106 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
4107 .await
4108 .unwrap();
4109
4110 let mut wire = BufReader::new(&mut server.write_stdout);
4111 let opened = read_framed_message(&mut wire).await;
4112 assert_eq!(opened["method"], "textDocument/didOpen");
4113
4114 std::fs::write(&path, "fn main() { println!(\"hi\"); }").unwrap();
4115 set_mtime(&path, settled_past());
4116 tracker
4117 .ensure_open_for_test(&path, &ServerId::from_static("rust"), &client)
4118 .await
4119 .unwrap();
4120
4121 let changed = read_framed_message(&mut wire).await;
4122 assert_eq!(changed["method"], "textDocument/didChange");
4123 let params = &changed["params"];
4124 assert_eq!(params["textDocument"]["version"], 2);
4125 let change = ¶ms["contentChanges"][0];
4126 assert!(
4127 change.get("range").is_none(),
4128 "range must be omitted, not null, for a full-replacement change"
4129 );
4130 assert!(
4131 change.get("rangeLength").is_none(),
4132 "rangeLength must be omitted, not null, for a full-replacement change"
4133 );
4134 assert_eq!(change["text"], "fn main() { println!(\"hi\"); }");
4135 }
4136
4137 #[tokio::test]
4142 async fn test_ensure_open_second_server_gets_didopen_no_disk_change() {
4143 let dir = TempDir::new().unwrap();
4144 let path = dir.path().join("a.rs");
4145 std::fs::write(&path, "fn main() {}").unwrap();
4146 set_mtime(&path, settled_past());
4147
4148 let (client_a, mut server_a) = fake_lsp_client();
4149 let (client_b, mut server_b) = fake_lsp_client();
4150 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4151
4152 let id_a = ServerId::from_static("server-a");
4153 let id_b = ServerId::from_static("server-b");
4154
4155 tracker
4156 .ensure_open_for_test(&path, &id_a, &client_a)
4157 .await
4158 .unwrap();
4159 let mut wire_a = BufReader::new(&mut server_a.write_stdout);
4160 let opened_a = read_framed_message(&mut wire_a).await;
4161 assert_eq!(opened_a["method"], "textDocument/didOpen");
4162
4163 tracker
4167 .ensure_open_for_test(&path, &id_b, &client_b)
4168 .await
4169 .unwrap();
4170 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
4171 let opened_b = read_framed_message(&mut wire_b).await;
4172 assert_eq!(opened_b["method"], "textDocument/didOpen");
4173 assert_eq!(opened_b["params"]["textDocument"]["version"], 1);
4174 assert_eq!(opened_b["params"]["textDocument"]["text"], "fn main() {}");
4175 }
4176
4177 #[tokio::test(start_paused = true)]
4181 async fn test_ensure_open_second_server_gets_didopen_unchanged_content_path() {
4182 let dir = TempDir::new().unwrap();
4183 let path = dir.path().join("a.rs");
4184 std::fs::write(&path, "fn main() {}").unwrap();
4185 let (client_a, _server_a) = fake_lsp_client();
4188 let (client_b, mut server_b) = fake_lsp_client();
4189 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4190
4191 tracker
4192 .ensure_open_for_test(&path, &ServerId::from_static("server-a"), &client_a)
4193 .await
4194 .unwrap();
4195
4196 tokio::time::advance(DISK_CHECK_DEBOUNCE + Duration::from_millis(1)).await;
4199
4200 tracker
4201 .ensure_open_for_test(&path, &ServerId::from_static("server-b"), &client_b)
4202 .await
4203 .unwrap();
4204 let mut wire_b = BufReader::new(&mut server_b.write_stdout);
4205 let opened_b = read_framed_message(&mut wire_b).await;
4206 assert_eq!(opened_b["method"], "textDocument/didOpen");
4207 }
4208
4209 #[tokio::test]
4216 async fn test_ensure_open_same_server_twice_sends_nothing_second_time() {
4217 let dir = TempDir::new().unwrap();
4218 let path = dir.path().join("a.rs");
4219 std::fs::write(&path, "fn main() {}").unwrap();
4220 set_mtime(&path, settled_past());
4221
4222 let (client, mut server) = fake_lsp_client();
4223 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4224 let id = ServerId::from_static("rust");
4225
4226 tracker
4227 .ensure_open_for_test(&path, &id, &client)
4228 .await
4229 .unwrap();
4230 tracker
4231 .ensure_open_for_test(&path, &id, &client)
4232 .await
4233 .unwrap();
4234
4235 let mut wire = BufReader::new(&mut server.write_stdout);
4236 let opened = read_framed_message(&mut wire).await;
4237 assert_eq!(opened["method"], "textDocument/didOpen");
4238 assert_eq!(
4239 tracker
4240 .get(&path)
4241 .unwrap()
4242 .synced_version(&id)
4243 .map(DocumentVersion::get),
4244 Some(1),
4245 "second call for the same server must not re-open or re-change"
4246 );
4247 }
4248
4249 #[tokio::test]
4253 async fn test_sync_phase_failed_didchange_does_not_disturb_other_server() {
4254 let dir = TempDir::new().unwrap();
4255 let path = dir.path().join("a.rs");
4256 std::fs::write(&path, "fn main() {}").unwrap();
4257 set_mtime(&path, settled_past());
4258
4259 let (client_a, _server_a) = fake_lsp_client();
4260 let (client_b, _server_b) = fake_lsp_client();
4261 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4262 let id_a = ServerId::from_static("server-a");
4263 let id_b = ServerId::from_static("server-b");
4264
4265 tracker
4266 .ensure_open_for_test(&path, &id_a, &client_a)
4267 .await
4268 .unwrap();
4269 tracker
4270 .ensure_open_for_test(&path, &id_b, &client_b)
4271 .await
4272 .unwrap();
4273
4274 let client_b_will_fail = client_b.clone();
4277 client_b.shutdown().await.unwrap();
4278
4279 std::fs::write(&path, "fn main() { updated(); }").unwrap();
4280 set_mtime(&path, settled_past());
4281
4282 let result = tracker
4283 .ensure_open_for_test(&path, &id_b, &client_b_will_fail)
4284 .await;
4285 assert!(result.is_err(), "B's didChange must fail and propagate");
4286
4287 assert_eq!(tracker.open_state(&path), DocumentOpenState::Open);
4293 assert_eq!(tracker.get(&path).unwrap().content(), "fn main() {}");
4294 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
4295 assert_eq!(
4296 tracker
4297 .get(&path)
4298 .unwrap()
4299 .synced_version(&id_a)
4300 .map(DocumentVersion::get),
4301 Some(1)
4302 );
4303 assert_eq!(
4304 tracker
4305 .get(&path)
4306 .unwrap()
4307 .synced_version(&id_b)
4308 .map(DocumentVersion::get),
4309 Some(1)
4310 );
4311
4312 tracker
4316 .ensure_open_for_test(&path, &id_a, &client_a)
4317 .await
4318 .unwrap();
4319 assert_eq!(
4320 tracker.get(&path).unwrap().content(),
4321 "fn main() { updated(); }"
4322 );
4323 assert_eq!(
4324 tracker
4325 .get(&path)
4326 .unwrap()
4327 .synced_version(&id_a)
4328 .map(DocumentVersion::get),
4329 Some(2)
4330 );
4331 assert_eq!(
4332 tracker
4333 .get(&path)
4334 .unwrap()
4335 .synced_version(&id_b)
4336 .map(DocumentVersion::get),
4337 Some(1)
4338 );
4339 }
4340
4341 #[tokio::test]
4364 async fn test_ensure_open_different_paths_do_not_serialize() {
4365 let dir = TempDir::new().unwrap();
4366 let path_a = dir.path().join("a.rs");
4367 let path_b = dir.path().join("b.rs");
4368
4369 std::fs::write(&path_a, "fn a() {}").unwrap();
4370 std::fs::write(&path_b, "fn b() {}").unwrap();
4371 set_mtime(&path_a, settled_past());
4372 set_mtime(&path_b, settled_past());
4373
4374 let (client_a, _server_a) = fake_lsp_client();
4375 let (client_b, _server_b) = fake_lsp_client();
4376 let tracker = Arc::new(DocumentTracker::new(
4377 ResourceLimits::default(),
4378 HashMap::new(),
4379 ));
4380
4381 let path_a_guard = tracker.lock_path(&path_a).await;
4382
4383 let tracker_for_a = Arc::clone(&tracker);
4387 let path_a_for_task = path_a.clone();
4388 let handle_a = tokio::spawn(async move {
4389 tracker_for_a
4390 .ensure_open_for_test(
4391 &path_a_for_task,
4392 &ServerId::from_static("server-a"),
4393 &client_a,
4394 )
4395 .await
4396 });
4397
4398 tokio::time::sleep(Duration::from_millis(200)).await;
4401
4402 tokio::time::timeout(
4405 Duration::from_secs(5),
4406 tracker.ensure_open_for_test(&path_b, &ServerId::from_static("server-b"), &client_b),
4407 )
4408 .await
4409 .unwrap()
4410 .unwrap();
4411
4412 drop(path_a_guard);
4413
4414 handle_a.await.unwrap().unwrap();
4415 assert_eq!(tracker.get(&path_a).unwrap().content(), "fn a() {}");
4416 }
4417
4418 #[tokio::test]
4424 async fn test_ensure_open_concurrent_same_path_single_didopen() {
4425 let dir = TempDir::new().unwrap();
4426 let path = dir.path().join("a.rs");
4427 std::fs::write(&path, "fn main() {}").unwrap();
4428 set_mtime(&path, settled_past());
4429
4430 let (client, mut server) = fake_lsp_client();
4431 let tracker = Arc::new(DocumentTracker::new(
4432 ResourceLimits::default(),
4433 HashMap::new(),
4434 ));
4435 let id = ServerId::from_static("rust");
4436
4437 let mut handles = Vec::new();
4438 for _ in 0..8 {
4439 let tracker = Arc::clone(&tracker);
4440 let client = client.clone();
4441 let path = path.clone();
4442 let id = id.clone();
4443 handles.push(tokio::spawn(async move {
4444 tracker.ensure_open_for_test(&path, &id, &client).await
4445 }));
4446 }
4447 for handle in handles {
4448 handle.await.unwrap().unwrap();
4449 }
4450
4451 let mut wire = BufReader::new(&mut server.write_stdout);
4452 let opened = read_framed_message(&mut wire).await;
4453 assert_eq!(opened["method"], "textDocument/didOpen");
4454
4455 let extra =
4458 tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut wire)).await;
4459 assert!(
4460 extra.is_err(),
4461 "expected no additional notification after the single didOpen"
4462 );
4463
4464 assert_eq!(
4465 tracker
4466 .get(&path)
4467 .unwrap()
4468 .synced_version(&id)
4469 .map(DocumentVersion::get),
4470 Some(1)
4471 );
4472 assert_eq!(tracker.get(&path).unwrap().version().get(), 1);
4473 }
4474
4475 #[tokio::test]
4482 async fn test_ensure_open_path_locks_evicted_after_completion() {
4483 let dir = TempDir::new().unwrap();
4484 let paths: Vec<_> = ["a.rs", "b.rs", "c.rs"]
4485 .iter()
4486 .map(|name| dir.path().join(name))
4487 .collect();
4488 for path in &paths {
4489 std::fs::write(path, "fn f() {}").unwrap();
4490 set_mtime(path, settled_past());
4491 }
4492
4493 let tracker = Arc::new(DocumentTracker::new(
4494 ResourceLimits::default(),
4495 HashMap::new(),
4496 ));
4497 let id = ServerId::from_static("rust");
4498
4499 let mut handles = Vec::new();
4500 let mut servers = Vec::new();
4501 for path in paths.clone() {
4502 let tracker = Arc::clone(&tracker);
4503 let (client, server) = fake_lsp_client();
4504 servers.push(server);
4505 let id = id.clone();
4506 handles.push(tokio::spawn(async move {
4507 tracker.ensure_open_for_test(&path, &id, &client).await
4508 }));
4509 }
4510 for handle in handles {
4511 handle.await.unwrap().unwrap();
4512 }
4513 drop(servers);
4514
4515 assert!(
4516 lock_std(&tracker.path_locks).is_empty(),
4517 "path_locks must be fully evicted once every ensure_open call \
4518 for every path has completed, otherwise the map grows \
4519 unbounded for the lifetime of the process"
4520 );
4521 }
4522
4523 #[cfg(unix)]
4535 #[tokio::test]
4536 async fn test_read_to_string_checked_rejects_fifo() {
4537 let dir = TempDir::new().unwrap();
4538 let path = dir.path().join("fifo");
4539 crate::test_lsp::make_fifo(&path);
4540
4541 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4542 let result = tokio::time::timeout(
4545 Duration::from_secs(5),
4546 tracker.read_to_string_checked(OpenGate::Ungated(&path)),
4547 )
4548 .await
4549 .unwrap();
4550
4551 assert_matches!(result, Err(Error::NotARegularFile(_)));
4552 }
4553
4554 #[cfg(windows)]
4563 #[tokio::test]
4564 async fn test_read_to_string_checked_rejects_nul_device() {
4565 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4566 let path = PathBuf::from("NUL");
4567 let result = tokio::time::timeout(
4568 Duration::from_secs(5),
4569 tracker.read_to_string_checked(OpenGate::Ungated(&path)),
4570 )
4571 .await
4572 .unwrap();
4573
4574 assert_matches!(result, Err(Error::NotARegularFile(_)));
4575 }
4576
4577 #[tokio::test]
4583 async fn test_read_to_string_checked_size_boundary() {
4584 let dir = TempDir::new().unwrap();
4585 let path = dir.path().join("boundary.rs");
4586 let tracker = DocumentTracker::new(
4587 ResourceLimits {
4588 max_documents: DocumentLimit::new(100),
4589 max_file_size: SizeLimit::from_static(10),
4590 },
4591 HashMap::new(),
4592 );
4593
4594 std::fs::write(&path, "a".repeat(10)).unwrap();
4595 let (content, ..) = tracker
4596 .read_to_string_checked(OpenGate::Ungated(&path))
4597 .await
4598 .unwrap();
4599 assert_eq!(content.len(), 10);
4600
4601 std::fs::write(&path, "a".repeat(11)).unwrap();
4602 let result = tracker
4603 .read_to_string_checked(OpenGate::Ungated(&path))
4604 .await;
4605 assert_matches!(
4606 result,
4607 Err(Error::FileSizeLimitExceeded(SizeExceeded { size: 11, max })) if max.get() == 10
4608 );
4609 }
4610
4611 #[tokio::test]
4617 async fn test_read_line_checked_does_not_read_past_target_line() {
4618 let dir = TempDir::new().unwrap();
4619 let path = dir.path().join("partial.rs");
4620 let mut content = b"hello\n".to_vec();
4621 content.extend_from_slice(&[0xFF, 0xFE]);
4622 content.push(b'\n');
4623 std::fs::write(&path, &content).unwrap();
4624
4625 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4626 let line = tracker.read_line_checked(&path, 0, u64::MAX).await.unwrap();
4627 assert_eq!(line.text.as_deref(), Some("hello"));
4628 }
4629
4630 #[tokio::test]
4634 async fn test_read_line_checked_returns_requested_non_zero_line() {
4635 let dir = TempDir::new().unwrap();
4636 let path = dir.path().join("multi.rs");
4637 std::fs::write(&path, "first\nsecond\nthird\nfourth\n").unwrap();
4638
4639 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4640 assert_eq!(
4641 tracker
4642 .read_line_checked(&path, 2, u64::MAX)
4643 .await
4644 .unwrap()
4645 .text
4646 .as_deref(),
4647 Some("third")
4648 );
4649 }
4650
4651 #[tokio::test]
4655 async fn test_read_line_checked_returns_none_past_last_line() {
4656 let dir = TempDir::new().unwrap();
4657 let path = dir.path().join("short.rs");
4658 std::fs::write(&path, "only one line").unwrap();
4659
4660 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4661 assert_eq!(
4662 tracker
4663 .read_line_checked(&path, 5, u64::MAX)
4664 .await
4665 .unwrap()
4666 .text,
4667 None
4668 );
4669 }
4670
4671 #[tokio::test]
4676 async fn test_read_line_checked_reads_last_line_without_trailing_newline() {
4677 let dir = TempDir::new().unwrap();
4678 let path = dir.path().join("no_newline.rs");
4679 std::fs::write(&path, "only one line").unwrap();
4680
4681 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4682 assert_eq!(
4683 tracker
4684 .read_line_checked(&path, 0, u64::MAX)
4685 .await
4686 .unwrap()
4687 .text
4688 .as_deref(),
4689 Some("only one line")
4690 );
4691 }
4692
4693 #[tokio::test]
4694 async fn test_read_line_checked_empty_file_line_zero_is_empty_line() {
4695 let dir = TempDir::new().unwrap();
4696 let path = dir.path().join("empty.rs");
4697 std::fs::write(&path, "").unwrap();
4698
4699 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4700 assert_eq!(
4701 tracker
4702 .read_line_checked(&path, 0, u64::MAX)
4703 .await
4704 .unwrap()
4705 .text
4706 .as_deref(),
4707 Some("")
4708 );
4709 assert_eq!(
4710 tracker
4711 .read_line_checked(&path, 1, u64::MAX)
4712 .await
4713 .unwrap()
4714 .text,
4715 None
4716 );
4717 }
4718
4719 #[tokio::test]
4720 async fn test_read_line_checked_trailing_empty_line_after_final_newline() {
4721 let dir = TempDir::new().unwrap();
4722 let path = dir.path().join("trailing.rs");
4723 std::fs::write(&path, "a\n").unwrap();
4724
4725 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4726 let read = |line| tracker.read_line_checked(&path, line, u64::MAX);
4727 assert_eq!(read(1).await.unwrap().text.as_deref(), Some(""));
4728 assert_eq!(read(2).await.unwrap().text, None);
4729 }
4730
4731 #[tokio::test]
4732 async fn test_read_line_checked_cap_at_line_boundary_is_not_empty_line() {
4733 let dir = TempDir::new().unwrap();
4734 let path = dir.path().join("capped.rs");
4735 std::fs::write(&path, "abc\nrest\n").unwrap();
4736
4737 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4738 assert_eq!(
4740 tracker.read_line_checked(&path, 1, 3).await.unwrap().text,
4741 None
4742 );
4743 }
4744
4745 #[tokio::test]
4747 async fn test_read_line_checked_strips_crlf_line_ending() {
4748 let dir = TempDir::new().unwrap();
4749 let path = dir.path().join("crlf.rs");
4750 std::fs::write(&path, "first\r\nsecond\r\n").unwrap();
4751
4752 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4753 assert_eq!(
4754 tracker
4755 .read_line_checked(&path, 0, u64::MAX)
4756 .await
4757 .unwrap()
4758 .text
4759 .as_deref(),
4760 Some("first")
4761 );
4762 assert_eq!(
4763 tracker
4764 .read_line_checked(&path, 1, u64::MAX)
4765 .await
4766 .unwrap()
4767 .text
4768 .as_deref(),
4769 Some("second")
4770 );
4771 }
4772
4773 #[tokio::test]
4778 async fn test_line_text_and_read_line_checked_agree_on_line_rule() {
4779 let dir = TempDir::new().unwrap();
4780 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4781
4782 let contents = [
4783 "",
4784 "abc",
4785 "abc\n",
4786 "abc\r\n",
4787 "abc\r\r\n",
4788 "abc\r",
4789 "a\n\nb",
4790 "a\r\nb\r\n",
4791 "a\rb",
4792 "a\r\r\nb",
4793 "\n\r",
4794 "\r\n\r\n",
4795 ];
4796 for (i, content) in contents.iter().enumerate() {
4797 let path = dir.path().join(format!("parity_{i}.rs"));
4798 std::fs::write(&path, content).unwrap();
4799 tracker.open(path.clone(), (*content).to_string()).unwrap();
4800 for line in 0..6 {
4801 let disk = tracker
4802 .read_line_checked(&path, line, u64::MAX)
4803 .await
4804 .unwrap()
4805 .text;
4806 assert_eq!(
4807 tracker.line_text(&path, line),
4808 disk,
4809 "content {content:?}, line {line}"
4810 );
4811 }
4812 }
4813 }
4814
4815 #[test]
4816 fn test_line_text_trailing_empty_line_and_lone_cr_terminator() {
4817 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4818 let path = PathBuf::from("/test/lines.rs");
4819 tracker
4820 .open(path.clone(), "ab\r\ncd\r\r\n".to_string())
4821 .unwrap();
4822
4823 assert_eq!(tracker.line_text(&path, 0).as_deref(), Some("ab"));
4824 assert_eq!(tracker.line_text(&path, 1).as_deref(), Some("cd"));
4825 assert_eq!(tracker.line_text(&path, 2).as_deref(), Some(""));
4826 assert_eq!(tracker.line_text(&path, 3).as_deref(), Some(""));
4827 assert_eq!(tracker.line_text(&path, 4), None);
4828
4829 let empty = PathBuf::from("/test/empty.rs");
4830 tracker.open(empty.clone(), String::new()).unwrap();
4831 assert_eq!(tracker.line_text(&empty, 0).as_deref(), Some(""));
4832 assert_eq!(tracker.line_text(&empty, 1), None);
4833 }
4834
4835 #[tokio::test]
4841 async fn test_read_line_checked_exact_max_file_size_reads_to_eof_without_error() {
4842 let dir = TempDir::new().unwrap();
4843 let path = dir.path().join("exact.rs");
4844 let content = "a".repeat(20);
4845 std::fs::write(&path, &content).unwrap();
4846
4847 let limits = ResourceLimits {
4848 max_documents: DocumentLimit::new(100),
4849 max_file_size: SizeLimit::from_static(20),
4850 };
4851 let tracker = DocumentTracker::new(limits, HashMap::new());
4852
4853 assert_eq!(
4854 tracker
4855 .read_line_checked(&path, 0, u64::MAX)
4856 .await
4857 .unwrap()
4858 .text
4859 .as_deref(),
4860 Some(content.as_str())
4861 );
4862 assert_eq!(
4863 tracker
4864 .read_line_checked(&path, 1, u64::MAX)
4865 .await
4866 .unwrap()
4867 .text,
4868 None,
4869 "a line past an exact-max_file_size file's only line must read to EOF cleanly, not \
4870 be misreported as truncated"
4871 );
4872 }
4873
4874 #[tokio::test]
4881 async fn test_read_line_checked_bounds_read_by_budget_not_just_max_file_size() {
4882 let dir = TempDir::new().unwrap();
4883 let path = dir.path().join("budget.rs");
4884 std::fs::write(&path, "a".repeat(1000)).unwrap();
4885
4886 let limits = ResourceLimits {
4887 max_documents: DocumentLimit::new(100),
4888 max_file_size: SizeLimit::from_static(1000),
4889 };
4890 let tracker = DocumentTracker::new(limits, HashMap::new());
4891
4892 let read = tracker.read_line_checked(&path, 0, 10).await.unwrap();
4893 assert_eq!(
4894 read.text, None,
4895 "a single line far longer than the budget must not be returned as if complete"
4896 );
4897 assert_eq!(
4898 read.bytes_read, 11,
4899 "the read must stop at exactly the budget's +1 slack (see the correctness-gate fix \
4900 below), not at max_file_size"
4901 );
4902 }
4903
4904 #[tokio::test]
4912 async fn test_read_line_checked_exact_budget_match_on_unterminated_line_not_truncated() {
4913 let dir = TempDir::new().unwrap();
4914 let path = dir.path().join("exact_budget.rs");
4915 let content = "twelve chars";
4916 std::fs::write(&path, content).unwrap();
4917
4918 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4919 let read = tracker
4920 .read_line_checked(&path, 0, content.len() as u64)
4921 .await
4922 .unwrap();
4923 assert_eq!(
4924 read.text.as_deref(),
4925 Some(content),
4926 "budget exactly matching the line's byte length must not be misreported as truncated"
4927 );
4928 assert_eq!(read.bytes_read, content.len() as u64);
4929 }
4930
4931 #[tokio::test]
4938 async fn test_read_line_checked_reports_bytes_read_for_invalid_utf8_line() {
4939 let dir = TempDir::new().unwrap();
4940 let path = dir.path().join("invalid_utf8.rs");
4941 let mut content = vec![0xFFu8, 0xFE, 0xFD];
4942 content.push(b'\n');
4943 std::fs::write(&path, &content).unwrap();
4944
4945 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4946 let read = tracker.read_line_checked(&path, 0, u64::MAX).await.unwrap();
4947 assert_eq!(read.text, None);
4948 assert_eq!(
4949 read.bytes_read,
4950 content.len() as u64,
4951 "bytes scanned must be reported even though the line wasn't valid UTF-8"
4952 );
4953 }
4954
4955 #[tokio::test]
4964 async fn test_read_line_checked_charges_nominal_amount_for_nonexistent_path() {
4965 let dir = TempDir::new().unwrap();
4966 let path = dir.path().join("does_not_exist.rs");
4967
4968 let tracker = DocumentTracker::new(ResourceLimits::default(), HashMap::new());
4969 let read = tracker.read_line_checked(&path, 0, u64::MAX).await.unwrap();
4971 assert_eq!(read.text, None);
4972 assert_eq!(read.bytes_read, OPEN_FAILURE_CHARGE_BYTES);
4973 }
4974
4975 fn naive_lines(content: &str) -> Vec<&str> {
4978 let bytes = content.as_bytes();
4979 let mut lines = Vec::new();
4980 let (mut start, mut i) = (0, 0);
4981 while i < bytes.len() {
4982 match bytes[i] {
4983 b'\n' => {
4984 lines.push(&content[start..i]);
4985 i += 1;
4986 start = i;
4987 }
4988 b'\r' => {
4989 lines.push(&content[start..i]);
4990 i += 1;
4991 if bytes.get(i) == Some(&b'\n') {
4992 i += 1;
4993 }
4994 start = i;
4995 }
4996 _ => i += 1,
4997 }
4998 }
4999 lines.push(&content[start..]);
5000 lines
5001 }
5002
5003 fn all_strings(max_len: usize) -> Vec<String> {
5005 let alphabet = ['a', 'é', '\r', '\n'];
5006 let mut all = vec![String::new()];
5007 let mut frontier = vec![String::new()];
5008 for _ in 0..max_len {
5009 frontier = frontier
5010 .iter()
5011 .flat_map(|s| {
5012 alphabet.iter().map(move |c| {
5013 let mut next = s.clone();
5014 next.push(*c);
5015 next
5016 })
5017 })
5018 .collect();
5019 all.extend(frontier.iter().cloned());
5020 }
5021 all
5022 }
5023
5024 #[test]
5028 fn test_document_text_line_matches_naive_splitter_exhaustively() {
5029 for content in all_strings(8) {
5030 let expected = naive_lines(&content);
5031 for stride in 1..=3 {
5032 let text = DocumentText::with_stride(content.clone(), stride);
5033 for (n, want) in expected.iter().enumerate() {
5034 assert_eq!(
5035 text.line(u32::try_from(n).unwrap()),
5036 Some(*want),
5037 "{content:?} stride {stride} line {n}"
5038 );
5039 }
5040 assert_eq!(
5041 text.line(u32::try_from(expected.len()).unwrap()),
5042 None,
5043 "{content:?} stride {stride} past the last line"
5044 );
5045 }
5046 }
5047 }
5048
5049 #[test]
5051 fn test_document_text_lines_from_matches_line_exhaustively() {
5052 for content in all_strings(7) {
5053 let expected = naive_lines(&content);
5054 for stride in 1..=3 {
5055 let text = DocumentText::with_stride(content.clone(), stride);
5056 for start in 0..=expected.len() {
5057 let got: Vec<&str> = text.lines_from(u32::try_from(start).unwrap()).collect();
5058 assert_eq!(
5059 got,
5060 expected[start.min(expected.len())..],
5061 "{content:?} {stride} {start}"
5062 );
5063 }
5064 }
5065 }
5066 }
5067
5068 #[tokio::test]
5071 async fn test_read_nth_line_matches_naive_splitter_exhaustively() {
5072 for content in all_strings(7) {
5073 let expected = naive_lines(&content);
5074 for capacity in [1, 2, 8 * 1024] {
5075 for n in 0..=expected.len() {
5076 let mut reader =
5077 tokio::io::BufReader::with_capacity(capacity, content.as_bytes());
5078 let read = read_nth_line(&mut reader, u32::try_from(n).unwrap(), u64::MAX)
5079 .await
5080 .unwrap();
5081 assert_eq!(
5082 read.text.as_deref(),
5083 expected.get(n).copied(),
5084 "{content:?} capacity {capacity} line {n}"
5085 );
5086 }
5087 }
5088 }
5089 }
5090
5091 #[test]
5094 fn test_document_text_checkpoint_at_eof_after_full_stride() {
5095 for terminator in ["\n", "\r\n", "\r"] {
5096 let content = format!("x{terminator}").repeat(LINE_CHECKPOINT_STRIDE);
5097 let text = DocumentText::new(content);
5098 let stride = u32::try_from(LINE_CHECKPOINT_STRIDE).unwrap();
5099 assert_eq!(text.line(stride - 1), Some("x"));
5100 assert_eq!(text.line(stride), Some(""), "terminator {terminator:?}");
5101 assert_eq!(text.line(stride + 1), None);
5102 }
5103 }
5104
5105 #[test]
5108 fn test_document_text_lines_across_default_stride_boundaries() {
5109 let content: String = (0..200)
5110 .map(|i| format!("line{i}\r\n"))
5111 .collect::<Vec<_>>()
5112 .concat();
5113 let text = DocumentText::new(content);
5114 for i in [0u32, 1, 63, 64, 65, 127, 128, 199] {
5115 assert_eq!(text.line(i), Some(format!("line{i}").as_str()), "line {i}");
5116 }
5117 assert_eq!(text.line(200), Some(""));
5118 assert_eq!(text.line(201), None);
5119 }
5120
5121 #[tokio::test]
5124 async fn test_document_text_build_above_inline_threshold_matches_inline() {
5125 let content = "ab\r\n".repeat(INLINE_INDEX_MAX_BYTES / 4 + 10);
5126 assert!(content.len() > INLINE_INDEX_MAX_BYTES);
5127 let built = DocumentText::build(content.clone()).await.unwrap();
5128 let inline = DocumentText::new(content);
5129 assert_eq!(built.checkpoints, inline.checkpoints);
5130 assert_eq!(built.line(1000), Some("ab"));
5131 }
5132
5133 #[test]
5134 fn test_document_text_checkpoint_memory_is_sparse() {
5135 let text = DocumentText::new("\n".repeat(6400));
5136 assert_eq!(text.checkpoints.len(), 6400 / LINE_CHECKPOINT_STRIDE + 1);
5137 }
5138
5139 #[tokio::test]
5142 async fn test_read_nth_line_crlf_split_across_fills_is_one_terminator() {
5143 let mut reader = tokio::io::BufReader::with_capacity(1, b"a\r\nb".as_slice());
5144 let read = read_nth_line(&mut reader, 1, u64::MAX).await.unwrap();
5145 assert_eq!(read.text.as_deref(), Some("b"));
5146 }
5147
5148 #[tokio::test]
5151 async fn test_read_nth_line_cap_exhausted_mid_line_is_none() {
5152 let mut reader =
5153 tokio::io::BufReader::new(tokio::io::AsyncReadExt::take(b"abcdef\n".as_slice(), 3));
5154 let read = read_nth_line(&mut reader, 0, 3).await.unwrap();
5155 assert_eq!(read.text, None);
5156 assert_eq!(read.bytes_read, 3);
5157 }
5158
5159 async fn open_for_servers(
5162 tracker: &DocumentTracker,
5163 dir: &TempDir,
5164 name: &str,
5165 servers: &[&str],
5166 ) -> (
5167 PathBuf,
5168 Vec<(ServerId, LspClient, crate::test_lsp::FakeServer)>,
5169 ) {
5170 let path = dir.path().join(name);
5171 std::fs::write(&path, name).unwrap();
5172 set_mtime(&path, settled_past());
5173 let mut out = Vec::new();
5174 for server in servers {
5175 let (client, fake) = fake_lsp_client();
5176 let id = ServerId::new(*server).unwrap();
5177 tracker
5178 .ensure_open_for_test(&path, &id, &client)
5179 .await
5180 .unwrap();
5181 out.push((id, client, fake));
5182 }
5183 (path, out)
5184 }
5185
5186 async fn evict_by_opening(tracker: &DocumentTracker, dir: &TempDir, other: &str) {
5188 let path = dir.path().join(other);
5189 std::fs::write(&path, other).unwrap();
5190 set_mtime(&path, settled_past());
5191 let (client, _fake) = fake_lsp_client();
5192 tracker
5193 .ensure_open_for_test(&path, &ServerId::from_static("other"), &client)
5194 .await
5195 .unwrap();
5196 }
5197
5198 fn one_document_tracker() -> DocumentTracker {
5199 DocumentTracker::new(
5200 ResourceLimits {
5201 max_documents: DocumentLimit::new(1),
5202 max_file_size: SizeLimit::UNLIMITED,
5203 },
5204 HashMap::new(),
5205 )
5206 }
5207
5208 #[tokio::test]
5211 async fn test_reopen_through_one_server_settles_only_that_servers_close() {
5212 let dir = TempDir::new().unwrap();
5213 let tracker = one_document_tracker();
5214 let (path, mut servers) = open_for_servers(&tracker, &dir, "p.rs", &["a", "d"]).await;
5215 evict_by_opening(&tracker, &dir, "q.rs").await;
5216 assert_eq!(
5217 pending_servers(&tracker, &path),
5218 HashSet::from([ServerId::from_static("a"), ServerId::from_static("d")])
5219 );
5220
5221 let (id_a, client_a, mut fake_a) = servers.remove(0);
5222 tracker
5223 .ensure_open_for_test(&path, &id_a, &client_a)
5224 .await
5225 .unwrap();
5226
5227 let mut wire = BufReader::new(&mut fake_a.write_stdout);
5228 assert_eq!(
5229 read_framed_message(&mut wire).await["method"],
5230 "textDocument/didOpen"
5231 );
5232 assert_eq!(
5233 read_framed_message(&mut wire).await["method"],
5234 "textDocument/didClose"
5235 );
5236 assert_eq!(
5237 read_framed_message(&mut wire).await["method"],
5238 "textDocument/didOpen"
5239 );
5240
5241 assert_eq!(
5242 pending_servers(&tracker, &path),
5243 HashSet::from([ServerId::from_static("d")])
5244 );
5245 let claim = tracker.try_claim_pending_close(&path).unwrap();
5246 assert_eq!(claim.servers, vec![ServerId::from_static("d")]);
5247 }
5248
5249 #[tokio::test]
5251 async fn test_claim_returns_every_owed_server_once() {
5252 let dir = TempDir::new().unwrap();
5253 let tracker = one_document_tracker();
5254 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a", "d"]).await;
5255 evict_by_opening(&tracker, &dir, "q.rs").await;
5256
5257 assert_eq!(tracker.pending_close_paths(), vec![path.clone()]);
5258 let mut claim = tracker.try_claim_pending_close(&path).unwrap();
5259 assert_eq!(claim.path, path);
5260 let mut claimed = claim.servers.clone();
5261 claimed.sort_by_key(ToString::to_string);
5262 assert_eq!(
5263 claimed,
5264 vec![ServerId::from_static("a"), ServerId::from_static("d")]
5265 );
5266 for server in claimed {
5267 claim.settle(&server);
5268 }
5269 drop(claim);
5270
5271 assert_eq!(tracker.pending_close_paths().len(), 0);
5272 assert!(tracker.try_claim_pending_close(&path).is_none());
5273 }
5274
5275 #[tokio::test]
5277 async fn test_claim_defers_busy_path_without_waiting() {
5278 let dir = TempDir::new().unwrap();
5279 let tracker = one_document_tracker();
5280 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a"]).await;
5281 evict_by_opening(&tracker, &dir, "q.rs").await;
5282
5283 let busy = tracker.lock_path(&path).await;
5284 assert!(tracker.try_claim_pending_close(&path).is_none());
5285 assert_eq!(
5286 pending_servers(&tracker, &path),
5287 HashSet::from([ServerId::from_static("a")]),
5288 "a deferred claim must leave the debt pending"
5289 );
5290 drop(busy);
5291 assert!(tracker.try_claim_pending_close(&path).is_some());
5292 assert!(
5293 lock_std(&tracker.path_locks).is_empty(),
5294 "failed and successful claims must not leak path-lock entries"
5295 );
5296 }
5297
5298 #[tokio::test]
5301 async fn test_claim_holds_path_lock_until_dropped() {
5302 let dir = TempDir::new().unwrap();
5303 let tracker = Arc::new(one_document_tracker());
5304 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a"]).await;
5305 evict_by_opening(&tracker, &dir, "q.rs").await;
5306
5307 let claim = tracker.try_claim_pending_close(&path).unwrap();
5308 let (client, _fake) = fake_lsp_client();
5309 let reopen = {
5310 let tracker = Arc::clone(&tracker);
5311 let path = path.clone();
5312 tokio::spawn(async move {
5313 tracker
5314 .ensure_open_for_test(&path, &ServerId::from_static("b"), &client)
5315 .await
5316 })
5317 };
5318 tokio::time::sleep(Duration::from_millis(50)).await;
5319 assert!(!reopen.is_finished(), "ensure_open must wait for the claim");
5320 drop(claim);
5321 reopen.await.unwrap().unwrap();
5322 }
5323
5324 #[tokio::test]
5328 async fn test_claim_dropped_unsettled_restores_the_owed_closes() {
5329 let dir = TempDir::new().unwrap();
5330 let tracker = one_document_tracker();
5331 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a", "d"]).await;
5332 evict_by_opening(&tracker, &dir, "q.rs").await;
5333
5334 let mut claim = tracker.try_claim_pending_close(&path).unwrap();
5335 claim.settle(&ServerId::from_static("a"));
5336 drop(claim);
5337
5338 assert_eq!(
5339 pending_servers(&tracker, &path),
5340 HashSet::from([ServerId::from_static("d")]),
5341 "only the close that was never settled stays owed"
5342 );
5343 }
5344
5345 #[tokio::test]
5347 async fn test_claim_dropped_after_the_server_was_forgotten_restores_nothing() {
5348 let dir = TempDir::new().unwrap();
5349 let tracker = one_document_tracker();
5350 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a"]).await;
5351 evict_by_opening(&tracker, &dir, "q.rs").await;
5352
5353 let claim = tracker.try_claim_pending_close(&path).unwrap();
5354 tracker.forget_server(&ServerId::from_static("a"));
5355 drop(claim);
5356
5357 assert!(pending_servers(&tracker, &path).is_empty());
5358 }
5359
5360 #[tokio::test]
5364 async fn test_sync_phase_dropped_while_waiting_for_queue_room_keeps_the_debt() {
5365 use crate::lsp::LspClient;
5366
5367 let dir = TempDir::new().unwrap();
5368 let tracker = one_document_tracker();
5369 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a"]).await;
5370 evict_by_opening(&tracker, &dir, "q.rs").await;
5371 let server = ServerId::from_static("a");
5372 let (client, mut commands) =
5373 LspClient::with_command_channel(crate::config::LspServerConfig::rust_analyzer(), 2);
5374 client
5375 .notify("test/fill", serde_json::json!({}))
5376 .await
5377 .unwrap();
5378 client
5379 .notify("test/fill", serde_json::json!({}))
5380 .await
5381 .unwrap();
5382
5383 let generation = tracker.generation(&server);
5384 let decision = tracker
5385 .disk_phase(OpenGate::Ungated(&path), Access::Use)
5386 .await
5387 .unwrap();
5388 {
5389 let sync = tracker.sync_phase(&path, &server, &client, decision, generation);
5390 let mut sync = std::pin::pin!(sync);
5391 assert!(futures::poll!(sync.as_mut()).is_pending());
5392 }
5393 assert_eq!(
5394 pending_servers(&tracker, &path),
5395 HashSet::from([server.clone()]),
5396 "the abandoned sync must leave the close owed"
5397 );
5398 assert!(
5399 tracker
5400 .get(&path)
5401 .unwrap()
5402 .synced_version(&server)
5403 .is_none()
5404 );
5405
5406 assert_eq!(commands.next_method().as_deref(), Some("test/fill"));
5407 assert_eq!(commands.next_method().as_deref(), Some("test/fill"));
5408 assert_eq!(
5409 commands.next_method(),
5410 None,
5411 "the abandoned sync sent something"
5412 );
5413
5414 let generation = tracker.generation(&server);
5415 let decision = tracker
5416 .disk_phase(OpenGate::Ungated(&path), Access::Use)
5417 .await
5418 .unwrap();
5419 tracker
5420 .sync_phase(&path, &server, &client, decision, generation)
5421 .await
5422 .unwrap();
5423 let methods: Vec<_> = std::iter::from_fn(|| commands.next_method()).collect();
5424 assert_eq!(
5425 methods,
5426 ["textDocument/didClose", "textDocument/didOpen"],
5427 "the debt is settled before the open, in one step"
5428 );
5429 assert!(pending_servers(&tracker, &path).is_empty());
5430 assert!(
5431 tracker
5432 .get(&path)
5433 .unwrap()
5434 .synced_version(&server)
5435 .is_some()
5436 );
5437 }
5438
5439 #[tokio::test]
5441 async fn test_repeated_eviction_merges_pending_closes() {
5442 let dir = TempDir::new().unwrap();
5443 let tracker = one_document_tracker();
5444 let (path, mut servers) = open_for_servers(&tracker, &dir, "p.rs", &["a", "d"]).await;
5445 evict_by_opening(&tracker, &dir, "q.rs").await;
5446
5447 let (id_d, client_d, _fake_d) = servers.remove(1);
5448 tracker
5449 .ensure_open_for_test(&path, &id_d, &client_d)
5450 .await
5451 .unwrap();
5452 evict_by_opening(&tracker, &dir, "r.rs").await;
5453
5454 assert_eq!(
5455 pending_servers(&tracker, &path),
5456 HashSet::from([ServerId::from_static("a"), ServerId::from_static("d")])
5457 );
5458 assert_eq!(tracker.pending_close_paths().len(), 2);
5459 }
5460
5461 #[tokio::test]
5464 async fn test_restore_pending_close_skips_forgotten_server() {
5465 let dir = TempDir::new().unwrap();
5466 let tracker = one_document_tracker();
5467 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a"]).await;
5468 evict_by_opening(&tracker, &dir, "q.rs").await;
5469 let server = ServerId::from_static("a");
5470 let uri = path_to_uri(&path).unwrap();
5471
5472 let generation = tracker.generation(&server);
5473 assert!(tracker.take_pending_close(&path, &server));
5474 tracker.forget_server(&server);
5475 tracker.restore_pending_close(&path, &uri, &server, generation);
5476 assert!(pending_servers(&tracker, &path).is_empty());
5477
5478 tracker.restore_pending_close(&path, &uri, &server, tracker.generation(&server));
5479 assert_eq!(pending_servers(&tracker, &path), HashSet::from([server]));
5480 }
5481
5482 #[tokio::test]
5485 async fn test_forget_server_purges_its_pending_closes() {
5486 let dir = TempDir::new().unwrap();
5487 let tracker = one_document_tracker();
5488 let (path, _servers) = open_for_servers(&tracker, &dir, "p.rs", &["a", "d"]).await;
5489 evict_by_opening(&tracker, &dir, "q.rs").await;
5490
5491 tracker.forget_server(&ServerId::from_static("a"));
5492 assert_eq!(
5493 pending_servers(&tracker, &path),
5494 HashSet::from([ServerId::from_static("d")])
5495 );
5496 tracker.forget_server(&ServerId::from_static("d"));
5497 assert!(!lock_std(&tracker.pending_closes).contains_key(&path));
5498 }
5499
5500 #[tokio::test]
5502 async fn test_failed_close_before_reopen_restores_the_debt() {
5503 let dir = TempDir::new().unwrap();
5504 let tracker = one_document_tracker();
5505 let (path, mut servers) = open_for_servers(&tracker, &dir, "p.rs", &["a"]).await;
5506 evict_by_opening(&tracker, &dir, "q.rs").await;
5507
5508 let (id_a, client_a, _fake_a) = servers.remove(0);
5509 let will_fail = client_a.clone();
5510 client_a.shutdown().await.unwrap();
5511 assert!(
5512 tracker
5513 .ensure_open_for_test(&path, &id_a, &will_fail)
5514 .await
5515 .is_err()
5516 );
5517 assert_eq!(
5518 pending_servers(&tracker, &path),
5519 HashSet::from([ServerId::from_static("a")])
5520 );
5521 }
5522}