use std::collections::BTreeMap;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;
use crate::fs::{DirEntry, FileSystem, PathKind};
#[derive(Debug, Clone, Default)]
pub struct BufferFs<F> {
base: F,
buffers: BTreeMap<PathBuf, Arc<str>>,
}
impl<F: FileSystem> BufferFs<F> {
#[must_use]
pub const fn new(base: F, buffers: BTreeMap<PathBuf, Arc<str>>) -> Self {
Self { base, buffers }
}
#[must_use]
pub fn with_buffer(mut self, path: impl Into<PathBuf>, text: impl Into<Arc<str>>) -> Self {
self.buffers.insert(path.into(), text.into());
self
}
#[must_use]
pub const fn base(&self) -> &F {
&self.base
}
fn buffer(&self, path: &Path) -> Option<&Arc<str>> {
self.buffers.get(path).filter(|_| self.base.kind(path).is_file)
}
}
impl<F: FileSystem> FileSystem for BufferFs<F> {
fn read_to_string(&self, path: &Path) -> io::Result<Arc<str>> {
match self.buffer(path) {
Some(text) => Ok(Arc::clone(text)),
None => self.base.read_to_string(path),
}
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
self.base.read_dir(path)
}
fn read_dir_shared(&self, path: &Path) -> io::Result<Arc<[DirEntry]>> {
self.base.read_dir_shared(path)
}
fn is_file(&self, path: &Path) -> bool {
self.base.is_file(path)
}
fn is_dir(&self, path: &Path) -> bool {
self.base.is_dir(path)
}
fn kind(&self, path: &Path) -> PathKind {
self.base.kind(path)
}
fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
self.base.canonicalize(path)
}
fn read_bytes(&self, path: &Path) -> io::Result<Vec<u8>> {
match self.buffer(path) {
Some(text) => Ok(text.as_bytes().to_vec()),
None => self.base.read_bytes(path),
}
}
fn modified(&self, path: &Path) -> Option<SystemTime> {
self.base.modified(path)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::fs::MemoryFs;
fn overlay() -> BufferFs<MemoryFs> {
let base = MemoryFs::new()
.with_file("/ws/package.json", "{}")
.with_file("/ws/src/a.ts", "saved");
BufferFs::new(base, BTreeMap::new())
.with_buffer("/ws/src/a.ts", "unsaved")
.with_buffer("/ws/src/new.ts", "never saved")
}
#[test]
fn a_buffer_replaces_the_text_of_an_existing_file() {
let fs = overlay();
assert_eq!(&*fs.read_to_string(Path::new("/ws/src/a.ts")).unwrap(), "unsaved");
assert_eq!(fs.read_bytes(Path::new("/ws/src/a.ts")).unwrap(), b"unsaved");
assert_eq!(&*fs.read_to_string(Path::new("/ws/package.json")).unwrap(), "{}");
}
#[test]
fn a_buffer_without_a_file_is_ignored() {
let fs = overlay();
let error = fs.read_to_string(Path::new("/ws/src/new.ts")).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::NotFound);
assert!(!fs.is_file(Path::new("/ws/src/new.ts")));
}
#[test]
fn listings_and_kinds_come_from_the_base() {
let fs = overlay();
let listed: Vec<PathBuf> = fs
.read_dir(Path::new("/ws/src"))
.unwrap()
.into_iter()
.map(|entry| entry.path)
.collect();
assert_eq!(listed, [PathBuf::from("/ws/src/a.ts")]);
assert_eq!(fs.kind(Path::new("/ws/src")), PathKind::DIR);
assert_eq!(fs.kind(Path::new("/ws/src/a.ts")), PathKind::FILE);
}
}