mirror of
https://github.com/rust-lang/rust.git
synced 2024-12-05 05:04:24 +00:00
Merge #7257
7257: vfs documentation r=matklad a=arnaudgolfouse This documents every item in the `vfs` crate, except for a few private, windows-specific items. Co-authored-by: Arnaud <arnaud.golfouse@free.fr>
This commit is contained in:
commit
d635806ea5
@ -26,14 +26,24 @@
|
||||
//! from the anchor than.
|
||||
use crate::FileId;
|
||||
|
||||
/// Path relative to a file.
|
||||
///
|
||||
/// Owned version of [`AnchoredPath`].
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct AnchoredPathBuf {
|
||||
/// File that this path is relative to.
|
||||
pub anchor: FileId,
|
||||
/// Path relative to `anchor`'s containing directory.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Path relative to a file.
|
||||
///
|
||||
/// Borrowed version of [`AnchoredPathBuf`].
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct AnchoredPath<'a> {
|
||||
/// File that this path is relative to.
|
||||
pub anchor: FileId,
|
||||
/// Path relative to `anchor`'s containing directory.
|
||||
pub path: &'a str,
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{AnchoredPath, FileId, Vfs, VfsPath};
|
||||
|
||||
/// A set of [`VfsPath`]s identified by [`FileId`]s.
|
||||
#[derive(Default, Clone, Eq, PartialEq)]
|
||||
pub struct FileSet {
|
||||
files: FxHashMap<VfsPath, FileId>,
|
||||
@ -16,9 +17,15 @@ pub struct FileSet {
|
||||
}
|
||||
|
||||
impl FileSet {
|
||||
/// Returns the number of stored paths.
|
||||
pub fn len(&self) -> usize {
|
||||
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> {
|
||||
let mut base = self.paths[&path.anchor].clone();
|
||||
base.pop();
|
||||
@ -26,19 +33,26 @@ impl FileSet {
|
||||
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> {
|
||||
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> {
|
||||
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) {
|
||||
self.files.insert(path.clone(), file_id);
|
||||
self.paths.insert(file_id, path);
|
||||
}
|
||||
|
||||
/// Iterate over this set's ids.
|
||||
pub fn iter(&self) -> impl Iterator<Item = FileId> + '_ {
|
||||
self.paths.keys().copied()
|
||||
}
|
||||
@ -50,9 +64,31 @@ 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)]
|
||||
pub struct FileSetConfig {
|
||||
/// Number of sets that `self` can partition a [`Vfs`] into.
|
||||
///
|
||||
/// This should be the number of sets in `self.map` + 1 for files that don't fit in any
|
||||
/// defined set.
|
||||
n_file_sets: usize,
|
||||
/// Map from encoded paths to the set they belong to.
|
||||
map: fst::Map<Vec<u8>>,
|
||||
}
|
||||
|
||||
@ -63,9 +99,14 @@ impl Default for FileSetConfig {
|
||||
}
|
||||
|
||||
impl FileSetConfig {
|
||||
/// Returns a builder for `FileSetConfig`.
|
||||
pub fn builder() -> FileSetConfigBuilder {
|
||||
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> {
|
||||
let mut scratch_space = Vec::new();
|
||||
let mut res = vec![FileSet::default(); self.len()];
|
||||
@ -75,9 +116,15 @@ impl FileSetConfig {
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Number of sets that `self` can partition a [`Vfs`] into.
|
||||
fn len(&self) -> usize {
|
||||
self.n_file_sets
|
||||
}
|
||||
|
||||
/// Returns the set index for the given `path`.
|
||||
///
|
||||
/// `scratch_space` is used as a buffer and will be entirely replaced.
|
||||
fn classify(&self, path: &VfsPath, scratch_space: &mut Vec<u8>) -> usize {
|
||||
scratch_space.clear();
|
||||
path.encode(scratch_space);
|
||||
@ -91,6 +138,7 @@ impl FileSetConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for [`FileSetConfig`].
|
||||
pub struct FileSetConfigBuilder {
|
||||
roots: Vec<Vec<VfsPath>>,
|
||||
}
|
||||
@ -102,12 +150,17 @@ impl Default for FileSetConfigBuilder {
|
||||
}
|
||||
|
||||
impl FileSetConfigBuilder {
|
||||
/// Returns the number of sets currently held.
|
||||
pub fn len(&self) -> usize {
|
||||
self.roots.len()
|
||||
}
|
||||
|
||||
/// Add a new set of paths prefixes.
|
||||
pub fn add_file_set(&mut self, roots: Vec<VfsPath>) {
|
||||
self.roots.push(roots)
|
||||
}
|
||||
|
||||
/// Build the `FileSetConfig`.
|
||||
pub fn build(self) -> FileSetConfig {
|
||||
let n_file_sets = self.roots.len() + 1;
|
||||
let map = {
|
||||
@ -127,11 +180,15 @@ impl FileSetConfigBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements [`fst::Automaton`]
|
||||
///
|
||||
/// It will match if `prefix_of` is a prefix of the given data.
|
||||
struct PrefixOf<'a> {
|
||||
prefix_of: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> PrefixOf<'a> {
|
||||
/// Creates a new `PrefixOf` from the given slice.
|
||||
fn new(prefix_of: &'a [u8]) -> Self {
|
||||
Self { prefix_of }
|
||||
}
|
||||
|
@ -53,9 +53,15 @@ pub use crate::{
|
||||
};
|
||||
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)]
|
||||
pub struct FileId(pub u32);
|
||||
|
||||
/// Storage for all files read by rust-analyzer.
|
||||
///
|
||||
/// For more informations see the [crate-level](crate) documentation.
|
||||
#[derive(Default)]
|
||||
pub struct Vfs {
|
||||
interner: PathInterner,
|
||||
@ -63,40 +69,73 @@ pub struct Vfs {
|
||||
changes: Vec<ChangedFile>,
|
||||
}
|
||||
|
||||
/// Changed file in the [`Vfs`].
|
||||
pub struct ChangedFile {
|
||||
/// Id of the changed file
|
||||
pub file_id: FileId,
|
||||
/// Kind of change
|
||||
pub change_kind: ChangeKind,
|
||||
}
|
||||
|
||||
impl ChangedFile {
|
||||
/// Returns `true` if the change is not [`Delete`](ChangeKind::Delete).
|
||||
pub fn exists(&self) -> bool {
|
||||
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 {
|
||||
matches!(self.change_kind, ChangeKind::Create | ChangeKind::Delete)
|
||||
}
|
||||
}
|
||||
|
||||
/// Kind of [file change](ChangedFile).
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
|
||||
pub enum ChangeKind {
|
||||
/// The file was (re-)created
|
||||
Create,
|
||||
/// The file was modified
|
||||
Modify,
|
||||
/// The file was deleted
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl Vfs {
|
||||
/// Amount of files currently stored.
|
||||
///
|
||||
/// Note that this includes deleted files.
|
||||
pub fn len(&self) -> usize {
|
||||
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> {
|
||||
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 {
|
||||
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] {
|
||||
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)> + '_ {
|
||||
(0..self.data.len())
|
||||
.map(|it| FileId(it as u32))
|
||||
@ -106,6 +145,13 @@ impl Vfs {
|
||||
(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 file_id = self.alloc_file_id(path);
|
||||
let change_kind = match (&self.get(file_id), &contents) {
|
||||
@ -120,12 +166,24 @@ impl Vfs {
|
||||
self.changes.push(ChangedFile { file_id, change_kind });
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns `true` if the `Vfs` contains [changes](ChangedFile).
|
||||
pub fn has_changes(&self) -> bool {
|
||||
!self.changes.is_empty()
|
||||
}
|
||||
|
||||
/// Drain and returns all the changes in the `Vfs`.
|
||||
pub fn take_changes(&mut self) -> Vec<ChangedFile> {
|
||||
mem::take(&mut self.changes)
|
||||
}
|
||||
|
||||
/// 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;
|
||||
@ -133,9 +191,21 @@ impl Vfs {
|
||||
self.data.resize_with(len, || None);
|
||||
file_id
|
||||
}
|
||||
|
||||
/// Returns the content associated with the given `file_id`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if no file is associated to that id.
|
||||
fn get(&self, file_id: FileId) -> &Option<Vec<u8>> {
|
||||
&self.data[file_id.0 as usize]
|
||||
}
|
||||
|
||||
/// Mutably returns the content associated with the given `file_id`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if no file is associated to that id.
|
||||
fn get_mut(&mut self, file_id: FileId) -> &mut Option<Vec<u8>> {
|
||||
&mut self.data[file_id.0 as usize]
|
||||
}
|
||||
|
@ -3,9 +3,12 @@ use std::fmt;
|
||||
|
||||
use paths::{AbsPath, AbsPathBuf};
|
||||
|
||||
/// A set of files on the file system.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Entry {
|
||||
/// The `Entry` is represented by a raw set of files.
|
||||
Files(Vec<AbsPathBuf>),
|
||||
/// The `Entry` is represented by `Directories`.
|
||||
Directories(Directories),
|
||||
}
|
||||
|
||||
@ -17,6 +20,8 @@ pub enum Entry {
|
||||
/// * it is not under `exclude` path
|
||||
///
|
||||
/// 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)]
|
||||
pub struct Directories {
|
||||
pub extensions: Vec<String>,
|
||||
@ -24,45 +29,99 @@ pub struct Directories {
|
||||
pub exclude: Vec<AbsPathBuf>,
|
||||
}
|
||||
|
||||
/// [`Handle`]'s configuration.
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
/// Set of initially loaded files.
|
||||
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>,
|
||||
}
|
||||
|
||||
/// Message about an action taken by a [`Handle`].
|
||||
pub enum Message {
|
||||
/// Indicate a gradual progress.
|
||||
///
|
||||
/// This is supposed to be the number of loaded files.
|
||||
Progress { n_total: usize, n_done: usize },
|
||||
/// The handle loaded the following files' content.
|
||||
Loaded { files: Vec<(AbsPathBuf, Option<Vec<u8>>)> },
|
||||
}
|
||||
|
||||
/// Type that will receive [`Messages`](Message) from a [`Handle`].
|
||||
pub type Sender = Box<dyn Fn(Message) + Send>;
|
||||
|
||||
/// Interface for reading and watching files.
|
||||
pub trait Handle: fmt::Debug {
|
||||
/// Spawn a new handle with the given `sender`.
|
||||
fn spawn(sender: Sender) -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Set this handle's configuration.
|
||||
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);
|
||||
|
||||
/// Load the content of the given file, returning [`None`] if it does not
|
||||
/// exists.
|
||||
fn load_sync(&mut self, path: &AbsPath) -> Option<Vec<u8>>;
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
/// Returns:
|
||||
/// ```text
|
||||
/// Entry::Directories(Directories {
|
||||
/// extensions: ["rs"],
|
||||
/// include: [base],
|
||||
/// exclude: [base/.git],
|
||||
/// })
|
||||
/// ```
|
||||
pub fn rs_files_recursively(base: AbsPathBuf) -> Entry {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
match self {
|
||||
Entry::Files(files) => files.iter().any(|it| it == 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 {
|
||||
match self {
|
||||
Entry::Files(_) => false,
|
||||
@ -72,6 +131,7 @@ impl Entry {
|
||||
}
|
||||
|
||||
impl Directories {
|
||||
/// Returns `true` if `path` is included in `self`.
|
||||
pub fn contains_file(&self, path: &AbsPath) -> bool {
|
||||
let ext = path.extension().unwrap_or_default();
|
||||
if self.extensions.iter().all(|it| it.as_str() != ext) {
|
||||
@ -79,9 +139,21 @@ impl Directories {
|
||||
}
|
||||
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 {
|
||||
self.includes_path(path)
|
||||
}
|
||||
|
||||
/// Returns `true` if `path` is included in `self`.
|
||||
///
|
||||
/// It is included if
|
||||
/// - An element in `self.include` is a prefix of `path`.
|
||||
/// - This path is longer than any element in `self.exclude` that is a prefix
|
||||
/// of `path`. In case of equality, exclusion wins.
|
||||
fn includes_path(&self, path: &AbsPath) -> bool {
|
||||
let mut include: Option<&AbsPathBuf> = None;
|
||||
for incl in &self.include {
|
||||
@ -105,6 +177,14 @@ impl Directories {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns :
|
||||
/// ```text
|
||||
/// Directories {
|
||||
/// extensions: ["rs"],
|
||||
/// include: [base],
|
||||
/// exclude: [base/<exclude>],
|
||||
/// }
|
||||
/// ```
|
||||
fn dirs(base: AbsPathBuf, exclude: &[&str]) -> Directories {
|
||||
let exclude = exclude.iter().map(|it| base.join(it)).collect::<Vec<_>>();
|
||||
Directories { extensions: vec!["rs".to_string()], include: vec![base], exclude }
|
||||
|
@ -5,6 +5,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{FileId, VfsPath};
|
||||
|
||||
/// Structure to map between [`VfsPath`] and [`FileId`].
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PathInterner {
|
||||
map: FxHashMap<VfsPath, FileId>,
|
||||
@ -12,9 +13,17 @@ pub(crate) struct PathInterner {
|
||||
}
|
||||
|
||||
impl PathInterner {
|
||||
/// Get the id corresponding to `path`.
|
||||
///
|
||||
/// If `path` does not exists in `self`, returns [`None`].
|
||||
pub(crate) fn get(&self, path: &VfsPath) -> Option<FileId> {
|
||||
self.map.get(path).copied()
|
||||
}
|
||||
|
||||
/// Insert `path` in `self`.
|
||||
///
|
||||
/// - If `path` already exists in `self`, returns its associated id;
|
||||
/// - Else, returns a newly allocated id.
|
||||
pub(crate) fn intern(&mut self, path: VfsPath) -> FileId {
|
||||
if let Some(id) = self.get(&path) {
|
||||
return id;
|
||||
@ -25,6 +34,11 @@ impl PathInterner {
|
||||
id
|
||||
}
|
||||
|
||||
/// Returns the path corresponding to `id`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `id` does not exists in `self`.
|
||||
pub(crate) fn lookup(&self, id: FileId) -> &VfsPath {
|
||||
&self.vec[id.0 as usize]
|
||||
}
|
||||
|
@ -3,25 +3,37 @@ use std::fmt;
|
||||
|
||||
use paths::{AbsPath, AbsPathBuf};
|
||||
|
||||
/// Path in [`Vfs`].
|
||||
///
|
||||
/// 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)]
|
||||
pub struct VfsPath(VfsPathRepr);
|
||||
|
||||
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
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `path` does not start with `'/'`.
|
||||
pub fn new_virtual_path(path: String) -> VfsPath {
|
||||
assert!(path.starts_with('/'));
|
||||
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> {
|
||||
match &self.0 {
|
||||
VfsPathRepr::PathBuf(it) => Some(it.as_path()),
|
||||
VfsPathRepr::VirtualPath(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `VfsPath` with `path` adjoined to `self`.
|
||||
pub fn join(&self, path: &str) -> Option<VfsPath> {
|
||||
match &self.0 {
|
||||
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 {
|
||||
match &mut self.0 {
|
||||
VfsPathRepr::PathBuf(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 {
|
||||
match (&self.0, &other.0) {
|
||||
(VfsPathRepr::PathBuf(lhs), VfsPathRepr::PathBuf(rhs)) => lhs.starts_with(rhs),
|
||||
@ -48,6 +78,10 @@ impl VfsPath {
|
||||
(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> {
|
||||
let mut parent = self.clone();
|
||||
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>)> {
|
||||
match &self.0 {
|
||||
VfsPathRepr::PathBuf(p) => Some((
|
||||
@ -67,7 +102,14 @@ impl VfsPath {
|
||||
}
|
||||
}
|
||||
|
||||
// Don't make this `pub`
|
||||
/// **Don't make this `pub`**
|
||||
///
|
||||
/// Encode the path in the given buffer.
|
||||
///
|
||||
/// The encoding will be `0` if [`AbsPathBuf`], `1` if [`VirtualPath`], followed
|
||||
/// by `self`'s representation.
|
||||
///
|
||||
/// Note that this encoding is dependent on the operating system.
|
||||
pub(crate) fn encode(&self, buf: &mut Vec<u8>) {
|
||||
let tag = match &self.0 {
|
||||
VfsPathRepr::PathBuf(_) => 0,
|
||||
@ -224,6 +266,7 @@ mod windows_paths {
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal, private representation of [`VfsPath`].
|
||||
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
||||
enum VfsPathRepr {
|
||||
PathBuf(AbsPathBuf),
|
||||
@ -260,13 +303,34 @@ impl fmt::Debug for VfsPathRepr {
|
||||
}
|
||||
}
|
||||
|
||||
/// `/`-separated virtual path.
|
||||
///
|
||||
/// This is used to describe files that do not reside on the file system.
|
||||
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
||||
struct VirtualPath(String);
|
||||
|
||||
impl VirtualPath {
|
||||
/// Returns `true` if `other` is a prefix of `self` (as strings).
|
||||
fn starts_with(&self, other: &VirtualPath) -> bool {
|
||||
self.0.starts_with(&other.0)
|
||||
}
|
||||
|
||||
/// Remove the last component of `self`.
|
||||
///
|
||||
/// This will find the last `'/'` in `self`, and remove everything after it,
|
||||
/// including the `'/'`.
|
||||
///
|
||||
/// If `self` contains no `'/'`, returns `false`; else returns `true`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let mut path = VirtualPath("/foo/bar".to_string());
|
||||
/// path.pop();
|
||||
/// assert_eq!(path.0, "/foo");
|
||||
/// path.pop();
|
||||
/// assert_eq!(path.0, "");
|
||||
/// ```
|
||||
fn pop(&mut self) -> bool {
|
||||
let pos = match self.0.rfind('/') {
|
||||
Some(pos) => pos,
|
||||
@ -275,6 +339,17 @@ impl VirtualPath {
|
||||
self.0 = self.0[..pos].to_string();
|
||||
true
|
||||
}
|
||||
|
||||
/// Append the given *relative* path `path` to `self`.
|
||||
///
|
||||
/// This will resolve any leading `"../"` in `path` before appending it.
|
||||
///
|
||||
/// Returns [`None`] if `path` has more leading `"../"` than the number of
|
||||
/// components in `self`.
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// In practice, appending here means `self/path` as strings.
|
||||
fn join(&self, mut path: &str) -> Option<VirtualPath> {
|
||||
let mut res = self.clone();
|
||||
while path.starts_with("../") {
|
||||
@ -287,7 +362,18 @@ impl VirtualPath {
|
||||
Some(res)
|
||||
}
|
||||
|
||||
pub(crate) fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
|
||||
/// Returns `self`'s base name and file extension.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `None` if `self` ends with `"//"`.
|
||||
/// - `Some((name, None))` if `self`'s base contains no `.`, or only one `.` at
|
||||
/// the start.
|
||||
/// - `Some((name, Some(extension))` else.
|
||||
///
|
||||
/// # Note
|
||||
/// The extension will not contains `.`. This means `"/foo/bar.baz.rs"` will
|
||||
/// return `Some(("bar.baz", Some("rs"))`.
|
||||
fn name_and_extension(&self) -> Option<(&str, Option<&str>)> {
|
||||
let file_path = if self.0.ends_with('/') { &self.0[..&self.0.len() - 1] } else { &self.0 };
|
||||
let file_name = match file_path.rfind('/') {
|
||||
Some(position) => &file_path[position + 1..],
|
||||
|
Loading…
Reference in New Issue
Block a user