108108import java .net .URI ;
109109import java .util .Collections ;
110110import java .util .List ;
111+ import java .util .Map ;
111112import java .util .concurrent .CompletableFuture ;
113+ import java .util .concurrent .ConcurrentHashMap ;
112114import java .util .concurrent .ExecutorService ;
113115import java .util .concurrent .Executors ;
116+ import java .util .concurrent .TimeUnit ;
114117
115118/**
116119 * Сервис обработки запросов, связанных с текстовым документом.
@@ -144,10 +147,17 @@ public class BSLTextDocumentService implements TextDocumentService, ProtocolExte
144147
145148 private final ExecutorService executorService = Executors .newCachedThreadPool (new CustomizableThreadFactory ("text-document-service-" ));
146149
150+ // Executors per document URI to serialize didChange operations and avoid race conditions
151+ private final Map <String , ExecutorService > documentExecutors = new ConcurrentHashMap <>();
152+
147153 private boolean clientSupportsPullDiagnostics ;
148154
149155 @ PreDestroy
150156 private void onDestroy () {
157+ // Shutdown all document executors
158+ documentExecutors .values ().forEach (ExecutorService ::shutdown );
159+ documentExecutors .clear ();
160+
151161 executorService .shutdown ();
152162 }
153163
@@ -392,6 +402,12 @@ public CompletableFuture<List<InlayHint>> inlayHint(InlayHintParams params) {
392402 public void didOpen (DidOpenTextDocumentParams params ) {
393403 var textDocumentItem = params .getTextDocument ();
394404 var documentContext = context .addDocument (URI .create (textDocumentItem .getUri ()));
405+
406+ // Create single-threaded executor for this document to serialize didChange operations
407+ // Use normalized URI from documentContext
408+ var normalizedUri = documentContext .getUri ().toString ();
409+ documentExecutors .computeIfAbsent (normalizedUri , key ->
410+ Executors .newSingleThreadExecutor (new CustomizableThreadFactory ("doc-" + documentContext .getUri ().getPath () + "-" )));
395411
396412 context .openDocument (documentContext , textDocumentItem .getText (), textDocumentItem .getVersion ());
397413
@@ -402,23 +418,36 @@ public void didOpen(DidOpenTextDocumentParams params) {
402418
403419 @ Override
404420 public void didChange (DidChangeTextDocumentParams params ) {
405-
406421 var documentContext = context .getDocument (params .getTextDocument ().getUri ());
407422 if (documentContext == null ) {
408423 return ;
409424 }
410-
411- var newContent = applyTextDocumentChanges (documentContext .getContent (), params .getContentChanges ());
412-
413- context .rebuildDocument (
414- documentContext ,
415- newContent ,
416- params .getTextDocument ().getVersion ()
417- );
418-
419- if (configuration .getDiagnosticsOptions ().getComputeTrigger () == ComputeTrigger .ONTYPE ) {
420- validate (documentContext );
425+
426+ // Use normalized URI from documentContext
427+ var normalizedUri = documentContext .getUri ().toString ();
428+ var version = params .getTextDocument ().getVersion ();
429+
430+ // Get executor for this document
431+ var executor = documentExecutors .get (normalizedUri );
432+ if (executor == null ) {
433+ // Document not opened or already closed
434+ return ;
421435 }
436+
437+ // Submit change operation to document's executor to serialize operations
438+ executor .submit (() -> {
439+ var newContent = applyTextDocumentChanges (documentContext .getContent (), params .getContentChanges ());
440+
441+ context .rebuildDocument (
442+ documentContext ,
443+ newContent ,
444+ version
445+ );
446+
447+ if (configuration .getDiagnosticsOptions ().getComputeTrigger () == ComputeTrigger .ONTYPE ) {
448+ validate (documentContext );
449+ }
450+ });
422451 }
423452
424453 @ Override
@@ -427,6 +456,24 @@ public void didClose(DidCloseTextDocumentParams params) {
427456 if (documentContext == null ) {
428457 return ;
429458 }
459+
460+ // Use normalized URI from documentContext
461+ var normalizedUri = documentContext .getUri ().toString ();
462+
463+ // Remove and shutdown the executor for this document, waiting for all pending changes
464+ var executor = documentExecutors .remove (normalizedUri );
465+ if (executor != null ) {
466+ executor .shutdown ();
467+ try {
468+ // Wait for all queued changes to complete (with timeout to avoid hanging)
469+ if (!executor .awaitTermination (30 , TimeUnit .SECONDS )) {
470+ executor .shutdownNow ();
471+ }
472+ } catch (InterruptedException e ) {
473+ executor .shutdownNow ();
474+ Thread .currentThread ().interrupt ();
475+ }
476+ }
430477
431478 context .closeDocument (documentContext );
432479
0 commit comments