@@ -297,7 +297,19 @@ impl ScanScheduler {
297297 Some ( root) if self . state . contains_key ( & root) => {
298298 self . buffered . entry ( root) . or_default ( ) . push ( event) ;
299299 } ,
300- _ => apply_one_event_surgically ( db, event) ,
300+ // No in-flight scan for this root: apply the event directly,
301+ // the watcher's single-file fast path.
302+ _ => match event. kind {
303+ FileEventKind :: Created | FileEventKind :: Changed => {
304+ match std:: fs:: read_to_string ( & path) {
305+ Ok ( contents) => add_watched_file ( db, event. url , contents) ,
306+ Err ( err) => {
307+ log:: warn!( "Skipped watched file {}: {err:?}" , path. display( ) )
308+ } ,
309+ }
310+ } ,
311+ FileEventKind :: Deleted => remove_watched_file ( db, event. url ) ,
312+ } ,
301313 }
302314 }
303315
@@ -339,21 +351,22 @@ impl ScanScheduler {
339351 let prior = self . state . remove ( & root) ;
340352 match prior {
341353 Some ( ScanState :: ScanningWithRescanQueued ) => {
342- // A rescan was queued mid-scan. Spawn it and keep the
343- // buffer; replay happens when this next scan finishes idle.
344- self . state . insert ( root, ScanState :: Scanning ) ;
345- let Some ( path) = root. path ( db) . to_file_path ( ) . warn_on_err ( ) else {
346- return Vec :: new ( ) ;
347- } ;
348- vec ! [ ScanRequest { root, path } ]
349- } ,
350- Some ( ScanState :: Scanning ) => {
351- // We're now idle. Drain any buffered events through the normal path.
352- match self . buffered . remove ( & root) {
353- Some ( buffered) => self . apply_watcher_events ( db, buffered, editor_owned) ,
354- None => Vec :: new ( ) ,
354+ // A rescan was queued mid-scan. Resolve its path before
355+ // re-marking the root `Scanning`: a path we can't resolve must
356+ // not leave the root `Scanning` with no scan in flight, or it
357+ // would stay pending forever. On success the buffer rides along
358+ // and replays when the requeued scan finishes. On failure we
359+ // fall back to the idle drain.
360+ match root. path ( db) . to_file_path ( ) . warn_on_err ( ) {
361+ Some ( path) => {
362+ self . state . insert ( root, ScanState :: Scanning ) ;
363+ vec ! [ ScanRequest { root, path } ]
364+ } ,
365+ None => self . drain_buffered ( db, root, editor_owned) ,
355366 }
356367 } ,
368+ // We're now idle. Drain any buffered events through the normal path.
369+ Some ( ScanState :: Scanning ) => self . drain_buffered ( db, root, editor_owned) ,
357370 None => {
358371 // A completion for a root we weren't tracking as scanning.
359372 // Every dispatched scan marks its root `Scanning`, so reaching
@@ -369,6 +382,21 @@ impl ScanScheduler {
369382 }
370383 }
371384
385+ /// Replay the watcher events buffered for `root` while its scan was in
386+ /// flight, now that the root is idle. Routes them through
387+ /// [`Self::apply_watcher_events`], which may itself return fresh requests.
388+ fn drain_buffered < DB : Db + DbInputs > (
389+ & mut self ,
390+ db : & mut DB ,
391+ root : Root ,
392+ editor_owned : & HashSet < UrlId > ,
393+ ) -> Vec < ScanRequest > {
394+ match self . buffered . remove ( & root) {
395+ Some ( buffered) => self . apply_watcher_events ( db, buffered, editor_owned) ,
396+ None => Vec :: new ( ) ,
397+ }
398+ }
399+
372400 fn request_rescan < DB : Db + DbInputs > (
373401 & mut self ,
374402 db : & mut DB ,
@@ -418,15 +446,38 @@ fn workspace_root_paths<DB: Db + DbInputs>(db: &DB) -> Vec<(PathBuf, Root)> {
418446 . collect ( )
419447}
420448
421- fn apply_one_event_surgically < DB : Db + DbInputs > ( db : & mut DB , event : FileEvent ) {
422- let Ok ( path) = event. url . to_file_path ( ) else {
423- return ;
424- } ;
425- match event. kind {
426- FileEventKind :: Created | FileEventKind :: Changed => match std:: fs:: read_to_string ( & path) {
427- Ok ( contents) => add_watched_file ( db, event. url , contents) ,
428- Err ( err) => log:: warn!( "Skipped watched file {}: {err:?}" , path. display( ) ) ,
429- } ,
430- FileEventKind :: Deleted => remove_watched_file ( db, event. url ) ,
449+ #[ cfg( test) ]
450+ mod tests {
451+ use oak_db:: OakDatabase ;
452+
453+ use super :: * ;
454+
455+ /// A root whose URL has no filesystem path (e.g. an `untitled:` buffer)
456+ /// can't produce a `ScanRequest`. When such a root is
457+ /// `ScanningWithRescanQueued` and its scan completes, the scheduler must
458+ /// not leave it `Scanning` with nothing in flight, or it would stay pending
459+ /// forever. This state is unreachable through the public API (workspace
460+ /// roots always come from `from_file_path`), so we build it by hand.
461+ #[ test]
462+ fn test_unresolvable_rescan_path_does_not_strand_root_scanning ( ) {
463+ let mut db = OakDatabase :: new ( ) ;
464+ let url = UrlId :: parse ( "untitled:Untitled-1" ) . unwrap ( ) ;
465+ let root = Root :: new ( & db, url, RootKind :: Workspace , Vec :: new ( ) , Vec :: new ( ) ) ;
466+ db. workspace_roots ( ) . set_roots ( & mut db) . to ( vec ! [ root] ) ;
467+
468+ let mut scheduler = ScanScheduler :: new ( ) ;
469+ scheduler
470+ . state
471+ . insert ( root, ScanState :: ScanningWithRescanQueued ) ;
472+
473+ let result = ScanCompleted {
474+ root,
475+ packages : Vec :: new ( ) ,
476+ scripts : Vec :: new ( ) ,
477+ } ;
478+ let requests = scheduler. apply_scan_completed ( & mut db, result, & HashSet :: new ( ) ) ;
479+
480+ assert ! ( requests. is_empty( ) ) ;
481+ assert ! ( !scheduler. has_pending_scans( ) ) ;
431482 }
432483}
0 commit comments