2323import java .util .List ;
2424import java .util .Optional ;
2525import java .util .Set ;
26+ import java .util .concurrent .Callable ;
27+ import java .util .concurrent .CancellationException ;
2628import java .util .concurrent .CompletableFuture ;
2729import java .util .concurrent .ExecutionException ;
30+ import java .util .concurrent .ExecutorService ;
31+ import java .util .concurrent .FutureTask ;
32+ import java .util .concurrent .LinkedBlockingQueue ;
33+ import java .util .concurrent .ThreadFactory ;
34+ import java .util .concurrent .ThreadPoolExecutor ;
2835import java .util .concurrent .TimeUnit ;
2936import java .util .concurrent .TimeoutException ;
3037import java .util .concurrent .atomic .AtomicInteger ;
@@ -60,6 +67,22 @@ class AsyncCompletionProposalPopup extends CompletionProposalPopup {
6067
6168 private static final int MAX_WAIT_IN_MS = 50 ; // TODO make it a preference
6269
70+ private static final ExecutorService EXECUTOR = new ThreadPoolExecutor (
71+ Runtime .getRuntime ().availableProcessors (),
72+ Runtime .getRuntime ().availableProcessors (),
73+ 3L , TimeUnit .SECONDS ,
74+ new LinkedBlockingQueue <>(),
75+ new ThreadFactory () {
76+ AtomicInteger count = new AtomicInteger (1 );
77+
78+ @ Override
79+ public Thread newThread (Runnable r ) {
80+ Thread t = new Thread (r , AsyncCompletionProposalPopup .class .getSimpleName () + "-worker-" + count .getAndIncrement ()); //$NON-NLS-1$
81+ t .setDaemon (true ); // No need to keep the JVM running just because of the completion proposals
82+ return t ;
83+ }
84+ });
85+
6386 /**
6487 * This is only used and set when populating the dialog is async (ie computation takes more than
6588 * MAX_WAIT_IN_MS
@@ -372,7 +395,7 @@ protected List<CompletableFuture<List<ICompletionProposal>>> buildCompletionFutu
372395 }
373396 List <CompletableFuture <List <ICompletionProposal >>> futures = new ArrayList <>(processors .size ());
374397 for (IContentAssistProcessor processor : processors ) {
375- futures .add (CompletableFuture . supplyAsync (() -> {
398+ futures .add (submitInterruptible (() -> {
376399 AtomicReference <List <ICompletionProposal >> result = new AtomicReference <>();
377400 SafeRunner .run (() -> {
378401 ICompletionProposal [] proposals = processor .computeCompletionProposals (fViewer , invocationOffset );
@@ -389,11 +412,46 @@ protected List<CompletableFuture<List<ICompletionProposal>>> buildCompletionFutu
389412 return Collections .emptyList ();
390413 }
391414 return proposals ;
392- }));
415+ }, EXECUTOR ));
393416 }
394417 return futures ;
395418 }
396419
420+ /**
421+ * Submit a task in such a way that it actually reacts to cancellation (i.e. calls to
422+ * {@code future.cancel(true)})
423+ *
424+ * @param executor Do not use the common pool here since that one does not cancel (interrupts)
425+ * worker threads
426+ * @return an interruptible future.
427+ */
428+ private static <T > CompletableFuture <T > submitInterruptible (
429+ Callable <T > task , ExecutorService executor ) {
430+
431+ CompletableFuture <T > cf = new CompletableFuture <>();
432+ FutureTask <T > ft = new FutureTask <>(task );
433+
434+ executor .submit (() -> {
435+ try {
436+ ft .run (); // executes in this thread
437+ cf .complete (ft .get ());
438+ } catch (CancellationException e ) {
439+ cf .cancel (true );
440+ } catch (Exception e ) {
441+ cf .completeExceptionally (e );
442+ }
443+ });
444+
445+ // make canceling the CF also cancel the FutureTask
446+ cf .whenComplete ((r , t ) -> {
447+ if (cf .isCancelled ()) {
448+ ft .cancel (true ); // this actually interrupts
449+ }
450+ });
451+
452+ return cf ;
453+ }
454+
397455 private String getTokenContentType (int invocationOffset ) throws BadLocationException {
398456 if (fContentAssistSubjectControl != null ) {
399457 IDocument document = fContentAssistSubjectControl .getDocument ();
0 commit comments