From 311ec70d03c27b1b37457ef44510e735fcce0885 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 12 Jan 2021 17:22:57 +0100 Subject: Document vfs public items --- crates/vfs/src/anchored_path.rs | 10 +++++++ crates/vfs/src/file_set.rs | 42 ++++++++++++++++++++++++++ crates/vfs/src/lib.rs | 50 +++++++++++++++++++++++++++++++ crates/vfs/src/loader.rs | 65 +++++++++++++++++++++++++++++++++++++++++ crates/vfs/src/vfs_path.rs | 39 +++++++++++++++++++++++-- 5 files changed, 204 insertions(+), 2 deletions(-) (limited to 'crates') diff --git a/crates/vfs/src/anchored_path.rs b/crates/vfs/src/anchored_path.rs index 02720a32e..db15a2a21 100644 --- a/crates/vfs/src/anchored_path.rs +++ b/crates/vfs/src/anchored_path.rs @@ -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, } diff --git a/crates/vfs/src/file_set.rs b/crates/vfs/src/file_set.rs index 49ca593ac..348b6dbfd 100644 --- a/crates/vfs/src/file_set.rs +++ b/crates/vfs/src/file_set.rs @@ -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, @@ -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 { 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 + '_ { 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)] pub struct FileSetConfig { n_file_sets: usize, @@ -63,9 +94,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 { let mut scratch_space = Vec::new(); let mut res = vec![FileSet::default(); self.len()]; @@ -91,6 +127,7 @@ impl FileSetConfig { } } +/// Builder for [`FileSetConfig`]. pub struct FileSetConfigBuilder { roots: Vec>, } @@ -102,12 +139,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) { self.roots.push(roots) } + + /// Build the `FileSetConfig`. pub fn build(self) -> FileSetConfig { let n_file_sets = self.roots.len() + 1; let map = { diff --git a/crates/vfs/src/lib.rs b/crates/vfs/src/lib.rs index 2b7b14524..bae2c6118 100644 --- a/crates/vfs/src/lib.rs +++ b/crates/vfs/src/lib.rs @@ -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, } +/// 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 { 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 + '_ { (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>) -> bool { let file_id = self.alloc_file_id(path); let change_kind = match (&self.get(file_id), &contents) { @@ -120,9 +166,13 @@ 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 { mem::take(&mut self.changes) } diff --git a/crates/vfs/src/loader.rs b/crates/vfs/src/loader.rs index 40cf96020..399015043 100644 --- a/crates/vfs/src/loader.rs +++ b/crates/vfs/src/loader.rs @@ -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), + /// 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, @@ -24,45 +29,99 @@ pub struct Directories { pub exclude: Vec, } +/// [`Handle`]'s configuration. #[derive(Debug)] pub struct Config { + /// Set of initially loaded files. pub load: Vec, + /// Index of watched entries in `load`. + /// + /// If a path in a watched entry is modified,the [`Handle`] should notify it. pub watch: Vec, } +/// 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>)> }, } +/// Type that will receive [`Messages`](Message) from a [`Handle`]. pub type Sender = Box; +/// 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>; } 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,6 +139,11 @@ 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) } diff --git a/crates/vfs/src/vfs_path.rs b/crates/vfs/src/vfs_path.rs index bd14911c9..74b6333e2 100644 --- a/crates/vfs/src/vfs_path.rs +++ b/crates/vfs/src/vfs_path.rs @@ -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 { 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 { 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(( -- cgit v1.2.3 From 4b71c8332daf9ef461891d278f2cf3530baaa833 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 12 Jan 2021 17:41:45 +0100 Subject: Document vfs private items --- crates/vfs/src/file_set.rs | 15 +++++++++++ crates/vfs/src/lib.rs | 20 +++++++++++++++ crates/vfs/src/loader.rs | 15 +++++++++++ crates/vfs/src/path_interner.rs | 14 +++++++++++ crates/vfs/src/vfs_path.rs | 55 +++++++++++++++++++++++++++++++++++++++-- 5 files changed, 117 insertions(+), 2 deletions(-) (limited to 'crates') diff --git a/crates/vfs/src/file_set.rs b/crates/vfs/src/file_set.rs index 348b6dbfd..0a4590c8d 100644 --- a/crates/vfs/src/file_set.rs +++ b/crates/vfs/src/file_set.rs @@ -83,7 +83,12 @@ impl fmt::Debug for FileSet { /// ``` #[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>, } @@ -111,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) -> usize { scratch_space.clear(); path.encode(scratch_space); @@ -169,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 } } diff --git a/crates/vfs/src/lib.rs b/crates/vfs/src/lib.rs index bae2c6118..e075d752b 100644 --- a/crates/vfs/src/lib.rs +++ b/crates/vfs/src/lib.rs @@ -176,6 +176,14 @@ impl Vfs { pub fn take_changes(&mut self) -> Vec { 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; @@ -183,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> { &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> { &mut self.data[file_id.0 as usize] } diff --git a/crates/vfs/src/loader.rs b/crates/vfs/src/loader.rs index 399015043..d3bdae562 100644 --- a/crates/vfs/src/loader.rs +++ b/crates/vfs/src/loader.rs @@ -147,6 +147,13 @@ impl Directories { 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 { @@ -170,6 +177,14 @@ impl Directories { } } +/// Returns : +/// ```text +/// Directories { +/// extensions: ["rs"], +/// include: [base], +/// exclude: [base/], +/// } +/// ``` fn dirs(base: AbsPathBuf, exclude: &[&str]) -> Directories { let exclude = exclude.iter().map(|it| base.join(it)).collect::>(); Directories { extensions: vec!["rs".to_string()], include: vec![base], exclude } diff --git a/crates/vfs/src/path_interner.rs b/crates/vfs/src/path_interner.rs index 4f70d61e8..2189e5e25 100644 --- a/crates/vfs/src/path_interner.rs +++ b/crates/vfs/src/path_interner.rs @@ -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, @@ -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 { 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] } diff --git a/crates/vfs/src/vfs_path.rs b/crates/vfs/src/vfs_path.rs index 74b6333e2..2b3d7fd84 100644 --- a/crates/vfs/src/vfs_path.rs +++ b/crates/vfs/src/vfs_path.rs @@ -102,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) { let tag = match &self.0 { VfsPathRepr::PathBuf(_) => 0, @@ -259,6 +266,7 @@ mod windows_paths { } } +/// Internal, private representation of [`VfsPath`]. #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] enum VfsPathRepr { PathBuf(AbsPathBuf), @@ -295,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, @@ -310,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 { let mut res = self.clone(); while path.starts_with("../") { @@ -322,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..], -- cgit v1.2.3