CompletableFuture<R> (@NotNull CompletableFuture<? extends @NotNull ProgressIndicator> progressFuture, @NotNull Semaphore modalityEntered, @NotNull Function<@NotNull ProgressIndicator, R> onThreadCallable) { if (isModal) { Function<ProgressIndicator, ProgressIndicator> modalityRunnable = progressIndicator -> { LaterInvocator.enterModal(progressIndicator, (ModalityStateEx)progressIndicator.getModalityState()); modalityEntered.up(); return progressIndicator; }; // If a progress indicator has not been calculated yet, grabbing IW lock might lead to deadlock, as progress might need it for init progressFuture = progressFuture.thenApplyAsync(modalityRunnable, r -> { if (ApplicationManager.getApplication().isWriteIntentLockAcquired()) { r.run(); } else { ApplicationManager.getApplication().invokeLaterOnWriteThread(r); } }); } CompletableFuture<R> resultFuture = launchTask(onThreadCallable, progressFuture); if (isModal) { CompletableFuture<Void> modalityExitFuture = resultFuture .handle((r, throwable) -> r) // ignore result computation exception .thenAcceptBoth(progressFuture, (r, progressIndicator) -> { if (ApplicationManager.getApplication().isWriteIntentLockAcquired()) { LaterInvocator.leaveModal(progressIndicator); } else { ApplicationManager.getApplication() .invokeLaterOnWriteThread(() -> LaterInvocator.leaveModal(progressIndicator), progressIndicator.getModalityState()); } }); // It's better to associate task future with modality exit so that future finish will lead to expected state (modality exit) resultFuture = resultFuture.thenCombine(modalityExitFuture, (r, __) -> r); } if (isSync) { waitForFutureUnlockingThread(resultFuture); } return resultFuture; }