vfs/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
//! # Virtual File System
//!
//! VFS records all file changes pushed to it via [`set_file_contents`].
//! As such it only ever stores changes, not the actual content of a file at any given moment.
//! All file changes are logged, and can be retrieved via
//! [`take_changes`] method. The pack of changes is then pushed to `salsa` and
//! triggers incremental recomputation.
//!
//! Files in VFS are identified with [`FileId`]s -- interned paths. The notion of
//! the path, [`VfsPath`] is somewhat abstract: at the moment, it is represented
//! as an [`std::path::PathBuf`] internally, but this is an implementation detail.
//!
//! VFS doesn't do IO or file watching itself. For that, see the [`loader`]
//! module. [`loader::Handle`] is an object-safe trait which abstracts both file
//! loading and file watching. [`Handle`] is dynamically configured with a set of
//! directory entries which should be scanned and watched. [`Handle`] then
//! asynchronously pushes file changes. Directory entries are configured in
//! free-form via list of globs, it's up to the [`Handle`] to interpret the globs
//! in any specific way.
//!
//! VFS stores a flat list of files. [`file_set::FileSet`] can partition this list
//! of files into disjoint sets of files. Traversal-like operations (including
//! getting the neighbor file by the relative path) are handled by the [`FileSet`].
//! [`FileSet`]s are also pushed to salsa and cause it to re-check `mod foo;`
//! declarations when files are created or deleted.
//!
//! [`FileSet`] and [`loader::Entry`] play similar, but different roles.
//! Both specify the "set of paths/files", one is geared towards file watching,
//! the other towards salsa changes. In particular, single [`FileSet`]
//! may correspond to several [`loader::Entry`]. For example, a crate from
//! crates.io which uses code generation would have two [`Entries`] -- for sources
//! in `~/.cargo`, and for generated code in `./target/debug/build`. It will
//! have a single [`FileSet`] which unions the two sources.
//!
//! [`set_file_contents`]: Vfs::set_file_contents
//! [`take_changes`]: Vfs::take_changes
//! [`FileSet`]: file_set::FileSet
//! [`Handle`]: loader::Handle
//! [`Entries`]: loader::Entry
mod anchored_path;
pub mod file_set;
pub mod loader;
mod path_interner;
mod vfs_path;
use std::{fmt, hash::BuildHasherDefault, mem};
use crate::path_interner::PathInterner;
pub use crate::{
anchored_path::{AnchoredPath, AnchoredPathBuf},
vfs_path::VfsPath,
};
use indexmap::{map::Entry, IndexMap};
pub use paths::{AbsPath, AbsPathBuf};
use rustc_hash::FxHasher;
use stdx::hash_once;
use tracing::{span, Level};
/// Handle to a file in [`Vfs`]
///
/// Most functions in rust-analyzer use this when they need to refer to a file.
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct FileId(u32);
// pub struct FileId(NonMaxU32);
impl FileId {
const MAX: u32 = 0x7fff_ffff;
#[inline]
pub const fn from_raw(raw: u32) -> FileId {
assert!(raw <= Self::MAX);
FileId(raw)
}
#[inline]
pub const fn index(self) -> u32 {
self.0
}
}
/// safe because `FileId` is a newtype of `u32`
impl nohash_hasher::IsEnabled for FileId {}
/// Storage for all file changes and the file id to path mapping.
///
/// For more information see the [crate-level](crate) documentation.
#[derive(Default)]
pub struct Vfs {
interner: PathInterner,
data: Vec<FileState>,
changes: IndexMap<FileId, ChangedFile, BuildHasherDefault<FxHasher>>,
}
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub enum FileState {
/// The file exists with the given content hash.
Exists(u64),
/// The file is deleted.
Deleted,
}
/// Changed file in the [`Vfs`].
#[derive(Debug)]
pub struct ChangedFile {
/// Id of the changed file
pub file_id: FileId,
/// Kind of change
pub change: Change,
}
impl ChangedFile {
/// Returns `true` if the change is not [`Delete`](ChangeKind::Delete).
pub fn exists(&self) -> bool {
!matches!(self.change, Change::Delete)
}
/// Returns `true` if the change is [`Create`](ChangeKind::Create) or
/// [`Delete`](Change::Delete).
pub fn is_created_or_deleted(&self) -> bool {
matches!(self.change, Change::Create(_, _) | Change::Delete)
}
/// Returns `true` if the change is [`Create`](ChangeKind::Create).
pub fn is_created(&self) -> bool {
matches!(self.change, Change::Create(_, _))
}
/// Returns `true` if the change is [`Modify`](ChangeKind::Modify).
pub fn is_modified(&self) -> bool {
matches!(self.change, Change::Modify(_, _))
}
pub fn kind(&self) -> ChangeKind {
match self.change {
Change::Create(_, _) => ChangeKind::Create,
Change::Modify(_, _) => ChangeKind::Modify,
Change::Delete => ChangeKind::Delete,
}
}
}
/// Kind of [file change](ChangedFile).
#[derive(Eq, PartialEq, Debug)]
pub enum Change {
/// The file was (re-)created
Create(Vec<u8>, u64),
/// The file was modified
Modify(Vec<u8>, u64),
/// The file was deleted
Delete,
}
/// Kind of [file change](ChangedFile).
#[derive(Eq, PartialEq, Debug)]
pub enum ChangeKind {
/// The file was (re-)created
Create,
/// The file was modified
Modify,
/// The file was deleted
Delete,
}
impl Vfs {
/// Id of the given path if it exists in the `Vfs` and is not deleted.
pub fn file_id(&self, path: &VfsPath) -> Option<FileId> {
self.interner.get(path).filter(|&it| matches!(self.get(it), FileState::Exists(_)))
}
/// File path corresponding to the given `file_id`.
///
/// # Panics
///
/// Panics if the id is not present in the `Vfs`.
pub fn file_path(&self, file_id: FileId) -> &VfsPath {
self.interner.lookup(file_id)
}
/// Returns an iterator over the stored ids and their corresponding paths.
///
/// This will skip deleted files.
pub fn iter(&self) -> impl Iterator<Item = (FileId, &VfsPath)> + '_ {
(0..self.data.len())
.map(|it| FileId(it as u32))
.filter(move |&file_id| matches!(self.get(file_id), FileState::Exists(_)))
.map(move |file_id| {
let path = self.interner.lookup(file_id);
(file_id, path)
})
}
/// Update the `path` with the given `contents`. `None` means the file was deleted.
///
/// Returns `true` if the file was modified, and saves the [change](ChangedFile).
///
/// If the path does not currently exists in the `Vfs`, allocates a new
/// [`FileId`] for it.
pub fn set_file_contents(&mut self, path: VfsPath, contents: Option<Vec<u8>>) -> bool {
let _p = span!(Level::INFO, "Vfs::set_file_contents").entered();
let file_id = self.alloc_file_id(path);
let state: FileState = self.get(file_id);
let change = match (state, contents) {
(FileState::Deleted, None) => return false,
(FileState::Deleted, Some(v)) => {
let hash = hash_once::<FxHasher>(&*v);
Change::Create(v, hash)
}
(FileState::Exists(_), None) => Change::Delete,
(FileState::Exists(hash), Some(v)) => {
let new_hash = hash_once::<FxHasher>(&*v);
if new_hash == hash {
return false;
}
Change::Modify(v, new_hash)
}
};
let mut set_data = |change_kind| {
self.data[file_id.0 as usize] = match change_kind {
&Change::Create(_, hash) | &Change::Modify(_, hash) => FileState::Exists(hash),
Change::Delete => FileState::Deleted,
};
};
let changed_file = ChangedFile { file_id, change };
match self.changes.entry(file_id) {
// two changes to the same file in one cycle, merge them appropriately
Entry::Occupied(mut o) => {
use Change::*;
match (&mut o.get_mut().change, changed_file.change) {
// newer `Delete` wins
(change, Delete) => *change = Delete,
// merge `Create` with `Create` or `Modify`
(Create(prev, old_hash), Create(new, new_hash) | Modify(new, new_hash)) => {
*prev = new;
*old_hash = new_hash;
}
// collapse identical `Modify`es
(Modify(prev, old_hash), Modify(new, new_hash)) => {
*prev = new;
*old_hash = new_hash;
}
// equivalent to `Modify`
(change @ Delete, Create(new, new_hash)) => {
*change = Modify(new, new_hash);
}
// shouldn't occur, but collapse into `Create`
(change @ Delete, Modify(new, new_hash)) => {
stdx::never!();
*change = Create(new, new_hash);
}
// shouldn't occur, but keep the Create
(prev @ Modify(_, _), new @ Create(_, _)) => *prev = new,
}
set_data(&o.get().change);
}
Entry::Vacant(v) => set_data(&v.insert(changed_file).change),
};
true
}
/// Drain and returns all the changes in the `Vfs`.
pub fn take_changes(&mut self) -> IndexMap<FileId, ChangedFile, BuildHasherDefault<FxHasher>> {
mem::take(&mut self.changes)
}
/// Provides a panic-less way to verify file_id validity.
pub fn exists(&self, file_id: FileId) -> bool {
matches!(self.get(file_id), FileState::Exists(_))
}
/// Returns the id associated with `path`
///
/// - If `path` does not exists in the `Vfs`, allocate a new id for it, associated with a
/// deleted file;
/// - Else, returns `path`'s id.
///
/// Does not record a change.
fn alloc_file_id(&mut self, path: VfsPath) -> FileId {
let file_id = self.interner.intern(path);
let idx = file_id.0 as usize;
let len = self.data.len().max(idx + 1);
self.data.resize(len, FileState::Deleted);
file_id
}
/// Returns the status of the file associated with the given `file_id`.
///
/// # Panics
///
/// Panics if no file is associated to that id.
fn get(&self, file_id: FileId) -> FileState {
self.data[file_id.0 as usize]
}
}
impl fmt::Debug for Vfs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Vfs").field("n_files", &self.data.len()).finish()
}
}