@@ -29,6 +29,7 @@ use stdext::result::ResultExt;
2929use tokio:: sync:: mpsc;
3030use tokio:: sync:: mpsc:: unbounded_channel as tokio_unbounded_channel;
3131use tokio:: task:: JoinHandle ;
32+ use tower_lsp:: jsonrpc;
3233use tower_lsp:: lsp_types;
3334use tower_lsp:: lsp_types:: Diagnostic ;
3435use tower_lsp:: lsp_types:: MessageType ;
@@ -39,6 +40,7 @@ use super::backend::RequestResponse;
3940use crate :: console:: ConsoleNotification ;
4041use crate :: lsp;
4142use crate :: lsp:: ark_file:: ArkFile ;
43+ use crate :: lsp:: backend:: LspError ;
4244use crate :: lsp:: backend:: LspMessage ;
4345use crate :: lsp:: backend:: LspNotification ;
4446use crate :: lsp:: backend:: LspRequest ;
@@ -602,6 +604,12 @@ fn respond<T>(
602604 let response = match std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( response) ) {
603605 Ok ( Ok ( t) ) => RequestResponse :: Result ( Ok ( into_lsp_response ( t) ) ) ,
604606 Ok ( Err ( e) ) => RequestResponse :: Result ( Err ( e) ) ,
607+ Err ( err) if err. downcast_ref :: < salsa:: Cancelled > ( ) . is_some ( ) => {
608+ // A salsa write cancelled an oak query while the handler ran.
609+ // Report `ContentModified` so the client knows the content moved
610+ // under us and re-requests.
611+ RequestResponse :: Result ( Err ( LspError :: JsonRpc ( jsonrpc:: Error :: content_modified ( ) ) ) )
612+ } ,
605613 Err ( err) => {
606614 // Set global crash flag to disable the LSP
607615 LSP_HAS_CRASHED . store ( true , Ordering :: Release ) ;
@@ -1048,11 +1056,17 @@ mod tests {
10481056 use aether_path:: FilePath ;
10491057 use oak_scan:: DbScan ;
10501058 use salsa:: Database ;
1059+ use tower_lsp:: jsonrpc;
10511060 use url:: Url ;
10521061
10531062 use super :: catch_cancellation;
10541063 use super :: refresh_diagnostics;
1064+ use super :: respond;
1065+ use super :: tokio_unbounded_channel;
10551066 use super :: RefreshDiagnosticsTask ;
1067+ use crate :: lsp:: backend:: LspError ;
1068+ use crate :: lsp:: backend:: LspResponse ;
1069+ use crate :: lsp:: backend:: RequestResponse ;
10561070 use crate :: lsp:: state:: WorldState ;
10571071
10581072 /// A salsa cancellation during the pass is swallowed into `None` by
@@ -1084,6 +1098,41 @@ mod tests {
10841098 assert ! ( catch_cancellation( || refresh_diagnostics( task) ) . is_none( ) ) ;
10851099 }
10861100
1101+ /// A `salsa::Cancelled` re-raised out of a request handler (by `r_task`,
1102+ /// after catching it on the R thread) must not crash the LSP. `respond`
1103+ /// recognises the payload and answers `ContentModified` so the client
1104+ /// re-requests, rather than taking the panic-is-a-crash path.
1105+ #[ test]
1106+ fn test_cancelled_request_reports_content_modified ( ) {
1107+ let mut state = WorldState :: default ( ) ;
1108+ let uri = Url :: parse ( "file:///test.R" ) . unwrap ( ) ;
1109+ let file = state
1110+ . db
1111+ . upsert_editor ( FilePath :: from_url ( & uri) , "foo" . to_string ( ) ) ;
1112+ state. insert_ark_file ( uri. clone ( ) , file, None ) ;
1113+
1114+ let file = state. ark_file ( & uri) . unwrap ( ) ;
1115+ let snapshot = state. diagnostics_snapshot ( ) ;
1116+ snapshot. db . cancellation_token ( ) . cancel ( ) ;
1117+
1118+ let ( response_tx, mut response_rx) = tokio_unbounded_channel :: < RequestResponse > ( ) ;
1119+ respond (
1120+ response_tx,
1121+ || {
1122+ let _ = file. tree_sitter ( & snapshot. db ) ;
1123+ Ok ( LspResponse :: Hover ( None ) )
1124+ } ,
1125+ |response| response,
1126+ )
1127+ . unwrap ( ) ;
1128+
1129+ let response = response_rx. try_recv ( ) . unwrap ( ) ;
1130+ let RequestResponse :: Result ( Err ( LspError :: JsonRpc ( error) ) ) = response else {
1131+ panic ! ( "Expected a jsonrpc error response" ) ;
1132+ } ;
1133+ assert_eq ! ( error. code, jsonrpc:: ErrorCode :: ContentModified ) ;
1134+ }
1135+
10871136 /// The central diagnostics refresh keys off the oak revision advancing
10881137 /// across a loop tick, so an oak write must bump the revision. This pins
10891138 /// that assumption: if a salsa upgrade changed it, the refresh would
0 commit comments