diff options
-rw-r--r-- | crates/ra_vfs/src/watcher.rs | 61 | ||||
-rw-r--r-- | crates/ra_vfs/tests/vfs.rs | 131 |
2 files changed, 129 insertions, 63 deletions
diff --git a/crates/ra_vfs/src/watcher.rs b/crates/ra_vfs/src/watcher.rs index 9d552f886..f0ef9bc0e 100644 --- a/crates/ra_vfs/src/watcher.rs +++ b/crates/ra_vfs/src/watcher.rs | |||
@@ -1,7 +1,7 @@ | |||
1 | use crate::io; | 1 | use crate::io; |
2 | use crossbeam_channel::Sender; | 2 | use crossbeam_channel::Sender; |
3 | use drop_bomb::DropBomb; | 3 | use drop_bomb::DropBomb; |
4 | use ignore; | 4 | use ignore::{gitignore::Gitignore, Walk}; |
5 | use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher}; | 5 | use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher}; |
6 | use parking_lot::Mutex; | 6 | use parking_lot::Mutex; |
7 | use std::{ | 7 | use std::{ |
@@ -40,8 +40,11 @@ fn handle_change_event( | |||
40 | sender.send(io::Task::HandleChange(WatcherChange::Rescan))?; | 40 | sender.send(io::Task::HandleChange(WatcherChange::Rescan))?; |
41 | } | 41 | } |
42 | DebouncedEvent::Create(path) => { | 42 | DebouncedEvent::Create(path) => { |
43 | if path.is_dir() { | 43 | // we have to check if `path` is ignored because Walk iterator doesn't check it |
44 | watch_recursive(watcher, &path); | 44 | // also childs are only ignored if they match a pattern |
45 | // (see `matched` vs `matched_path_or_any_parents` in `Gitignore`) | ||
46 | if path.is_dir() && !should_ignore_dir(&path) { | ||
47 | watch_recursive(watcher, &path, Some(sender)); | ||
45 | } | 48 | } |
46 | sender.send(io::Task::HandleChange(WatcherChange::Create(path)))?; | 49 | sender.send(io::Task::HandleChange(WatcherChange::Create(path)))?; |
47 | } | 50 | } |
@@ -63,15 +66,18 @@ fn handle_change_event( | |||
63 | Ok(()) | 66 | Ok(()) |
64 | } | 67 | } |
65 | 68 | ||
66 | fn watch_one(watcher: &mut RecommendedWatcher, path: &Path) { | 69 | fn watch_one(watcher: &mut RecommendedWatcher, dir: &Path) { |
67 | match watcher.watch(path, RecursiveMode::NonRecursive) { | 70 | match watcher.watch(dir, RecursiveMode::NonRecursive) { |
68 | Ok(()) => log::debug!("watching \"{}\"", path.display()), | 71 | Ok(()) => log::debug!("watching \"{}\"", dir.display()), |
69 | Err(e) => log::warn!("could not watch \"{}\": {}", path.display(), e), | 72 | Err(e) => log::warn!("could not watch \"{}\": {}", dir.display(), e), |
70 | } | 73 | } |
71 | } | 74 | } |
72 | 75 | ||
73 | fn watch_recursive(watcher: &Arc<Mutex<Option<RecommendedWatcher>>>, path: &Path) { | 76 | fn watch_recursive( |
74 | log::debug!("watch_recursive \"{}\"", path.display()); | 77 | watcher: &Arc<Mutex<Option<RecommendedWatcher>>>, |
78 | dir: &Path, | ||
79 | sender: Option<&Sender<io::Task>>, | ||
80 | ) { | ||
75 | let mut watcher = watcher.lock(); | 81 | let mut watcher = watcher.lock(); |
76 | let mut watcher = match *watcher { | 82 | let mut watcher = match *watcher { |
77 | Some(ref mut watcher) => watcher, | 83 | Some(ref mut watcher) => watcher, |
@@ -80,20 +86,47 @@ fn watch_recursive(watcher: &Arc<Mutex<Option<RecommendedWatcher>>>, path: &Path | |||
80 | return; | 86 | return; |
81 | } | 87 | } |
82 | }; | 88 | }; |
83 | // TODO it seems path itself isn't checked against ignores | 89 | for res in Walk::new(dir) { |
84 | // check if path should be ignored before walking it | ||
85 | for res in ignore::Walk::new(path) { | ||
86 | match res { | 90 | match res { |
87 | Ok(entry) => { | 91 | Ok(entry) => { |
88 | if entry.path().is_dir() { | 92 | if entry.path().is_dir() { |
89 | watch_one(&mut watcher, entry.path()); | 93 | watch_one(&mut watcher, entry.path()); |
90 | } | 94 | } |
95 | if let Some(sender) = sender { | ||
96 | // emit as create because we haven't seen it yet | ||
97 | if let Err(e) = sender.send(io::Task::HandleChange(WatcherChange::Create( | ||
98 | entry.path().to_path_buf(), | ||
99 | ))) { | ||
100 | log::warn!("watcher error: {}", e) | ||
101 | } | ||
102 | } | ||
91 | } | 103 | } |
92 | Err(e) => log::warn!("watcher error: {}", e), | 104 | Err(e) => log::warn!("watcher error: {}", e), |
93 | } | 105 | } |
94 | } | 106 | } |
95 | } | 107 | } |
96 | 108 | ||
109 | fn should_ignore_dir(dir: &Path) -> bool { | ||
110 | let mut parent = dir; | ||
111 | loop { | ||
112 | parent = match parent.parent() { | ||
113 | Some(p) => p, | ||
114 | None => break, | ||
115 | }; | ||
116 | let gitignore = parent.join(".gitignore"); | ||
117 | if gitignore.exists() { | ||
118 | let gitignore = Gitignore::new(gitignore).0; | ||
119 | if gitignore.matched_path_or_any_parents(dir, true).is_ignore() { | ||
120 | log::debug!("ignored {}", dir.display()); | ||
121 | return true; | ||
122 | } | ||
123 | } | ||
124 | } | ||
125 | false | ||
126 | } | ||
127 | |||
128 | const WATCHER_DELAY: Duration = Duration::from_millis(250); | ||
129 | |||
97 | impl Watcher { | 130 | impl Watcher { |
98 | pub(crate) fn start( | 131 | pub(crate) fn start( |
99 | output_sender: Sender<io::Task>, | 132 | output_sender: Sender<io::Task>, |
@@ -101,7 +134,7 @@ impl Watcher { | |||
101 | let (input_sender, input_receiver) = mpsc::channel(); | 134 | let (input_sender, input_receiver) = mpsc::channel(); |
102 | let watcher = Arc::new(Mutex::new(Some(notify::watcher( | 135 | let watcher = Arc::new(Mutex::new(Some(notify::watcher( |
103 | input_sender, | 136 | input_sender, |
104 | Duration::from_millis(250), | 137 | WATCHER_DELAY, |
105 | )?))); | 138 | )?))); |
106 | let w = watcher.clone(); | 139 | let w = watcher.clone(); |
107 | let thread = thread::spawn(move || { | 140 | let thread = thread::spawn(move || { |
@@ -119,7 +152,7 @@ impl Watcher { | |||
119 | } | 152 | } |
120 | 153 | ||
121 | pub fn watch(&mut self, root: impl AsRef<Path>) { | 154 | pub fn watch(&mut self, root: impl AsRef<Path>) { |
122 | watch_recursive(&self.watcher, root.as_ref()); | 155 | watch_recursive(&self.watcher, root.as_ref(), None); |
123 | } | 156 | } |
124 | 157 | ||
125 | pub fn shutdown(mut self) -> thread::Result<()> { | 158 | pub fn shutdown(mut self) -> thread::Result<()> { |
diff --git a/crates/ra_vfs/tests/vfs.rs b/crates/ra_vfs/tests/vfs.rs index 8266a0bd5..71b25a5c9 100644 --- a/crates/ra_vfs/tests/vfs.rs +++ b/crates/ra_vfs/tests/vfs.rs | |||
@@ -11,9 +11,21 @@ fn process_tasks(vfs: &mut Vfs, num_tasks: u32) { | |||
11 | } | 11 | } |
12 | } | 12 | } |
13 | 13 | ||
14 | macro_rules! assert_match { | ||
15 | ($x:expr, $pat:pat) => { | ||
16 | assert_match!($x, $pat, assert!(true)) | ||
17 | }; | ||
18 | ($x:expr, $pat:pat, $assert:expr) => { | ||
19 | match $x { | ||
20 | $pat => $assert, | ||
21 | x => assert!(false, "Expected {}, got {:?}", stringify!($pat), x), | ||
22 | }; | ||
23 | }; | ||
24 | } | ||
25 | |||
14 | #[test] | 26 | #[test] |
15 | fn test_vfs_works() -> std::io::Result<()> { | 27 | fn test_vfs_works() -> std::io::Result<()> { |
16 | // Logger::with_str("debug").start().unwrap(); | 28 | // Logger::with_str("vfs=debug,ra_vfs=debug").start().unwrap(); |
17 | 29 | ||
18 | let files = [ | 30 | let files = [ |
19 | ("a/foo.rs", "hello"), | 31 | ("a/foo.rs", "hello"), |
@@ -21,13 +33,16 @@ fn test_vfs_works() -> std::io::Result<()> { | |||
21 | ("a/b/baz.rs", "nested hello"), | 33 | ("a/b/baz.rs", "nested hello"), |
22 | ]; | 34 | ]; |
23 | 35 | ||
24 | let dir = tempdir()?; | 36 | let dir = tempdir().unwrap(); |
25 | for (path, text) in files.iter() { | 37 | for (path, text) in files.iter() { |
26 | let file_path = dir.path().join(path); | 38 | let file_path = dir.path().join(path); |
27 | fs::create_dir_all(file_path.parent().unwrap())?; | 39 | fs::create_dir_all(file_path.parent().unwrap()).unwrap(); |
28 | fs::write(file_path, text)? | 40 | fs::write(file_path, text)? |
29 | } | 41 | } |
30 | 42 | ||
43 | let gitignore = dir.path().join("a/.gitignore"); | ||
44 | fs::write(gitignore, "/target").unwrap(); | ||
45 | |||
31 | let a_root = dir.path().join("a"); | 46 | let a_root = dir.path().join("a"); |
32 | let b_root = dir.path().join("a/b"); | 47 | let b_root = dir.path().join("a/b"); |
33 | 48 | ||
@@ -62,79 +77,97 @@ fn test_vfs_works() -> std::io::Result<()> { | |||
62 | } | 77 | } |
63 | 78 | ||
64 | fs::write(&dir.path().join("a/b/baz.rs"), "quux").unwrap(); | 79 | fs::write(&dir.path().join("a/b/baz.rs"), "quux").unwrap(); |
65 | // 2 tasks per watcher change, first for HandleChange then for LoadChange | 80 | // 2 tasks per change, HandleChange and then LoadChange |
66 | process_tasks(&mut vfs, 2); | 81 | process_tasks(&mut vfs, 2); |
67 | match vfs.commit_changes().as_slice() { | 82 | assert_match!( |
68 | [VfsChange::ChangeFile { text, .. }] => assert_eq!(text.as_str(), "quux"), | 83 | vfs.commit_changes().as_slice(), |
69 | xs => panic!("unexpected changes {:?}", xs), | 84 | [VfsChange::ChangeFile { text, .. }], |
70 | } | 85 | assert_eq!(text.as_str(), "quux") |
86 | ); | ||
71 | 87 | ||
72 | vfs.change_file_overlay(&dir.path().join("a/b/baz.rs"), "m".to_string()); | 88 | vfs.change_file_overlay(&dir.path().join("a/b/baz.rs"), "m".to_string()); |
73 | match vfs.commit_changes().as_slice() { | 89 | assert_match!( |
74 | [VfsChange::ChangeFile { text, .. }] => assert_eq!(text.as_str(), "m"), | 90 | vfs.commit_changes().as_slice(), |
75 | xs => panic!("unexpected changes {:?}", xs), | 91 | [VfsChange::ChangeFile { text, .. }], |
76 | } | 92 | assert_eq!(text.as_str(), "m") |
93 | ); | ||
77 | 94 | ||
78 | // removing overlay restores data on disk | 95 | // removing overlay restores data on disk |
79 | vfs.remove_file_overlay(&dir.path().join("a/b/baz.rs")); | 96 | vfs.remove_file_overlay(&dir.path().join("a/b/baz.rs")); |
80 | match vfs.commit_changes().as_slice() { | 97 | assert_match!( |
81 | [VfsChange::ChangeFile { text, .. }] => assert_eq!(text.as_str(), "quux"), | 98 | vfs.commit_changes().as_slice(), |
82 | xs => panic!("unexpected changes {:?}", xs), | 99 | [VfsChange::ChangeFile { text, .. }], |
83 | } | 100 | assert_eq!(text.as_str(), "quux") |
101 | ); | ||
84 | 102 | ||
85 | vfs.add_file_overlay(&dir.path().join("a/b/spam.rs"), "spam".to_string()); | 103 | vfs.add_file_overlay(&dir.path().join("a/b/spam.rs"), "spam".to_string()); |
86 | match vfs.commit_changes().as_slice() { | 104 | assert_match!( |
87 | [VfsChange::AddFile { text, path, .. }] => { | 105 | vfs.commit_changes().as_slice(), |
106 | [VfsChange::AddFile { text, path, .. }], | ||
107 | { | ||
88 | assert_eq!(text.as_str(), "spam"); | 108 | assert_eq!(text.as_str(), "spam"); |
89 | assert_eq!(path, "spam.rs"); | 109 | assert_eq!(path, "spam.rs"); |
90 | } | 110 | } |
91 | xs => panic!("unexpected changes {:?}", xs), | 111 | ); |
92 | } | ||
93 | 112 | ||
94 | vfs.remove_file_overlay(&dir.path().join("a/b/spam.rs")); | 113 | vfs.remove_file_overlay(&dir.path().join("a/b/spam.rs")); |
95 | match vfs.commit_changes().as_slice() { | 114 | assert_match!( |
96 | [VfsChange::RemoveFile { path, .. }] => assert_eq!(path, "spam.rs"), | 115 | vfs.commit_changes().as_slice(), |
97 | xs => panic!("unexpected changes {:?}", xs), | 116 | [VfsChange::RemoveFile { path, .. }], |
98 | } | 117 | assert_eq!(path, "spam.rs") |
99 | 118 | ); | |
100 | fs::write(&dir.path().join("a/new.rs"), "new hello").unwrap(); | 119 | |
101 | process_tasks(&mut vfs, 2); | 120 | fs::create_dir_all(dir.path().join("a/c")).unwrap(); |
102 | match vfs.commit_changes().as_slice() { | 121 | fs::write(dir.path().join("a/c/new.rs"), "new hello").unwrap(); |
103 | [VfsChange::AddFile { text, path, .. }] => { | 122 | process_tasks(&mut vfs, 4); |
123 | assert_match!( | ||
124 | vfs.commit_changes().as_slice(), | ||
125 | [VfsChange::AddFile { text, path, .. }], | ||
126 | { | ||
104 | assert_eq!(text.as_str(), "new hello"); | 127 | assert_eq!(text.as_str(), "new hello"); |
105 | assert_eq!(path, "new.rs"); | 128 | assert_eq!(path, "c/new.rs"); |
106 | } | 129 | } |
107 | xs => panic!("unexpected changes {:?}", xs), | 130 | ); |
108 | } | ||
109 | 131 | ||
110 | fs::rename(&dir.path().join("a/new.rs"), &dir.path().join("a/new1.rs")).unwrap(); | 132 | fs::rename( |
133 | &dir.path().join("a/c/new.rs"), | ||
134 | &dir.path().join("a/c/new1.rs"), | ||
135 | ) | ||
136 | .unwrap(); | ||
111 | process_tasks(&mut vfs, 4); | 137 | process_tasks(&mut vfs, 4); |
112 | match vfs.commit_changes().as_slice() { | 138 | assert_match!( |
139 | vfs.commit_changes().as_slice(), | ||
113 | [VfsChange::RemoveFile { | 140 | [VfsChange::RemoveFile { |
114 | path: removed_path, .. | 141 | path: removed_path, .. |
115 | }, VfsChange::AddFile { | 142 | }, VfsChange::AddFile { |
116 | text, | 143 | text, |
117 | path: added_path, | 144 | path: added_path, |
118 | .. | 145 | .. |
119 | }] => { | 146 | }], |
120 | assert_eq!(removed_path, "new.rs"); | 147 | { |
121 | assert_eq!(added_path, "new1.rs"); | 148 | assert_eq!(removed_path, "c/new.rs"); |
149 | assert_eq!(added_path, "c/new1.rs"); | ||
122 | assert_eq!(text.as_str(), "new hello"); | 150 | assert_eq!(text.as_str(), "new hello"); |
123 | } | 151 | } |
124 | xs => panic!("unexpected changes {:?}", xs), | 152 | ); |
125 | } | ||
126 | 153 | ||
127 | fs::remove_file(&dir.path().join("a/new1.rs")).unwrap(); | 154 | fs::remove_file(&dir.path().join("a/c/new1.rs")).unwrap(); |
128 | process_tasks(&mut vfs, 2); | 155 | process_tasks(&mut vfs, 2); |
129 | match vfs.commit_changes().as_slice() { | 156 | assert_match!( |
130 | [VfsChange::RemoveFile { path, .. }] => assert_eq!(path, "new1.rs"), | 157 | vfs.commit_changes().as_slice(), |
131 | xs => panic!("unexpected changes {:?}", xs), | 158 | [VfsChange::RemoveFile { path, .. }], |
132 | } | 159 | assert_eq!(path, "c/new1.rs") |
133 | 160 | ); | |
134 | match vfs.task_receiver().try_recv() { | 161 | |
135 | Err(crossbeam_channel::TryRecvError::Empty) => (), | 162 | fs::create_dir_all(dir.path().join("a/target")).unwrap(); |
136 | res => panic!("unexpected {:?}", res), | 163 | // should be ignored |
137 | } | 164 | fs::write(&dir.path().join("a/target/new.rs"), "ignore me").unwrap(); |
165 | process_tasks(&mut vfs, 1); // 1 task because no LoadChange will happen, just HandleChange for dir creation | ||
166 | |||
167 | assert_match!( | ||
168 | vfs.task_receiver().try_recv(), | ||
169 | Err(crossbeam_channel::TryRecvError::Empty) | ||
170 | ); | ||
138 | 171 | ||
139 | vfs.shutdown().unwrap(); | 172 | vfs.shutdown().unwrap(); |
140 | Ok(()) | 173 | Ok(()) |