Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions crates/vfs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "vfs"
version = "0.1.0"
authors = ["rust-analyzer developers"]
edition = "2018"

[dependencies]
rustc-hash = "1.0"
jod-thread = "0.1.0"
walkdir = "2.3.1"
globset = "0.4.5"
crossbeam-channel = "0.4.0"

paths = { path = "../paths" }
99 changes: 99 additions & 0 deletions crates/vfs/src/file_set.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Partitions a list of files into disjoint subsets.
//!
//! Files which do not belong to any explicitly configured `FileSet` belong to
//! the default `FileSet`.
use std::{cmp, fmt, iter};

use paths::AbsPathBuf;
use rustc_hash::FxHashMap;

use crate::{FileId, Vfs, VfsPath};

#[derive(Default, Clone, Eq, PartialEq)]
pub struct FileSet {
files: FxHashMap<VfsPath, FileId>,
paths: FxHashMap<FileId, VfsPath>,
}

impl FileSet {
pub fn resolve_path(&self, anchor: FileId, path: &str) -> Option<FileId> {
let mut base = self.paths[&anchor].clone();
base.pop();
let path = base.join(path);
let res = self.files.get(&path).copied();
res
}
pub fn insert(&mut self, file_id: FileId, path: VfsPath) {
self.files.insert(path.clone(), file_id);
self.paths.insert(file_id, path);
}
pub fn iter(&self) -> impl Iterator<Item = FileId> + '_ {
self.paths.keys().copied()
}
}

impl fmt::Debug for FileSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FileSet").field("n_files", &self.files.len()).finish()
}
}

#[derive(Debug)]
pub struct FileSetConfig {
n_file_sets: usize,
roots: Vec<(AbsPathBuf, usize)>,
}

impl FileSetConfig {
pub fn builder() -> FileSetConfigBuilder {
FileSetConfigBuilder::default()
}
pub fn partition(&self, vfs: &Vfs) -> Vec<FileSet> {
let mut res = vec![FileSet::default(); self.len()];
for (file_id, path) in vfs.iter() {
let root = self.classify(&path);
res[root].insert(file_id, path)
}
res
}
fn len(&self) -> usize {
self.n_file_sets
}
fn classify(&self, path: &VfsPath) -> usize {
for (root, idx) in self.roots.iter() {
if let Some(path) = path.as_path() {
if path.starts_with(root) {
return *idx;
}
}
}
self.len() - 1
}
}

pub struct FileSetConfigBuilder {
roots: Vec<Vec<AbsPathBuf>>,
}

impl Default for FileSetConfigBuilder {
fn default() -> Self {
FileSetConfigBuilder { roots: Vec::new() }
}
}

impl FileSetConfigBuilder {
pub fn add_file_set(&mut self, roots: Vec<AbsPathBuf>) {
self.roots.push(roots)
}
pub fn build(self) -> FileSetConfig {
let n_file_sets = self.roots.len() + 1;
let mut roots: Vec<(AbsPathBuf, usize)> = self
.roots
.into_iter()
.enumerate()
.flat_map(|(i, paths)| paths.into_iter().zip(iter::repeat(i)))
.collect();
roots.sort_by_key(|(path, _)| cmp::Reverse(path.to_string_lossy().len()));
FileSetConfig { n_file_sets, roots }
}
}
138 changes: 138 additions & 0 deletions crates/vfs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//! # Virtual File System
//!
//! VFS stores all files read by rust-analyzer. Reading file contents from VFS
//! always returns the same contents, unless VFS was explicitly modified with
//! `set_file_contents`. All changes to VFS 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.
//!
//! A simple `WalkdirLoaderHandle` is provided, which doesn't implement watching
//! and just scans the directory using walkdir.
//!
//! VFS stores a flat list of files. `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.
//!
//! `file_set::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 `file_set::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.
mod vfs_path;
mod path_interner;
pub mod file_set;
pub mod loader;
pub mod walkdir_loader;

use std::{fmt, mem};

use crate::path_interner::PathInterner;

pub use crate::vfs_path::VfsPath;
pub use paths::{AbsPath, AbsPathBuf};

#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct FileId(pub u32);

#[derive(Default)]
pub struct Vfs {
interner: PathInterner,
data: Vec<Option<Vec<u8>>>,
changes: Vec<ChangedFile>,
}

pub struct ChangedFile {
pub file_id: FileId,
pub change_kind: ChangeKind,
}

impl ChangedFile {
pub fn exists(&self) -> bool {
self.change_kind != ChangeKind::Delete
}
pub fn is_created_or_deleted(&self) -> bool {
matches!(self.change_kind, ChangeKind::Create | ChangeKind::Delete)
}
}

#[derive(Eq, PartialEq)]
pub enum ChangeKind {
Create,
Modify,
Delete,
}

impl Vfs {
pub fn file_id(&self, path: &VfsPath) -> Option<FileId> {
self.interner.get(path).filter(|&it| self.get(it).is_some())
}
pub fn file_path(&self, file_id: FileId) -> VfsPath {
self.interner.lookup(file_id).clone()
}
pub fn file_contents(&self, file_id: FileId) -> &[u8] {
self.get(file_id).as_deref().unwrap()
}
pub fn iter(&self) -> impl Iterator<Item = (FileId, VfsPath)> + '_ {
(0..self.data.len())
.map(|it| FileId(it as u32))
.filter(move |&file_id| self.get(file_id).is_some())
.map(move |file_id| {
let path = self.interner.lookup(file_id).clone();
(file_id, path)
})
}
pub fn set_file_contents(&mut self, path: VfsPath, contents: Option<Vec<u8>>) {
let file_id = self.alloc_file_id(path);
let change_kind = match (&self.get(file_id), &contents) {
(None, None) => return,
(None, Some(_)) => ChangeKind::Create,
(Some(_), None) => ChangeKind::Delete,
(Some(old), Some(new)) if old == new => return,
(Some(_), Some(_)) => ChangeKind::Modify,
};

*self.get_mut(file_id) = contents;
self.changes.push(ChangedFile { file_id, change_kind })
}
pub fn has_changes(&self) -> bool {
!self.changes.is_empty()
}
pub fn take_changes(&mut self) -> Vec<ChangedFile> {
mem::take(&mut self.changes)
}
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_with(len, || None);
file_id
}
fn get(&self, file_id: FileId) -> &Option<Vec<u8>> {
&self.data[file_id.0 as usize]
}
fn get_mut(&mut self, file_id: FileId) -> &mut Option<Vec<u8>> {
&mut 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()
}
}
69 changes: 69 additions & 0 deletions crates/vfs/src/loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Object safe interface for file watching and reading.
use std::fmt;

use paths::AbsPathBuf;

pub enum Entry {
Files(Vec<AbsPathBuf>),
Directory { path: AbsPathBuf, globs: Vec<String> },
}

pub struct Config {
pub load: Vec<Entry>,
pub watch: Vec<usize>,
}

pub enum Message {
DidSwitchConfig { n_entries: usize },
DidLoadAllEntries,
Loaded { files: Vec<(AbsPathBuf, Option<Vec<u8>>)> },
}

pub type Sender = Box<dyn Fn(Message) + Send>;

pub trait Handle: fmt::Debug {
fn spawn(sender: Sender) -> Self
where
Self: Sized;
fn set_config(&mut self, config: Config);
fn invalidate(&mut self, path: AbsPathBuf);
fn load_sync(&mut self, path: &AbsPathBuf) -> Option<Vec<u8>>;
}

impl Entry {
pub fn rs_files_recursively(base: AbsPathBuf) -> Entry {
Entry::Directory { path: base, globs: globs(&["*.rs"]) }
}
pub fn local_cargo_package(base: AbsPathBuf) -> Entry {
Entry::Directory { path: base, globs: globs(&["*.rs", "!/target/"]) }
}
pub fn cargo_package_dependency(base: AbsPathBuf) -> Entry {
Entry::Directory {
path: base,
globs: globs(&["*.rs", "!/tests/", "!/examples/", "!/benches/"]),
}
}
}

fn globs(globs: &[&str]) -> Vec<String> {
globs.iter().map(|it| it.to_string()).collect()
}

impl fmt::Debug for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Message::Loaded { files } => {
f.debug_struct("Loaded").field("n_files", &files.len()).finish()
}
Message::DidSwitchConfig { n_entries } => {
f.debug_struct("DidSwitchConfig").field("n_entries", n_entries).finish()
}
Message::DidLoadAllEntries => f.debug_struct("DidLoadAllEntries").finish(),
}
}
}

#[test]
fn handle_is_object_safe() {
fn _assert(_: &dyn Handle) {}
}
31 changes: 31 additions & 0 deletions crates/vfs/src/path_interner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! Maps paths to compact integer ids. We don't care about clearings paths which
//! no longer exist -- the assumption is total size of paths we ever look at is
//! not too big.
use rustc_hash::FxHashMap;

use crate::{FileId, VfsPath};

#[derive(Default)]
pub(crate) struct PathInterner {
map: FxHashMap<VfsPath, FileId>,
vec: Vec<VfsPath>,
}

impl PathInterner {
pub(crate) fn get(&self, path: &VfsPath) -> Option<FileId> {
self.map.get(path).copied()
}
pub(crate) fn intern(&mut self, path: VfsPath) -> FileId {
if let Some(id) = self.get(&path) {
return id;
}
let id = FileId(self.vec.len() as u32);
self.map.insert(path.clone(), id);
self.vec.push(path);
id
}

pub(crate) fn lookup(&self, id: FileId) -> &VfsPath {
&self.vec[id.0 as usize]
}
}
Loading