Document vfs public items

This commit is contained in:
Arnaud 2021-01-12 17:22:57 +01:00
parent 52fa926f00
commit 311ec70d03
5 changed files with 204 additions and 2 deletions

View file

@ -26,14 +26,24 @@
//! from the anchor than. //! from the anchor than.
use crate::FileId; use crate::FileId;
/// Path relative to a file.
///
/// Owned version of [`AnchoredPath`].
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct AnchoredPathBuf { pub struct AnchoredPathBuf {
/// File that this path is relative to.
pub anchor: FileId, pub anchor: FileId,
/// Path relative to `anchor`'s containing directory.
pub path: String, pub path: String,
} }
/// Path relative to a file.
///
/// Borrowed version of [`AnchoredPathBuf`].
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct AnchoredPath<'a> { pub struct AnchoredPath<'a> {
/// File that this path is relative to.
pub anchor: FileId, pub anchor: FileId,
/// Path relative to `anchor`'s containing directory.
pub path: &'a str, pub path: &'a str,
} }

View file

@ -9,6 +9,7 @@ use rustc_hash::FxHashMap;
use crate::{AnchoredPath, FileId, Vfs, VfsPath}; use crate::{AnchoredPath, FileId, Vfs, VfsPath};
/// A set of [`VfsPath`]s identified by [`FileId`]s.
#[derive(Default, Clone, Eq, PartialEq)] #[derive(Default, Clone, Eq, PartialEq)]
pub struct FileSet { pub struct FileSet {
files: FxHashMap<VfsPath, FileId>, files: FxHashMap<VfsPath, FileId>,
@ -16,9 +17,15 @@ pub struct FileSet {
} }
impl FileSet { impl FileSet {
/// Returns the number of stored paths.
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.files.len() self.files.len()
} }
/// Get the id of the file corresponding to `path`.
///
/// If either `path`'s [`anchor`](AnchoredPath::anchor) or the resolved path is not in
/// the set, returns [`None`].
pub fn resolve_path(&self, path: AnchoredPath<'_>) -> Option<FileId> { pub fn resolve_path(&self, path: AnchoredPath<'_>) -> Option<FileId> {
let mut base = self.paths[&path.anchor].clone(); let mut base = self.paths[&path.anchor].clone();
base.pop(); base.pop();
@ -26,19 +33,26 @@ impl FileSet {
self.files.get(&path).copied() self.files.get(&path).copied()
} }
/// Get the id corresponding to `path` if it exists in the set.
pub fn file_for_path(&self, path: &VfsPath) -> Option<&FileId> { pub fn file_for_path(&self, path: &VfsPath) -> Option<&FileId> {
self.files.get(path) self.files.get(path)
} }
/// Get the path corresponding to `file` if it exists in the set.
pub fn path_for_file(&self, file: &FileId) -> Option<&VfsPath> { pub fn path_for_file(&self, file: &FileId) -> Option<&VfsPath> {
self.paths.get(file) self.paths.get(file)
} }
/// Insert the `file_id, path` pair into the set.
///
/// # Note
/// Multiple [`FileId`] can be mapped to the same [`VfsPath`], and vice-versa.
pub fn insert(&mut self, file_id: FileId, path: VfsPath) { pub fn insert(&mut self, file_id: FileId, path: VfsPath) {
self.files.insert(path.clone(), file_id); self.files.insert(path.clone(), file_id);
self.paths.insert(file_id, path); self.paths.insert(file_id, path);
} }
/// Iterate over this set's ids.
pub fn iter(&self) -> impl Iterator<Item = FileId> + '_ { pub fn iter(&self) -> impl Iterator<Item = FileId> + '_ {
self.paths.keys().copied() self.paths.keys().copied()
} }
@ -50,6 +64,23 @@ impl fmt::Debug for FileSet {
} }
} }
/// This contains path prefixes to partition a [`Vfs`] into [`FileSet`]s.
///
/// # Example
/// ```rust
/// # use vfs::{file_set::FileSetConfigBuilder, VfsPath, Vfs};
/// let mut builder = FileSetConfigBuilder::default();
/// builder.add_file_set(vec![VfsPath::new_virtual_path("/src".to_string())]);
/// let config = builder.build();
/// let mut file_system = Vfs::default();
/// file_system.set_file_contents(VfsPath::new_virtual_path("/src/main.rs".to_string()), Some(vec![]));
/// file_system.set_file_contents(VfsPath::new_virtual_path("/src/lib.rs".to_string()), Some(vec![]));
/// file_system.set_file_contents(VfsPath::new_virtual_path("/build.rs".to_string()), Some(vec![]));
/// // contains the sets :
/// // { "/src/main.rs", "/src/lib.rs" }
/// // { "build.rs" }
/// let sets = config.partition(&file_system);
/// ```
#[derive(Debug)] #[derive(Debug)]
pub struct FileSetConfig { pub struct FileSetConfig {
n_file_sets: usize, n_file_sets: usize,
@ -63,9 +94,14 @@ impl Default for FileSetConfig {
} }
impl FileSetConfig { impl FileSetConfig {
/// Returns a builder for `FileSetConfig`.
pub fn builder() -> FileSetConfigBuilder { pub fn builder() -> FileSetConfigBuilder {
FileSetConfigBuilder::default() FileSetConfigBuilder::default()
} }
/// Partition `vfs` into `FileSet`s.
///
/// Creates a new [`FileSet`] for every set of prefixes in `self`.
pub fn partition(&self, vfs: &Vfs) -> Vec<FileSet> { pub fn partition(&self, vfs: &Vfs) -> Vec<FileSet> {
let mut scratch_space = Vec::new(); let mut scratch_space = Vec::new();
let mut res = vec![FileSet::default(); self.len()]; let mut res = vec![FileSet::default(); self.len()];
@ -91,6 +127,7 @@ impl FileSetConfig {
} }
} }
/// Builder for [`FileSetConfig`].
pub struct FileSetConfigBuilder { pub struct FileSetConfigBuilder {
roots: Vec<Vec<VfsPath>>, roots: Vec<Vec<VfsPath>>,
} }
@ -102,12 +139,17 @@ impl Default for FileSetConfigBuilder {
} }
impl FileSetConfigBuilder { impl FileSetConfigBuilder {
/// Returns the number of sets currently held.
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.roots.len() self.roots.len()
} }
/// Add a new set of paths prefixes.
pub fn add_file_set(&mut self, roots: Vec<VfsPath>) { pub fn add_file_set(&mut self, roots: Vec<VfsPath>) {
self.roots.push(roots) self.roots.push(roots)
} }
/// Build the `FileSetConfig`.
pub fn build(self) -> FileSetConfig { pub fn build(self) -> FileSetConfig {
let n_file_sets = self.roots.len() + 1; let n_file_sets = self.roots.len() + 1;
let map = { let map = {

View file

@ -53,9 +53,15 @@ pub use crate::{
}; };
pub use paths::{AbsPath, AbsPathBuf}; pub use paths::{AbsPath, AbsPathBuf};
/// 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)] #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct FileId(pub u32); pub struct FileId(pub u32);
/// Storage for all files read by rust-analyzer.
///
/// For more informations see the [crate-level](crate) documentation.
#[derive(Default)] #[derive(Default)]
pub struct Vfs { pub struct Vfs {
interner: PathInterner, interner: PathInterner,
@ -63,40 +69,73 @@ pub struct Vfs {
changes: Vec<ChangedFile>, changes: Vec<ChangedFile>,
} }
/// Changed file in the [`Vfs`].
pub struct ChangedFile { pub struct ChangedFile {
/// Id of the changed file
pub file_id: FileId, pub file_id: FileId,
/// Kind of change
pub change_kind: ChangeKind, pub change_kind: ChangeKind,
} }
impl ChangedFile { impl ChangedFile {
/// Returns `true` if the change is not [`Delete`](ChangeKind::Delete).
pub fn exists(&self) -> bool { pub fn exists(&self) -> bool {
self.change_kind != ChangeKind::Delete self.change_kind != ChangeKind::Delete
} }
/// Returns `true` if the change is [`Create`](ChangeKind::Create) or
/// [`Delete`](ChangeKind::Delete).
pub fn is_created_or_deleted(&self) -> bool { pub fn is_created_or_deleted(&self) -> bool {
matches!(self.change_kind, ChangeKind::Create | ChangeKind::Delete) matches!(self.change_kind, ChangeKind::Create | ChangeKind::Delete)
} }
} }
/// Kind of [file change](ChangedFile).
#[derive(Eq, PartialEq, Copy, Clone, Debug)] #[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub enum ChangeKind { pub enum ChangeKind {
/// The file was (re-)created
Create, Create,
/// The file was modified
Modify, Modify,
/// The file was deleted
Delete, Delete,
} }
impl Vfs { impl Vfs {
/// Amount of files currently stored.
///
/// Note that this includes deleted files.
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.data.len() self.data.len()
} }
/// Id of the given path if it exists in the `Vfs` and is not deleted.
pub fn file_id(&self, path: &VfsPath) -> Option<FileId> { pub fn file_id(&self, path: &VfsPath) -> Option<FileId> {
self.interner.get(path).filter(|&it| self.get(it).is_some()) self.interner.get(path).filter(|&it| self.get(it).is_some())
} }
/// 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 { pub fn file_path(&self, file_id: FileId) -> VfsPath {
self.interner.lookup(file_id).clone() self.interner.lookup(file_id).clone()
} }
/// File content corresponding to the given `file_id`.
///
/// # Panics
///
/// Panics if the id is not present in the `Vfs`, or if the corresponding file is
/// deleted.
pub fn file_contents(&self, file_id: FileId) -> &[u8] { pub fn file_contents(&self, file_id: FileId) -> &[u8] {
self.get(file_id).as_deref().unwrap() self.get(file_id).as_deref().unwrap()
} }
/// 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)> + '_ { pub fn iter(&self) -> impl Iterator<Item = (FileId, &VfsPath)> + '_ {
(0..self.data.len()) (0..self.data.len())
.map(|it| FileId(it as u32)) .map(|it| FileId(it as u32))
@ -106,6 +145,13 @@ impl Vfs {
(file_id, path) (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 { pub fn set_file_contents(&mut self, path: VfsPath, contents: Option<Vec<u8>>) -> bool {
let file_id = self.alloc_file_id(path); let file_id = self.alloc_file_id(path);
let change_kind = match (&self.get(file_id), &contents) { let change_kind = match (&self.get(file_id), &contents) {
@ -120,9 +166,13 @@ impl Vfs {
self.changes.push(ChangedFile { file_id, change_kind }); self.changes.push(ChangedFile { file_id, change_kind });
true true
} }
/// Returns `true` if the `Vfs` contains [changes](ChangedFile).
pub fn has_changes(&self) -> bool { pub fn has_changes(&self) -> bool {
!self.changes.is_empty() !self.changes.is_empty()
} }
/// Drain and returns all the changes in the `Vfs`.
pub fn take_changes(&mut self) -> Vec<ChangedFile> { pub fn take_changes(&mut self) -> Vec<ChangedFile> {
mem::take(&mut self.changes) mem::take(&mut self.changes)
} }

View file

@ -3,9 +3,12 @@ use std::fmt;
use paths::{AbsPath, AbsPathBuf}; use paths::{AbsPath, AbsPathBuf};
/// A set of files on the file system.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Entry { pub enum Entry {
/// The `Entry` is represented by a raw set of files.
Files(Vec<AbsPathBuf>), Files(Vec<AbsPathBuf>),
/// The `Entry` is represented by `Directories`.
Directories(Directories), Directories(Directories),
} }
@ -17,6 +20,8 @@ pub enum Entry {
/// * it is not under `exclude` path /// * it is not under `exclude` path
/// ///
/// If many include/exclude paths match, the longest one wins. /// If many include/exclude paths match, the longest one wins.
///
/// If a path is in both `include` and `exclude`, the `exclude` one wins.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Directories { pub struct Directories {
pub extensions: Vec<String>, pub extensions: Vec<String>,
@ -24,45 +29,99 @@ pub struct Directories {
pub exclude: Vec<AbsPathBuf>, pub exclude: Vec<AbsPathBuf>,
} }
/// [`Handle`]'s configuration.
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
/// Set of initially loaded files.
pub load: Vec<Entry>, pub load: Vec<Entry>,
/// Index of watched entries in `load`.
///
/// If a path in a watched entry is modified,the [`Handle`] should notify it.
pub watch: Vec<usize>, pub watch: Vec<usize>,
} }
/// Message about an action taken by a [`Handle`].
pub enum Message { pub enum Message {
/// Indicate a gradual progress.
///
/// This is supposed to be the number of loaded files.
Progress { n_total: usize, n_done: usize }, Progress { n_total: usize, n_done: usize },
/// The handle loaded the following files' content.
Loaded { files: Vec<(AbsPathBuf, Option<Vec<u8>>)> }, Loaded { files: Vec<(AbsPathBuf, Option<Vec<u8>>)> },
} }
/// Type that will receive [`Messages`](Message) from a [`Handle`].
pub type Sender = Box<dyn Fn(Message) + Send>; pub type Sender = Box<dyn Fn(Message) + Send>;
/// Interface for reading and watching files.
pub trait Handle: fmt::Debug { pub trait Handle: fmt::Debug {
/// Spawn a new handle with the given `sender`.
fn spawn(sender: Sender) -> Self fn spawn(sender: Sender) -> Self
where where
Self: Sized; Self: Sized;
/// Set this handle's configuration.
fn set_config(&mut self, config: Config); fn set_config(&mut self, config: Config);
/// The file's content at `path` has been modified, and should be reloaded.
fn invalidate(&mut self, path: AbsPathBuf); fn invalidate(&mut self, path: AbsPathBuf);
/// Load the content of the given file, returning [`None`] if it does not
/// exists.
fn load_sync(&mut self, path: &AbsPath) -> Option<Vec<u8>>; fn load_sync(&mut self, path: &AbsPath) -> Option<Vec<u8>>;
} }
impl Entry { impl Entry {
/// Returns:
/// ```text
/// Entry::Directories(Directories {
/// extensions: ["rs"],
/// include: [base],
/// exclude: [base/.git],
/// })
/// ```
pub fn rs_files_recursively(base: AbsPathBuf) -> Entry { pub fn rs_files_recursively(base: AbsPathBuf) -> Entry {
Entry::Directories(dirs(base, &[".git"])) Entry::Directories(dirs(base, &[".git"]))
} }
/// Returns:
/// ```text
/// Entry::Directories(Directories {
/// extensions: ["rs"],
/// include: [base],
/// exclude: [base/.git, base/target],
/// })
/// ```
pub fn local_cargo_package(base: AbsPathBuf) -> Entry { pub fn local_cargo_package(base: AbsPathBuf) -> Entry {
Entry::Directories(dirs(base, &[".git", "target"])) Entry::Directories(dirs(base, &[".git", "target"]))
} }
/// Returns:
/// ```text
/// Entry::Directories(Directories {
/// extensions: ["rs"],
/// include: [base],
/// exclude: [base/.git, /tests, /examples, /benches],
/// })
/// ```
pub fn cargo_package_dependency(base: AbsPathBuf) -> Entry { pub fn cargo_package_dependency(base: AbsPathBuf) -> Entry {
Entry::Directories(dirs(base, &[".git", "/tests", "/examples", "/benches"])) Entry::Directories(dirs(base, &[".git", "/tests", "/examples", "/benches"]))
} }
/// Returns `true` if `path` is included in `self`.
///
/// See [`Directories::contains_file`].
pub fn contains_file(&self, path: &AbsPath) -> bool { pub fn contains_file(&self, path: &AbsPath) -> bool {
match self { match self {
Entry::Files(files) => files.iter().any(|it| it == path), Entry::Files(files) => files.iter().any(|it| it == path),
Entry::Directories(dirs) => dirs.contains_file(path), Entry::Directories(dirs) => dirs.contains_file(path),
} }
} }
/// Returns `true` if `path` is included in `self`.
///
/// - If `self` is `Entry::Files`, returns `false`
/// - Else, see [`Directories::contains_dir`].
pub fn contains_dir(&self, path: &AbsPath) -> bool { pub fn contains_dir(&self, path: &AbsPath) -> bool {
match self { match self {
Entry::Files(_) => false, Entry::Files(_) => false,
@ -72,6 +131,7 @@ impl Entry {
} }
impl Directories { impl Directories {
/// Returns `true` if `path` is included in `self`.
pub fn contains_file(&self, path: &AbsPath) -> bool { pub fn contains_file(&self, path: &AbsPath) -> bool {
let ext = path.extension().unwrap_or_default(); let ext = path.extension().unwrap_or_default();
if self.extensions.iter().all(|it| it.as_str() != ext) { if self.extensions.iter().all(|it| it.as_str() != ext) {
@ -79,6 +139,11 @@ impl Directories {
} }
self.includes_path(path) self.includes_path(path)
} }
/// Returns `true` if `path` is included in `self`.
///
/// Since `path` is supposed to be a directory, this will not take extension
/// into account.
pub fn contains_dir(&self, path: &AbsPath) -> bool { pub fn contains_dir(&self, path: &AbsPath) -> bool {
self.includes_path(path) self.includes_path(path)
} }

View file

@ -3,25 +3,37 @@ use std::fmt;
use paths::{AbsPath, AbsPathBuf}; use paths::{AbsPath, AbsPathBuf};
/// Path in [`Vfs`].
///
/// Long-term, we want to support files which do not reside in the file-system, /// Long-term, we want to support files which do not reside in the file-system,
/// so we treat VfsPaths as opaque identifiers. /// so we treat `VfsPath`s as opaque identifiers.
///
/// [`Vfs`]: crate::Vfs
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct VfsPath(VfsPathRepr); pub struct VfsPath(VfsPathRepr);
impl VfsPath { impl VfsPath {
/// Creates an "in-memory" path from `/`-separates string. /// Creates an "in-memory" path from `/`-separated string.
///
/// This is most useful for testing, to avoid windows/linux differences /// This is most useful for testing, to avoid windows/linux differences
///
/// # Panics
///
/// Panics if `path` does not start with `'/'`.
pub fn new_virtual_path(path: String) -> VfsPath { pub fn new_virtual_path(path: String) -> VfsPath {
assert!(path.starts_with('/')); assert!(path.starts_with('/'));
VfsPath(VfsPathRepr::VirtualPath(VirtualPath(path))) VfsPath(VfsPathRepr::VirtualPath(VirtualPath(path)))
} }
/// Returns the `AbsPath` representation of `self` if `self` is on the file system.
pub fn as_path(&self) -> Option<&AbsPath> { pub fn as_path(&self) -> Option<&AbsPath> {
match &self.0 { match &self.0 {
VfsPathRepr::PathBuf(it) => Some(it.as_path()), VfsPathRepr::PathBuf(it) => Some(it.as_path()),
VfsPathRepr::VirtualPath(_) => None, VfsPathRepr::VirtualPath(_) => None,
} }
} }
/// Creates a new `VfsPath` with `path` adjoined to `self`.
pub fn join(&self, path: &str) -> Option<VfsPath> { pub fn join(&self, path: &str) -> Option<VfsPath> {
match &self.0 { match &self.0 {
VfsPathRepr::PathBuf(it) => { VfsPathRepr::PathBuf(it) => {
@ -34,12 +46,30 @@ impl VfsPath {
} }
} }
} }
/// Remove the last component of `self` if there is one.
///
/// If `self` has no component, returns `false`; else returns `true`.
///
/// # Example
///
/// ```
/// # use vfs::{AbsPathBuf, VfsPath};
/// let mut path = VfsPath::from(AbsPathBuf::assert("/foo/bar".into()));
/// assert!(path.pop());
/// assert_eq!(path, VfsPath::from(AbsPathBuf::assert("/foo".into())));
/// assert!(path.pop());
/// assert_eq!(path, VfsPath::from(AbsPathBuf::assert("/".into())));
/// assert!(!path.pop());
/// ```
pub fn pop(&mut self) -> bool { pub fn pop(&mut self) -> bool {
match &mut self.0 { match &mut self.0 {
VfsPathRepr::PathBuf(it) => it.pop(), VfsPathRepr::PathBuf(it) => it.pop(),
VfsPathRepr::VirtualPath(it) => it.pop(), VfsPathRepr::VirtualPath(it) => it.pop(),
} }
} }
/// Returns `true` if `other` is a prefix of `self`.
pub fn starts_with(&self, other: &VfsPath) -> bool { pub fn starts_with(&self, other: &VfsPath) -> bool {
match (&self.0, &other.0) { match (&self.0, &other.0) {
(VfsPathRepr::PathBuf(lhs), VfsPathRepr::PathBuf(rhs)) => lhs.starts_with(rhs), (VfsPathRepr::PathBuf(lhs), VfsPathRepr::PathBuf(rhs)) => lhs.starts_with(rhs),
@ -48,6 +78,10 @@ impl VfsPath {
(VfsPathRepr::VirtualPath(_), _) => false, (VfsPathRepr::VirtualPath(_), _) => false,
} }
} }
/// Returns the `VfsPath` without its final component, if there is one.
///
/// Returns [`None`] if the path is a root or prefix.
pub fn parent(&self) -> Option<VfsPath> { pub fn parent(&self) -> Option<VfsPath> {
let mut parent = self.clone(); let mut parent = self.clone();
if parent.pop() { if parent.pop() {
@ -57,6 +91,7 @@ impl VfsPath {
} }
} }
/// Returns `self`'s base name and file extension.
pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> { pub fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
match &self.0 { match &self.0 {
VfsPathRepr::PathBuf(p) => Some(( VfsPathRepr::PathBuf(p) => Some((