@@ -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 ;
@@ -593,6 +595,12 @@ fn respond<T>(
593595 let response = match std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( response) ) {
594596 Ok ( Ok ( t) ) => RequestResponse :: Result ( Ok ( into_lsp_response ( t) ) ) ,
595597 Ok ( Err ( e) ) => RequestResponse :: Result ( Err ( e) ) ,
598+ Err ( err) if err. downcast_ref :: < salsa:: Cancelled > ( ) . is_some ( ) => {
599+ // A salsa write cancelled an oak query while the handler ran.
600+ // Report `ContentModified` so the client knows the content moved
601+ // under us and re-requests.
602+ RequestResponse :: Result ( Err ( LspError :: JsonRpc ( jsonrpc:: Error :: content_modified ( ) ) ) )
603+ } ,
596604 Err ( err) => {
597605 // Set global crash flag to disable the LSP
598606 LSP_HAS_CRASHED . store ( true , Ordering :: Release ) ;
@@ -1029,11 +1037,17 @@ mod tests {
10291037 use aether_path:: FilePath ;
10301038 use oak_scan:: DbScan ;
10311039 use salsa:: Database ;
1040+ use tower_lsp:: jsonrpc;
10321041 use url:: Url ;
10331042
10341043 use super :: catch_cancellation;
10351044 use super :: refresh_diagnostics;
1045+ use super :: respond;
1046+ use super :: tokio_unbounded_channel;
10361047 use super :: RefreshDiagnosticsTask ;
1048+ use crate :: lsp:: backend:: LspError ;
1049+ use crate :: lsp:: backend:: LspResponse ;
1050+ use crate :: lsp:: backend:: RequestResponse ;
10371051 use crate :: lsp:: state:: WorldState ;
10381052
10391053 /// A salsa cancellation during the pass is swallowed into `None` by
@@ -1065,6 +1079,41 @@ mod tests {
10651079 assert ! ( catch_cancellation( || refresh_diagnostics( task) ) . is_none( ) ) ;
10661080 }
10671081
1082+ /// A `salsa::Cancelled` re-raised out of a request handler (by `r_task`,
1083+ /// after catching it on the R thread) must not crash the LSP. `respond`
1084+ /// recognises the payload and answers `ContentModified` so the client
1085+ /// re-requests, rather than taking the panic-is-a-crash path.
1086+ #[ test]
1087+ fn test_cancelled_request_reports_content_modified ( ) {
1088+ let mut state = WorldState :: default ( ) ;
1089+ let uri = Url :: parse ( "file:///test.R" ) . unwrap ( ) ;
1090+ let file = state
1091+ . db
1092+ . upsert_editor ( FilePath :: from_url ( & uri) , "foo" . to_string ( ) ) ;
1093+ state. insert_ark_file ( uri. clone ( ) , file, None ) ;
1094+
1095+ let file = state. ark_file ( & uri) . unwrap ( ) ;
1096+ let snapshot = state. diagnostics_snapshot ( ) ;
1097+ snapshot. db . cancellation_token ( ) . cancel ( ) ;
1098+
1099+ let ( response_tx, mut response_rx) = tokio_unbounded_channel :: < RequestResponse > ( ) ;
1100+ respond (
1101+ response_tx,
1102+ || {
1103+ let _ = file. tree_sitter ( & snapshot. db ) ;
1104+ Ok ( LspResponse :: Hover ( None ) )
1105+ } ,
1106+ |response| response,
1107+ )
1108+ . unwrap ( ) ;
1109+
1110+ let response = response_rx. try_recv ( ) . unwrap ( ) ;
1111+ let RequestResponse :: Result ( Err ( LspError :: JsonRpc ( error) ) ) = response else {
1112+ panic ! ( "Expected a jsonrpc error response" ) ;
1113+ } ;
1114+ assert_eq ! ( error. code, jsonrpc:: ErrorCode :: ContentModified ) ;
1115+ }
1116+
10681117 /// The central diagnostics refresh keys off the oak revision advancing
10691118 /// across a loop tick, so an oak write must bump the revision. This pins
10701119 /// that assumption: if a salsa upgrade changed it, the refresh would
0 commit comments