Skip to content
This repository was archived by the owner on Aug 20, 2020. It is now read-only.

Implement Root based filtering for files and folders in Vfs #4

Merged
merged 6 commits into from
Mar 18, 2019
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
11 changes: 6 additions & 5 deletions src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use relative_path::RelativePathBuf;
use walkdir::WalkDir;
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher as _Watcher};

use crate::{Roots, VfsRoot, VfsTask};
use crate::{Roots, VfsRoot, VfsTask, roots::FileType};

pub(crate) enum Task {
AddRoot { root: VfsRoot },
Expand Down Expand Up @@ -220,14 +220,15 @@ fn handle_change(
path: PathBuf,
kind: ChangeKind,
) {
let (root, rel_path) = match roots.find(&path) {
let ft = if path.is_file() { FileType::File } else { FileType::Dir };
let (root, rel_path) = match roots.find(&path, ft) {
None => return,
Some(it) => it,
};
match kind {
ChangeKind::Create => {
let mut paths = Vec::new();
if path.is_dir() {
if ft.is_dir() {
paths.extend(watch_recursive(watcher, &path, roots, root));
} else {
paths.push(rel_path);
Expand Down Expand Up @@ -259,15 +260,15 @@ fn watch_recursive(
let mut files = Vec::new();
for entry in WalkDir::new(dir)
.into_iter()
.filter_entry(|it| roots.contains(root, it.path()).is_some())
.filter_entry(|it| roots.contains(root, it.path(), it.file_type().into()).is_some())
.filter_map(|it| it.map_err(|e| log::warn!("watcher error: {}", e)).ok())
{
if entry.file_type().is_dir() {
if let Some(watcher) = &mut watcher {
watch_one(watcher, entry.path());
}
} else {
let path = roots.contains(root, entry.path()).unwrap();
let path = roots.contains(root, entry.path(), FileType::File).unwrap();
files.push(path.to_owned());
}
}
Expand Down
72 changes: 68 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,80 @@ use std::{
};

use crossbeam_channel::Receiver;
use relative_path::{RelativePath, RelativePathBuf};
pub use relative_path::{RelativePath, RelativePathBuf};
use rustc_hash::{FxHashMap, FxHashSet};

use crate::{
io::{TaskResult, Worker},
roots::Roots,
roots::{Roots, FileType},
};

pub use crate::roots::VfsRoot;

/// a `Filter` is used to determine whether a file or a folder
/// under the specific root is included.
///
/// *NOTE*: If the parent folder of a file is not included, then
/// `include_file` will not be called.
///
/// # Example
///
/// Implementing `Filter` for rust files:
///
/// ```
/// use ra_vfs::{Filter, RelativePath};
///
/// struct IncludeRustFiles;
///
/// impl Filter for IncludeRustFiles {
/// fn include_dir(&self, dir_path: &RelativePath) -> bool {
/// // These folders are ignored
/// const IGNORED_FOLDERS: &[&str] = &["node_modules", "target", ".git"];
///
/// let is_ignored = dir_path.components().any(|c| IGNORED_FOLDERS.contains(&c.as_str()));
///
/// !is_ignored
/// }
///
/// fn include_file(&self, file_path: &RelativePath) -> bool {
/// // Only include rust files
/// file_path.extension() == Some("rs")
/// }
/// }
/// ```
pub trait Filter: Send + Sync {
fn include_dir(&self, dir_path: &RelativePath) -> bool;
fn include_file(&self, file_path: &RelativePath) -> bool;
}

/// RootEntry identifies a root folder with a given filter
/// used to determine whether to include or exclude files and folders under it.
pub struct RootEntry {
path: PathBuf,
filter: Box<dyn Filter>,
}

impl std::fmt::Debug for RootEntry {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "RootEntry({})", self.path.display())
}
}

impl Eq for RootEntry {}
impl PartialEq for RootEntry {
fn eq(&self, other: &Self) -> bool {
// Entries are equal based on their paths
self.path == other.path
}
}

impl RootEntry {
/// Create a new `RootEntry` with the given `filter` applied to
/// files and folder under it.
pub fn new(path: PathBuf, filter: Box<dyn Filter>) -> Self {
RootEntry { path, filter }
}
}
/// Opaque wrapper around file-system event.
///
/// Calling code is expected to just pass `VfsTask` to `handle_task` method. It
Expand Down Expand Up @@ -85,7 +149,7 @@ pub enum VfsChange {
}

impl Vfs {
pub fn new(roots: Vec<PathBuf>) -> (Vfs, Vec<VfsRoot>) {
pub fn new(roots: Vec<RootEntry>) -> (Vfs, Vec<VfsRoot>) {
let roots = Arc::new(Roots::new(roots));
let worker = io::start(Arc::clone(&roots));
let mut root2files = FxHashMap::default();
Expand Down Expand Up @@ -281,7 +345,7 @@ impl Vfs {
}

fn find_root(&self, path: &Path) -> Option<(VfsRoot, RelativePathBuf, Option<VfsFile>)> {
let (root, path) = self.roots.find(&path)?;
let (root, path) = self.roots.find(&path, FileType::File)?;
let file = self.find_file(root, &path);
Some((root, path, file))
}
Expand Down
172 changes: 112 additions & 60 deletions src/roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,44 @@ use std::{
path::{Path, PathBuf},
};

use relative_path::{ RelativePath, RelativePathBuf};
use relative_path::RelativePathBuf;

use super::{RootEntry, Filter};

/// VfsRoot identifies a watched directory on the file system.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct VfsRoot(pub u32);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FileType {
File,
Dir,
}

impl FileType {
pub(crate) fn is_dir(&self) -> bool {
*self == FileType::Dir
}
}

impl std::convert::From<std::fs::FileType> for FileType {
fn from(v: std::fs::FileType) -> Self {
if v.is_file() {
FileType::File
} else {
FileType::Dir
}
}
}

/// Describes the contents of a single source root.
///
/// `RootConfig` can be thought of as a glob pattern like `src/**.rs` which
/// `RootData` can be thought of as a glob pattern like `src/**.rs` which
/// specifies the source root or as a function which takes a `PathBuf` and
/// returns `true` iff path belongs to the source root
/// returns `true` if path belongs to the source root
struct RootData {
path: PathBuf,
root: PathBuf,
filter: Box<dyn Filter>,
// result of `root.canonicalize()` if that differs from `root`; `None` otherwise.
canonical_path: Option<PathBuf>,
excluded_dirs: Vec<RelativePathBuf>,
Expand All @@ -26,22 +51,39 @@ pub(crate) struct Roots {
}

impl Roots {
pub(crate) fn new(mut paths: Vec<PathBuf>) -> Roots {
let mut roots = Vec::new();
pub(crate) fn new(mut paths: Vec<RootEntry>) -> Roots {
// A hack to make nesting work.
paths.sort_by_key(|it| std::cmp::Reverse(it.as_os_str().len()));
paths.sort_by_key(|it| std::cmp::Reverse(it.path.as_os_str().len()));
paths.dedup();
for (i, path) in paths.iter().enumerate() {
let nested_roots =
paths[..i].iter().filter_map(|it| rel_path(path, it)).collect::<Vec<_>>();

roots.push(RootData::new(path.clone(), nested_roots));
}
// First gather all the nested roots for each path
let nested_roots = paths
.iter()
.enumerate()
.map(|(i, entry)| {
paths[..i]
.iter()
.filter_map(|it| rel_path(&entry.path, &it.path))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();

// Then combine the entry with the matching nested_roots
let roots = paths
.into_iter()
.zip(nested_roots.into_iter())
.map(|(entry, nested_roots)| RootData::new(entry, nested_roots))
.collect::<Vec<_>>();

Roots { roots }
}
pub(crate) fn find(&self, path: &Path) -> Option<(VfsRoot, RelativePathBuf)> {
pub(crate) fn find(
&self,
path: &Path,
expected: FileType,
) -> Option<(VfsRoot, RelativePathBuf)> {
self.iter().find_map(|root| {
let rel_path = self.contains(root, path)?;
let rel_path = self.contains(root, path, expected)?;
Some((root, rel_path))
})
}
Expand All @@ -52,16 +94,21 @@ impl Roots {
(0..self.roots.len()).into_iter().map(|idx| VfsRoot(idx as u32))
}
pub(crate) fn path(&self, root: VfsRoot) -> &Path {
self.root(root).path.as_path()
self.root(root).path().as_path()
}
/// Checks if root contains a path and returns a root-relative path.
pub(crate) fn contains(&self, root: VfsRoot, path: &Path) -> Option<RelativePathBuf> {

/// Checks if root contains a path with the given `FileType`
/// and returns a root-relative path.
pub(crate) fn contains(
&self,
root: VfsRoot,
path: &Path,
expected: FileType,
) -> Option<RelativePathBuf> {
let data = self.root(root);
iter::once(&data.path)
iter::once(data.path())
.chain(data.canonical_path.as_ref().into_iter())
.find_map(|base| rel_path(base, path))
.filter(|path| !data.excluded_dirs.contains(path))
.filter(|path| !data.is_excluded(path))
.find_map(|base| to_relative_path(base, path, &data, expected))
}

fn root(&self, root: VfsRoot) -> &RootData {
Expand All @@ -70,58 +117,63 @@ impl Roots {
}

impl RootData {
fn new(path: PathBuf, excluded_dirs: Vec<RelativePathBuf>) -> RootData {
let mut canonical_path = path.canonicalize().ok();
if Some(&path) == canonical_path.as_ref() {
fn new(entry: RootEntry, excluded_dirs: Vec<RelativePathBuf>) -> RootData {
let mut canonical_path = entry.path.canonicalize().ok();
if Some(&entry.path) == canonical_path.as_ref() {
canonical_path = None;
}
RootData { path, canonical_path, excluded_dirs }
RootData { root: entry.path, filter: entry.filter, canonical_path, excluded_dirs }
}

fn is_excluded(&self, path: &RelativePath) -> bool {
if self.excluded_dirs.iter().any(|it| it == path) {
return true;
}
// Ignore some common directories.
//
// FIXME: don't hard-code, specify at source-root creation time using
// gitignore
for (i, c) in path.components().enumerate() {
if let relative_path::Component::Normal(c) = c {
if (i == 0 && c == "target") || c == ".git" || c == "node_modules" {
return true;
}
}
}

match path.extension() {
Some("rs") => false,
Some(_) => true,
// Exclude extension-less and hidden files
None => is_extensionless_or_hidden_file(&self.path, path),
}
fn path(&self) -> &PathBuf {
&self.root
}
}

fn is_extensionless_or_hidden_file<P: AsRef<Path>>(base: P, relative_path: &RelativePath) -> bool {
// Exclude files/paths starting with "."
if relative_path.file_stem().map(|s| s.starts_with(".")).unwrap_or(false) {
return true;
}
/// Returns true if the given `RelativePath` is included inside this `RootData`
fn is_included(&self, rel_path: &RelativePathBuf, expected: FileType) -> bool {
if self.excluded_dirs.iter().any(|d| rel_path.starts_with(d)) {
return false;
}

if relative_path.extension().is_some() {
return false;
}
let parent_included =
rel_path.parent().map(|d| self.filter.include_dir(&d)).unwrap_or(true);

let path = relative_path.to_path(base);
if !parent_included {
return false;
}

std::fs::metadata(path)
.map(|m| m.is_file())
.unwrap_or(false)
match expected {
FileType::File => self.filter.include_file(&rel_path),
FileType::Dir => self.filter.include_dir(&rel_path),
}
}
}

/// Returns the path relative to `base`
fn rel_path(base: &Path, path: &Path) -> Option<RelativePathBuf> {
let path = path.strip_prefix(base).ok()?;
let path = RelativePathBuf::from_path(path).unwrap();
Some(path)
}

/// Returns the path relative to `base` with filtering applied based on `data`
fn to_relative_path(
base: &Path,
path: &Path,
data: &RootData,
expected: FileType,
) -> Option<RelativePathBuf> {
let rel_path = rel_path(base, path)?;

// Apply filtering _only_ if the relative path is non-empty
// if it's empty, it means we are currently processing the root
if rel_path.as_str().is_empty() {
return Some(rel_path);
}

if data.is_included(&rel_path, expected) {
Some(rel_path)
} else {
None
}
}
Loading