Repository: rust-analyzer/rust-analyzer (rust)

Issue:
Make it possible to ignore directories
For example by reading `.gitignore`. I have a project which contains a full rust-lang/rust checkout in a subdirectory. I would like that to be ignored by ra, because I don't edit it anyway, and it takes a lot of time to index it. (Especially because it does contain artifacts from a `./x.py test` run. Those are several files for every single test file.)

Update(12min later): Currently indexing `src/llvm-project`.

Pull request description:
Ignore
closes #1616 

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index a5f56853d3..365b77f41d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -117,6 +117,14 @@ dependencies = [
  "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "bstr"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "byte-tools"
 version = "0.3.1"
@@ -478,6 +486,11 @@ dependencies = [
  "yansi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "fnv"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "format-buf"
 version = "1.0.0"
@@ -571,6 +584,18 @@ name = "glob"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "globset"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bstr 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "heck"
 version = "0.3.1"
@@ -1145,6 +1170,7 @@ dependencies = [
  "ra_ide_api 0.1.0",
  "ra_project_model 0.1.0",
  "ra_vfs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ra_vfs_glob 0.1.0",
  "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -1250,6 +1276,7 @@ dependencies = [
  "ra_syntax 0.1.0",
  "ra_text_edit 0.1.0",
  "ra_vfs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ra_vfs_glob 0.1.0",
  "relative-path 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1301,7 +1328,6 @@ dependencies = [
  "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "ra_arena 0.1.0",
  "ra_db 0.1.0",
- "relative-path 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1369,6 +1395,14 @@ dependencies = [
  "walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "ra_vfs_glob"
+version = "0.1.0"
+dependencies = [
+ "globset 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ra_vfs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rand"
 version = "0.6.5"
@@ -2116,6 +2150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd"
 "checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
 "checksum block-padding 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6d4dc3af3ee2e12f3e5d224e5e1e3d73668abbeb69e566d361f7d5563a4fdf09"
+"checksum bstr 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e0a692f1c740e7e821ca71a22cf99b9b2322dfa94d10f71443befb1797b3946a"
 "checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
 "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
 "checksum c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101"
@@ -2158,6 +2193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum filetime 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2f8c63033fcba1f51ef744505b3cad42510432b904c062afa67ad7ece008429d"
 "checksum fixedbitset 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "86d4de0081402f5e88cdac65c8dcdcc73118c1a7a465e2a05f0da05843a8ea33"
 "checksum flexi_logger 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2be2727ae1f60711d1991788605245e70cc500bd1d92edb909af160634e1a869"
+"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
 "checksum format-buf 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f7aea5a5909a74969507051a3b17adc84737e31a5f910559892aedce026f4d53"
 "checksum fs_extra 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5f2a4a2034423744d2cc7ca2068453168dcdb82c438419e639a26bd87839c674"
 "checksum fsevent 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
@@ -2170,6 +2206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum getrandom 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "34f33de6f0ae7c9cb5e574502a562e2b512799e32abb801cd1e79ad952b62b49"
 "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
 "checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+"checksum globset 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "925aa2cac82d8834e2b2a4415b6f6879757fb5c0928fc445ae76461a12eed8f2"
 "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
 "checksum humansize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
 "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
diff --git a/crates/ra_batch/Cargo.toml b/crates/ra_batch/Cargo.toml
index 0ee94c4451..8e23826a41 100644
--- a/crates/ra_batch/Cargo.toml
+++ b/crates/ra_batch/Cargo.toml
@@ -9,6 +9,7 @@ log = "0.4.5"
 rustc-hash = "1.0"
 
 ra_vfs = "0.2.0"
+ra_vfs_glob = { path = "../ra_vfs_glob" }
 ra_db = { path = "../ra_db" }
 ra_ide_api = { path = "../ra_ide_api" }
 ra_hir = { path = "../ra_hir" }
diff --git a/crates/ra_batch/src/lib.rs b/crates/ra_batch/src/lib.rs
index c01574fbc8..0db7514658 100644
--- a/crates/ra_batch/src/lib.rs
+++ b/crates/ra_batch/src/lib.rs
@@ -1,5 +1,3 @@
-mod vfs_filter;
-
 use std::{collections::HashSet, error::Error, path::Path};
 
 use rustc_hash::FxHashMap;
@@ -7,8 +5,8 @@ use rustc_hash::FxHashMap;
 use ra_db::{CrateGraph, FileId, SourceRootId};
 use ra_ide_api::{AnalysisChange, AnalysisHost};
 use ra_project_model::{PackageRoot, ProjectWorkspace};
-use ra_vfs::{Vfs, VfsChange};
-use vfs_filter::IncludeRustFiles;
+use ra_vfs::{RootEntry, Vfs, VfsChange};
+use ra_vfs_glob::RustPackageFilterBuilder;
 
 type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync>>;
 
@@ -23,7 +21,19 @@ pub fn load_cargo(root: &Path) -> Result<(AnalysisHost, FxHashMap<SourceRootId,
     let root = std::env::current_dir()?.join(root);
     let ws = ProjectWorkspace::discover(root.as_ref())?;
     let project_roots = ws.to_roots();
-    let (mut vfs, roots) = Vfs::new(IncludeRustFiles::from_roots(project_roots.clone()).collect());
+    let (mut vfs, roots) = Vfs::new(
+        project_roots
+            .iter()
+            .map(|pkg_root| {
+                RootEntry::new(
+                    pkg_root.path().clone(),
+                    RustPackageFilterBuilder::default()
+                        .set_member(pkg_root.is_member())
+                        .into_vfs_filter(),
+                )
+            })
+            .collect(),
+    );
     let crate_graph = ws.to_crate_graph(&mut |path: &Path| {
         let vfs_file = vfs.load(path);
         log::debug!("vfs file {:?} -> {:?}", path, vfs_file);
diff --git a/crates/ra_batch/src/vfs_filter.rs b/crates/ra_batch/src/vfs_filter.rs
deleted file mode 100644
index 63bf77704a..0000000000
--- a/crates/ra_batch/src/vfs_filter.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-use ra_project_model::PackageRoot;
-use ra_vfs::{Filter, RelativePath, RootEntry};
-use std::path::PathBuf;
-
-/// `IncludeRustFiles` is used to convert
-/// from `PackageRoot` to `RootEntry` for VFS
-pub struct IncludeRustFiles {
-    root: PackageRoot,
-}
-
-impl IncludeRustFiles {
-    pub fn from_roots<R>(roots: R) -> impl Iterator<Item = RootEntry>
-    where
-        R: IntoIterator<Item = PackageRoot>,
-    {
-        roots.into_iter().map(IncludeRustFiles::from_root)
-    }
-
-    pub fn from_root(root: PackageRoot) -> RootEntry {
-        IncludeRustFiles::from(root).into()
-    }
-
-    #[allow(unused)]
-    pub fn external(path: PathBuf) -> RootEntry {
-        IncludeRustFiles::from_root(PackageRoot::new(path, false))
-    }
-
-    pub fn member(path: PathBuf) -> RootEntry {
-        IncludeRustFiles::from_root(PackageRoot::new(path, true))
-    }
-}
-
-impl Filter for IncludeRustFiles {
-    fn include_dir(&self, dir_path: &RelativePath) -> bool {
-        self.root.include_dir(dir_path)
-    }
-
-    fn include_file(&self, file_path: &RelativePath) -> bool {
-        self.root.include_file(file_path)
-    }
-}
-
-impl From<PackageRoot> for IncludeRustFiles {
-    fn from(v: PackageRoot) -> IncludeRustFiles {
-        IncludeRustFiles { root: v }
-    }
-}
-
-impl From<IncludeRustFiles> for RootEntry {
-    fn from(v: IncludeRustFiles) -> RootEntry {
-        let path = v.root.path().clone();
-        RootEntry::new(path, Box::new(v))
-    }
-}
diff --git a/crates/ra_lsp_server/Cargo.toml b/crates/ra_lsp_server/Cargo.toml
index cec360667d..c282d6db8e 100644
--- a/crates/ra_lsp_server/Cargo.toml
+++ b/crates/ra_lsp_server/Cargo.toml
@@ -25,6 +25,7 @@ ra_ide_api = { path = "../ra_ide_api" }
 gen_lsp_server = { path = "../gen_lsp_server" }
 ra_project_model = { path = "../ra_project_model" }
 ra_prof = { path = "../ra_prof" }
+ra_vfs_glob = { path = "../ra_vfs_glob" }
 
 [dev-dependencies]
 tempfile = "3"
diff --git a/crates/ra_lsp_server/src/init.rs b/crates/ra_lsp_server/src/config.rs
similarity index 85%
rename from crates/ra_lsp_server/src/init.rs
rename to crates/ra_lsp_server/src/config.rs
index b894b449d7..6dcdc695a7 100644
--- a/crates/ra_lsp_server/src/init.rs
+++ b/crates/ra_lsp_server/src/config.rs
@@ -1,9 +1,9 @@
 use serde::{Deserialize, Deserializer};
 
 /// Client provided initialization options
-#[derive(Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
 #[serde(rename_all = "camelCase", default)]
-pub struct InitializationOptions {
+pub struct ServerConfig {
     /// Whether the client supports our custom highlighting publishing decorations.
     /// This is different to the highlightingOn setting, which is whether the user
     /// wants our custom highlighting to be used.
@@ -18,14 +18,17 @@ pub struct InitializationOptions {
     #[serde(deserialize_with = "nullable_bool_true")]
     pub show_workspace_loaded: bool,
 
+    pub exclude_globs: Vec<String>,
+
     pub lru_capacity: Option<usize>,
 }
 
-impl Default for InitializationOptions {
-    fn default() -> InitializationOptions {
-        InitializationOptions {
+impl Default for ServerConfig {
+    fn default() -> ServerConfig {
+        ServerConfig {
             publish_decorations: false,
             show_workspace_loaded: true,
+            exclude_globs: Vec::new(),
             lru_capacity: None,
         }
     }
@@ -56,7 +59,7 @@ mod test {
     #[test]
     fn deserialize_init_options_defaults() {
         // check that null == default for both fields
-        let default = InitializationOptions::default();
+        let default = ServerConfig::default();
         assert_eq!(default, serde_json::from_str(r#"{}"#).unwrap());
         assert_eq!(
             default,
diff --git a/crates/ra_lsp_server/src/lib.rs b/crates/ra_lsp_server/src/lib.rs
index 56a263aa56..795f863838 100644
--- a/crates/ra_lsp_server/src/lib.rs
+++ b/crates/ra_lsp_server/src/lib.rs
@@ -4,13 +4,11 @@ mod conv;
 mod main_loop;
 mod markdown;
 mod project_model;
-mod vfs_filter;
 pub mod req;
-pub mod init;
+pub mod config;
 mod world;
 
 pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
 pub use crate::{
-    caps::server_capabilities, init::InitializationOptions, main_loop::main_loop,
-    main_loop::LspError,
+    caps::server_capabilities, config::ServerConfig, main_loop::main_loop, main_loop::LspError,
 };
diff --git a/crates/ra_lsp_server/src/main.rs b/crates/ra_lsp_server/src/main.rs
index c1f8243be2..1a2ab1bc2f 100644
--- a/crates/ra_lsp_server/src/main.rs
+++ b/crates/ra_lsp_server/src/main.rs
@@ -2,7 +2,7 @@ use flexi_logger::{Duplicate, Logger};
 use gen_lsp_server::{run_server, stdio_transport};
 use serde::Deserialize;
 
-use ra_lsp_server::{InitializationOptions, Result};
+use ra_lsp_server::{Result, ServerConfig};
 use ra_prof;
 
 fn main() -> Result<()> {
@@ -48,7 +48,7 @@ fn main_inner() -> Result<()> {
 
         let opts = params
             .initialization_options
-            .and_then(|v| InitializationOptions::deserialize(v).ok())
+            .and_then(|v| ServerConfig::deserialize(v).ok())
             .unwrap_or_default();
 
         ra_lsp_server::main_loop(workspace_roots, params.capabilities, opts, r, s)
diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs
index 9a38d43d2a..9d540a87e6 100644
--- a/crates/ra_lsp_server/src/main_loop.rs
+++ b/crates/ra_lsp_server/src/main_loop.rs
@@ -23,7 +23,7 @@ use crate::{
     project_model::workspace_loader,
     req,
     world::{Options, WorldSnapshot, WorldState},
-    InitializationOptions, Result,
+    Result, ServerConfig,
 };
 
 const THREADPOOL_SIZE: usize = 8;
@@ -52,10 +52,11 @@ impl Error for LspError {}
 pub fn main_loop(
     ws_roots: Vec<PathBuf>,
     client_caps: ClientCapabilities,
-    options: InitializationOptions,
+    config: ServerConfig,
     msg_receiver: &Receiver<RawMessage>,
     msg_sender: &Sender<RawMessage>,
 ) -> Result<()> {
+    log::debug!("server_config: {:?}", config);
     // FIXME: support dynamic workspace loading.
     let workspaces = {
         let ws_worker = workspace_loader();
@@ -77,14 +78,19 @@ pub fn main_loop(
         }
         loaded_workspaces
     };
-
+    let globs = config
+        .exclude_globs
+        .iter()
+        .map(|glob| ra_vfs_glob::Glob::new(glob))
+        .collect::<std::result::Result<Vec<_>, _>>()?;
     let mut state = WorldState::new(
         ws_roots,
         workspaces,
-        options.lru_capacity,
+        config.lru_capacity,
+        &globs,
         Options {
-            publish_decorations: options.publish_decorations,
-            show_workspace_loaded: options.show_workspace_loaded,
+            publish_decorations: config.publish_decorations,
+            show_workspace_loaded: config.show_workspace_loaded,
             supports_location_link: client_caps
                 .text_document
                 .and_then(|it| it.definition)
diff --git a/crates/ra_lsp_server/src/vfs_filter.rs b/crates/ra_lsp_server/src/vfs_filter.rs
deleted file mode 100644
index abdc8dbad2..0000000000
--- a/crates/ra_lsp_server/src/vfs_filter.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-use ra_project_model::PackageRoot;
-use ra_vfs::{Filter, RelativePath, RootEntry};
-use std::path::PathBuf;
-
-/// `IncludeRustFiles` is used to convert
-/// from `PackageRoot` to `RootEntry` for VFS
-pub struct IncludeRustFiles {
-    root: PackageRoot,
-}
-
-impl IncludeRustFiles {
-    pub fn from_roots<R>(roots: R) -> impl Iterator<Item = RootEntry>
-    where
-        R: IntoIterator<Item = PackageRoot>,
-    {
-        roots.into_iter().map(IncludeRustFiles::from_root)
-    }
-
-    pub fn from_root(root: PackageRoot) -> RootEntry {
-        IncludeRustFiles::from(root).into()
-    }
-
-    #[allow(unused)]
-    pub fn external(path: PathBuf) -> RootEntry {
-        IncludeRustFiles::from_root(PackageRoot::new(path, false))
-    }
-
-    pub fn member(path: PathBuf) -> RootEntry {
-        IncludeRustFiles::from_root(PackageRoot::new(path, true))
-    }
-}
-
-impl Filter for IncludeRustFiles {
-    fn include_dir(&self, dir_path: &RelativePath) -> bool {
-        self.root.include_dir(dir_path)
-    }
-
-    fn include_file(&self, file_path: &RelativePath) -> bool {
-        self.root.include_file(file_path)
-    }
-}
-
-impl std::convert::From<PackageRoot> for IncludeRustFiles {
-    fn from(v: PackageRoot) -> IncludeRustFiles {
-        IncludeRustFiles { root: v }
-    }
-}
-
-impl std::convert::From<IncludeRustFiles> for RootEntry {
-    fn from(v: IncludeRustFiles) -> RootEntry {
-        let path = v.root.path().clone();
-        RootEntry::new(path, Box::new(v))
-    }
-}
diff --git a/crates/ra_lsp_server/src/world.rs b/crates/ra_lsp_server/src/world.rs
index b57cdf925a..9990ef62e1 100644
--- a/crates/ra_lsp_server/src/world.rs
+++ b/crates/ra_lsp_server/src/world.rs
@@ -9,13 +9,13 @@ use parking_lot::RwLock;
 use ra_ide_api::{
     Analysis, AnalysisChange, AnalysisHost, CrateGraph, FileId, LibraryData, SourceRootId,
 };
-use ra_vfs::{Vfs, VfsChange, VfsFile, VfsRoot};
+use ra_vfs::{RootEntry, Vfs, VfsChange, VfsFile, VfsRoot};
+use ra_vfs_glob::{Glob, RustPackageFilterBuilder};
 use relative_path::RelativePathBuf;
 
 use crate::{
     main_loop::pending_requests::{CompletedRequest, LatestRequests},
     project_model::ProjectWorkspace,
-    vfs_filter::IncludeRustFiles,
     LspError, Result,
 };
 
@@ -56,14 +56,28 @@ impl WorldState {
         folder_roots: Vec<PathBuf>,
         workspaces: Vec<ProjectWorkspace>,
         lru_capacity: Option<usize>,
+        exclude_globs: &[Glob],
         options: Options,
     ) -> WorldState {
         let mut change = AnalysisChange::new();
 
         let mut roots = Vec::new();
-        roots.extend(folder_roots.iter().cloned().map(IncludeRustFiles::member));
+        roots.extend(folder_roots.iter().map(|path| {
+            let mut filter = RustPackageFilterBuilder::default().set_member(true);
+            for glob in exclude_globs.iter() {
+                filter = filter.exclude(glob.clone());
+            }
+            RootEntry::new(path.clone(), filter.into_vfs_filter())
+        }));
         for ws in workspaces.iter() {
-            roots.extend(IncludeRustFiles::from_roots(ws.to_roots()));
+            roots.extend(ws.to_roots().into_iter().map(|pkg_root| {
+                let mut filter =
+                    RustPackageFilterBuilder::default().set_member(pkg_root.is_member());
+                for glob in exclude_globs.iter() {
+                    filter = filter.exclude(glob.clone());
+                }
+                RootEntry::new(pkg_root.path().clone(), filter.into_vfs_filter())
+            }));
         }
 
         let (mut vfs, vfs_roots) = Vfs::new(roots);
diff --git a/crates/ra_project_model/Cargo.toml b/crates/ra_project_model/Cargo.toml
index 3545d23c91..4fd6c75efb 100644
--- a/crates/ra_project_model/Cargo.toml
+++ b/crates/ra_project_model/Cargo.toml
@@ -7,7 +7,6 @@ authors = ["rust-analyzer developers"]
 [dependencies]
 log = "0.4.5"
 rustc-hash = "1.0"
-relative-path = "0.4.0"
 
 cargo_metadata = "0.8.0"
 
diff --git a/crates/ra_project_model/src/lib.rs b/crates/ra_project_model/src/lib.rs
index c7167046bf..55b94b9113 100644
--- a/crates/ra_project_model/src/lib.rs
+++ b/crates/ra_project_model/src/lib.rs
@@ -10,7 +10,6 @@ use std::{
 };
 
 use ra_db::{CrateGraph, Edition, FileId};
-use relative_path::RelativePath;
 use rustc_hash::FxHashMap;
 use serde_json::from_reader;
 
@@ -54,28 +53,6 @@ impl PackageRoot {
     pub fn is_member(&self) -> bool {
         self.is_member
     }
-
-    pub fn include_dir(&self, dir_path: &RelativePath) -> bool {
-        const COMMON_IGNORED_DIRS: &[&str] = &["node_modules", "target", ".git"];
-        const EXTERNAL_IGNORED_DIRS: &[&str] = &["examples", "tests", "benches"];
-
-        let is_ignored = if self.is_member {
-            dir_path.components().any(|c| COMMON_IGNORED_DIRS.contains(&c.as_str()))
-        } else {
-            dir_path.components().any(|c| {
-                let path = c.as_str();
-                COMMON_IGNORED_DIRS.contains(&path) || EXTERNAL_IGNORED_DIRS.contains(&path)
-            })
-        };
-
-        let hidden = dir_path.components().any(|c| c.as_str().starts_with('.'));
-
-        !is_ignored && !hidden
-    }
-
-    pub fn include_file(&self, file_path: &RelativePath) -> bool {
-        file_path.extension() == Some("rs")
-    }
 }
 
 impl ProjectWorkspace {
diff --git a/crates/ra_vfs_glob/Cargo.toml b/crates/ra_vfs_glob/Cargo.toml
new file mode 100644
index 0000000000..0390d7da16
--- /dev/null
+++ b/crates/ra_vfs_glob/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+edition = "2018"
+name = "ra_vfs_glob"
+version = "0.1.0"
+authors = ["rust-analyzer developers"]
+
+[dependencies]
+ra_vfs = "0.2.0"
+globset = "0.4.4"
diff --git a/crates/ra_vfs_glob/src/lib.rs b/crates/ra_vfs_glob/src/lib.rs
new file mode 100644
index 0000000000..12401d75a8
--- /dev/null
+++ b/crates/ra_vfs_glob/src/lib.rs
@@ -0,0 +1,94 @@
+//! `ra_vfs_glob` crate implements exclusion rules for vfs.
+//!
+//! By default, we include only `.rs` files, and skip some know offenders like
+//! `/target` or `/node_modules` altogether.
+//!
+//! It's also possible to add custom exclusion globs.
+
+use globset::{GlobSet, GlobSetBuilder};
+use ra_vfs::{Filter, RelativePath};
+
+pub use globset::{Glob, GlobBuilder};
+
+const ALWAYS_IGNORED: &[&str] = &["target/**", "**/node_modules/**", "**/.git/**"];
+const IGNORED_FOR_NON_MEMBERS: &[&str] = &["examples/**", "tests/**", "benches/**"];
+
+pub struct RustPackageFilterBuilder {
+    is_member: bool,
+    exclude: GlobSetBuilder,
+}
+
+impl Default for RustPackageFilterBuilder {
+    fn default() -> RustPackageFilterBuilder {
+        RustPackageFilterBuilder { is_member: false, exclude: GlobSetBuilder::new() }
+    }
+}
+
+impl RustPackageFilterBuilder {
+    pub fn set_member(mut self, is_member: bool) -> RustPackageFilterBuilder {
+        self.is_member = is_member;
+        self
+    }
+    pub fn exclude(mut self, glob: Glob) -> RustPackageFilterBuilder {
+        self.exclude.add(glob);
+        self
+    }
+    pub fn into_vfs_filter(self) -> Box<dyn Filter> {
+        let RustPackageFilterBuilder { is_member, mut exclude } = self;
+        for &glob in ALWAYS_IGNORED {
+            exclude.add(Glob::new(glob).unwrap());
+        }
+        if !is_member {
+            for &glob in IGNORED_FOR_NON_MEMBERS {
+                exclude.add(Glob::new(glob).unwrap());
+            }
+        }
+        Box::new(RustPackageFilter { exclude: exclude.build().unwrap() })
+    }
+}
+
+struct RustPackageFilter {
+    exclude: GlobSet,
+}
+
+impl Filter for RustPackageFilter {
+    fn include_dir(&self, dir_path: &RelativePath) -> bool {
+        !self.exclude.is_match(dir_path.as_str())
+    }
+
+    fn include_file(&self, file_path: &RelativePath) -> bool {
+        file_path.extension() == Some("rs")
+    }
+}
+
+#[test]
+fn test_globs() {
+    let filter = RustPackageFilterBuilder::default().set_member(true).into_vfs_filter();
+
+    assert!(filter.include_dir(RelativePath::new("src/tests")));
+    assert!(filter.include_dir(RelativePath::new("src/target")));
+    assert!(filter.include_dir(RelativePath::new("tests")));
+    assert!(filter.include_dir(RelativePath::new("benches")));
+
+    assert!(!filter.include_dir(RelativePath::new("target")));
+    assert!(!filter.include_dir(RelativePath::new("src/foo/.git")));
+    assert!(!filter.include_dir(RelativePath::new("foo/node_modules")));
+
+    let filter = RustPackageFilterBuilder::default().set_member(false).into_vfs_filter();
+
+    assert!(filter.include_dir(RelativePath::new("src/tests")));
+    assert!(filter.include_dir(RelativePath::new("src/target")));
+
+    assert!(!filter.include_dir(RelativePath::new("target")));
+    assert!(!filter.include_dir(RelativePath::new("src/foo/.git")));
+    assert!(!filter.include_dir(RelativePath::new("foo/node_modules")));
+    assert!(!filter.include_dir(RelativePath::new("tests")));
+    assert!(!filter.include_dir(RelativePath::new("benches")));
+
+    let filter = RustPackageFilterBuilder::default()
+        .set_member(true)
+        .exclude(Glob::new("src/llvm-project/**").unwrap())
+        .into_vfs_filter();
+
+    assert!(!filter.include_dir(RelativePath::new("src/llvm-project/clang")));
+}
diff --git a/docs/user/README.md b/docs/user/README.md
index a5e17f6041..7990d1d31b 100644
--- a/docs/user/README.md
+++ b/docs/user/README.md
@@ -71,6 +71,9 @@ See https://github.com/microsoft/vscode/issues/72308[microsoft/vscode#72308] for
 * `rust-analyzer.raLspServerPath`: path to `ra_lsp_server` executable
 * `rust-analyzer.enableCargoWatchOnStartup`: prompt to install & enable `cargo
   watch` for live error highlighting (note, this **does not** use rust-analyzer)
+* `rust-analyzer.excludeGlobs`: a list of glob-patterns for exclusion (see globset [docs](https://docs.rs/globset) for syntax).
+  Note: glob patterns are applied to all Cargo packages and a rooted at a package root.
+  This is not very intuitive and a limitation of a current implementation.
 * `rust-analyzer.cargo-watch.check-arguments`: cargo-watch check arguments.
   (e.g: `--features="shumway,pdf"` will run as `cargo watch -x "check --features="shumway,pdf""` )
 * `rust-analyzer.trace.server`: enables internal logging
diff --git a/editors/code/package.json b/editors/code/package.json
index 808dc5dc1b..48ab886bf8 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -197,6 +197,11 @@
                     ],
                     "description": "Whether to run `cargo watch` on startup"
                 },
+                "rust-analyzer.excludeGlobs": {
+                    "type": "array",
+                    "default": "[]",
+                    "description": "Paths to exclude from analysis"
+                },
                 "rust-analyzer.cargo-watch.arguments": {
                     "type": "string",
                     "description": "`cargo-watch` arguments. (e.g: `--features=\"shumway,pdf\"` will run as `cargo watch -x \"check --features=\"shumway,pdf\"\"` )",
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 4d58a1a936..4df6b50ef7 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -22,6 +22,7 @@ export class Config {
     public showWorkspaceLoadedNotification = true;
     public lruCapacity: null | number = null;
     public displayInlayHints = true;
+    public excludeGlobs = [];
     public cargoWatchOptions: CargoWatchOptions = {
         enableOnStartup: 'ask',
         trace: 'off',
@@ -128,5 +129,8 @@ export class Config {
         if (config.has('displayInlayHints')) {
             this.displayInlayHints = config.get('displayInlayHints') as boolean;
         }
+        if (config.has('excludeGlobs')) {
+            this.excludeGlobs = config.get('excludeGlobs') || [];
+        }
     }
 }
diff --git a/editors/code/src/server.ts b/editors/code/src/server.ts
index 7029142fdc..2b4c25c284 100644
--- a/editors/code/src/server.ts
+++ b/editors/code/src/server.ts
@@ -36,7 +36,8 @@ export class Server {
                 publishDecorations: true,
                 showWorkspaceLoaded:
                     Server.config.showWorkspaceLoadedNotification,
-                lruCapacity: Server.config.lruCapacity
+                lruCapacity: Server.config.lruCapacity,
+                excludeGlobs: Server.config.excludeGlobs
             },
             traceOutputChannel
         };


Test patch:
diff --git a/crates/ra_lsp_server/tests/heavy_tests/support.rs b/crates/ra_lsp_server/tests/heavy_tests/support.rs
index 5dddbbe172..ba8ee8b062 100644
--- a/crates/ra_lsp_server/tests/heavy_tests/support.rs
+++ b/crates/ra_lsp_server/tests/heavy_tests/support.rs
@@ -22,7 +22,7 @@ use tempfile::TempDir;
 use test_utils::{find_mismatch, parse_fixture};
 use thread_worker::Worker;
 
-use ra_lsp_server::{main_loop, req, InitializationOptions};
+use ra_lsp_server::{main_loop, req, ServerConfig};
 
 pub struct Project<'a> {
     fixture: &'a str,
@@ -107,7 +107,7 @@ impl Server {
                         window: None,
                         experimental: None,
                     },
-                    InitializationOptions::default(),
+                    ServerConfig::default(),
                     &msg_receiver,
                     &msg_sender,
                 )