You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by sd...@apache.org on 2022/03/08 18:54:57 UTC

[netbeans] branch master updated: Alternative ProjectProblems presenter for LSP server.

This is an automated email from the ASF dual-hosted git repository.

sdedic pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans.git


The following commit(s) were added to refs/heads/master by this push:
     new 540fb3e  Alternative ProjectProblems presenter for LSP server.
     new d9bd08e  Merge pull request #3568 from sdedic/projects/broken-references
540fb3e is described below

commit 540fb3ea373cd79bd92036bb4cd2560486b398a7
Author: Svata Dedic <sv...@oracle.com>
AuthorDate: Sat Jan 15 16:48:26 2022 +0100

    Alternative ProjectProblems presenter for LSP server.
---
 .../project/ui/problems/BrokenReferencesImpl.java  |  212 ++--
 ide/projectuiapi/apichanges.xml                    |   15 +
 ide/projectuiapi/nbproject/project.properties      |    2 +-
 .../netbeans/api/project/ui/ProjectProblems.java   |   25 +-
 .../project/ui/ProjectProblemsImplementation.java  |   49 +
 .../nbcode/integration/nbproject/project.xml       |   11 +-
 .../integration/LspBrokenReferencesImpl.java       |   35 +
 java/java.lsp.server/nbproject/project.properties  |    2 +-
 java/java.lsp.server/nbproject/project.xml         |   12 +
 .../lsp/server/project/BrokenReferencesImpl.java   |  160 +++
 .../lsp/server/project/BrokenReferencesModel.java  |  318 +++++
 .../lsp/server/project/ProjectAlertPresenter.java  |  808 ++++++++++++
 .../lsp/server/ui/AbstractLspBrokenReferences.java |   48 +
 .../project/LspBrokenReferencesImplTest.java       | 1287 ++++++++++++++++++++
 14 files changed, 2894 insertions(+), 90 deletions(-)

diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/problems/BrokenReferencesImpl.java b/ide/projectui/src/org/netbeans/modules/project/ui/problems/BrokenReferencesImpl.java
index 195ed02..263ef563 100644
--- a/ide/projectui/src/org/netbeans/modules/project/ui/problems/BrokenReferencesImpl.java
+++ b/ide/projectui/src/org/netbeans/modules/project/ui/problems/BrokenReferencesImpl.java
@@ -22,13 +22,14 @@ import java.awt.Dialog;
 import java.awt.GraphicsEnvironment;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.concurrent.CompletableFuture;
 import javax.swing.JButton;
+import javax.swing.SwingUtilities;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 import org.netbeans.api.annotations.common.NonNull;
 import org.netbeans.api.project.Project;
 import org.netbeans.api.project.ProjectUtils;
-import org.netbeans.modules.project.uiapi.BrokenReferencesImplementation;
 import org.openide.DialogDescriptor;
 import org.openide.DialogDisplayer;
 import org.openide.util.NbBundle;
@@ -36,6 +37,7 @@ import org.openide.util.RequestProcessor;
 import org.openide.util.lookup.ServiceProvider;
 import org.openide.windows.WindowManager;
 import static org.netbeans.modules.project.ui.problems.Bundle.*;
+import org.netbeans.spi.project.ui.ProjectProblemsImplementation;
 import org.openide.awt.StatusDisplayer;
 import org.openide.util.Parameters;
 
@@ -43,15 +45,19 @@ import org.openide.util.Parameters;
  *
  * @author Tomas Zezula
  */
-@ServiceProvider(service=BrokenReferencesImplementation.class)
-public class BrokenReferencesImpl implements BrokenReferencesImplementation {
+@ServiceProvider(service=ProjectProblemsImplementation.class)
+public class BrokenReferencesImpl implements ProjectProblemsImplementation {
 
     private static final boolean suppressBrokenRefAlert = Boolean.getBoolean("BrokenReferencesSupport.suppressBrokenRefAlert"); //NOI18N
     private static final RequestProcessor RP = new RequestProcessor(BrokenReferencesImpl.class);
     private static int BROKEN_ALERT_TIMEOUT = 1000;
 
+    // @GuardedBy(this)
     private BrokenReferencesModel.Context context;
+    // @GuardedBy(this)
     private RequestProcessor.Task rpTask;
+    // @GuardedBy(this)
+    private CompletableFuture<Void> runningFuture;
     
 
     @Override
@@ -65,95 +71,121 @@ public class BrokenReferencesImpl implements BrokenReferencesImplementation {
         "AD_Broken_References_Resolve_Panel_Close=N/A",
         "LBL_Broken_References_Resolve_Panel_Title=Resolve Project Problems"
     })
-    public void showAlert(Project project) {
+    public CompletableFuture<Void> showAlert(Project project) {
+        CompletableFuture<Void> result;
         Parameters.notNull("project", project); //NOI18N
+
         if (!BrokenReferencesSettings.isShowAgainBrokenRefAlert() || suppressBrokenRefAlert) {
-            return;
-        } else if (context == null) {
-            assert rpTask == null;
+            return CompletableFuture.completedFuture(null);
+        } 
+        
+        final BrokenReferencesModel.Context ctx;
+        final RequestProcessor.Task t;
+        
+        synchronized (this) {
+            if (context == null) {
+                assert rpTask == null;
 
-            final Runnable task = new Runnable() {
-                @Override
-                public void run() {
-                    final BrokenReferencesModel.Context ctx;
-                    synchronized (BrokenReferencesImpl.this) {
-                        rpTask = null;
-                        ctx = context;
-                    }
-                    if (ctx == null) {
-                        return;
-                    }
-                    try {
-                        final JButton resolveOption = new JButton(CTL_Broken_References_Resolve());
-                        resolveOption.getAccessibleContext().setAccessibleDescription(AD_Broken_References_Resolve());
-                        JButton closeOption = new JButton (CTL_Broken_References_Close());
-                        closeOption.getAccessibleContext().setAccessibleDescription(AD_Broken_References_Close());
-                        DialogDescriptor dd = new DialogDescriptor(new BrokenReferencesAlertPanel(),
-                            MSG_Broken_References_Title(),
-                            true,
-                            new Object[] {resolveOption, closeOption},
-                            closeOption,
-                            DialogDescriptor.DEFAULT_ALIGN,
-                            null,
-                            null);
-                        dd.setMessageType(DialogDescriptor.WARNING_MESSAGE);
-                        ctx.addChangeListener(new ChangeListener() {
-                            @Override
-                            public void stateChanged(ChangeEvent e) {
-                                resolveOption.setVisible(!ctx.isEmpty());
-                            }
-                        });
-                        resolveOption.setVisible(!ctx.isEmpty());
-                        if (DialogDisplayer.getDefault().notify(dd) == resolveOption) {
-                            final BrokenReferencesModel model = new BrokenReferencesModel(ctx, true);
-                            if (GraphicsEnvironment.isHeadless()) {
-                                fixAllProblems(model, new HashSet<>());
-                                return;
+                final Runnable task = new Runnable() {
+                    @Override
+                    public void run() {
+                        final BrokenReferencesModel.Context ctx;
+                        CompletableFuture<Void> res = null;
+                        synchronized (BrokenReferencesImpl.this) {
+                            rpTask = null;
+                            ctx = context;
+                            if (ctx == null) {
+                                res = runningFuture;
+                                runningFuture = null;
                             }
-                            final BrokenReferencesCustomizer customizer = new BrokenReferencesCustomizer(model);
-                            JButton close = new JButton (Bundle.LBL_Broken_References_Resolve_Panel_Close());
-                            close.getAccessibleContext ().setAccessibleDescription (Bundle.AD_Broken_References_Resolve_Panel_Close());
-                            dd = new DialogDescriptor(customizer,
-                                Bundle.LBL_Broken_References_Resolve_Panel_Title(),
+                        }
+                        if (res != null) {
+                            res.complete(null);
+                            return;
+                        }
+                        try {
+                            final JButton resolveOption = new JButton(CTL_Broken_References_Resolve());
+                            resolveOption.getAccessibleContext().setAccessibleDescription(AD_Broken_References_Resolve());
+                            JButton closeOption = new JButton (CTL_Broken_References_Close());
+                            closeOption.getAccessibleContext().setAccessibleDescription(AD_Broken_References_Close());
+                            DialogDescriptor dd = new DialogDescriptor(new BrokenReferencesAlertPanel(),
+                                MSG_Broken_References_Title(),
                                 true,
-                                new Object[] {closeOption},
+                                new Object[] {resolveOption, closeOption},
                                 closeOption,
                                 DialogDescriptor.DEFAULT_ALIGN,
                                 null,
                                 null);
-                            customizer.setNotificationLineSupport(dd.createNotificationLineSupport());
-                            DialogDisplayer.getDefault().notify(dd);
-                        }
-                    } finally {
-                        synchronized (BrokenReferencesImpl.this) {
-                            //Clean seen references and start from empty list
-                            context = null;
+                            dd.setMessageType(DialogDescriptor.WARNING_MESSAGE);
+                            ctx.addChangeListener(new ChangeListener() {
+                                @Override
+                                public void stateChanged(ChangeEvent e) {
+                                    resolveOption.setVisible(!ctx.isEmpty());
+                                }
+                            });
+                            resolveOption.setVisible(!ctx.isEmpty());
+                            if (DialogDisplayer.getDefault().notify(dd) == resolveOption) {
+                                final BrokenReferencesModel model = new BrokenReferencesModel(ctx, true);
+                                if (GraphicsEnvironment.isHeadless()) {
+                                    fixAllProblems(model, new HashSet<>());
+                                    return;
+                                }
+                                final BrokenReferencesCustomizer customizer = new BrokenReferencesCustomizer(model);
+                                JButton close = new JButton (Bundle.LBL_Broken_References_Resolve_Panel_Close());
+                                close.getAccessibleContext ().setAccessibleDescription (Bundle.AD_Broken_References_Resolve_Panel_Close());
+                                dd = new DialogDescriptor(customizer,
+                                    Bundle.LBL_Broken_References_Resolve_Panel_Title(),
+                                    true,
+                                    new Object[] {closeOption},
+                                    closeOption,
+                                    DialogDescriptor.DEFAULT_ALIGN,
+                                    null,
+                                    null);
+                                customizer.setNotificationLineSupport(dd.createNotificationLineSupport());
+                                DialogDisplayer.getDefault().notify(dd);
+                            }
+                        } finally {
+                            synchronized (BrokenReferencesImpl.this) {
+                                //Clean seen references and start from empty list
+                                context = null;
+                                res = runningFuture;
+                                runningFuture = null;
+                            }
+                            res.complete(null);
                         }
                     }
-                }
-            };
-
-            context = new BrokenReferencesModel.Context();
-            rpTask = RP.create(new Runnable() {
-                @Override
-                public void run() {
-                    if (GraphicsEnvironment.isHeadless()) {
-                        task.run();
-                    } else {
-                        WindowManager.getDefault().invokeWhenUIReady(task);
+                };
+                
+                context = new BrokenReferencesModel.Context();
+                rpTask = RP.create(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (GraphicsEnvironment.isHeadless()) {
+                            task.run();
+                        } else {
+                            WindowManager.getDefault().invokeWhenUIReady(task);
+                        }
                     }
-                }
-            });
+                });
+                this.runningFuture = new CompletableFuture<>();
+                return runningFuture;
+            }
+            
+            ctx = this.context;
+            result = this.runningFuture;
+            t = this.rpTask;
         }
 
-        assert context != null;
+        assert ctx != null;
+        assert result != null;
         if (project != null) {
-            context.offer(project);
+            ctx.offer(project);
         }
-        if (rpTask != null) {
+        if (t != null) {
             //Not yet shown, move
-            rpTask.schedule(BROKEN_ALERT_TIMEOUT);
+            t.schedule(BROKEN_ALERT_TIMEOUT);
         }
+        return result;
     }
     
     @NbBundle.Messages({
@@ -197,7 +229,8 @@ public class BrokenReferencesImpl implements BrokenReferencesImplementation {
         "LBL_BrokenLinksCustomizer_Title=Resolve Project Problems - \"{0}\" Project"
     })
     @Override
-    public void showCustomizer(@NonNull Project project) {
+    public CompletableFuture<Void> showCustomizer(@NonNull Project project) {
+        CompletableFuture<Void> result = new CompletableFuture<>();
         Parameters.notNull("project", project); //NOI18N
         BrokenReferencesModel model = new BrokenReferencesModel(project);
         BrokenReferencesCustomizer customizer = new BrokenReferencesCustomizer(model);
@@ -208,15 +241,30 @@ public class BrokenReferencesImpl implements BrokenReferencesImplementation {
             LBL_BrokenLinksCustomizer_Title(projectDisplayName), // NOI18N
             true, new Object[] {close}, close, DialogDescriptor.DEFAULT_ALIGN, null, null);
         customizer.setNotificationLineSupport(dd.createNotificationLineSupport());
-        Dialog dlg = null;
-        try {
-            dlg = DialogDisplayer.getDefault().createDialog(dd);
-            dlg.setVisible(true);
-        } finally {
-            if (dlg != null) {
-                dlg.dispose();
+        Runnable r = new Runnable() {
+            public void run() {
+                Dialog dlg = null;
+                try {
+                    dlg = DialogDisplayer.getDefault().createDialog(dd);
+                    if (SwingUtilities.isEventDispatchThread()) {
+                        dlg.setVisible(true);
+                    }
+                } catch (RuntimeException ex) {
+                    result.completeExceptionally(ex);
+                } finally {
+                    if (dlg != null) {
+                        dlg.dispose();
+                    }
+                    result.complete(null);
+                }
             }
+        };
+        if (SwingUtilities.isEventDispatchThread()) {
+            r.run();
+        } else {
+            SwingUtilities.invokeLater(r);
         }
+        return result;
     }
 
 }
diff --git a/ide/projectuiapi/apichanges.xml b/ide/projectuiapi/apichanges.xml
index f948174..7d9158a 100644
--- a/ide/projectuiapi/apichanges.xml
+++ b/ide/projectuiapi/apichanges.xml
@@ -83,6 +83,21 @@ is the proper place.
     <!-- ACTUAL CHANGES BEGIN HERE: -->
 
     <changes>
+        <change id="project-problems-implementation">
+            <api name="general"/>
+            <summary>Allow pluggable implementation for ProjectProblems API.</summary>
+            <version major="1" minor="105"/>
+            <date day="22" month="2" year="2022"/>
+            <author login="sdedic"/>
+            <compatibility addition="yes"/>
+            <description>
+                <p>
+                    <a href="@TOP@/org/netbeans/api/project/ui/ProjectProblems.html">ProjectProblems API</a> now delegates to 
+                    <a href="@TOP@/org/netbeans/spi/project/ui/ProjectProblemsImplementation .html">ProjectProblemsImplementation</a> 
+                    registered in default Lookup.
+                </p>
+            </description>
+        </change>
         <change id="newProjectAction.preselectCategory">
             <api name="general"/>
             <summary>Provide a method to create an action that invokes the new project wizard, 
diff --git a/ide/projectuiapi/nbproject/project.properties b/ide/projectuiapi/nbproject/project.properties
index c34da8a..b52ab37 100644
--- a/ide/projectuiapi/nbproject/project.properties
+++ b/ide/projectuiapi/nbproject/project.properties
@@ -17,7 +17,7 @@
 
 javac.compilerargs=-Xlint -Xlint:-serial
 javac.source=1.8
-spec.version.base=1.104.0
+spec.version.base=1.105.0
 is.autoload=true
 javadoc.arch=${basedir}/arch.xml
 javadoc.apichanges=${basedir}/apichanges.xml
diff --git a/ide/projectuiapi/src/org/netbeans/api/project/ui/ProjectProblems.java b/ide/projectuiapi/src/org/netbeans/api/project/ui/ProjectProblems.java
index 9e8261c..942ed28 100644
--- a/ide/projectuiapi/src/org/netbeans/api/project/ui/ProjectProblems.java
+++ b/ide/projectuiapi/src/org/netbeans/api/project/ui/ProjectProblems.java
@@ -18,9 +18,11 @@
  */
 package org.netbeans.api.project.ui;
 
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
 import org.netbeans.api.annotations.common.NonNull;
 import org.netbeans.api.project.Project;
-import org.netbeans.modules.project.uiapi.BrokenReferencesImplementation;
+import org.netbeans.spi.project.ui.ProjectProblemsImplementation;
 import org.netbeans.spi.project.ui.ProjectProblemsProvider;
 import org.openide.util.Lookup;
 import org.openide.util.Parameters;
@@ -68,21 +70,34 @@ public class ProjectProblems {
      */
     public static void showAlert(@NonNull final Project project) {
         Parameters.notNull("project", project); //NOI18N
-        final BrokenReferencesImplementation impl = Lookup.getDefault().lookup(BrokenReferencesImplementation.class);
+        final ProjectProblemsImplementation impl = Lookup.getDefault().lookup(ProjectProblemsImplementation.class);
         if (impl != null) {
             impl.showAlert(project);
         }
     }
-
+    
     /**
      * Shows an UI customizer which gives users chance to fix encountered problems.
      * @param project the project for which the customizer should be shown.
      */
     public static void showCustomizer(@NonNull final Project project) {
         Parameters.notNull("project", project); //NOI18N
-        final BrokenReferencesImplementation impl = Lookup.getDefault().lookup(BrokenReferencesImplementation.class);
+        final ProjectProblemsImplementation impl = Lookup.getDefault().lookup(ProjectProblemsImplementation.class);
         if (impl != null) {
-            impl.showCustomizer(project);
+            try {
+                // compatibility: wait for the process to complete.
+                impl.showCustomizer(project).get();
+            } catch (InterruptedException ex) {
+                throw new CompletionException(ex);
+            } catch (ExecutionException ex) {
+                if (ex.getCause() instanceof Error) {
+                    throw (Error)ex.getCause();
+                }
+                if (ex.getCause() instanceof RuntimeException) {
+                    throw (RuntimeException)ex.getCause();
+                }
+                throw new CompletionException(ex.getCause());
+            }
         }
     }
 
diff --git a/ide/projectuiapi/src/org/netbeans/spi/project/ui/ProjectProblemsImplementation.java b/ide/projectuiapi/src/org/netbeans/spi/project/ui/ProjectProblemsImplementation.java
new file mode 100644
index 0000000..585edc8
--- /dev/null
+++ b/ide/projectuiapi/src/org/netbeans/spi/project/ui/ProjectProblemsImplementation.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.spi.project.ui;
+
+import java.util.concurrent.CompletableFuture;
+import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.api.project.Project;
+
+/**
+ * Interface that plugs in the actual implementation of UI for {@link ProjectProblems} API.
+ * 
+ * @author Tomas Zezula, Svatopluk Dedic
+ */
+public interface ProjectProblemsImplementation {
+    
+    /**
+     * Show alert message box informing user that a project has problems (broken references).
+     * references. The implementation should handle gracefully repeated alerts for the same
+     * project, by e.g. ignoring requests to alert for a project that has still its problem
+     * resolution UI opened.
+     * 
+     * @param project to show the alert for.
+     * @return future that will be completed when the user finishes the UI.
+     */
+    CompletableFuture<Void> showAlert(@NonNull Project project);
+
+    /**
+     * Shows a customizer, or another UI to handle project problems.
+     * @param project whose problems should be resolved.
+     * @return future that will be completed once the customizer finishes.
+     */
+    CompletableFuture<Void> showCustomizer(@NonNull Project project);
+}
diff --git a/java/java.lsp.server/nbcode/integration/nbproject/project.xml b/java/java.lsp.server/nbcode/integration/nbproject/project.xml
index 10d20aa..f19f6fb 100644
--- a/java/java.lsp.server/nbcode/integration/nbproject/project.xml
+++ b/java/java.lsp.server/nbcode/integration/nbproject/project.xml
@@ -81,7 +81,16 @@
                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>1.16</specification-version>
+                        <specification-version>1.19</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.projectuiapi</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.104.0.8</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/LspBrokenReferencesImpl.java b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/LspBrokenReferencesImpl.java
new file mode 100644
index 0000000..19bcc91
--- /dev/null
+++ b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/LspBrokenReferencesImpl.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.nbcode.integration;
+
+import org.netbeans.modules.java.lsp.server.ui.AbstractLspBrokenReferences;
+import org.netbeans.spi.project.ui.ProjectProblemsImplementation;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author sdedic
+ */
+@ServiceProvider(service = ProjectProblemsImplementation.class, position = 1000)
+public class LspBrokenReferencesImpl extends AbstractLspBrokenReferences {
+
+    public LspBrokenReferencesImpl() {
+    }
+    
+}
diff --git a/java/java.lsp.server/nbproject/project.properties b/java/java.lsp.server/nbproject/project.properties
index 4c44f5b..8394988 100644
--- a/java/java.lsp.server/nbproject/project.properties
+++ b/java/java.lsp.server/nbproject/project.properties
@@ -17,7 +17,7 @@
 
 javac.source=1.8
 javac.compilerargs=-Xlint -Xlint:-serial
-spec.version.base=1.18.0
+spec.version.base=1.19.0
 javadoc.arch=${basedir}/arch.xml
 requires.nb.javac=true
 lsp.build.dir=vscode/nbcode
diff --git a/java/java.lsp.server/nbproject/project.xml b/java/java.lsp.server/nbproject/project.xml
index 8c8c0ec..93fbe85 100644
--- a/java/java.lsp.server/nbproject/project.xml
+++ b/java/java.lsp.server/nbproject/project.xml
@@ -462,6 +462,17 @@
                         <specification-version>1.101</specification-version>
                     </run-dependency>
                 </dependency>
+
+                <dependency>
+                    <code-name-base>org.netbeans.modules.projectuiapi</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <implementation-version/>
+                    </run-dependency>
+                </dependency>
+
                 <dependency>
                     <code-name-base>org.netbeans.modules.queries</code-name-base>
                     <build-prerequisite/>
@@ -738,6 +749,7 @@
                     <test-dependency>
                         <code-name-base>org.netbeans.modules.projectui</code-name-base>
                         <recursive/>
+                        <compile-dependency/>
                     </test-dependency>
                     <test-dependency>
                         <code-name-base>org.netbeans.modules.settings</code-name-base>
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/BrokenReferencesImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/BrokenReferencesImpl.java
new file mode 100644
index 0000000..79f26cf
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/BrokenReferencesImpl.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.java.lsp.server.project;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.api.project.Project;
+import org.netbeans.spi.project.ui.ProjectProblemsImplementation;
+
+/**
+ * Implements the ProjectProblem resolution process in a way more suitable for LSP. When a project Alert
+ * or Customizer is requested, a ProjectAlertPresenter is created and will be live until the user processes
+ * all the alerts, or the responses time out. With a new alert, the alerts will show up again.
+ * Each Presenter works with a single Project.
+ * <p>
+ * When fatal (non-resolvable) problems occur, they are reported using ShowMessage requests. Once they are
+ * confirmed, resolvable issues will be processed. There's a timeout {@link #WAKEUP_DELAY} for the user
+ * inactivity after last user's response (message ok/dismiss): if no response is received, the process
+ * continues.
+ * <p>
+ * If there are recoverable problems, the user is presented with the first one's description. If there are
+ * more problems, a note "There are XX other fixable problems." is added.
+ * <p>
+ * The main loop supports two modes: autoresolve true will immediately attempt to resolve 1st resolvable
+ * problem, while autoresolve false provides the description and asks for confirmation. This allows the
+ * "OK" and "Rest" button to switch between the two modes:
+ * <ul>
+ * <li>resolve first, report issues if any
+ * <li>ask with details about the fix first, then resolve
+ * </ul>
+ * When the user is asked to decide, a timeout task is scheduled: if the user does not respond within
+ * the defined timeout, the process continues.
+ * <p>
+ * To avoid possible issues with multiple project actions, <b>all resolve calls from all ProblemResolvers</b>
+ * are serialized to a single dedicated RP.
+ * <p/>
+ * A request to <b>alert</b> the project will just push the process further: if timeout is under way, it will 
+ * terminate it and display the next question.
+ * <p/>
+ * A request to <b>display a customizer</b> will invalidate the current presenter, and display all the issues
+ * anew. This allows for an explicit CodeAction or Command that will reiterate questions the user may have closed.
+ * <p/>
+ * Note: if a ShowMessage request is delivered to vscode client and the same ShowMessage request is pending, the client 
+ * will cancel the former ShowMessage as if the user pressed ESC or dismissed the message. The new ShowMessage will be 
+ * displayed
+ * @author sdedic
+ */
+public class BrokenReferencesImpl implements ProjectProblemsImplementation, ProjectAlertPresenter.Env {
+    private static final Logger LOG = Logger.getLogger(BrokenReferencesImpl.class.getName());
+    
+    // @GuardedBy(this)
+    /**
+     * Holds all alerted projects. Will be freed after the last project's Presenter will
+     * be removed.
+     */
+    private BrokenReferencesModel.Context   context;
+    
+    /**
+     * Active Presenters
+     */
+    // @GuadedBy(this)
+    private final Map<Project, ProjectAlertPresenter> presenters = new WeakHashMap<>();
+    
+    @Override
+    public CompletableFuture<Void> showAlert(@NonNull Project project) {
+        ProjectAlertPresenter p;
+        
+        synchronized (this) {
+            if (context == null) {
+                context = new BrokenReferencesModel.Context();
+                LOG.log(Level.FINEST, "Initializing new Context");
+            }
+            context.offer(project);
+            
+            p = presenters.computeIfAbsent(project, (p2) -> createPresenter(p2, new BrokenReferencesModel(context, true)));
+        }
+        p.cleanAndProcess(false);
+        return p.getCompletion().thenApply(x -> null);
+    }
+    
+    ProjectAlertPresenter createPresenter(Project project, BrokenReferencesModel model) {
+        return new ProjectAlertPresenter(project, model, this);
+    }
+
+    @Override
+    public CompletableFuture<Void> showCustomizer(@NonNull Project project) {
+        return fixBrokenProject(project).thenApply(x -> null);
+    }
+    
+    public CompletableFuture<Boolean> fixBrokenProject(@NonNull Project project) {
+        ProjectAlertPresenter p;
+        
+        synchronized (this) {
+            p = new ProjectAlertPresenter(project, new BrokenReferencesModel(context, true), this);
+            presenters.put(project, p);
+        }
+        p.processProject(true);
+        return p.getCompletion();
+    }
+    
+    /**
+     * Checks if the presenter is the active one for the project.
+     * @param p presenter to check
+     * @return true, if the presenter is active
+     */
+    @Override
+    public boolean isActivePresenter(ProjectAlertPresenter p) {
+        synchronized (this) {
+            return p == presenters.get(p.getProject());
+        }
+    }
+    
+    /**
+     * Finishes the work on a project. If the presenter is the active one, the method will
+     * remove the presenter from the presenters queue. Removing the last presenter will also
+     * release context, that is all project references.
+     * 
+     * @param presenter presenter to terminate
+     * @param p the presenter's project
+     */
+    @Override
+    public void finishProject(ProjectAlertPresenter presenter) {
+        synchronized (this) {
+            presenter.completion.complete(presenter.allProcessed);
+            if (!presenters.remove(presenter.getProject(), presenter)) {
+                return;
+            }
+            if (presenters.isEmpty()) {
+                context = null;
+            }
+        }
+    }
+    
+    /* tests only */
+    ProjectAlertPresenter getPresenter(Project p) {
+        synchronized (this) {
+            return presenters.get(p);
+        }
+    }
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/BrokenReferencesModel.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/BrokenReferencesModel.java
new file mode 100644
index 0000000..b686c35
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/BrokenReferencesModel.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.java.lsp.server.project;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import javax.swing.AbstractListModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.api.project.ProjectUtils;
+import org.openide.util.ChangeSupport;
+import org.netbeans.spi.project.ui.ProjectProblemsProvider;
+import org.netbeans.spi.project.ui.ProjectProblemsProvider.ProjectProblem;
+import org.openide.util.Mutex;
+import org.openide.util.NbBundle;
+import org.openide.util.WeakListeners;
+
+/**
+ * This is a copy of broken references model from Project UI API, slightly improved.
+ * 
+ */
+public final class BrokenReferencesModel extends AbstractListModel implements PropertyChangeListener, ChangeListener {
+
+    private static final Logger LOG = Logger.getLogger(BrokenReferencesModel.class.getName());
+
+    private final Context ctx;
+    private final boolean global;
+    private final Object lock = new Object();
+    //@GuardedBy("lock")
+    private final Map<ProjectProblemsProvider,PropertyChangeListener> providers;
+    //@GuardedBy("lock")
+    private final List<ProblemReference> problems;
+
+
+    BrokenReferencesModel(@NonNull final Context ctx, boolean global) {
+        assert ctx != null;
+        this.ctx = ctx;
+        this.global = global;
+        problems = new ArrayList<ProblemReference>();
+        providers = new WeakHashMap<ProjectProblemsProvider, PropertyChangeListener>();
+        refresh();
+        ctx.addChangeListener(this);
+    }
+
+    BrokenReferencesModel(@NonNull final Project project) {
+        this(new Context(), false);
+        this.ctx.offer(project);
+    }
+
+    @Override
+    public Object getElementAt(int index) {
+        return getOneReference(index);
+    }
+
+    @Override
+    public int getSize() {
+        synchronized (lock) {
+            return problems.size();
+        }
+    }
+    
+    public List<Project> projects() {
+        synchronized (this) {
+            return problems.stream().sequential().map(r -> r.project).collect(Collectors.toList());
+        }
+    }
+    
+    public List<ProblemReference> projectProblems(Project project, boolean includeSeen) {
+        synchronized (this) {
+            return problems.stream().sequential().filter(r -> r.project == project).collect(Collectors.toList());
+        }
+    }
+    
+    void refresh() {
+        refresh(true);
+    }
+    
+    
+    /**
+     * Refreshes the model + allows to suppress change events. Call refresh(false) from problem processing methods to avoid
+     * recursive invocation, as the change events are fired even though no problems are added/removed.
+     * @param fire 
+     */
+    void refresh(boolean fire) {
+        AtomicBoolean changed = new AtomicBoolean(false);
+        final int size = ProjectManager.mutex().readAccess(new Mutex.Action<Integer>() {
+            @Override
+            public Integer run() {
+                synchronized (lock) {
+                    final Map<ProjectProblemsProvider,Project> newProviders = new LinkedHashMap<ProjectProblemsProvider,Project>();
+                    for (Project bprj : ctx.getBrokenProjects()) {
+                        final ProjectProblemsProvider provider = bprj.getLookup().lookup(ProjectProblemsProvider.class);
+                        if (provider != null) {
+                            newProviders.put(provider, bprj);
+                        }
+                    }
+                    for (Iterator<Map.Entry<ProjectProblemsProvider,PropertyChangeListener>> it = providers.entrySet().iterator(); it.hasNext();) {
+                        final Map.Entry<ProjectProblemsProvider,PropertyChangeListener> e = it.next();
+                        if (!newProviders.containsKey(e.getKey())) {
+                            e.getKey().removePropertyChangeListener(e.getValue());
+                            it.remove();
+                        }
+                    }
+                    final Set<ProblemReference> all = new LinkedHashSet<ProblemReference>();
+                    for (Map.Entry<ProjectProblemsProvider,Project> ne : newProviders.entrySet()) {
+                        final ProjectProblemsProvider ppp = ne.getKey();
+                        final Project bprj = ne.getValue();
+                        if (!providers.containsKey(ppp)) {
+                            final PropertyChangeListener l = WeakListeners.propertyChange(BrokenReferencesModel.this, ppp);
+                            ppp.addPropertyChangeListener(l);
+                            providers.put(ppp, l);
+                        }
+                        for (ProjectProblem problem : ppp.getProblems()) {
+                            all.add(new ProblemReference(problem, bprj, global));
+                        }
+                    }
+                    changed.set(updateReferencesList(problems, all));
+                    return getSize();
+                }
+            }
+        });
+        if (fire && changed.get()) {
+            Mutex.EVENT.readAccess(new Runnable() {
+                @Override
+                public void run() {
+                    fireContentsChanged(BrokenReferencesModel.this, 0, size);
+                }
+            });
+        }
+    }
+
+    private ProblemReference getOneReference(int index) {
+        synchronized (lock) {
+            assert index>=0 && index<problems.size();
+            return problems.get(index);
+        }
+    }
+
+    private static boolean updateReferencesList(List<ProblemReference> oldBroken, Set<ProblemReference> newBroken) {
+        boolean change = false;
+        LOG.log(Level.FINE, "References updated from {0} to {1}", new Object[] {oldBroken, newBroken});
+        for (ProblemReference or : oldBroken) {
+            or.resolved = !newBroken.contains(or);
+        }
+        for (ProblemReference or : newBroken) {
+            if (!oldBroken.contains(or)) {
+                change = true;
+                oldBroken.add(or);
+            }
+        }
+        return change;
+    }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (ProjectProblemsProvider.PROP_PROBLEMS.equals(evt.getPropertyName())) {
+            refresh();
+        }
+    }
+
+    @Override
+    public void stateChanged(ChangeEvent e) {
+        refresh();
+    }
+
+    static final class ProblemReference {
+        private final boolean global;
+        private final Project project;
+        final ProjectProblem problem;
+
+
+        volatile boolean resolved;
+        volatile boolean seen;
+
+        ProblemReference(
+                @NonNull final ProjectProblem problem,
+                @NonNull final Project project,
+                final boolean global) {
+            assert problem != null;
+            this.problem = problem;
+            this.project = project;
+            this.global = global;
+        }
+
+
+        String getDisplayName() {
+            final String displayName = problem.getDisplayName();
+            String message;
+            if (global) {
+                final String projectName = ProjectUtils.getInformation(project).getDisplayName();
+                message = NbBundle.getMessage(
+                        BrokenReferencesModel.class,
+                        "FMT_ProblemInProject",
+                        projectName,
+                        displayName);
+
+            } else {
+                message = displayName;
+            }
+            return message;
+        
+        
+        }
+
+        public Project getProject() {
+            return project;
+        }
+        
+        public void markSeen() {
+            seen = true;
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return String.format(
+              "Problem: %s %s", //NOI18N
+              problem,
+              resolved ? "resolved" : "unresolved");    //NOI18N
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = 17;
+            hash = 31 * hash + problem.hashCode();
+            hash = 31 * hash + project.hashCode();
+            return hash;
+        }
+
+        @Override
+        public boolean equals (Object other) {
+            if (!(other instanceof ProblemReference)) {
+                return false;
+            }
+            final ProblemReference otherRef = (ProblemReference) other;
+            return problem.equals(otherRef.problem) &&
+                   project.equals(otherRef.project);
+        }
+
+    }
+
+    static final class Context {
+        private final Set<Project> toResolve;
+        private final ChangeSupport support;
+
+        public Context() {
+            toResolve = Collections.synchronizedSet(new LinkedHashSet<Project>());
+            support = new ChangeSupport(this);
+        }
+
+        public boolean offer(@NonNull final Project broken) {
+            assert broken != null;
+            if (broken.getLookup().lookup(ProjectProblemsProvider.class) != null) {
+                boolean r = this.toResolve.add(broken);
+                if (r) {
+                    support.fireChange();
+                }
+                return r;
+            } else {
+                return false;
+            }
+        }
+
+        public boolean isEmpty() {
+            return this.toResolve.isEmpty();
+        }
+
+        public Project[] getBrokenProjects() {
+            synchronized (toResolve) {
+                return toResolve.toArray(new Project[toResolve.size()]);
+            }
+        }
+
+        public void addChangeListener(final @NonNull ChangeListener listener) {
+            assert listener != null;
+            support.addChangeListener(listener);
+        }
+
+        public void removeChangeListener(final @NonNull ChangeListener listener) {
+            assert listener != null;
+            support.removeChangeListener(listener);
+        }
+    }
+
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/ProjectAlertPresenter.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/ProjectAlertPresenter.java
new file mode 100644
index 0000000..0482e7c
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/ProjectAlertPresenter.java
@@ -0,0 +1,808 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.java.lsp.server.project;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.function.BiFunction;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectInformation;
+import org.netbeans.api.project.ProjectUtils;
+import org.netbeans.spi.project.ui.ProjectProblemsProvider;
+import org.openide.DialogDisplayer;
+import org.openide.NotifyDescriptor;
+import org.openide.awt.StatusDisplayer;
+import org.openide.util.NbBundle;
+import org.openide.util.RequestProcessor;
+
+/**
+ *
+ * @author sdedic
+ */
+@NbBundle.Messages(value = {
+    "# {0} - project name", 
+    "# {1} - problem's display name", 
+    "ProjectProblem_Title=Project {0}: {1}", 
+    
+    "# {0} - project name", 
+    "ProjectProblems_Resolved_Info=Project {0} issues resolved", 
+    
+    "# {0} - project name", 
+    "ProjectProblems_Resolved_Warning=Project {0} issues resolved", 
+    
+    "# {0} - project name", 
+    "ProjectProblems_Resolved_Error=Project {0} issues not fixed", 
+    
+    "# {0} - problem display", 
+    "# {1} - message", 
+    "ProjectProblems_Resolved_ErrorMessage1=The fix for \"{0}\" failed: {1}", 
+    
+    "# {0} - problem display", 
+    "# {1} - message", 
+    "ProjectProblems_Resolved_WarningMessage1=Problem \"{0}\" was resolved: {1}", 
+    
+    "# {0} - problem display", 
+    "# {1} - message", 
+    "# {2} - other errors note", 
+    "ProjectProblems_Resolved_ErrorMessage2=The fix for \"{0}\" failed: {1}. {2}", 
+    
+    "# {0} - problem display", 
+    "# {1} - message", 
+    "ProjectProblems_Resolved_WarningMessage2=Problem \"{0}\" was resolved: {1}", 
+    
+    "# {0} - number of fixes", 
+    "# {1} - the main message", 
+    "ProjectProblems_Additional={1}. Additional fixes ({0}) are available."})
+class ProjectAlertPresenter {
+    private static final Logger LOG = Logger.getLogger(ProjectAlertPresenter.class.getName());
+    
+    static int MAX_PRESENTED_ERRORS = Integer.getInteger(ProjectAlertPresenter.class.getName() + ".maxErrors", 10);
+    static int ERRORS_WAKEUP_DELAY = Integer.getInteger(ProjectAlertPresenter.class.getName() + ".errorsWakeUp", 2 * 60 * 1000);
+    static int QUESTION_WAKEUP_DELAY = Integer.getInteger(ProjectAlertPresenter.class.getName() + ".questionWakeUp", 5 * 60 * 1000);
+    
+    /**
+     * All resolutions are executed in this thread, to prevent interactions between project actions.
+     */
+    private static final RequestProcessor  RESOLVE_RP = new RequestProcessor(BrokenReferencesImpl.class.getName());
+    
+    /**
+     * Mainly a 'watcher' thread that continues the process if the user does not react on prompts.
+     */
+    private static final RequestProcessor  RP = new RequestProcessor(BrokenReferencesImpl.class.getName());
+    
+    private static final int WAKEUP_DELAY = 2 * 60 * 1000;
+    
+
+    /**
+     * The project.
+     */
+    private final Project project;
+    
+    /**
+     * Project name.
+     */
+    private final String projectName;
+    
+    /**
+     * Updating model of the project's problems.
+     */
+    private final BrokenReferencesModel model;
+    
+    /**
+     * The controller.
+     */
+    private final Env controller;
+    
+    /**
+     * Problems that were already seen and presented to the user.
+     */
+    private final Set<ProjectProblemsProvider.ProjectProblem> seen = Collections.synchronizedSet(new HashSet<>());
+    
+    final Object restOption = Bundle.ProjectProblems_RestOption();
+    final Object detailOption = Bundle.ProjectProblems_DetailsOption();
+
+    /**
+     * Overall result. The future will complete after all errors were seen, all questions were answered or timed out.
+     */
+    final CompletableFuture<Boolean> completion;
+    
+    /**
+     * All errors already acknowledged by the process, but not necessarily presented
+     */
+    // @GuardedBy(this)
+    Set<BrokenReferencesModel.ProblemReference> snapshot = Collections.emptySet();
+    
+    /**
+     * Invocation attempt, i.e. after a timeout or new project alert. The process continues after user response only
+     * if another run attempt has not started yet.
+     */
+    // @GuardedBy(this)
+    int runAttempt;
+
+    /**
+     * If true, will re-display project problems that have been already presented to the client.
+     */
+    boolean includeSeen;
+
+    /**
+     * Timer task that will push the process further if the user ignores the prompts. The task is run by {@link #RP} processor.
+     */
+    // @GuardedBy(this)
+    private RequestProcessor.Task wakeUpTask;
+    
+    /**
+     * The resolution process is cancelled. The process will terminate after the first timeout or user input.
+     */
+    // @GuardedBy(this)
+    private volatile CancellationException cancelled;
+    
+    /**
+     * Indicates that all questions were answered. Will eventually reset to {@code false} during the process, if the user
+     * cancels further fixes.
+     */
+    // @GuardedBy(this)
+    boolean allProcessed = true;
+    
+    public ProjectAlertPresenter(Project project, BrokenReferencesModel model, final Env master) {
+        this.controller = master;
+        this.project = project;
+        this.model = model;
+        ProjectInformation pi = ProjectUtils.getInformation(project);
+        String pn = pi.getDisplayName();
+        if (pn == null) {
+            pn = pi.getName();
+        }
+        this.projectName = pn;
+        LOG.log(Level.FINE, "Initializing alert presenter for {0}", projectName);
+        completion = new CompletableFuture<Boolean>() {
+            @Override
+            public boolean cancel(boolean mayInterruptIfRunning) {
+                boolean r = super.cancel(mayInterruptIfRunning);
+                if (r) {
+                    ProjectAlertPresenter.this.cancel();
+                }
+                return r;
+            }
+        };
+        model.addListDataListener(new ListDataListener() {
+            @Override
+            public void intervalAdded(ListDataEvent e) {
+                awake();
+            }
+
+            @Override
+            public void intervalRemoved(ListDataEvent e) {
+                
+            }
+
+            @Override
+            public void contentsChanged(ListDataEvent e) {
+                awake();
+            }
+        });
+    }
+
+    public Project getProject() {
+        return project;
+    }
+
+    public CompletableFuture<Boolean> getCompletion() {
+        return completion;
+    }
+
+    public void cancel() {
+        synchronized (this) {
+            cancelled = new CancellationException();
+            if (wakeUpTask != null) {
+                wakeUpTask.cancel();
+            }
+        }
+    }
+    
+    /**
+     * Resumes the process after user timeout.
+     * @param autoResolve
+     */ 
+    void resumeAfterTimeout(Ctx ctx) {
+        assert RP.isRequestProcessorThread();
+        LOG.log(Level.FINE, "User unresponsive for project {0}, continue", projectName);
+        ErrorQueue oq;
+        synchronized (this) {
+            if (!ctx.valid()) {
+                return;
+            }
+            if (wakeUpTask != null && !wakeUpTask.isFinished()) {
+                wakeUpTask.cancel();
+                wakeUpTask = null;
+            }
+            oq = errorQueue;
+            errorQueue = null;
+            runAttempt++;
+            ctx = new Ctx(ctx.autoResolve);
+        }
+        if (oq != null) {
+            oq.terminate(true);
+        }
+        processOneRound(ctx);
+    }
+    
+    private void awake() {
+        processProject(false);
+    }
+    
+    Set<ProjectProblemsProvider.ProjectProblem> seenProblems() {
+        return new LinkedHashSet<>(this.seen);
+    }
+    
+    public void cleanAndProcess(boolean autoResolve) {
+        synchronized (this) {
+            if (wakeUpTask != null) {
+                wakeUpTask.cancel();
+            }
+            wakeUpTask = null;
+            snapshot = Collections.emptySet();
+        }
+        processProject(autoResolve);
+    }
+    
+    /**
+     * Starts or restarts processing. If {@code autoResolve} is {@code true}, 
+     * @param autoResolve 
+     */
+    public void processProject(boolean autoResolve) {
+        model.refresh(false);
+        Ctx ctx;
+        synchronized (this) {
+            List<BrokenReferencesModel.ProblemReference> probs = new ArrayList<>();
+            List<BrokenReferencesModel.ProblemReference> fatals = findProblems(probs);
+            LOG.log(Level.FINE, "Processing {0} fatals, {1} resolvables of {2}", new Object[]{fatals.size(), probs.size(), projectName});
+            if (snapshot.containsAll(probs) && snapshot.containsAll(fatals) && wakeUpTask != null) {
+                // activate only if something new happens, there's timeout task that will eventually
+                // continue if user is unresponsive.
+                LOG.log(Level.FINE, "All already seen or being processed, wakeup task available");
+                return;
+            }
+            if (seen.containsAll(fatals) && wakeUpTask != null) {
+                // all fatals reported, but user did not dismissed - a new fix will be handled by the
+                // running process.
+                LOG.log(Level.FINE, "Fatals reported, wakeup task available, wait for user or timeout");
+                return;
+            }
+            if (wakeUpTask != null && !wakeUpTask.isFinished()) {
+                if (!wakeUpTask.cancel()) {
+                    // will invoke process anyway
+                    return;
+                }
+            }
+            if (cancelled != null) {
+                finishThis();
+                return;
+            }
+            runAttempt++;
+            ctx = new Ctx(autoResolve);
+            LOG.log(Level.FINE, "Wakeup cancelled, new presenter run");
+        }
+        processOneRound(ctx);
+    }
+
+    private List<BrokenReferencesModel.ProblemReference> findProblems(List<BrokenReferencesModel.ProblemReference> probs) {
+        List<BrokenReferencesModel.ProblemReference> fatals = new ArrayList<>();
+        for (BrokenReferencesModel.ProblemReference ref : model.projectProblems(project, includeSeen)) {
+            if (seen.contains(ref.problem) || ref.resolved) {
+                continue;
+            }
+            if (ref.seen && !includeSeen) {
+                continue;
+            }
+            ProjectProblemsProvider.ProjectProblem pp = ref.problem;
+            if (!pp.isResolvable()) {
+                fatals.add(ref);
+            } else {
+                probs.add(ref);
+            }
+        }
+        return fatals;
+    }
+    
+    void finishThis() {
+        controller.finishProject(this);
+        if (cancelled != null) {
+            if (completion.completeExceptionally(cancelled)) {
+                return;
+            }
+        } 
+        completion.complete(allProcessed);
+    }
+    
+    final class Ctx {
+        final int attempt;
+        final boolean autoResolve;
+
+        public Ctx(boolean autoResolve) {
+            this(runAttempt, autoResolve);
+        }
+        
+        public Ctx(int attempt, boolean autoResolve) {
+            this.attempt = attempt;
+            this.autoResolve = autoResolve;
+        }
+        
+        boolean valid() {
+            synchronized (ProjectAlertPresenter.this) {
+                return this.attempt == runAttempt && controller.isActivePresenter(ProjectAlertPresenter.this);
+            }
+        }
+        
+        Ctx autoresolve(boolean r) {
+            return new Ctx(attempt, r);
+        }
+    }
+
+    /**
+     * Hnadles display of errors limited to some number. The value is cleared when all
+     * the errors were presented + confirmed.
+     */
+    // @GuardedBy(this)
+    ErrorQueue errorQueue = null;
+    
+    /**
+     * Displays fatal errors, at most {@link #MAX_PRESENTED_ERRORS} at a time. When an error message is closed,
+     * displays a new one, if it is in the queue. Completes the {@link #allDone} Future after all messages
+     * have been confirmed or were cancelled.
+     */
+    class ErrorQueue {
+        final Ctx ctx;
+        
+        /**
+         * All messages to be processed.
+         */
+        final List<BrokenReferencesModel.ProblemReference> toProcess = new ArrayList<>();
+        
+        /**
+         * Messages already processed.
+         */
+        final Set<BrokenReferencesModel.ProblemReference> processed = new HashSet<>();
+        
+        /**
+         * Future which will be completed after all the messages are shown or the process is cancelled.
+         */
+        CompletableFuture allDone = new CompletableFuture();
+        
+        /**
+         * Becomes true once the process displays the 1st dialog.
+         */
+        boolean started;
+        
+        /**
+         * The number of messages currently displayed, should be at most {@link #MAX_PRESENTED_ERRORS}.
+         */
+        int fatalErrorsOnScreen;
+        
+        /**
+         * Non-null if some of the messages complete with an exception.
+         */
+        Throwable error;
+
+
+        public ErrorQueue(Ctx ctx) {
+            this.ctx = ctx;
+        }
+        
+        public void runOrDelay(BrokenReferencesModel.ProblemReference p) {
+            synchronized (this) {
+                if (fatalErrorsOnScreen >= MAX_PRESENTED_ERRORS) {
+                    // delay:
+                    return;
+                }
+                fatalErrorsOnScreen++;
+            } 
+            displayError(p);
+        }
+        
+        CompletableFuture moreErrors(List<BrokenReferencesModel.ProblemReference> newErrors) {
+            synchronized (this) {
+                if (started && processed.containsAll(toProcess)) {
+                    return null;
+                }
+                started = true;
+                toProcess.addAll(newErrors);
+            }
+            for (BrokenReferencesModel.ProblemReference r : newErrors) {
+                runOrDelay(r);
+            }
+            return allDone;
+        }
+        
+        void terminate(boolean all) {
+            Throwable exception;
+            
+            synchronized (this) {
+                for (BrokenReferencesModel.ProblemReference ref : toProcess) {
+                    if (!all && seen.contains(ref.problem)) {
+                        continue;
+                    }
+                    processed.add(ref);
+                }
+                if (!processed.containsAll(toProcess)) {
+                    return;
+                }
+                synchronized (ProjectAlertPresenter.this) {
+                    if (this == errorQueue) {
+                        errorQueue = null;
+                    }
+                }
+                exception = error;
+            }
+            // complete the future outside the lock.
+            if (exception != null) {
+                allDone.completeExceptionally(exception);
+            } else {
+                allDone.complete(null);
+            }
+        }
+        
+        public void displayError(BrokenReferencesModel.ProblemReference toPresent) {
+            ProjectProblemsProvider.ProjectProblem p = toPresent.problem;
+            seen.add(p);
+            LOG.log(Level.FINE, "Reporting fatal {0}", p.getDisplayName());
+            int type;
+            switch (p.getSeverity()) {
+                default:
+                case ERROR:
+                    type = NotifyDescriptor.ERROR_MESSAGE;
+                    break;
+                case WARNING:
+                    type = NotifyDescriptor.WARNING_MESSAGE;
+                    break;
+            }
+            // hack: the LSP protocol does not support title. Until fixed, or implemented through a custom message,
+            // embed the title into description:
+            String title = Bundle.ProjectProblem_Title(projectName, p.getDisplayName());
+            NotifyDescriptor msg = new NotifyDescriptor(title + ": " + p.getDescription(), title, NotifyDescriptor.DEFAULT_OPTION, type, new Object[]{NotifyDescriptor.OK_OPTION}, null);
+
+            // Note: the number of 'fatal' dialogs displayed at the same time is limited by the RP throughput. Dialog API does not support CompletableFuture<> interface
+            // so threads may dangle.
+            CompletableFuture<Integer> running = DialogDisplayer.getDefault().notifyFuture(msg).handle((n, e) -> {
+                Optional<BrokenReferencesModel.ProblemReference> o;
+                int result = 0;
+                synchronized (this) {
+                    processed.add(toPresent);
+                    if (!ctx.valid() || cancelled != null) {
+                        // will complete the 'running' future.
+                        return 2;
+                    }
+                    o = toProcess.stream().filter((a) -> 
+                        !seen.contains(a.problem)).findFirst();
+                    // o.isPresent(), will replace the current error with a new one, no change to the counter.
+                    if (!o.isPresent()) {
+                        result = 1;
+                        if (fatalErrorsOnScreen > 0) {
+                            fatalErrorsOnScreen--;
+                        }
+                    }
+                }
+                if (o.isPresent()) {
+                    // just run outside the synchronized block - display another error.
+                    displayError(o.get());
+                }
+                return result;
+            });
+            // chaining instead of try - finally ;)
+            running.whenComplete((t, ex) -> {
+                if (t > 0) {
+                    terminate(t > 1);
+                } else {
+                    synchronized (ProjectAlertPresenter.this) {
+                        // attempt to postpone the task after user's reaction.
+                        if (wakeUpTask != null) {
+                            LOG.log(Level.FINER, "Trying to postpone wakeup for {0}ms", WAKEUP_DELAY);
+                            wakeUpTask.schedule(ERRORS_WAKEUP_DELAY);
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    @NbBundle.Messages(value = {"# {0} - project name", "# {1} - issue title", "ProjectProblems_Fixable_Title=Project {0}: {1}"})
+    void processOneRound(Ctx ctx) {
+        if (!RP.isRequestProcessorThread()) {
+            RP.post(() -> processOneRound(ctx));
+            return;
+        }
+        
+        assert RP.isRequestProcessorThread();
+
+        model.refresh(false);
+        List<BrokenReferencesModel.ProblemReference> probs = new ArrayList<>();
+        List<BrokenReferencesModel.ProblemReference> fatals = findProblems(probs);
+        synchronized (this) {
+            if (!ctx.valid()) {
+                return;
+            }
+            wakeUpTask = null;
+            snapshot = new HashSet<>();
+            snapshot.addAll(probs);
+            snapshot.addAll(fatals);
+        }
+        if (probs.isEmpty() && fatals.isEmpty()) {
+            LOG.log(Level.FINE, "Project {0} clear, finishing", projectName);
+            finishThis();
+            return;
+        }
+        if (cancelled != null) {
+            finishThis();
+            return;
+        }
+
+        if (!fatals.isEmpty()) {
+            ErrorQueue activeBatch;
+            CompletableFuture f = null;
+            boolean newBatch;
+            
+            // loop in case the batch just terminates & clears the reference.
+            do {
+                newBatch = false;
+                synchronized (this) {
+                    activeBatch = errorQueue;
+                    if (activeBatch == null) {
+                        errorQueue = activeBatch = new ErrorQueue(ctx);
+                        newBatch = true;
+                    }
+                }
+                f = activeBatch.moreErrors(fatals);
+                
+                synchronized (this) {
+                    if (f == null && errorQueue == activeBatch) {
+                        errorQueue = null;
+                    }
+                }
+            } while (f == null);
+            
+            if (newBatch) {
+                synchronized (this) {
+                    LOG.log(Level.FINE, "Waiting for {0} items, scheduling wakeup task", fatals.size());
+                    wakeUpTask = RP.create(() -> resumeAfterTimeout(ctx));
+                    wakeUpTask.schedule(ERRORS_WAKEUP_DELAY);
+                }
+            }
+            f.thenAccept(r -> continueResetPending(ctx));
+            return;
+        }
+        
+        if (probs.isEmpty()) {
+            finishThis();
+            return;
+        }
+        BrokenReferencesModel.ProblemReference ref = probs.iterator().next();
+        
+        BiFunction<ProjectProblemsProvider.Result, Throwable, Void> handlerFn = (ProjectProblemsProvider.Result r, Throwable e)  -> {
+            if (!ctx.valid()) {
+                return null;
+            }
+            if (e instanceof CompletionException) {
+                e = e.getCause();
+            }
+            if (e instanceof CancellationException) {
+                // do not proceeed further
+                finishIfNoMoreErrors(ctx, probs);
+                return null;
+            }
+            if (r != null) {
+                processOneRound(ctx.autoresolve(true));
+            } else {
+                processOneRound(ctx.autoresolve(false));
+            }
+            return null;
+        };
+        if (ctx.autoResolve) {
+            postResolveProblem(ctx, ref).handle(handlerFn);
+        } else {
+            seen.add(ref.problem);
+            // present a question
+            
+            if (cancelled != null) {
+                finishThis();
+                return;
+            }
+
+            RequestProcessor.Task t;
+            synchronized (this) {
+                if (wakeUpTask == null) {
+                    t = wakeUpTask = RP.post(() -> resumeAfterTimeout(ctx),  QUESTION_WAKEUP_DELAY);
+                } else {
+                    t = null;
+                }
+            }
+            
+            String title = Bundle.ProjectProblems_Fixable_Title(projectName, ref.problem.getDisplayName());
+            String msg = ref.problem.getDescription();
+            if (probs.size() > 1) {
+                msg = Bundle.ProjectProblems_Additional(probs.size() - 1, msg);
+            }
+            Object[] options = probs.size() > 1 ? new Object[]{NotifyDescriptor.OK_OPTION, restOption, NotifyDescriptor.CANCEL_OPTION} : new Object[]{NotifyDescriptor.OK_OPTION, NotifyDescriptor.CANCEL_OPTION};
+            NotifyDescriptor desc = new NotifyDescriptor(title + ": " + msg, title, NotifyDescriptor.DEFAULT_OPTION, NotifyDescriptor.QUESTION_MESSAGE, options, null);
+            // FIXME - Bad bad - this should be done in a separate RP as it may block in the client.
+            DialogDisplayer.getDefault().notifyFuture(desc).whenComplete((d, ex) -> {
+                synchronized (this) {
+                    if (t != null && wakeUpTask == t) {
+                        wakeUpTask.cancel();
+                        wakeUpTask = null;
+                    }
+                }
+                if (cancelled != null) {
+                    finishThis();
+                    return;
+                }
+                Object res = d.getValue();
+                if (restOption.equals(res)) {
+                    synchronized (this) {
+                        if (!ctx.valid()) {
+                            // resolve just that one issue, the rest will be solved by other questions.
+                            postResolveProblem(ctx.autoresolve(false), ref).handle(handlerFn);
+                            return;
+                        } else {
+                            seen.remove(ref.problem);
+                        }
+                    }
+                    processOneRound(ctx.autoresolve(true));
+                } else if (res != NotifyDescriptor.OK_OPTION) {
+                    handlerFn.apply(null, null);
+                } else {
+                    postResolveProblem(ctx.autoresolve(false), ref).handle(handlerFn);
+                }
+            }).exceptionally(x -> {
+                if (x instanceof CompletionException) {
+                    x = x.getCause();
+                }
+                if (x instanceof CancellationException) {
+                    finishIfNoMoreErrors(ctx, probs);
+                }
+                return null;
+            });
+        }
+    }
+
+    /**
+     * Finishes the process, if no more errors is reported in the meantime. Will dismiss all questions
+     * collected so far.
+     * @param dismiss questions to dismiss.
+     */
+    private void finishIfNoMoreErrors(Ctx ctx, Collection<BrokenReferencesModel.ProblemReference> dismiss) {
+        synchronized (this) {
+            allProcessed &= dismiss.isEmpty();
+            seen.addAll(dismiss.stream().map(r -> r.problem).collect(Collectors.toList()));
+        }
+        List<BrokenReferencesModel.ProblemReference> probs = new ArrayList<>();
+        List<BrokenReferencesModel.ProblemReference> fatals = findProblems(probs);
+        if (!fatals.isEmpty()) {
+            processOneRound(ctx);
+        } else {
+            finishThis();
+        }
+    }
+
+    /**
+     * Schedules problem resolution into the common RequestProcessor. The returned CompletableFuture
+     * will complete after the fix is done and potentially after the user answers question after fix reports
+     * some warning or error. {@code non-null} is returned to indicate that no further questions are to be asked.
+     * If the user cancels the dialog, [@link CancellationException} will be recorded in the Future.
+     * @param ref problem to resolve
+     * @return future with null/non-null result or cancellation
+     */
+    @NbBundle.Messages(value = {"ProjectProblems_RestOption=&Fix All", "ProjectProblems_DetailsOption=Display &Details"})
+    private CompletableFuture<ProjectProblemsProvider.Result> postResolveProblem(Ctx ctx, BrokenReferencesModel.ProblemReference ref) {
+        CompletableFuture<ProjectProblemsProvider.Result> f = // convert the exception to an error message to the user.
+        // do not bother
+        CompletableFuture.supplyAsync(() -> {
+            try {
+                seen.add(ref.problem);
+                return ref.problem.resolve().get();
+            } catch (ExecutionException ex) {
+                // convert the exception to an error message to the user.
+                return ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.UNRESOLVED, ex.getCause().getLocalizedMessage());
+            } catch (InterruptedException ex) {
+                // do not bother
+                return null;
+            }
+        }, RESOLVE_RP).thenApply(r -> {
+            if (r.isResolved()) {
+                if (r.getMessage() != null) {
+                    StatusDisplayer.getDefault().setStatusText(r.getMessage());
+                }
+                return ctx.autoResolve ? r : null;
+            }
+            List<BrokenReferencesModel.ProblemReference> probs = new ArrayList<>();
+            List<BrokenReferencesModel.ProblemReference> fatals = findProblems(probs);
+            NotifyDescriptor desc = createNotifyDescriptor(ref.problem, r, probs.size());
+            if (fatals.isEmpty() && !probs.isEmpty()) {
+                desc.setOptions(new Object[]{restOption, detailOption, NotifyDescriptor.CANCEL_OPTION});
+                desc.setValue(detailOption);
+            }
+            Object v = DialogDisplayer.getDefault().notify(desc);
+            if (cancelled != null) {
+                throw new CancellationException();            
+            }
+            if (v == NotifyDescriptor.CANCEL_OPTION || v == null) {
+                throw new CancellationException();
+            } else if (restOption.equals(v)) {
+                return r;
+            } else {
+                return null;
+            }
+        });
+        return f;
+    }
+
+    private NotifyDescriptor createNotifyDescriptor(ProjectProblemsProvider.ProjectProblem pp, ProjectProblemsProvider.Result r, int probs) {
+        String title;
+        String msg;
+        int type;
+        if (r.getStatus() == ProjectProblemsProvider.Status.UNRESOLVED) {
+            title = Bundle.ProjectProblems_Resolved_Error(projectName);
+            msg = Bundle.ProjectProblems_Resolved_ErrorMessage1(pp.getDisplayName(), r.getMessage());
+            type = NotifyDescriptor.ERROR_MESSAGE;
+        } else {
+            title = Bundle.ProjectProblems_Resolved_Warning(projectName);
+            msg = Bundle.ProjectProblems_Resolved_WarningMessage1(pp.getDisplayName(), r.getMessage());
+            type = NotifyDescriptor.WARNING_MESSAGE;
+        }
+        if (probs > 0) {
+            msg = Bundle.ProjectProblems_Additional(probs, msg);
+        }
+        // hack: the LSP protocol does not support title. Until fixed, or implemented through a custom message,
+        // embed the title into description:
+        msg = title + ": " + msg; // NOI18N
+        NotifyDescriptor desc = new NotifyDescriptor(msg, title, NotifyDescriptor.DEFAULT_OPTION, type, new Object[]{NotifyDescriptor.OK_OPTION}, null);
+        return desc;
+    }
+
+    private void continueResetPending(Ctx ctx) {
+        LOG.log(Level.FINE, "All dialogs confirmed");
+        synchronized (this) {
+            if (!ctx.valid()) {
+                return;
+            }
+            if (wakeUpTask != null && !wakeUpTask.isFinished() && !wakeUpTask.cancel()) {
+                return;
+            }
+        }
+        processOneRound(ctx);
+    }
+    
+    interface Env {
+        public void finishProject(ProjectAlertPresenter p);
+        public boolean isActivePresenter(ProjectAlertPresenter p);
+    }
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/AbstractLspBrokenReferences.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/AbstractLspBrokenReferences.java
new file mode 100644
index 0000000..f2ffe86
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/AbstractLspBrokenReferences.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.java.lsp.server.ui;
+
+import java.util.concurrent.CompletableFuture;
+import org.netbeans.api.project.Project;
+import org.netbeans.modules.java.lsp.server.project.BrokenReferencesImpl;
+import org.netbeans.spi.project.ui.ProjectProblemsImplementation;
+
+/**
+ * Implementation of {@link ProjectProblemsImplementation} implemented using
+ * dialogs over LSP.
+ * @since 1.19
+ * @author sdedic
+ */
+public abstract class AbstractLspBrokenReferences implements ProjectProblemsImplementation {
+    private final BrokenReferencesImpl delegate;
+
+    protected AbstractLspBrokenReferences() {
+        delegate = new BrokenReferencesImpl();
+    }
+
+    @Override
+    public CompletableFuture<Void> showAlert(Project project) {
+        return delegate.showAlert(project);
+    }
+
+    @Override
+    public CompletableFuture<Void> showCustomizer(Project project) {
+        return delegate.showCustomizer(project);
+    }
+}
diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/project/LspBrokenReferencesImplTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/project/LspBrokenReferencesImplTest.java
new file mode 100644
index 0000000..cb18998
--- /dev/null
+++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/project/LspBrokenReferencesImplTest.java
@@ -0,0 +1,1287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.java.lsp.server.project;
+
+import java.awt.Dialog;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import static junit.framework.TestCase.fail;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.api.project.ui.OpenProjects;
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.modules.project.ui.problems.BrokenProjectNotifier;
+import org.netbeans.spi.project.LookupProvider;
+import org.netbeans.spi.project.ui.ProjectProblemResolver;
+import org.netbeans.spi.project.ui.ProjectProblemsProvider;
+import org.openide.DialogDescriptor;
+import org.openide.DialogDisplayer;
+import org.openide.NotifyDescriptor;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.Lookups;
+import org.openide.util.test.MockLookup;
+import org.netbeans.api.annotations.common.NonNull;
+import org.openide.util.RequestProcessor;
+
+/**
+ *
+ * @author sdedic
+ */
+public class LspBrokenReferencesImplTest extends NbTestCase {
+
+    public LspBrokenReferencesImplTest(String name) {
+        super(name);
+    }
+    
+    
+    @Override
+    protected void setUp() throws Exception {
+        reporter = new TestProblemReporter();
+        super.setUp();
+        
+        clearWorkDir();
+        FileObject r = FileUtil.getConfigRoot();
+        // Hack: create .instance registration in the config area. We do not have project lookup mocking service - yet.
+        FileObject dir = FileUtil.createFolder(r, "Projects/org-netbeans-modules-maven/Lookup");
+        dir.getFileSystem().runAtomicAction(() -> {
+            FileObject regFile = dir.createData("test-problem-provider.instance");
+            // works with the in-memory filesystem. Will not work if module system gets loaded.
+            regFile.setAttribute("instanceCreate", forProjectServices());
+            regFile.setAttribute("position", 10000);
+        });
+        BrokenProjectNotifier.getInstnace().start();
+
+        MockLookup.setLayersAndInstances(displayer, tested);
+        
+        // causes project info to be loaded synchronously
+        System.setProperty("test.load.sync", Boolean.TRUE.toString());
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        BrokenProjectNotifier.getInstnace().stop();
+        // Hack: create .instance registration in the config area. We do not have project lookup mocking service - yet.
+        FileObject f = FileUtil.getConfigFile("Projects/org-netbeans-modules-maven/Lookup/test-problem-provider.instance");
+        if (f != null) {
+            f.delete();
+        }
+        if (displayer != null) {
+            for (Response r : displayer.expectedResponses) {
+                if (r.responseLock != null) {
+                    r.responseLock.countDown();
+                }
+            }
+            Response r = displayer.currentResponse;
+            if (r != null && r.responseLock != null) {
+                r.responseLock.countDown();
+            }
+            while (displayer.currentResponse != null) {
+                synchronized (displayer) {
+                    displayer.notifyAll();
+                }
+            }
+        }
+        // reset custom thresholds
+        ProjectAlertPresenter.MAX_PRESENTED_ERRORS = 10;
+        ProjectAlertPresenter.ERRORS_WAKEUP_DELAY = 2 * 60 * 1000;
+        ProjectAlertPresenter.QUESTION_WAKEUP_DELAY = 5 * 60 * 1000;
+        super.tearDown();
+    }
+    
+    private TestProblemReporter reporter = new TestProblemReporter();
+    
+    private FileObject createProject(FileObject dir, String name, String... lines) throws IOException {
+        final FileObject[] ret = new FileObject[1];
+        dir.getFileSystem().runAtomicAction(() -> {
+            FileObject projDir = name == null ? dir : dir.createFolder(name);
+            ret[0] = projDir;
+            try (OutputStream os = projDir.createAndOpen("pom.xml");
+                 OutputStreamWriter wr = new OutputStreamWriter(os);
+                 PrintWriter pw = new PrintWriter(wr)) {
+                for (String l : lines) {
+                    pw.println(l);
+                }
+            }
+        });
+        return ret[0];
+    }
+    
+    final DialogController displayer = new DialogController();
+    final TestedImpl tested = new TestedImpl();
+    
+    final AtomicBoolean resolveCalled = new AtomicBoolean(false);
+    final AtomicReference<ProjectProblemsProvider.ProjectProblem> ref = new AtomicReference();
+    
+    /**
+     * Fills in 'ref'
+     */
+    private void createFixableError() {
+        ProjectProblemsProvider.ProjectProblem pp = ProjectProblemsProvider.ProjectProblem.createError(
+                "Test",
+                "Fixable-error",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    reporter.reportProblems.remove(ref.get());
+                    resolveCalled.set(true);
+                    return CompletableFuture.completedFuture(ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.RESOLVED));
+                }
+            }
+        );
+        ref.set(pp);
+        reporter.reportProblems.add(pp);
+    }
+    
+    private FileObject createSimpleProject() throws IOException {
+        
+        File wdBase = getWorkDir();
+        FileObject wdFile = FileUtil.toFileObject(wdBase);
+        
+        projectServices.put(wdBase.toPath(), Arrays.asList(reporter));
+        FileObject pdir = createProject(wdFile, null, 
+            "<project xmlns='http://maven.apache.org/POM/4.0.0'>",
+            "   <modelVersion>4.0.0</modelVersion>",
+            "   <artifactId>m</artifactId>" +
+            "   <groupId>g</groupId>" +
+            "   <version>1.0-SNAPSHOT</version>" +
+            "   <name>Test Project</name>" +
+            "</project>"
+        );
+        return pdir;
+    }
+        
+    /**
+     * Checks that a fixable error appears to the user, and can be resolved.
+     */
+    public void testFixableReport() throws Exception {
+        FileObject pdir = createSimpleProject();
+        
+        Semaphore block = new Semaphore(0);
+        
+        displayer.expectedResponses.add(new Response("Test Project", "Fixable-error", NotifyDescriptor.OK_OPTION));
+        displayer.responsePermits.drainPermits();
+        
+        createFixableError();
+        
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        
+        tested.presenterNotify.put(prj.getProjectDirectory(), block);
+        
+        assertNotNull(prj);
+        ProjectProblemsProvider prov = prj.getLookup().lookup(ProjectProblemsProvider.class);
+        assertNotNull(prov);
+        assertFalse(prov.getProblems().isEmpty());
+       
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        assertTrue(block.tryAcquire(10000, TimeUnit.SECONDS));
+        ProjectAlertPresenter presenter = tested.getPresenter(prj);
+        
+        // The displayer execution is now blocked, so the presenter remains.
+        assertNotNull(presenter);
+        
+        // release the displayer -> let the presenter to finish.
+        displayer.responsePermits.release();
+        
+        assertTrue("Fixes accepted", presenter.getCompletion().get());
+        assertNull("Presenter has finished", tested.getPresenter(prj));
+        
+        Collection<? extends ProjectProblemsProvider.ProjectProblem> probs = prov.getProblems();
+        assertTrue(probs.isEmpty());
+    }
+    
+    /**
+     * Check that a project with 'hard' errors will issue the messages and then
+     * allow the user to fix the fixable ones.
+     * @throws Exception 
+     */
+    public void testHardErrorComesBeforeFixable() throws Exception {
+        FileObject pdir = createSimpleProject();
+
+        // deliberately in the reverse order
+        createFixableError();
+
+        ProjectProblemsProvider.ProjectProblem unfixable = ProjectProblemsProvider.ProjectProblem.createError(
+                "Unfixable",
+                "Bye-bye",
+                null);
+        reporter.reportProblems.add(unfixable);
+        reporter.response.release(100);
+
+        // response to the fixable error
+        Response r = new Response("Test Project", "Fixable-error", NotifyDescriptor.OK_OPTION);
+        r.responseSelected = new Semaphore(0);
+        r.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r);
+        
+        // response to the unfixable error
+        displayer.expectedResponses.add(new Response("Test Project", "Unfixable", NotifyDescriptor.OK_OPTION));
+
+        // at the time the response is answered
+        
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+        
+        assertTrue(r.responseSelected.tryAcquire(100, TimeUnit.SECONDS));
+        assertSame(r, displayer.currentResponse);
+       
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        
+        // check that the notified list already contains the 'error' message
+        assertTrue(displayer.notifyNow.stream().anyMatch(nd -> nd.getMessage().toString().contains("Bye-bye")));
+        
+        r.responseLock.countDown();
+        
+        assertTrue(p.getCompletion().get());
+    }
+    
+    private List<Response> responses;
+    
+    private Semaphore responseCount;
+    
+    private FileObject pdir;
+    
+    /**
+     * Fills in pdir, responseCount, responses.
+     */
+    private void setupProjectWithUnfixableErrors() throws Exception {
+        pdir = createSimpleProject();
+
+        List<ProjectProblemsProvider.ProjectProblem> unfixables = new ArrayList<>();
+        responses = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            unfixables.add(ProjectProblemsProvider.ProjectProblem.createError(
+                "Unfixable",
+                "Unfixable" + i,
+                null));
+        }
+        reporter.reportProblems.addAll(unfixables);
+        reporter.response.release(100);
+
+        responseCount = new Semaphore(0);
+        for (int i = 0; i < 5; i++) {
+            Response r = new Response("Test Project", "Unfixable" + i, NotifyDescriptor.OK_OPTION);
+            r.responseSelected = responseCount;
+            r.responseLock = new CountDownLatch(1);
+            responses.add(r);
+        }
+        displayer.expectedResponses.addAll(responses);
+        
+    }
+    
+    /**
+     * Checks that if there are too many errors (more than permitted threshold), they are still
+     * displayed - after user confirms some of the earlier ones, the rest will appear.
+     * @throws Exception 
+     */
+    public void testTooManyErrorsStillDisplayed() throws Exception {
+        ProjectAlertPresenter.MAX_PRESENTED_ERRORS = 2;
+        setupProjectWithUnfixableErrors();
+        // at the time the response is answered
+        
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+        
+        responses.get(4).responseLock.countDown();
+        responses.get(3).responseLock.countDown();
+        responses.get(2).responseLock.countDown();
+
+        // wait till >= two responses are selected
+        assertTrue(responseCount.tryAcquire(2, 10, TimeUnit.SECONDS));
+        
+        assertEquals("Two notify Descriptors received, others blocked by RP", 2, displayer.notifyNow.size());
+        assertEquals("None response answered yet", 0, displayer.answeredResponses.size());
+        
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        assertFalse(p.getCompletion().isDone());
+
+        // release three problems
+        responses.get(0).responseLock.countDown();
+        responses.get(1).responseLock.countDown();
+        
+        // the presenter should now be able to finish
+        assertTrue("The presenter is able to finish", p.getCompletion().get());
+    }
+    
+    /**
+     * Checks that if there are errors displayed, they will eventually time out and
+     * the process will terminate.
+     * @throws Exception 
+     */
+    public void testIgnoredErrorsTimeout() throws Exception {
+        // reduce the timeout to a manageable time:
+        ProjectAlertPresenter.ERRORS_WAKEUP_DELAY = 1000;
+        setupProjectWithUnfixableErrors();
+
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        // let the notifications to be fired.
+        assertTrue(responseCount.tryAcquire(3, 10, TimeUnit.SECONDS));
+        // wait some more
+        Thread.sleep(250);
+        
+        
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        assertFalse("Presenter is still running", p.getCompletion().isDone());
+        
+        assertTrue("The presenter finishes because of timeout", p.getCompletion().get());
+    }
+    
+    /**
+     * Checks that a fixable error *is* presented despite many hard errors, after a timeout.
+     * @throws Exception 
+     */
+    public void testQuestionPresentedWithManyErrors() throws Exception {
+        // reduce the timeout to a manageable time:
+        ProjectAlertPresenter.ERRORS_WAKEUP_DELAY = 1000;
+        setupProjectWithUnfixableErrors();
+        createFixableError();
+
+        Response r = new Response("Test Project", "Fixable-error", NotifyDescriptor.OK_OPTION);
+        r.responseSelected = new Semaphore(0);
+        displayer.expectedResponses.add(r);
+
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+        
+        assertTrue("Fixable error must be reported after timeout", r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+    }
+    
+    AtomicReference<ProjectProblemsProvider.ProjectProblem> ref2 = new AtomicReference<>();
+    AtomicBoolean resolveCalled2 = new AtomicBoolean();
+    
+    private void setupTwoFixables() throws Exception {
+        pdir = createSimpleProject();
+        ProjectProblemsProvider.ProjectProblem pp = ProjectProblemsProvider.ProjectProblem.createError(
+                "Test",
+                "Fixable1",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    reporter.reportProblems.remove(ref.get());
+                    resolveCalled.set(true);
+                    return CompletableFuture.completedFuture(ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.RESOLVED));
+                }
+            }
+        );
+        ref.set(pp);
+        reporter.reportProblems.add(pp);
+        
+        ProjectProblemsProvider.ProjectProblem pp2 = ProjectProblemsProvider.ProjectProblem.createError(
+                "Test",
+                "Fixable2",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    reporter.reportProblems.remove(ref2.get());
+                    resolveCalled2.set(true);
+                    return CompletableFuture.completedFuture(ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.RESOLVED));
+                }
+            }
+        );
+        ref2.set(pp);
+        reporter.reportProblems.add(pp2);
+    }
+    
+    public void testOneQuestionFollowsOther() throws Exception {
+        setupTwoFixables();
+        Response r = new Response("Test Project", "Fixable1", NotifyDescriptor.OK_OPTION);
+        r.responseSelected = new Semaphore(0);
+        r.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r);
+
+        Response r2 = new Response("Test Project", "Fixable2", NotifyDescriptor.OK_OPTION);
+        r2.responseSelected = new Semaphore(0);
+        r2.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r2);
+        
+        
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        // wait for the 1st response to be selected
+        assertTrue(r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        // the presenter is active, and the 2nd question is NOT yet even asked
+        
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        assertFalse(p.getCompletion().isDone());
+        assertTrue(displayer.notifyNow.stream().anyMatch(d -> d.getMessage().toString().contains("Fixable1")));
+        assertTrue(displayer.notifyNow.stream().noneMatch(d -> d.getMessage().toString().contains("Fixable2")));
+        
+        // release the response
+        r.responseLock.countDown();
+        
+        // 2nd response is presented - at this time 1st problem must be already resolved.
+        assertTrue(r2.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        assertTrue(resolveCalled.get());
+        
+        r2.responseLock.countDown();
+        assertTrue("The presenter is able to finish", p.getCompletion().get());        
+        assertTrue(resolveCalled2.get());
+    }
+    
+    public void testOneResolveOneNot() throws Exception {
+        setupTwoFixables();
+        Response r = new Response("Test Project", "Fixable1", NotifyDescriptor.OK_OPTION);
+        r.responseSelected = new Semaphore(0);
+        r.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r);
+
+        Response r2 = new Response("Test Project", "Fixable2", NotifyDescriptor.OK_OPTION);
+        r2.responseSelected = new Semaphore(0);
+        r2.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r2);
+        
+        
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        // wait for the 1st response to be selected
+        assertTrue(r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        // the presenter is active, and the 2nd question is NOT yet even asked
+        
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        assertFalse(p.getCompletion().isDone());
+        assertTrue(displayer.notifyNow.stream().anyMatch(d -> d.getMessage().toString().contains("Fixable1")));
+        assertTrue(displayer.notifyNow.stream().noneMatch(d -> d.getMessage().toString().contains("Fixable2")));
+        
+        // release the response
+        r.responseLock.countDown();
+        
+        // 2nd response is presented - at this time 1st problem must be already resolved.
+        assertTrue(r2.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        assertTrue(resolveCalled.get());
+        
+        r2.responseLock.countDown();
+        assertTrue("The presenter is able to finish", p.getCompletion().get());        
+        assertTrue(resolveCalled2.get());
+    }
+    
+    public void testRejectOneAcceptOther() throws Exception {
+        setupTwoFixables();
+        Response r = new Response("Test Project", "Fixable1", NotifyDescriptor.NO_OPTION);
+        r.responseSelected = new Semaphore(0);
+        r.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r);
+
+        Response r2 = new Response("Test Project", "Fixable2", NotifyDescriptor.OK_OPTION);
+        r2.responseSelected = new Semaphore(0);
+        r2.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r2);
+        
+        
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        // wait for the 1st response to be selected
+        assertTrue(r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+
+        // the presenter is active, and the 2nd question is NOT yet even asked
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        assertFalse(p.getCompletion().isDone());
+        assertTrue(displayer.notifyNow.stream().anyMatch(d -> d.getMessage().toString().contains("Fixable1")));
+        /*
+        if (!displayer.notifyNow.stream().noneMatch(d -> d.getMessage().toString().contains("Fixable2"))) {
+            System.err.println("");
+        }
+        */
+        assertTrue(displayer.notifyNow.stream().noneMatch(d -> d.getMessage().toString().contains("Fixable2")));
+        
+        // release the response
+        r.responseLock.countDown();
+        
+        // 2nd response is presented - at this time 1st problem must be already resolved.
+        assertTrue(r2.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        // the rejected fix did not happen
+        assertFalse(resolveCalled.get());
+        // the 2nd question presented
+        assertTrue(displayer.notifyNow.stream().anyMatch(d -> d.getMessage().toString().contains("Fixable2")));
+        
+        r2.responseLock.countDown();
+        assertTrue("The presenter is able to finish", p.getCompletion().get());        
+        assertTrue(resolveCalled2.get());
+    }
+
+    public void testRestResolvesWithoutAsking() throws Exception {
+        setupTwoFixables();
+        Response r = new Response("Test Project", "Fixable1", Bundle.ProjectProblems_RestOption());
+        r.responseSelected = new Semaphore(0);
+        r.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r);
+
+        Response r2 = new Response("Test Project", "Fixable2", NotifyDescriptor.OK_OPTION);
+        r2.responseSelected = new Semaphore(0);
+        displayer.expectedResponses.add(r2);
+        
+        
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        // wait for the 1st response to be selected
+        assertTrue(r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+
+        // the presenter is active, and the 2nd question is NOT yet even asked
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        assertFalse(p.getCompletion().isDone());
+        assertTrue("1st dialog shown", displayer.notifyNow.stream().anyMatch(d -> d.getMessage().toString().contains("Fixable1")));
+        assertTrue("2nd dialog not shown yet", displayer.notifyNow.stream().noneMatch(d -> d.getMessage().toString().contains("Fixable2")));
+        
+        // release the response
+        r.responseLock.countDown();
+        assertTrue("The presenter is able to finish", p.getCompletion().get());
+        
+        assertTrue("2nd dialog never presented", displayer.notifyNow.stream().noneMatch(d -> d.getMessage().toString().contains("Fixable2")));
+        
+        // both fixes happened
+        assertTrue("Fix 1 applied", resolveCalled.get());
+        assertTrue("Fix 2 applied", resolveCalled2.get());
+    }
+    
+    /**
+     * After problem fix fails, a report with that error must be displayed.
+     */
+    public void testProblemFixFails() throws Exception {
+        pdir = createSimpleProject();
+        ProjectProblemsProvider.ProjectProblem pp = ProjectProblemsProvider.ProjectProblem.createError(
+                "TestFixable1",
+                "Fixable1",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    resolveCalled.set(true);
+                    return CompletableFuture.completedFuture(
+                            ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.UNRESOLVED, "NotResolved1")
+                    );
+                }
+            }
+        );
+        ref.set(pp);
+        reporter.reportProblems.add(pp);
+
+        Response r = new Response("Test Project", "Fixable1", NotifyDescriptor.OK_OPTION);
+        r.responseSelected = new Semaphore(0);
+        r.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r);
+
+        Response errResp = new Response("Test Project", "failed: NotResolved1", NotifyDescriptor.OK_OPTION);
+        errResp.responseSelected = new Semaphore(0);
+        displayer.expectedResponses.add(errResp);
+
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        assertTrue(r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        
+        r.responseLock.countDown();
+
+        // wait for the 1st response to be selected
+        assertTrue("Fix response selected", errResp.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+
+        assertTrue("The presenter finished", p.getCompletion().get());
+        assertTrue(resolveCalled.get());
+        
+        Optional<NotifyDescriptor> nd = displayer.notifyNow.stream().filter(d -> d.getMessage().toString().contains("failed: NotResolved1")).findAny();
+        assertTrue("Error dialog shown", nd.isPresent());
+        assertEquals("Just OK is present.", 1, nd.get().getOptions().length);
+    }
+    
+    private void setupFailAndOkFixables() throws Exception {
+        pdir = createSimpleProject();
+        ProjectProblemsProvider.ProjectProblem pp = ProjectProblemsProvider.ProjectProblem.createError(
+                "TestFixable1",
+                "Fixable1",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    resolveCalled.set(true);
+                    return CompletableFuture.completedFuture(
+                            ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.UNRESOLVED, "NotResolved1")
+                    );
+                }
+            }
+        );
+
+        ProjectProblemsProvider.ProjectProblem pp2 = ProjectProblemsProvider.ProjectProblem.createError(
+                "TestFixable2",
+                "Fixable2",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    reporter.reportProblems.add(ref2.get());
+                    resolveCalled2.set(true);
+                    return CompletableFuture.completedFuture(ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.RESOLVED));
+                }
+            }
+        );
+        ref.set(pp);
+        ref2.set(pp2);
+        reporter.reportProblems.add(pp);
+        reporter.reportProblems.add(pp2);
+
+        Response r = new Response("Test Project", "Fixable1", NotifyDescriptor.OK_OPTION);
+        r.responseSelected = new Semaphore(0);
+        r.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r);
+
+        Response errResp = new Response("Test Project", "failed: NotResolved1", NotifyDescriptor.OK_OPTION);
+        errResp.responseSelected = new Semaphore(0);
+        displayer.expectedResponses.add(errResp);
+
+        Response r2 = new Response("Test Project", "Fixable2", NotifyDescriptor.OK_OPTION);
+        r2.responseSelected = new Semaphore(0);
+        displayer.expectedResponses.add(r2);
+    }
+    
+    
+    /**
+     * 1st question confirmed, resolution fails. Error message appears, user clicks details. Next question appears.
+     * @throws Exception 
+     */
+    public void testFirstProblemFailsExecuteNext() throws Exception {
+        setupFailAndOkFixables();
+        Response r = displayer.expectedResponses.get(0);
+        Response errResp = displayer.expectedResponses.get(1);
+        
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        assertTrue(r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        
+        r.responseLock.countDown();
+
+        // wait for the 1st response to be selected
+        assertTrue("Fix response selected", errResp.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+
+        assertTrue("The presenter finished", p.getCompletion().get());
+        assertTrue(resolveCalled.get());
+        
+        Optional<NotifyDescriptor> nd = displayer.notifyNow.stream().filter(d -> d.getMessage().toString().contains("failed: NotResolved1")).findAny();
+        assertTrue("Error dialog shown", nd.isPresent());
+        assertEquals("Fix all, Details, Cancel should be present.", 3, nd.get().getOptions().length);
+        assertTrue(resolveCalled2.get());
+        assertTrue("Second question displayed.", 
+                displayer.notifyNow.stream().anyMatch(d -> d.getMessage().toString().contains("Fixable2")));
+    }
+    
+    /**
+     * After 1st fix fails, "Fix all" is pressed. The next dialog is not displayed, but the
+     * fix is applied.
+     * @throws Exception 
+     */
+    public void testFirstProblemFailsExecuteRest() throws Exception {
+       setupFailAndOkFixables();
+        Response r = displayer.expectedResponses.get(0);
+        Response errResp = displayer.expectedResponses.get(1);
+        // let the rest of fixes happen
+        errResp.response = Bundle.ProjectProblems_RestOption();
+        
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        assertTrue(r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        
+        r.responseLock.countDown();
+
+        // wait for the 1st response to be selected
+        assertTrue("Fix response selected", errResp.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+
+        assertTrue("The presenter finished", p.getCompletion().get());
+        assertTrue(resolveCalled.get());
+        
+        Optional<NotifyDescriptor> nd = displayer.notifyNow.stream().filter(d -> d.getMessage().toString().contains("failed: NotResolved1")).findAny();
+        assertTrue("Error dialog shown", nd.isPresent());
+        assertEquals("Fix all, Details, Cancel should be present.", 3, nd.get().getOptions().length);
+        assertTrue(resolveCalled2.get());
+        assertTrue("Second question NOT displayed.", 
+                displayer.notifyNow.stream().noneMatch(d -> d.getMessage().toString().contains("Fixable2")));
+    }
+    
+    /**
+     * The process is cancelled after 1st failure.
+     * @throws Exception 
+     */
+    public void testFirstProblemFailsCancel() throws Exception {
+        setupFailAndOkFixables();
+        Response r = displayer.expectedResponses.get(0);
+        Response errResp = displayer.expectedResponses.get(1);
+        
+        errResp.response = NotifyDescriptor.CANCEL_OPTION;
+        
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        assertTrue(r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        
+        ProjectAlertPresenter p = tested.getPresenter(prj);
+        assertNotNull(p);
+        
+        r.responseLock.countDown();
+
+        // wait for the 1st response to be selected
+        assertTrue("Fix response selected", errResp.responseSelected.tryAcquire(100, TimeUnit.SECONDS));
+
+        assertFalse("The presenter finished", p.getCompletion().get());
+        assertTrue(resolveCalled.get());
+        
+        Optional<NotifyDescriptor> nd = displayer.notifyNow.stream().filter(d -> d.getMessage().toString().contains("failed: NotResolved1")).findAny();
+        assertTrue("Error dialog shown", nd.isPresent());
+        assertEquals("Fix all, Details, Cancel should be present.", 3, nd.get().getOptions().length);
+        assertFalse("Second fixable skipped.", resolveCalled2.get());
+        assertTrue("Second question NOT displayed.", 
+                displayer.notifyNow.stream().noneMatch(d -> d.getMessage().toString().contains("failed: NotResolved2")));
+    }
+    
+    /**
+     * Let the user ignore the 1st reported problem. The next problem should be displayed after timeout and
+     * should proceed, of OKed.
+     */
+    public void testFirstProblemTimeoutNextOK() throws Exception {
+        ProjectAlertPresenter.QUESTION_WAKEUP_DELAY = 500;
+        setupTwoFixables();
+
+        Response r = new Response("Test Project", "Fixable1", USER_IGNORED);
+        r.responseSelected = new Semaphore(0);
+        displayer.expectedResponses.add(r);
+        
+        Response r2 = new Response("Test Project", "Fixable2", NotifyDescriptor.OK_OPTION);
+        r2.responseSelected = new Semaphore(0);
+        r2.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r2);
+
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        assertTrue(r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        
+        TestedPresenter p = (TestedPresenter)tested.getPresenter(prj);
+        assertNotNull(p);
+        
+        // wait for the timeout
+        assertTrue(p.timeoutSem.tryAcquire(10, TimeUnit.SECONDS));
+
+        
+        // release the 2nd response
+        r2.responseLock.countDown();
+        assertTrue(r2.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        
+        assertTrue("The presenter finished", p.getCompletion().get());
+        assertTrue(resolveCalled2.get());
+    }
+    
+    /**
+     * Simulate ignoring the question, then a next alert() comes for the project, so the next
+     * question should be displayed.
+     * @throws Exception 
+     */
+    public void testTimeoutNextDisplaysAfterAlert() throws Exception {
+        ProjectAlertPresenter.QUESTION_WAKEUP_DELAY = 100000;
+        setupTwoFixables();
+
+        Response r = new Response("Test Project", "Fixable1", USER_IGNORED);
+        r.responseSelected = new Semaphore(0);
+        displayer.expectedResponses.add(r);
+        
+        Response r2 = new Response("Test Project", "Fixable2", NotifyDescriptor.OK_OPTION);
+        r2.responseSelected = new Semaphore(0);
+        r2.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r2);
+
+
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        assertTrue("First question ignored", r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        
+        TestedPresenter p = (TestedPresenter)tested.getPresenter(prj);
+        assertNotNull(p);
+        
+        // wait some considerable time
+        Thread.sleep(500);
+        assertEquals("Second response not picked yet", 0, r2.responseSelected.availablePermits());
+        assertEquals("Second dialog not displayed yet", 1, displayer.notifyNow.size());
+        
+        tested.showAlert(prj);
+
+        assertTrue("Second question selected", r2.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        assertEquals("Second dialog displayer", 2, displayer.notifyNow.size());        
+        
+        r2.responseLock.countDown();
+        
+        assertTrue("The presenter finished", p.getCompletion().get());
+        assertTrue(resolveCalled2.get());
+        assertFalse(resolveCalled.get());
+    }
+    
+    /**
+     * If a next problem is discovered during a problem's fix, the additional problem will be displayed at the end,
+     * but will not obscur existing conversation.
+     */
+    public void testDisplayAdditionalProblem() throws Exception {
+        pdir = createSimpleProject();
+        
+        
+        AtomicBoolean resolveCalled3 = new AtomicBoolean();
+        AtomicReference<ProjectProblemsProvider.ProjectProblem> ref3 = new AtomicReference<>();
+        ProjectProblemsProvider.ProjectProblem pp3 = ProjectProblemsProvider.ProjectProblem.createError(
+                "TestFixable3",
+                "Fixable3",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    reporter.reportProblems.remove(ref3.get());
+                    resolveCalled3.set(true);
+                    return CompletableFuture.completedFuture(ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.RESOLVED));
+                }
+            }
+        );
+        ref3.set(pp3);
+
+        ProjectProblemsProvider.ProjectProblem pp = ProjectProblemsProvider.ProjectProblem.createError(
+                "Test",
+                "Fixable1",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    reporter.reportProblems.remove(ref.get());
+                    resolveCalled.set(true);
+                    reporter.reportProblems.add(pp3);
+                    // report an error
+                    reporter.fireProblems();
+                    return CompletableFuture.completedFuture(ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.RESOLVED));
+                }
+            }
+        );
+
+        ProjectProblemsProvider.ProjectProblem pp2 = ProjectProblemsProvider.ProjectProblem.createError(
+                "TestFixable2",
+                "Fixable2",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    reporter.reportProblems.add(ref2.get());
+                    resolveCalled2.set(true);
+                    return CompletableFuture.completedFuture(ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.RESOLVED));
+                }
+            }
+        );
+        ref.set(pp);
+        ref2.set(pp2);
+        reporter.reportProblems.add(pp);
+        reporter.reportProblems.add(pp2);
+
+        Response r = new Response("Test Project", "Fixable1", NotifyDescriptor.OK_OPTION);
+        r.responseSelected = new Semaphore(0);
+        r.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r);
+        
+        Response r2 = new Response("Test Project", "Fixable2", NotifyDescriptor.OK_OPTION);
+        r2.responseSelected = new Semaphore(0);
+        r2.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r2);
+
+        Response r3 = new Response("Test Project", "Fixable3", NotifyDescriptor.OK_OPTION);
+        displayer.expectedResponses.add(r3);
+
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        assertTrue("First question answered", r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        
+        TestedPresenter p = (TestedPresenter)tested.getPresenter(prj);
+        assertNotNull(p);
+        
+        r.responseLock.countDown();
+        
+        assertTrue("2nd question answered", r2.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        assertEquals("First 2 problems presented", 2, displayer.notifyNow.size());
+        
+        r2.responseLock.countDown();
+        
+        assertTrue("The presenter finished", p.getCompletion().get());
+        assertTrue(resolveCalled.get());
+        assertTrue(resolveCalled2.get());
+        assertTrue(resolveCalled3.get());
+        
+        assertTrue("Late problem was presented", displayer.notifyNow.stream().anyMatch(d -> d.getMessage().toString().contains("Fixable3")));
+
+    }
+    
+    /**
+     * If an error is reported during problem fix, it is shown immediately
+     */
+    public void testDisplayAdditionalError() throws Exception {
+        pdir = createSimpleProject();
+        ProjectProblemsProvider.ProjectProblem pp = ProjectProblemsProvider.ProjectProblem.createError(
+                "Test",
+                "Fixable1",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    reporter.reportProblems.remove(ref.get());
+                    resolveCalled.set(true);
+                    reporter.reportProblems.add(ProjectProblemsProvider.ProjectProblem.createError("Test2", "Unfixable1", null));
+                    // report an error
+                    reporter.fireProblems();
+                    return CompletableFuture.completedFuture(ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.RESOLVED));
+                }
+            }
+        );
+
+        ProjectProblemsProvider.ProjectProblem pp2 = ProjectProblemsProvider.ProjectProblem.createError(
+                "TestFixable2",
+                "Fixable2",
+                new ProjectProblemResolver() {
+                @Override
+                public Future<ProjectProblemsProvider.Result> resolve() {
+                    reporter.reportProblems.add(ref2.get());
+                    resolveCalled2.set(true);
+                    return CompletableFuture.completedFuture(ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.RESOLVED));
+                }
+            }
+        );
+        ref.set(pp);
+        ref2.set(pp2);
+        reporter.reportProblems.add(pp);
+        reporter.reportProblems.add(pp2);
+
+        Response r = new Response("Test Project", "Fixable1", NotifyDescriptor.OK_OPTION);
+        r.responseSelected = new Semaphore(0);
+        r.responseLock = new CountDownLatch(1);
+        displayer.expectedResponses.add(r);
+        
+        Response r2 = new Response("Test Project", "Fixable2", NotifyDescriptor.OK_OPTION);
+        displayer.expectedResponses.add(r2);
+
+        Response r3 = new Response("Test Project", "Unfixable1", NotifyDescriptor.OK_OPTION);
+        displayer.expectedResponses.add(r3);
+
+        Project prj = ProjectManager.getDefault().findProject(pdir);
+        OpenProjects.getDefault().open(new Project[] { prj } , true);
+        OpenProjects.getDefault().openProjects().get();
+
+        assertTrue("First question answered", r.responseSelected.tryAcquire(10, TimeUnit.SECONDS));
+        
+        TestedPresenter p = (TestedPresenter)tested.getPresenter(prj);
+        assertNotNull(p);
+        
+        r.responseLock.countDown();
+        
+        assertTrue("The presenter finished", p.getCompletion().get());
+        assertTrue(resolveCalled.get());
+        assertTrue(resolveCalled2.get());
+        
+        assertTrue("Late error was presented", displayer.notifyNow.stream().anyMatch(d -> d.getMessage().toString().contains("Unfixable1")));
+    }
+    
+    static Map<Path, Collection> projectServices = Collections.synchronizedMap(new HashMap<>());
+    
+    static class TestedPresenter extends ProjectAlertPresenter {
+        Semaphore timeoutSem = new Semaphore(0);
+        
+        public TestedPresenter(Project project, BrokenReferencesModel model, Env master) {
+            super(project, model, master);
+        }
+
+        @Override
+        void processOneRound(Ctx ctx) {
+            super.processOneRound(ctx);
+        }
+        
+        void resumeAfterTimeout(Ctx ctx) {
+            super.resumeAfterTimeout(ctx);
+            timeoutSem.release();
+        }
+    }
+    
+    static class TestedImpl extends BrokenReferencesImpl {
+        Map<FileObject, Semaphore> presenterLock = Collections.synchronizedMap(new HashMap<>());
+        Map<FileObject, Semaphore> presenterNotify = Collections.synchronizedMap(new HashMap<>());
+
+        @Override
+        ProjectAlertPresenter createPresenter(Project project, BrokenReferencesModel model) {
+            return new TestedPresenter(project, model, this);
+        }
+        
+        @Override
+        public CompletableFuture<Void> showAlert(@NonNull Project project) {
+            Semaphore s = presenterLock.get(project.getProjectDirectory());
+            if (s != null) {
+                try {
+                    s.acquire();
+                } catch (InterruptedException ex) {
+                    fail();
+                }
+            }
+            CompletableFuture<Void> r = super.showAlert(project);
+            s = presenterNotify.get(project.getProjectDirectory());
+            if (s != null) {
+                s.release();
+            }        
+            return r;
+        }
+    }
+    
+    static class Response {
+        String projectName;
+        String description;
+        String label;
+        Object response;
+        volatile Semaphore responseSelected;
+        volatile CountDownLatch responseLock;
+
+        public Response(String projectName, String label, Object response) {
+            this.projectName = projectName;
+            this.label = label;
+            this.response = response;
+        }
+
+        public Response(String projectName, String label, String description, Object response) {
+            this.projectName = projectName;
+            this.description = description;
+            this.label = label;
+            this.response = response;
+        }
+        
+        public boolean matches(NotifyDescriptor d) {
+            return (d.getTitle().contains(projectName) && (label == null || d.getMessage().toString().contains(label)));
+        }
+        
+        public Object respond(NotifyDescriptor d) {
+            if (responseSelected != null) {
+                responseSelected.release();
+            }
+            if (responseLock != null) {
+                try {
+                    responseLock.await();
+                } catch (InterruptedException ex) {
+                    fail();
+                }
+            }
+            return response;
+        }
+    }
+    
+    /**
+     * Controllable problem reporter.
+     */
+    static class TestProblemReporter implements ProjectProblemsProvider {
+        List<ProjectProblem> reportProblems = Collections.synchronizedList(new ArrayList<>());
+        Semaphore response = new Semaphore(50);
+        Semaphore called = new Semaphore(0);
+        PropertyChangeSupport supp = new PropertyChangeSupport(this);
+        
+        void clear() {
+            reportProblems.clear();
+            supp.firePropertyChange(PROP_PROBLEMS, null, null);
+        }
+        
+        void fireProblems() {
+            supp.firePropertyChange(PROP_PROBLEMS, null, null);
+        }
+        
+        @Override
+        public void addPropertyChangeListener(PropertyChangeListener listener) {
+            supp.addPropertyChangeListener(listener);
+        }
+
+        @Override
+        public void removePropertyChangeListener(PropertyChangeListener listener) {
+            supp.removePropertyChangeListener(listener);
+        }
+
+        @Override
+        public Collection<? extends ProjectProblem> getProblems() {
+            try {
+                called.release();
+                response.acquire();
+                return reportProblems;
+            } catch (InterruptedException ex) {
+                fail();
+                return null;
+            }
+        }
+    }
+
+    static final String USER_IGNORED = "user-ignore";
+    
+    /**
+     * Mock displayer, so that user responses can be controlled.
+     */
+    static class DialogController extends DialogDisplayer {
+        final List<NotifyDescriptor> notifyLater = Collections.synchronizedList(new ArrayList<>());
+        final List<NotifyDescriptor> notifyNow = Collections.synchronizedList(new ArrayList<>());
+        
+        final Semaphore responsePermits = new Semaphore(100);
+        final List<Response> expectedResponses =  Collections.synchronizedList(new ArrayList<>());
+        final List<Response> answeredResponses =  Collections.synchronizedList(new ArrayList<>());
+        volatile Response currentResponse =  null;
+        final Semaphore responseAnswered = new Semaphore(0);
+
+        @Override
+        public Object notify(NotifyDescriptor descriptor) {
+            try {
+                notifyNow.add(descriptor);
+                responsePermits.acquire();
+            } catch (InterruptedException ex) {
+                fail("Interrupted");
+            }
+            
+            List<Response> lst = new ArrayList<>(expectedResponses);
+            for (Response r : lst) {
+                if (r.matches(descriptor)) {
+                    expectedResponses.remove(r);
+                    currentResponse = r;
+                    responseAnswered.release();
+                    Object o = r.respond(descriptor);
+                    answeredResponses.add(r);
+                    if (o == USER_IGNORED) {
+                        synchronized (this) {
+                            try {
+                                // block
+                                wait();
+                            } catch (InterruptedException ex) {
+                                fail();
+                            }
+                        }
+                        o = NotifyDescriptor.CLOSED_OPTION;
+                    }
+                    descriptor.setValue(o);
+                    currentResponse = null;
+                    return o;
+                }
+            }
+            fail("Unexpected NotifyDescriptor: " + descriptor);
+            return null;
+        }
+
+        @Override
+        public void notifyLater(NotifyDescriptor descriptor) {
+            notifyLater.add(descriptor);
+        }
+
+        @Override
+        public Dialog createDialog(DialogDescriptor descriptor) {
+            fail();
+            return null;
+        }
+        
+        // ensure greater throughput for the otherwise simple default DialogDescriptor.notifyFuture
+        RequestProcessor futureProcessor = new RequestProcessor("requests", 20);
+
+        public <T extends NotifyDescriptor> CompletableFuture<T> notifyFuture(final T descriptor) {
+            CompletableFuture<T> r = new CompletableFuture<>();
+            // preserve potential context
+            Lookup def = Lookup.getDefault();
+            futureProcessor.post(new Runnable() {
+                public void run() {
+                    Lookups.executeWith(def, () -> {
+                        try {
+                            DialogController.this.notify(descriptor);
+                            r.complete(descriptor);
+                        } catch (ThreadDeath td) {
+                            throw td;
+                        } catch (Throwable t) {
+                            r.completeExceptionally(t);
+                        }
+                    });
+                }
+            });
+            return r;
+        }
+    }
+    
+    
+    public static LookupProvider forProjectServices() {
+        return new ProjectTestServices();
+    }
+    
+    /**
+     * This class is registered in the (maven) project Lookup, so that 
+     * the test can mock / inject problem reporter service.
+     */
+    private static class ProjectTestServices implements LookupProvider {
+        @Override
+        public Lookup createAdditionalLookup(Lookup baseContext) {
+            Project p = baseContext.lookup(Project.class);
+            Collection services = projectServices.get(FileUtil.toFile(p.getProjectDirectory()).toPath());
+            if (services == null) {
+                return Lookup.EMPTY;
+            } else {
+                return Lookups.fixed(services.toArray(new Object[services.size()]));
+            }
+        }
+    }
+    
+}

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@netbeans.apache.org
For additional commands, e-mail: commits-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists