Skip to content

Commit 6b21758

Browse files
bors[bot]matklad
andauthored
Merge #4891
4891: New VFS API r=matklad a=matklad cc @flodiebold , @jonas-schievink Co-authored-by: Aleksey Kladov <[email protected]>
2 parents 4342b71 + c002322 commit 6b21758

File tree

8 files changed

+520
-0
lines changed

8 files changed

+520
-0
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vfs/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "vfs"
3+
version = "0.1.0"
4+
authors = ["rust-analyzer developers"]
5+
edition = "2018"
6+
7+
[dependencies]
8+
rustc-hash = "1.0"
9+
jod-thread = "0.1.0"
10+
walkdir = "2.3.1"
11+
globset = "0.4.5"
12+
crossbeam-channel = "0.4.0"
13+
14+
paths = { path = "../paths" }

crates/vfs/src/file_set.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//! Partitions a list of files into disjoint subsets.
2+
//!
3+
//! Files which do not belong to any explicitly configured `FileSet` belong to
4+
//! the default `FileSet`.
5+
use std::{cmp, fmt, iter};
6+
7+
use paths::AbsPathBuf;
8+
use rustc_hash::FxHashMap;
9+
10+
use crate::{FileId, Vfs, VfsPath};
11+
12+
#[derive(Default, Clone, Eq, PartialEq)]
13+
pub struct FileSet {
14+
files: FxHashMap<VfsPath, FileId>,
15+
paths: FxHashMap<FileId, VfsPath>,
16+
}
17+
18+
impl FileSet {
19+
pub fn resolve_path(&self, anchor: FileId, path: &str) -> Option<FileId> {
20+
let mut base = self.paths[&anchor].clone();
21+
base.pop();
22+
let path = base.join(path);
23+
let res = self.files.get(&path).copied();
24+
res
25+
}
26+
pub fn insert(&mut self, file_id: FileId, path: VfsPath) {
27+
self.files.insert(path.clone(), file_id);
28+
self.paths.insert(file_id, path);
29+
}
30+
pub fn iter(&self) -> impl Iterator<Item = FileId> + '_ {
31+
self.paths.keys().copied()
32+
}
33+
}
34+
35+
impl fmt::Debug for FileSet {
36+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37+
f.debug_struct("FileSet").field("n_files", &self.files.len()).finish()
38+
}
39+
}
40+
41+
#[derive(Debug)]
42+
pub struct FileSetConfig {
43+
n_file_sets: usize,
44+
roots: Vec<(AbsPathBuf, usize)>,
45+
}
46+
47+
impl FileSetConfig {
48+
pub fn builder() -> FileSetConfigBuilder {
49+
FileSetConfigBuilder::default()
50+
}
51+
pub fn partition(&self, vfs: &Vfs) -> Vec<FileSet> {
52+
let mut res = vec![FileSet::default(); self.len()];
53+
for (file_id, path) in vfs.iter() {
54+
let root = self.classify(&path);
55+
res[root].insert(file_id, path)
56+
}
57+
res
58+
}
59+
fn len(&self) -> usize {
60+
self.n_file_sets
61+
}
62+
fn classify(&self, path: &VfsPath) -> usize {
63+
for (root, idx) in self.roots.iter() {
64+
if let Some(path) = path.as_path() {
65+
if path.starts_with(root) {
66+
return *idx;
67+
}
68+
}
69+
}
70+
self.len() - 1
71+
}
72+
}
73+
74+
pub struct FileSetConfigBuilder {
75+
roots: Vec<Vec<AbsPathBuf>>,
76+
}
77+
78+
impl Default for FileSetConfigBuilder {
79+
fn default() -> Self {
80+
FileSetConfigBuilder { roots: Vec::new() }
81+
}
82+
}
83+
84+
impl FileSetConfigBuilder {
85+
pub fn add_file_set(&mut self, roots: Vec<AbsPathBuf>) {
86+
self.roots.push(roots)
87+
}
88+
pub fn build(self) -> FileSetConfig {
89+
let n_file_sets = self.roots.len() + 1;
90+
let mut roots: Vec<(AbsPathBuf, usize)> = self
91+
.roots
92+
.into_iter()
93+
.enumerate()
94+
.flat_map(|(i, paths)| paths.into_iter().zip(iter::repeat(i)))
95+
.collect();
96+
roots.sort_by_key(|(path, _)| cmp::Reverse(path.to_string_lossy().len()));
97+
FileSetConfig { n_file_sets, roots }
98+
}
99+
}

crates/vfs/src/lib.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//! # Virtual File System
2+
//!
3+
//! VFS stores all files read by rust-analyzer. Reading file contents from VFS
4+
//! always returns the same contents, unless VFS was explicitly modified with
5+
//! `set_file_contents`. All changes to VFS are logged, and can be retrieved via
6+
//! `take_changes` method. The pack of changes is then pushed to `salsa` and
7+
//! triggers incremental recomputation.
8+
//!
9+
//! Files in VFS are identified with `FileId`s -- interned paths. The notion of
10+
//! the path, `VfsPath` is somewhat abstract: at the moment, it is represented
11+
//! as an `std::path::PathBuf` internally, but this is an implementation detail.
12+
//!
13+
//! VFS doesn't do IO or file watching itself. For that, see the `loader`
14+
//! module. `loader::Handle` is an object-safe trait which abstracts both file
15+
//! loading and file watching. `Handle` is dynamically configured with a set of
16+
//! directory entries which should be scanned and watched. `Handle` then
17+
//! asynchronously pushes file changes. Directory entries are configured in
18+
//! free-form via list of globs, it's up to the `Handle` to interpret the globs
19+
//! in any specific way.
20+
//!
21+
//! A simple `WalkdirLoaderHandle` is provided, which doesn't implement watching
22+
//! and just scans the directory using walkdir.
23+
//!
24+
//! VFS stores a flat list of files. `FileSet` can partition this list of files
25+
//! into disjoint sets of files. Traversal-like operations (including getting
26+
//! the neighbor file by the relative path) are handled by the `FileSet`.
27+
//! `FileSet`s are also pushed to salsa and cause it to re-check `mod foo;`
28+
//! declarations when files are created or deleted.
29+
//!
30+
//! `file_set::FileSet` and `loader::Entry` play similar, but different roles.
31+
//! Both specify the "set of paths/files", one is geared towards file watching,
32+
//! the other towards salsa changes. In particular, single `file_set::FileSet`
33+
//! may correspond to several `loader::Entry`. For example, a crate from
34+
//! crates.io which uses code generation would have two `Entries` -- for sources
35+
//! in `~/.cargo`, and for generated code in `./target/debug/build`. It will
36+
//! have a single `FileSet` which unions the two sources.
37+
mod vfs_path;
38+
mod path_interner;
39+
pub mod file_set;
40+
pub mod loader;
41+
pub mod walkdir_loader;
42+
43+
use std::{fmt, mem};
44+
45+
use crate::path_interner::PathInterner;
46+
47+
pub use crate::vfs_path::VfsPath;
48+
pub use paths::{AbsPath, AbsPathBuf};
49+
50+
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
51+
pub struct FileId(pub u32);
52+
53+
#[derive(Default)]
54+
pub struct Vfs {
55+
interner: PathInterner,
56+
data: Vec<Option<Vec<u8>>>,
57+
changes: Vec<ChangedFile>,
58+
}
59+
60+
pub struct ChangedFile {
61+
pub file_id: FileId,
62+
pub change_kind: ChangeKind,
63+
}
64+
65+
impl ChangedFile {
66+
pub fn exists(&self) -> bool {
67+
self.change_kind != ChangeKind::Delete
68+
}
69+
pub fn is_created_or_deleted(&self) -> bool {
70+
matches!(self.change_kind, ChangeKind::Create | ChangeKind::Delete)
71+
}
72+
}
73+
74+
#[derive(Eq, PartialEq)]
75+
pub enum ChangeKind {
76+
Create,
77+
Modify,
78+
Delete,
79+
}
80+
81+
impl Vfs {
82+
pub fn file_id(&self, path: &VfsPath) -> Option<FileId> {
83+
self.interner.get(path).filter(|&it| self.get(it).is_some())
84+
}
85+
pub fn file_path(&self, file_id: FileId) -> VfsPath {
86+
self.interner.lookup(file_id).clone()
87+
}
88+
pub fn file_contents(&self, file_id: FileId) -> &[u8] {
89+
self.get(file_id).as_deref().unwrap()
90+
}
91+
pub fn iter(&self) -> impl Iterator<Item = (FileId, VfsPath)> + '_ {
92+
(0..self.data.len())
93+
.map(|it| FileId(it as u32))
94+
.filter(move |&file_id| self.get(file_id).is_some())
95+
.map(move |file_id| {
96+
let path = self.interner.lookup(file_id).clone();
97+
(file_id, path)
98+
})
99+
}
100+
pub fn set_file_contents(&mut self, path: VfsPath, contents: Option<Vec<u8>>) {
101+
let file_id = self.alloc_file_id(path);
102+
let change_kind = match (&self.get(file_id), &contents) {
103+
(None, None) => return,
104+
(None, Some(_)) => ChangeKind::Create,
105+
(Some(_), None) => ChangeKind::Delete,
106+
(Some(old), Some(new)) if old == new => return,
107+
(Some(_), Some(_)) => ChangeKind::Modify,
108+
};
109+
110+
*self.get_mut(file_id) = contents;
111+
self.changes.push(ChangedFile { file_id, change_kind })
112+
}
113+
pub fn has_changes(&self) -> bool {
114+
!self.changes.is_empty()
115+
}
116+
pub fn take_changes(&mut self) -> Vec<ChangedFile> {
117+
mem::take(&mut self.changes)
118+
}
119+
fn alloc_file_id(&mut self, path: VfsPath) -> FileId {
120+
let file_id = self.interner.intern(path);
121+
let idx = file_id.0 as usize;
122+
let len = self.data.len().max(idx + 1);
123+
self.data.resize_with(len, || None);
124+
file_id
125+
}
126+
fn get(&self, file_id: FileId) -> &Option<Vec<u8>> {
127+
&self.data[file_id.0 as usize]
128+
}
129+
fn get_mut(&mut self, file_id: FileId) -> &mut Option<Vec<u8>> {
130+
&mut self.data[file_id.0 as usize]
131+
}
132+
}
133+
134+
impl fmt::Debug for Vfs {
135+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136+
f.debug_struct("Vfs").field("n_files", &self.data.len()).finish()
137+
}
138+
}

crates/vfs/src/loader.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//! Object safe interface for file watching and reading.
2+
use std::fmt;
3+
4+
use paths::AbsPathBuf;
5+
6+
pub enum Entry {
7+
Files(Vec<AbsPathBuf>),
8+
Directory { path: AbsPathBuf, globs: Vec<String> },
9+
}
10+
11+
pub struct Config {
12+
pub load: Vec<Entry>,
13+
pub watch: Vec<usize>,
14+
}
15+
16+
pub enum Message {
17+
DidSwitchConfig { n_entries: usize },
18+
DidLoadAllEntries,
19+
Loaded { files: Vec<(AbsPathBuf, Option<Vec<u8>>)> },
20+
}
21+
22+
pub type Sender = Box<dyn Fn(Message) + Send>;
23+
24+
pub trait Handle: fmt::Debug {
25+
fn spawn(sender: Sender) -> Self
26+
where
27+
Self: Sized;
28+
fn set_config(&mut self, config: Config);
29+
fn invalidate(&mut self, path: AbsPathBuf);
30+
fn load_sync(&mut self, path: &AbsPathBuf) -> Option<Vec<u8>>;
31+
}
32+
33+
impl Entry {
34+
pub fn rs_files_recursively(base: AbsPathBuf) -> Entry {
35+
Entry::Directory { path: base, globs: globs(&["*.rs"]) }
36+
}
37+
pub fn local_cargo_package(base: AbsPathBuf) -> Entry {
38+
Entry::Directory { path: base, globs: globs(&["*.rs", "!/target/"]) }
39+
}
40+
pub fn cargo_package_dependency(base: AbsPathBuf) -> Entry {
41+
Entry::Directory {
42+
path: base,
43+
globs: globs(&["*.rs", "!/tests/", "!/examples/", "!/benches/"]),
44+
}
45+
}
46+
}
47+
48+
fn globs(globs: &[&str]) -> Vec<String> {
49+
globs.iter().map(|it| it.to_string()).collect()
50+
}
51+
52+
impl fmt::Debug for Message {
53+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54+
match self {
55+
Message::Loaded { files } => {
56+
f.debug_struct("Loaded").field("n_files", &files.len()).finish()
57+
}
58+
Message::DidSwitchConfig { n_entries } => {
59+
f.debug_struct("DidSwitchConfig").field("n_entries", n_entries).finish()
60+
}
61+
Message::DidLoadAllEntries => f.debug_struct("DidLoadAllEntries").finish(),
62+
}
63+
}
64+
}
65+
66+
#[test]
67+
fn handle_is_object_safe() {
68+
fn _assert(_: &dyn Handle) {}
69+
}

crates/vfs/src/path_interner.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//! Maps paths to compact integer ids. We don't care about clearings paths which
2+
//! no longer exist -- the assumption is total size of paths we ever look at is
3+
//! not too big.
4+
use rustc_hash::FxHashMap;
5+
6+
use crate::{FileId, VfsPath};
7+
8+
#[derive(Default)]
9+
pub(crate) struct PathInterner {
10+
map: FxHashMap<VfsPath, FileId>,
11+
vec: Vec<VfsPath>,
12+
}
13+
14+
impl PathInterner {
15+
pub(crate) fn get(&self, path: &VfsPath) -> Option<FileId> {
16+
self.map.get(path).copied()
17+
}
18+
pub(crate) fn intern(&mut self, path: VfsPath) -> FileId {
19+
if let Some(id) = self.get(&path) {
20+
return id;
21+
}
22+
let id = FileId(self.vec.len() as u32);
23+
self.map.insert(path.clone(), id);
24+
self.vec.push(path);
25+
id
26+
}
27+
28+
pub(crate) fn lookup(&self, id: FileId) -> &VfsPath {
29+
&self.vec[id.0 as usize]
30+
}
31+
}

0 commit comments

Comments
 (0)