11use std:: future:: Future ;
2+ use std:: panic:: AssertUnwindSafe ;
23use std:: sync:: Arc ;
34
45use djls_project:: Db as ProjectDb ;
@@ -17,6 +18,9 @@ use crate::ext::UriExt;
1718use crate :: logging:: LoggingGuard ;
1819use crate :: queue:: Queue ;
1920use crate :: session:: Session ;
21+ use crate :: session:: SessionSnapshot ;
22+
23+ const SNAPSHOT_CANCEL_RETRIES : usize = 2 ;
2024
2125pub ( crate ) struct DjangoLanguageServer {
2226 client : Client ,
@@ -52,6 +56,47 @@ impl DjangoLanguageServer {
5256 f ( & mut session)
5357 }
5458
59+ /// Capture a snapshot under a brief lock, then compute on the blocking
60+ /// pool so the single-threaded event loop stays responsive.
61+ pub ( crate ) async fn with_snapshot < F , R > ( & self , f : F ) -> R
62+ where
63+ F : Fn ( & SessionSnapshot ) -> R + Send + Sync + ' static ,
64+ R : Default + Send + ' static ,
65+ {
66+ let f = Arc :: new ( f) ;
67+
68+ for attempt in 0 ..=SNAPSHOT_CANCEL_RETRIES {
69+ let snapshot = { self . session . lock ( ) . await . snapshot ( ) } ;
70+ let f = Arc :: clone ( & f) ;
71+ let result = tokio:: task:: spawn_blocking ( move || {
72+ salsa:: Cancelled :: catch ( AssertUnwindSafe ( || f ( & snapshot) ) )
73+ } )
74+ . await
75+ . expect ( "snapshot task must not panic" ) ;
76+
77+ match result {
78+ Ok ( result) => return result,
79+ Err ( cancelled) if attempt < SNAPSHOT_CANCEL_RETRIES => {
80+ tracing:: debug!(
81+ ?cancelled,
82+ attempt = attempt + 1 ,
83+ "Snapshot request cancelled; retrying with fresh snapshot"
84+ ) ;
85+ }
86+ Err ( cancelled) => {
87+ tracing:: debug!(
88+ ?cancelled,
89+ retries = SNAPSHOT_CANCEL_RETRIES ,
90+ "Snapshot request cancelled; returning fallback"
91+ ) ;
92+ return R :: default ( ) ;
93+ }
94+ }
95+ }
96+
97+ unreachable ! ( "snapshot retry loop must return" )
98+ }
99+
55100 pub ( crate ) async fn with_session_mut_task < F , Fut > (
56101 & self ,
57102 f : F ,
@@ -85,14 +130,15 @@ impl DjangoLanguageServer {
85130 }
86131
87132 async fn maybe_push_diagnostics ( & self , document : & TextDocument ) {
133+ let file = document. file ( ) ;
88134 let Some ( diagnostics) = self
89- . with_session ( |session | {
90- if session . client_info ( ) . supports_pull_diagnostics ( ) {
135+ . with_snapshot ( move |snapshot | {
136+ if snapshot . client_info ( ) . supports_pull_diagnostics ( ) {
91137 tracing:: debug!( "Client supports pull diagnostics, skipping push" ) ;
92138 return None ;
93139 }
94140
95- djls_ide:: collect_diagnostics ( session . db ( ) , document . file ( ) )
141+ djls_ide:: collect_diagnostics ( snapshot . db ( ) , file)
96142 } )
97143 . await
98144 else {
@@ -260,13 +306,13 @@ impl LanguageServer for DjangoLanguageServer {
260306 params : ls_types:: CompletionParams ,
261307 ) -> LspResult < Option < ls_types:: CompletionResponse > > {
262308 let response = self
263- . with_session ( |session | {
264- let ( file, offset) = session . position_for_document_request (
309+ . with_snapshot ( move |snapshot | {
310+ let ( file, offset) = snapshot . position_for_document_request (
265311 & params. text_document_position . text_document ,
266312 params. text_document_position . position ,
267313 "completion" ,
268314 ) ?;
269- let db = session . db ( ) ;
315+ let db = snapshot . db ( ) ;
270316
271317 if * file. source ( db) . kind ( ) != FileKind :: Template {
272318 return None ;
@@ -276,8 +322,8 @@ impl LanguageServer for DjangoLanguageServer {
276322 db,
277323 file,
278324 offset,
279- session . client_info ( ) . position_encoding ( ) ,
280- session . client_info ( ) . supports_snippets ( ) ,
325+ snapshot . client_info ( ) . position_encoding ( ) ,
326+ snapshot . client_info ( ) . supports_snippets ( ) ,
281327 )
282328 } )
283329 . await ;
@@ -287,13 +333,13 @@ impl LanguageServer for DjangoLanguageServer {
287333
288334 async fn hover ( & self , params : ls_types:: HoverParams ) -> LspResult < Option < ls_types:: Hover > > {
289335 let response = self
290- . with_session ( |session | {
291- let ( file, offset) = session . position_for_document_request (
336+ . with_snapshot ( move |snapshot | {
337+ let ( file, offset) = snapshot . position_for_document_request (
292338 & params. text_document_position_params . text_document ,
293339 params. text_document_position_params . position ,
294340 "hover" ,
295341 ) ?;
296- let db = session . db ( ) ;
342+ let db = snapshot . db ( ) ;
297343
298344 if * file. source ( db) . kind ( ) != FileKind :: Template {
299345 return None ;
@@ -316,14 +362,14 @@ impl LanguageServer for DjangoLanguageServer {
316362 ) ;
317363
318364 let diagnostics = self
319- . with_session ( |session | {
365+ . with_snapshot ( move |snapshot | {
320366 let Some ( file) =
321- session . file_for_document_request ( & params. text_document , "diagnostic" )
367+ snapshot . file_for_document_request ( & params. text_document , "diagnostic" )
322368 else {
323369 return Vec :: new ( ) ;
324370 } ;
325371
326- djls_ide:: collect_diagnostics ( session . db ( ) , file) . unwrap_or_default ( )
372+ djls_ide:: collect_diagnostics ( snapshot . db ( ) , file) . unwrap_or_default ( )
327373 } )
328374 . await ;
329375
@@ -345,13 +391,13 @@ impl LanguageServer for DjangoLanguageServer {
345391 params : ls_types:: FoldingRangeParams ,
346392 ) -> LspResult < Option < Vec < ls_types:: FoldingRange > > > {
347393 let ranges = self
348- . with_session ( |session | {
394+ . with_snapshot ( move |snapshot | {
349395 let Some ( file) =
350- session . file_for_document_request ( & params. text_document , "folding" )
396+ snapshot . file_for_document_request ( & params. text_document , "folding" )
351397 else {
352398 return Vec :: new ( ) ;
353399 } ;
354- let db = session . db ( ) ;
400+ let db = snapshot . db ( ) ;
355401
356402 if * file. source ( db) . kind ( ) != FileKind :: Template {
357403 return Vec :: new ( ) ;
@@ -369,13 +415,13 @@ impl LanguageServer for DjangoLanguageServer {
369415 params : ls_types:: DocumentSymbolParams ,
370416 ) -> LspResult < Option < ls_types:: DocumentSymbolResponse > > {
371417 let symbols = self
372- . with_session ( |session | {
418+ . with_snapshot ( move |snapshot | {
373419 let Some ( file) =
374- session . file_for_document_request ( & params. text_document , "document symbol" )
420+ snapshot . file_for_document_request ( & params. text_document , "document symbol" )
375421 else {
376422 return Vec :: new ( ) ;
377423 } ;
378- let db = session . db ( ) ;
424+ let db = snapshot . db ( ) ;
379425
380426 if * file. source ( db) . kind ( ) != FileKind :: Template {
381427 return Vec :: new ( ) ;
@@ -393,13 +439,13 @@ impl LanguageServer for DjangoLanguageServer {
393439 params : ls_types:: GotoDefinitionParams ,
394440 ) -> LspResult < Option < ls_types:: GotoDefinitionResponse > > {
395441 let response = self
396- . with_session ( |session | {
397- let ( file, offset) = session . position_for_document_request (
442+ . with_snapshot ( move |snapshot | {
443+ let ( file, offset) = snapshot . position_for_document_request (
398444 & params. text_document_position_params . text_document ,
399445 params. text_document_position_params . position ,
400446 "goto definition" ,
401447 ) ?;
402- let db = session . db ( ) ;
448+ let db = snapshot . db ( ) ;
403449
404450 if * file. source ( db) . kind ( ) != FileKind :: Template {
405451 return None ;
@@ -417,13 +463,13 @@ impl LanguageServer for DjangoLanguageServer {
417463 params : ls_types:: ReferenceParams ,
418464 ) -> LspResult < Option < Vec < ls_types:: Location > > > {
419465 let response = self
420- . with_session ( |session | {
421- let ( file, offset) = session . position_for_document_request (
466+ . with_snapshot ( move |snapshot | {
467+ let ( file, offset) = snapshot . position_for_document_request (
422468 & params. text_document_position . text_document ,
423469 params. text_document_position . position ,
424470 "references" ,
425471 ) ?;
426- let db = session . db ( ) ;
472+ let db = snapshot . db ( ) ;
427473
428474 if * file. source ( db) . kind ( ) != FileKind :: Template {
429475 return None ;
@@ -441,13 +487,13 @@ impl LanguageServer for DjangoLanguageServer {
441487 params : ls_types:: DocumentFormattingParams ,
442488 ) -> LspResult < Option < Vec < ls_types:: TextEdit > > > {
443489 let edits = self
444- . with_session ( |session | {
490+ . with_snapshot ( move |snapshot | {
445491 let Some ( file) =
446- session . file_for_document_request ( & params. text_document , "formatting" )
492+ snapshot . file_for_document_request ( & params. text_document , "formatting" )
447493 else {
448494 return Vec :: new ( ) ;
449495 } ;
450- let db = session . db ( ) ;
496+ let db = snapshot . db ( ) ;
451497 let format_config = db. settings ( ) . format ( ) . clone ( ) ;
452498
453499 if !format_config. enabled ( ) {
@@ -462,7 +508,7 @@ impl LanguageServer for DjangoLanguageServer {
462508 djls_ide:: format_document (
463509 db,
464510 file,
465- session . client_info ( ) . position_encoding ( ) ,
511+ snapshot . client_info ( ) . position_encoding ( ) ,
466512 format_config. backend ( ) ,
467513 & params. options ,
468514 )
0 commit comments