@@ -562,3 +562,82 @@ async fn test_dropping_the_server_stops_its_background_tasks() {
562562 "background tasks outlived the server they belong to"
563563 ) ;
564564}
565+
566+ /// A deletion is handled immediately and a change is debounced, so an edit made
567+ /// shortly before a file is deleted is still queued when the deletion arrives.
568+ /// Flushing it afterwards puts the file back in the index, and the cross-file
569+ /// rules then keep answering questions about a file that is no longer there.
570+ ///
571+ /// Renaming reaches this on every keystroke-then-rename: the editor reports a
572+ /// rename as a deletion of the old path.
573+ #[ tokio:: test]
574+ async fn test_a_deleted_file_is_not_resurrected_by_a_pending_edit ( ) {
575+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
576+ let root = write_workspace (
577+ & temp,
578+ & [
579+ ( ".rumdl.toml" , ENABLE_MD051 ) ,
580+ (
581+ "a.md" ,
582+ "# A\n \n See [gone](./b.md#missing) and [kept](./c.md#missing).\n " ,
583+ ) ,
584+ ( "b.md" , "# B\n " ) ,
585+ ( "c.md" , "# C\n " ) ,
586+ ] ,
587+ ) ;
588+ let ( a, b) = ( root. join ( "a.md" ) , root. join ( "b.md" ) ) ;
589+ let a_text = "# A\n \n See [gone](./b.md#missing) and [kept](./c.md#missing).\n " ;
590+
591+ let client = LspTestClient :: start ( & root. join ( ".rumdl.toml" ) ) ;
592+ client. initialize ( & root, push_capabilities ( ) ) . await ;
593+ client. notify ( "initialized" , json ! ( { } ) ) . await ;
594+ client. wait_for_index_ready ( ) . await ;
595+
596+ client. did_open ( & a, a_text) . await ;
597+ let before = client. wait_for_publishes ( & a, 1 ) . await ;
598+ assert ! (
599+ before[ 0 ] . iter( ) . any( |d| d. contains( "./b.md" ) ) ,
600+ "control: b.md is indexed and lacks the anchor, so its fragment is reported \
601+ before the deletion, got {before:?}"
602+ ) ;
603+
604+ // The edit queues a debounced update for b.md; the deletion arrives while
605+ // that update is still waiting.
606+ client. did_open ( & b, "# B\n " ) . await ;
607+ client. did_change ( & b, 2 , "# B\n \n Edited.\n " ) . await ;
608+ std:: fs:: remove_file ( & b) . unwrap ( ) ;
609+ client
610+ . notify (
611+ "workspace/didChangeWatchedFiles" ,
612+ json ! ( { "changes" : [ { "uri" : Url :: from_file_path( & b) . unwrap( ) , "type" : 3 } ] } ) ,
613+ )
614+ . await ;
615+
616+ // Long enough for the pending edit to have been flushed if it survived the
617+ // deletion, which is the whole failure mode.
618+ tokio:: time:: sleep ( Duration :: from_millis ( 600 ) ) . await ;
619+ let resurrected = {
620+ let index = client. server . workspace_index . read ( ) . await ;
621+ index. get_file ( & b) . is_some ( )
622+ } ;
623+ assert ! ( !resurrected, "a deleted file must not be back in the workspace index" ) ;
624+
625+ // What the editor shows: an edit of a.md re-lints it, and the link into the
626+ // deleted file no longer has an index entry to be judged against. Count the
627+ // publishes first, so this reads the lint that ran after the deletion rather
628+ // than the one from did_open.
629+ let seen = client. wait_for_publishes ( & a, 1 ) . await . len ( ) ;
630+ let a_edited = "# A\n \n See [gone](./b.md#missing) and [kept](./c.md#missing). Edited.\n " ;
631+ client. did_change ( & a, 2 , a_edited) . await ;
632+ let after = client. wait_for_publishes ( & a, seen + 1 ) . await ;
633+ let latest = after. last ( ) . unwrap ( ) ;
634+ assert ! (
635+ latest. iter( ) . any( |d| d. contains( "./c.md" ) ) ,
636+ "control: the surviving file is still judged, so a lint that simply went \
637+ quiet cannot pass for a fixed one, got {latest:?}"
638+ ) ;
639+ assert ! (
640+ !latest. iter( ) . any( |d| d. contains( "./b.md" ) ) ,
641+ "the deleted file must stop answering fragment questions, got {latest:?}"
642+ ) ;
643+ }
0 commit comments