Skip to content

Commit eac965e

Browse files
authored
[red-knot] Watch search paths (#12407)
1 parent 8659f2f commit eac965e

File tree

16 files changed

+409
-37
lines changed

16 files changed

+409
-37
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/red_knot/src/main.rs

+9-9
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use tracing_tree::time::Uptime;
1212

1313
use red_knot::db::RootDatabase;
1414
use red_knot::watch;
15-
use red_knot::watch::Watcher;
15+
use red_knot::watch::WorkspaceWatcher;
1616
use red_knot::workspace::WorkspaceMetadata;
1717
use ruff_db::program::{ProgramSettings, SearchPathSettings};
1818
use ruff_db::system::{OsSystem, System, SystemPathBuf};
@@ -142,7 +142,7 @@ struct MainLoop {
142142
receiver: crossbeam_channel::Receiver<MainLoopMessage>,
143143

144144
/// The file system watcher, if running in watch mode.
145-
watcher: Option<Watcher>,
145+
watcher: Option<WorkspaceWatcher>,
146146

147147
verbosity: Option<VerbosityLevel>,
148148
}
@@ -164,26 +164,23 @@ impl MainLoop {
164164

165165
fn watch(mut self, db: &mut RootDatabase) -> anyhow::Result<()> {
166166
let sender = self.sender.clone();
167-
let mut watcher = watch::directory_watcher(move |event| {
167+
let watcher = watch::directory_watcher(move |event| {
168168
sender.send(MainLoopMessage::ApplyChanges(event)).unwrap();
169169
})?;
170170

171-
watcher.watch(db.workspace().root(db))?;
172-
173-
self.watcher = Some(watcher);
174-
171+
self.watcher = Some(WorkspaceWatcher::new(watcher, db));
175172
self.run(db);
176173

177174
Ok(())
178175
}
179176

180177
#[allow(clippy::print_stderr)]
181-
fn run(self, db: &mut RootDatabase) {
178+
fn run(mut self, db: &mut RootDatabase) {
182179
// Schedule the first check.
183180
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
184181
let mut revision = 0usize;
185182

186-
for message in &self.receiver {
183+
while let Ok(message) = self.receiver.recv() {
187184
tracing::trace!("Main Loop: Tick");
188185

189186
match message {
@@ -224,6 +221,9 @@ impl MainLoop {
224221
revision += 1;
225222
// Automatically cancels any pending queries and waits for them to complete.
226223
db.apply_changes(changes);
224+
if let Some(watcher) = self.watcher.as_mut() {
225+
watcher.update(db);
226+
}
227227
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
228228
}
229229
MainLoopMessage::Exit => {

crates/red_knot/src/watch.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use ruff_db::system::{SystemPath, SystemPathBuf};
22
pub use watcher::{directory_watcher, EventHandler, Watcher};
3+
pub use workspace_watcher::WorkspaceWatcher;
34

45
mod watcher;
6+
mod workspace_watcher;
57

68
/// Classification of a file system change event.
79
///
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use crate::db::RootDatabase;
2+
use crate::watch::Watcher;
3+
use ruff_db::system::SystemPathBuf;
4+
use rustc_hash::FxHashSet;
5+
use std::fmt::{Formatter, Write};
6+
use tracing::info;
7+
8+
/// Wrapper around a [`Watcher`] that watches the relevant paths of a workspace.
9+
pub struct WorkspaceWatcher {
10+
watcher: Watcher,
11+
12+
/// The paths that need to be watched. This includes paths for which setting up file watching failed.
13+
watched_paths: FxHashSet<SystemPathBuf>,
14+
15+
/// Paths that should be watched but setting up the watcher failed for some reason.
16+
/// This should be rare.
17+
errored_paths: Vec<SystemPathBuf>,
18+
}
19+
20+
impl WorkspaceWatcher {
21+
/// Create a new workspace watcher.
22+
pub fn new(watcher: Watcher, db: &RootDatabase) -> Self {
23+
let mut watcher = Self {
24+
watcher,
25+
watched_paths: FxHashSet::default(),
26+
errored_paths: Vec::new(),
27+
};
28+
29+
watcher.update(db);
30+
31+
watcher
32+
}
33+
34+
pub fn update(&mut self, db: &RootDatabase) {
35+
let new_watch_paths = db.workspace().paths_to_watch(db);
36+
37+
let mut added_folders = new_watch_paths.difference(&self.watched_paths).peekable();
38+
let mut removed_folders = self.watched_paths.difference(&new_watch_paths).peekable();
39+
40+
if added_folders.peek().is_none() && removed_folders.peek().is_none() {
41+
return;
42+
}
43+
44+
for added_folder in added_folders {
45+
// Log a warning. It's not worth aborting if registering a single folder fails because
46+
// Ruff otherwise stills works as expected.
47+
if let Err(error) = self.watcher.watch(added_folder) {
48+
// TODO: Log a user-facing warning.
49+
tracing::warn!("Failed to setup watcher for path '{added_folder}': {error}. You have to restart Ruff after making changes to files under this path or you might see stale results.");
50+
self.errored_paths.push(added_folder.clone());
51+
}
52+
}
53+
54+
for removed_path in removed_folders {
55+
if let Some(index) = self
56+
.errored_paths
57+
.iter()
58+
.position(|path| path == removed_path)
59+
{
60+
self.errored_paths.swap_remove(index);
61+
continue;
62+
}
63+
64+
if let Err(error) = self.watcher.unwatch(removed_path) {
65+
info!("Failed to remove the file watcher for the path '{removed_path}: {error}.");
66+
}
67+
}
68+
69+
info!(
70+
"Set up file watchers for {}",
71+
DisplayWatchedPaths {
72+
paths: &new_watch_paths
73+
}
74+
);
75+
76+
self.watched_paths = new_watch_paths;
77+
}
78+
79+
/// Returns `true` if setting up watching for any path failed.
80+
pub fn has_errored_paths(&self) -> bool {
81+
!self.errored_paths.is_empty()
82+
}
83+
84+
pub fn flush(&self) {
85+
self.watcher.flush();
86+
}
87+
88+
pub fn stop(self) {
89+
self.watcher.stop();
90+
}
91+
}
92+
93+
struct DisplayWatchedPaths<'a> {
94+
paths: &'a FxHashSet<SystemPathBuf>,
95+
}
96+
97+
impl std::fmt::Display for DisplayWatchedPaths<'_> {
98+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
99+
f.write_char('[')?;
100+
101+
let mut iter = self.paths.iter();
102+
if let Some(first) = iter.next() {
103+
write!(f, "\"{first}\"")?;
104+
105+
for path in iter {
106+
write!(f, ", \"{path}\"")?;
107+
}
108+
}
109+
110+
f.write_char(']')
111+
}
112+
}

crates/red_knot/src/workspace.rs

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::{collections::BTreeMap, sync::Arc};
66
use rustc_hash::{FxBuildHasher, FxHashSet};
77

88
pub use metadata::{PackageMetadata, WorkspaceMetadata};
9+
use red_knot_module_resolver::system_module_search_paths;
910
use ruff_db::{
1011
files::{system_path_to_file, File},
1112
system::{walk_directory::WalkState, SystemPath, SystemPathBuf},
@@ -240,6 +241,17 @@ impl Workspace {
240241
FxHashSet::default()
241242
}
242243
}
244+
245+
/// Returns the paths that should be watched.
246+
///
247+
/// The paths that require watching might change with every revision.
248+
pub fn paths_to_watch(self, db: &dyn Db) -> FxHashSet<SystemPathBuf> {
249+
ruff_db::system::deduplicate_nested_paths(
250+
std::iter::once(self.root(db)).chain(system_module_search_paths(db.upcast())),
251+
)
252+
.map(SystemPath::to_path_buf)
253+
.collect()
254+
}
243255
}
244256

245257
#[salsa::tracked]

0 commit comments

Comments
 (0)