You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by lk...@apache.org on 2020/10/26 22:46:02 UTC

[netbeans] branch delivery updated: Infrastructure to display simple confirmations/questions in LSP client. (#2493)

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

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


The following commit(s) were added to refs/heads/delivery by this push:
     new cee80c2  Infrastructure to display simple confirmations/questions in LSP client. (#2493)
cee80c2 is described below

commit cee80c2704dac3e35f71e5cefea67916a7ff7dc2
Author: Svatopluk Dedic <sv...@oracle.com>
AuthorDate: Mon Oct 26 23:45:51 2020 +0100

    Infrastructure to display simple confirmations/questions in LSP client. (#2493)
    
    * Added A11Y properties to the panel.
    
    * Infrastructure to display simple confirmations/questions in LSP client.
    
    * Only translate html-starting content.
    
    * Leftover cleanup.
---
 .../modules/gradle/execute/TrustProjectPanel.java  |   6 +-
 .../modules/project/ui/problems/Bundle.properties  |   2 +-
 .../nbcode/integration/nbproject/project.xml       |   8 +
 .../nbcode/integration/LspDialogDisplayer.java     |  31 ++
 java/java.lsp.server/nbproject/project.xml         |   8 +
 .../modules/java/lsp/server/LspServerUtils.java    |  91 ++++++
 .../lsp/server/protocol/NbCodeLanguageClient.java  |   4 +
 .../modules/java/lsp/server/protocol/Server.java   | 229 +++++++++++----
 .../lsp/server/protocol/WorkspaceUIContext.java    |   9 +-
 .../lsp/server/ui/AbstractDialogDisplayer.java     |  68 +++++
 .../lsp/server/ui/NotifyDescriptorAdapter.java     | 325 +++++++++++++++++++++
 .../modules/java/lsp/server/ui/UIContext.java      |  39 ++-
 .../lsp/server/ui/AbstractDialogDisplayerTest.java | 156 ++++++++++
 13 files changed, 910 insertions(+), 66 deletions(-)

diff --git a/extide/gradle/src/org/netbeans/modules/gradle/execute/TrustProjectPanel.java b/extide/gradle/src/org/netbeans/modules/gradle/execute/TrustProjectPanel.java
index e4bbfcf..910a70c 100644
--- a/extide/gradle/src/org/netbeans/modules/gradle/execute/TrustProjectPanel.java
+++ b/extide/gradle/src/org/netbeans/modules/gradle/execute/TrustProjectPanel.java
@@ -42,19 +42,23 @@ public class TrustProjectPanel extends javax.swing.JPanel {
             + " allows arbitrary code execution.</p>",
         "TrustProjectPanel.INFO_UNKNOWN=<html><p>NetBeans is about to invoke a Gradle build process.</p>"
             + " <p>Executing Gradle can be potentially un-safe as it"
-            + " allows arbitrary code execution.</p>"
+            + " allows arbitrary code execution.</p>",
+        "ProjectTrustDlg.TITLE=Not a Trusted Project"
     })
     public TrustProjectPanel(Project project) {
         initComponents();
         ProjectInformation info = project != null ? project.getLookup().lookup(ProjectInformation.class) : null;
+        getAccessibleContext().setAccessibleName(Bundle.ProjectTrustDlg_TITLE());
         if (project == null) {
             cbTrustProject.setEnabled(false);
             cbTrustProject.setVisible(false);
         }
         if (info == null) {
             lbTrustMessage.setText(Bundle.TrustProjectPanel_INFO_UNKNOWN());
+            getAccessibleContext().setAccessibleDescription(Bundle.TrustProjectPanel_INFO_UNKNOWN());
         } else {
             lbTrustMessage.setText(Bundle.TrustProjectPanel_INFO(info.getDisplayName()));
+            getAccessibleContext().setAccessibleDescription(Bundle.TrustProjectPanel_INFO(info.getDisplayName()));
         }
     }
 
diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/problems/Bundle.properties b/ide/projectui/src/org/netbeans/modules/project/ui/problems/Bundle.properties
index d022650..f9d5d34 100644
--- a/ide/projectui/src/org/netbeans/modules/project/ui/problems/Bundle.properties
+++ b/ide/projectui/src/org/netbeans/modules/project/ui/problems/Bundle.properties
@@ -20,7 +20,7 @@ ACSN_BrokenReferencesAlertPanel_notAgain=Do not show this message again
 ACSD_BrokenReferencesAlertPanel_notAgain=N/A
 MSG_Broken_References=<html>One or more project resources could not be found.<br>Right-click the project in the Projects window and choose<br><b>Resolve Project Problems</b> to find the missing resources.</html>
 ACSN_BrokenReferencesAlertPanel=Broken Project Panel
-ACSD_BrokenReferencesAlertPanel=N/A
+ACSD_BrokenReferencesAlertPanel=The project contains broken references or other problems.
 FMT_ProblemInProject={1} (in {0})
 
 LBL_BrokenLinksCustomizer_List=Project &Problems:
diff --git a/java/java.lsp.server/nbcode/integration/nbproject/project.xml b/java/java.lsp.server/nbcode/integration/nbproject/project.xml
index ff60f27..3a36d95 100644
--- a/java/java.lsp.server/nbcode/integration/nbproject/project.xml
+++ b/java/java.lsp.server/nbcode/integration/nbproject/project.xml
@@ -51,6 +51,14 @@
                     </run-dependency>
                 </dependency>
                 <dependency>
+                    <code-name-base>org.openide.dialogs</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.52</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
                     <code-name-base>org.openide.util.lookup</code-name-base>
                     <build-prerequisite/>
                     <compile-dependency/>
diff --git a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/LspDialogDisplayer.java b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/LspDialogDisplayer.java
new file mode 100644
index 0000000..98e4416
--- /dev/null
+++ b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/LspDialogDisplayer.java
@@ -0,0 +1,31 @@
+/*
+ * 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.AbstractDialogDisplayer;
+import org.openide.DialogDisplayer;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author sdedic
+ */
+//@ServiceProvider(service = DialogDisplayer.class, position = 1000)
+public class LspDialogDisplayer extends AbstractDialogDisplayer {
+}
diff --git a/java/java.lsp.server/nbproject/project.xml b/java/java.lsp.server/nbproject/project.xml
index 89d6327..7d0d84b 100644
--- a/java/java.lsp.server/nbproject/project.xml
+++ b/java/java.lsp.server/nbproject/project.xml
@@ -303,6 +303,14 @@
                     </run-dependency>
                 </dependency>
                 <dependency>
+                    <code-name-base>org.openide.dialogs</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.52</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
                     <code-name-base>org.openide.filesystems</code-name-base>
                     <build-prerequisite/>
                     <compile-dependency/>
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspServerUtils.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspServerUtils.java
new file mode 100644
index 0000000..ab14aed
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspServerUtils.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+import org.netbeans.api.annotations.common.CheckForNull;
+import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.java.lsp.server.protocol.Server;
+import org.openide.util.Lookup;
+
+/**
+ *
+ * @author sdedic
+ */
+public class LspServerUtils {
+    
+    /**
+     * Locates the client associated with the current context. Use this method as a
+     * last resort, for testing & all other practical purposes it is always better to
+     * have own LSP client reference or a context Lookup instance.
+     * @param context the processing context.
+     * @return LanguageClient instance or {@code null}, if no client associated with the context or thread.
+     */
+    @CheckForNull
+    public static final NbCodeLanguageClient findLspClient(Lookup context) {
+        NbCodeLanguageClient client = context != null ? context.lookup(NbCodeLanguageClient.class) : null;
+        if (client == null && context != Lookup.getDefault()) {
+            client = Lookup.getDefault().lookup(NbCodeLanguageClient.class);
+        }
+        return client;
+    }
+
+    /**
+     * Locates the client associated with the current context. Use this method as a
+     * last resort, for testing & all other practical purposes it is always better to
+     * have own LSP client reference or a context Lookup instance.
+     * <p>
+     * This method always return a client, but if no real context is given, the client returns
+     * just stub values and logs all method calls as warnings.
+     * 
+     * @param context the processing context.
+     * @return LanguageClient instance, never null.
+     */
+    @NonNull
+    public static final NbCodeLanguageClient requireLspClient(Lookup context) {
+        NbCodeLanguageClient client = findLspClient(context);
+        return client != null ? client : Server.getStubClient();
+    }
+    
+    /**
+     * Checks whether the calling thread is the one serving client's communication.
+     * Such thread cannot be blocked. If {@code null} is passed, returns {@code true}
+     * if the calling thread serves any client.
+     * 
+     * @param client client instance or {@code null}.
+     * @return true, if communication with the client would be broken
+     */
+    public static final boolean isClientResponseThread(NbCodeLanguageClient client) {
+        return Server.isClientResponseThread(client);
+    }
+    
+    /**
+     * Ensures that the caller does not serve the client associated with the context. If it does,
+     * throws IllegalStateException. Call to avoid blocking communication with the client before
+     * waiting on some remote response.
+     * 
+     * @param context execution context
+     */
+    public static final void avoidClientMessageThread(Lookup context) {
+        NbCodeLanguageClient client = LspServerUtils.findLspClient(context);
+        if (LspServerUtils.isClientResponseThread(client)) {
+            throw new IllegalStateException("Can not block LSP server message loop. Use RequestProcessor to run the calling code, or use notifyLater()");
+        }
+    }
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java
index 10ad5a6..06177ae 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeLanguageClient.java
@@ -46,4 +46,8 @@ public interface NbCodeLanguageClient extends LanguageClient {
      * @return code capabilities.
      */
     public NbCodeClientCapabilities getNbCodeCapabilities();
+    
+    public default boolean isRequestDispatcherThread() {
+        return Boolean.TRUE.equals(Server.DISPATCHERS.get());
+    }
 }
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
index 91bf541..a67ad69 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
@@ -26,17 +26,24 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
+import java.util.function.Function;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import org.eclipse.lsp4j.CompletionOptions;
 import org.eclipse.lsp4j.ExecuteCommandOptions;
 import org.eclipse.lsp4j.InitializeParams;
 import org.eclipse.lsp4j.InitializeResult;
+import org.eclipse.lsp4j.MessageActionItem;
+import org.eclipse.lsp4j.MessageParams;
 import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.PublishDiagnosticsParams;
 import org.eclipse.lsp4j.ServerCapabilities;
+import org.eclipse.lsp4j.ShowMessageRequestParams;
 import org.eclipse.lsp4j.TextDocumentSyncKind;
 import org.eclipse.lsp4j.WorkspaceFolder;
 import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
@@ -61,6 +68,7 @@ import org.netbeans.api.project.ui.OpenProjects;
 import org.openide.filesystems.FileObject;
 import org.openide.util.Exceptions;
 import org.openide.util.Lookup;
+import org.openide.util.RequestProcessor;
 import org.openide.util.lookup.AbstractLookup;
 import org.openide.util.lookup.InstanceContent;
 import org.openide.util.lookup.Lookups;
@@ -71,13 +79,28 @@ import org.openide.util.lookup.ProxyLookup;
  * @author lahvac
  */
 public final class Server {
+    private static final Logger LOG = Logger.getLogger(Server.class.getName());
+    
     private Server() {
     }
     
+    public static NbCodeLanguageClient getStubClient() {
+        return STUB_CLIENT;
+    }
+    
+    public static boolean isClientResponseThread(NbCodeLanguageClient client) {
+        return client != null ? 
+                DISPATCHERS.get() == client :
+                DISPATCHERS.get() != null;
+    }
+    
     public static void launchServer(InputStream in, OutputStream out) {
         LanguageServerImpl server = new LanguageServerImpl();
-        Launcher<NbCodeLanguageClient> serverLauncher = createLauncher(server, in, out);
-        ((LanguageClientAware) server).connect(serverLauncher.getRemoteProxy());
+        ConsumeWithLookup msgProcessor = new ConsumeWithLookup(server.getSessionLookup());
+        Launcher<NbCodeLanguageClient> serverLauncher = createLauncher(server, in, out, msgProcessor::attachLookup);
+        NbCodeLanguageClient remote = serverLauncher.getRemoteProxy();
+        ((LanguageClientAware) server).connect(remote);
+        msgProcessor.attachClient(server.client);
         Future<Void> runningServer = serverLauncher.startListening();
         try {
             runningServer.get();
@@ -86,39 +109,56 @@ public final class Server {
         }
     }
     
-    private static Launcher<NbCodeLanguageClient> createLauncher(LanguageServerImpl server, InputStream in, OutputStream out) {
+    private static Launcher<NbCodeLanguageClient> createLauncher(LanguageServerImpl server, InputStream in, OutputStream out,
+            Function<MessageConsumer, MessageConsumer> processor) {
         return new LSPLauncher.Builder<NbCodeLanguageClient>()
             .setLocalService(server)
             .setRemoteInterface(NbCodeLanguageClient.class)
             .setInput(in)
             .setOutput(out)
-            .wrapMessages(new ConsumeWithLookup(server.getSessionLookup())::attachLookup)
+            .wrapMessages(processor)
             .create();
     }
     
+    static final ThreadLocal<NbCodeLanguageClient>   DISPATCHERS = new ThreadLocal<>();
+    
     /**
      * Processes message while the default Lookup is set to 
      * {@link LanguageServerImpl#getSessionLookup()}.
      */
     private static class ConsumeWithLookup {
         private final Lookup sessionLookup;
-
+        private NbCodeLanguageClient client;
+        
         public ConsumeWithLookup(Lookup sessionLookup) {
             this.sessionLookup = sessionLookup;
         }
         
+        synchronized void attachClient(NbCodeLanguageClient client) {
+            this.client = client;
+        }
+        
         public MessageConsumer attachLookup(MessageConsumer delegate) {
             return new MessageConsumer() {
                 @Override
                 public void consume(Message msg) throws MessageIssueException, JsonRpcException {
-                    Lookups.executeWith(sessionLookup, () -> {
-                        delegate.consume(msg);
-                    });
+                    try {
+                        DISPATCHERS.set(client);
+                        Lookups.executeWith(sessionLookup, () -> {
+                            delegate.consume(msg);
+                        });
+                    } finally {
+                        DISPATCHERS.remove();
+                    }
                 }
             };
         }
     }
     
+    // change to a greater throughput if the initialization waits on more processes than just (serialized) project open.
+    private static final RequestProcessor SERVER_INIT_RP = new RequestProcessor(LanguageServerImpl.class.getName());
+    
+    
     private static class LanguageServerImpl implements LanguageServer, LanguageClientAware {
 
         private static final Logger LOG = Logger.getLogger(LanguageServerImpl.class.getName());
@@ -135,62 +175,45 @@ public final class Server {
             return sessionLookup;
         }
         
-        @Override
-        public CompletableFuture<InitializeResult> initialize(InitializeParams init) {
-            NbCodeClientCapabilities capa = NbCodeClientCapabilities.get(init);
-            client.setClientCaps(capa);
-            List<FileObject> projectCandidates = new ArrayList<>();
-            List<WorkspaceFolder> folders = init.getWorkspaceFolders();
-            if (folders != null) {
-                for (WorkspaceFolder w : folders) {
-                    try {
-                        projectCandidates.add(TextDocumentServiceImpl.fromUri(w.getUri()));
-                    } catch (MalformedURLException ex) {
-                        LOG.log(Level.FINE, null, ex);
+        private void asyncOpenSelectedProjects(CompletableFuture f, List<FileObject> projectCandidates) {
+            List<Project> projects = new ArrayList<>();
+            try {
+                for (FileObject candidate : projectCandidates) {
+                    Project prj = FileOwnerQuery.getOwner(candidate);
+                    if (prj != null) {
+                        projects.add(prj);
                     }
                 }
-            } else {
-                String root = init.getRootUri();
-
-                if (root != null) {
-                    try {
-                        projectCandidates.add(TextDocumentServiceImpl.fromUri(root));
-                    } catch (MalformedURLException ex) {
-                        LOG.log(Level.FINE, null, ex);
+                try {
+                    Project[] previouslyOpened = OpenProjects.getDefault().openProjects().get();
+                    if (previouslyOpened.length > 0) {
+                        Level level = Level.FINEST;
+                        assert (level = Level.CONFIG) != null;
+                        for (Project p : previouslyOpened) {
+                            LOG.log(level, "Previously opened project at {0}", p.getProjectDirectory());
+                        }
                     }
-                } else {
-                    //TODO: use getRootPath()?
+                } catch (InterruptedException | ExecutionException ex) {
+                    throw new IllegalStateException(ex);
                 }
-            }
-            List<Project> projects = new ArrayList<>();
-            for (FileObject candidate : projectCandidates) {
-                Project prj = FileOwnerQuery.getOwner(candidate);
-                if (prj != null) {
-                    projects.add(prj);
+                OpenProjects.getDefault().open(projects.toArray(new Project[0]), false);
+                try {
+                    OpenProjects.getDefault().openProjects().get();
+                } catch (InterruptedException | ExecutionException ex) {
+                    throw new IllegalStateException(ex);
                 }
-            }
-            try {
-                Project[] previouslyOpened = OpenProjects.getDefault().openProjects().get();
-                if (previouslyOpened.length > 0) {
-                    Level level = Level.FINEST;
-                    assert (level = Level.CONFIG) != null;
-                    for (Project p : previouslyOpened) {
-                        LOG.log(level, "Previously opened project at {0}", p.getProjectDirectory());
-                    }
+                for (Project prj : projects) {
+                    //init source groups/FileOwnerQuery:
+                    ProjectUtils.getSources(prj).getSourceGroups(Sources.TYPE_GENERIC);
                 }
-            } catch (InterruptedException | ExecutionException ex) {
-                throw new IllegalStateException(ex);
-            }
-            OpenProjects.getDefault().open(projects.toArray(new Project[0]), false);
-            try {
-                OpenProjects.getDefault().openProjects().get();
-            } catch (InterruptedException | ExecutionException ex) {
-                throw new IllegalStateException(ex);
-            }
-            for (Project prj : projects) {
-                //init source groups/FileOwnerQuery:
-                ProjectUtils.getSources(prj).getSourceGroups(Sources.TYPE_GENERIC);
+                Project[] prjs = projects.toArray(new Project[projects.size()]);
+                f.complete(prjs);
+            } catch (RuntimeException ex) {
+                f.completeExceptionally(ex);
             }
+        }
+        
+        private void showIndexingCompleted() {
             try {
                 JavaSource.create(ClasspathInfo.create(ClassPath.EMPTY, ClassPath.EMPTY, ClassPath.EMPTY))
                           .runWhenScanFinished(cc -> {
@@ -204,6 +227,9 @@ public final class Server {
             } catch (IOException ex) {
                 throw new IllegalStateException(ex);
             }
+        }
+        
+        private InitializeResult constructInitResponse() {
             ServerCapabilities capabilities = new ServerCapabilities();
             capabilities.setTextDocumentSync(TextDocumentSyncKind.Incremental);
             CompletionOptions completionOptions = new CompletionOptions();
@@ -216,7 +242,42 @@ public final class Server {
             capabilities.setDocumentHighlightProvider(true);
             capabilities.setReferencesProvider(true);
             capabilities.setExecuteCommandProvider(new ExecuteCommandOptions(Arrays.asList(JAVA_BUILD_WORKSPACE, GRAALVM_PAUSE_SCRIPT)));
-            return CompletableFuture.completedFuture(new InitializeResult(capabilities));
+            return new InitializeResult(capabilities);
+        }
+        
+        @Override
+        public CompletableFuture<InitializeResult> initialize(InitializeParams init) {
+            NbCodeClientCapabilities capa = NbCodeClientCapabilities.get(init);
+            client.setClientCaps(capa);
+            List<FileObject> projectCandidates = new ArrayList<>();
+            List<WorkspaceFolder> folders = init.getWorkspaceFolders();
+            if (folders != null) {
+                for (WorkspaceFolder w : folders) {
+                    try {
+                        projectCandidates.add(TextDocumentServiceImpl.fromUri(w.getUri()));
+                    } catch (MalformedURLException ex) {
+                        LOG.log(Level.FINE, null, ex);
+                    }
+                }
+            } else {
+                String root = init.getRootUri();
+
+                if (root != null) {
+                    try {
+                        projectCandidates.add(TextDocumentServiceImpl.fromUri(root));
+                    } catch (MalformedURLException ex) {
+                        LOG.log(Level.FINE, null, ex);
+                    }
+                } else {
+                    //TODO: use getRootPath()?
+                }
+            }
+            CompletableFuture<Project[]> fProjects = new CompletableFuture<>();
+            SERVER_INIT_RP.post(() -> asyncOpenSelectedProjects(fProjects, projectCandidates));
+            
+            return fProjects.
+                    thenRun(this::showIndexingCompleted).
+                    thenApply((v) -> constructInitResponse());
         }
 
         @Override
@@ -241,7 +302,7 @@ public final class Server {
         @Override
         public void connect(LanguageClient aClient) {
             this.client = new NbCodeClientWrapper((NbCodeLanguageClient)aClient);
-            
+            sessionServices.add(client);
             sessionServices.add(new WorkspaceIOContext() {
                 @Override
                 protected LanguageClient client() {
@@ -254,8 +315,56 @@ public final class Server {
             ((LanguageClientAware) getWorkspaceService()).connect(aClient);
         }
     }
-
+    
     public static final String JAVA_BUILD_WORKSPACE =  "java.build.workspace";
     public static final String GRAALVM_PAUSE_SCRIPT =  "graalvm.pause.script";
     static final String INDEXING_COMPLETED = "Indexing completed.";
+    
+    static final NbCodeLanguageClient STUB_CLIENT = new NbCodeLanguageClient() {
+        private final NbCodeClientCapabilities caps = new NbCodeClientCapabilities();
+        
+        private void logWarning(Object... args) {
+            LOG.log(Level.WARNING, "LSP Client called without proper context with param(s): {0}", 
+                    Arrays.asList(args));
+        }
+        
+        @Override
+        public void showStatusBarMessage(ShowStatusMessageParams params) {
+            logWarning(params);
+        }
+
+        @Override
+        public NbCodeClientCapabilities getNbCodeCapabilities() {
+            logWarning();
+            return caps;
+        }
+
+        @Override
+        public void telemetryEvent(Object object) {
+            logWarning(object);
+        }
+
+        @Override
+        public void publishDiagnostics(PublishDiagnosticsParams diagnostics) {
+            logWarning(diagnostics);
+        }
+
+        @Override
+        public void showMessage(MessageParams messageParams) {
+            logWarning(messageParams);
+        }
+
+        @Override
+        public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams requestParams) {
+            logWarning(requestParams);
+            CompletableFuture<MessageActionItem> x = new CompletableFuture<>();
+            x.complete(null);
+            return x;
+        }
+
+        @Override
+        public void logMessage(MessageParams message) {
+            logWarning(message);
+        }
+    };
 }
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceUIContext.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceUIContext.java
index c20575c..eb66a47 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceUIContext.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceUIContext.java
@@ -18,8 +18,10 @@
  */
 package org.netbeans.modules.java.lsp.server.protocol;
 
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.MessageActionItem;
 import org.eclipse.lsp4j.MessageParams;
-import org.eclipse.lsp4j.services.LanguageClient;
+import org.eclipse.lsp4j.ShowMessageRequestParams;
 import org.netbeans.modules.java.lsp.server.ui.UIContext;
 import org.openide.awt.StatusDisplayer;
 
@@ -40,6 +42,11 @@ class WorkspaceUIContext extends UIContext {
     }
 
     @Override
+    protected CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams msg) {
+        return client.showMessageRequest(msg);
+    }
+
+    @Override
     protected void showMessage(MessageParams msg) {
         client.showMessage(msg);
     }
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/AbstractDialogDisplayer.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/AbstractDialogDisplayer.java
new file mode 100644
index 0000000..35e05a0
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/AbstractDialogDisplayer.java
@@ -0,0 +1,68 @@
+/*
+ * 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.awt.Dialog;
+import java.awt.HeadlessException;
+import org.netbeans.modules.java.lsp.server.LspServerUtils;
+import org.openide.DialogDescriptor;
+import org.openide.DialogDisplayer;
+import org.openide.NotifyDescriptor;
+import org.openide.util.Lookup;
+
+/**
+ * Remoting implementation of {@link DialogDisplayer}. The implementation will refuse to 
+ * display dialogs that block the message processing thread ({@link IllegalStateException} will be thrown.
+ * 
+ * @author sdedic
+ */
+public class AbstractDialogDisplayer extends DialogDisplayer {
+    private final Lookup context;
+    
+    public AbstractDialogDisplayer() {
+        this(Lookup.getDefault());
+    }
+
+    AbstractDialogDisplayer(Lookup context) {
+        this.context = context;
+    }
+    
+    @Override
+    public Object notify(NotifyDescriptor descriptor) {
+        LspServerUtils.avoidClientMessageThread(context);
+        UIContext ctx = UIContext.find(context);
+        NotifyDescriptorAdapter adapter = new NotifyDescriptorAdapter(descriptor, ctx);
+        return adapter.clientNotify();
+    }
+    
+    @Override
+    public void notifyLater(final NotifyDescriptor descriptor) {
+        UIContext ctx = context.lookup(UIContext.class);
+        if (ctx == null) {
+            ctx = UIContext.find();
+        }
+        NotifyDescriptorAdapter adapter = new NotifyDescriptorAdapter(descriptor, ctx);
+        adapter.clientNotifyLater();
+    }
+
+    @Override
+    public Dialog createDialog(DialogDescriptor descriptor) {
+        throw new HeadlessException();
+    }
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/NotifyDescriptorAdapter.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/NotifyDescriptorAdapter.java
new file mode 100644
index 0000000..f10318e
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/NotifyDescriptorAdapter.java
@@ -0,0 +1,325 @@
+/*
+ * 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.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import javax.accessibility.Accessible;
+import javax.accessibility.AccessibleContext;
+import javax.accessibility.AccessibleIcon;
+import javax.accessibility.AccessibleText;
+import javax.swing.Icon;
+import javax.swing.JButton;
+import org.eclipse.lsp4j.MessageActionItem;
+import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.ShowMessageRequestParams;
+import org.openide.NotifyDescriptor;
+import org.openide.util.NbBundle;
+
+/**
+ * Adapts a {@link NotifyDescriptor} to a {@link ShowMessageRequestParams} call.
+ * @author sdedic
+ */
+class NotifyDescriptorAdapter {
+    private static final Logger LOG = Logger.getLogger(NotifyDescriptorAdapter.class.getName());
+    
+    private final UIContext client;
+    private final NotifyDescriptor  descriptor;
+    private final Map<MessageActionItem, Object> item2Option = new LinkedHashMap<>();
+    private final Map<String, Object> text2Option = new HashMap<>();
+    private final Map<Object, List<ActionListener>> optionListeners = new HashMap<>();
+    private final Map<Object, JButton> option2Button = new HashMap<>();
+    
+    private static final Set<String> warnedClasses = new HashSet<>();
+    
+    private ShowMessageRequestParams request;
+
+    private static final Object[] YES_NO_CANCEL = new Object[] {
+        NotifyDescriptor.YES_OPTION,
+        NotifyDescriptor.NO_OPTION,
+        NotifyDescriptor.CANCEL_OPTION
+    };
+    
+    private static final Object[] YES_NO = new Object[] {
+        NotifyDescriptor.YES_OPTION,
+        NotifyDescriptor.NO_OPTION,
+    };
+    
+    private static final Object[] OK_CANCEL = new Object[] {
+        NotifyDescriptor.OK_OPTION,
+        NotifyDescriptor.CANCEL_OPTION
+    };
+    
+    private static final Object[] JUST_OK = new Object[] {
+        NotifyDescriptor.OK_OPTION
+    };
+    
+    public NotifyDescriptorAdapter(NotifyDescriptor descriptor, UIContext client) {
+        this.descriptor = descriptor;
+        this.client = client;
+    }
+    
+    private MessageType translateMessageType() {
+        switch(descriptor.getMessageType()) {
+            case NotifyDescriptor.ERROR_MESSAGE:
+                return MessageType.Error;
+            case NotifyDescriptor.WARNING_MESSAGE:
+                return MessageType.Warning;
+            
+            case NotifyDescriptor.QUESTION_MESSAGE:
+            case NotifyDescriptor.PLAIN_MESSAGE:
+            case NotifyDescriptor.INFORMATION_MESSAGE:
+                return MessageType.Info;
+            default:
+                return MessageType.Log;
+        }
+    }
+    
+    /**
+     * Strip HTML from the message; VSCode standard showMessage does not support HTML.
+     * @param original
+     * @return 
+     */
+    private String translateText(String original) {
+        if (!original.startsWith("<html>")) { // NOI18N
+            return original;
+        }
+        String res = 
+                original.replaceAll("<p/>|</p>|<br>", "\n"). // NOI18N
+                    replaceAll( "<[^>]*>", "" ). // NOI18N 
+                    replaceAll( "&nbsp;", " " ); // NOI18N 
+        res = res.trim();
+        return res;
+    }
+    
+    public String getAccessibleDescription(Object o) {
+        if (!(o instanceof Accessible)) {
+            return null;
+        }
+        AccessibleContext ac = ((Accessible)o).getAccessibleContext();
+        String s = ac.getAccessibleDescription();
+        if (s != null && !"N/A".equals(s)) {
+            return s;
+        }
+        return ac.getAccessibleName();
+    }
+    
+    public ShowMessageRequestParams createShowMessageRequest() {
+        if (this.request != null) {
+            return request;
+        }
+        Object msg = descriptor.getMessage();
+        String displayText = null;
+        
+        if (msg instanceof String) {
+            displayText = msg.toString();
+        } else {
+            displayText = getAccessibleDescription(msg);
+        }
+        if (displayText == null) {
+            return null;
+        }
+        mapDescriptorOptions();
+        ShowMessageRequestParams request = new ShowMessageRequestParams();
+        request.setMessage(translateText(displayText));
+        request.setActions(getActionItems());
+        request.setType(translateMessageType());
+        return this.request = request;
+    }
+    
+    public Object actionToOption(MessageActionItem item) {
+        return item2Option.get(item);
+    }
+    
+    public List<MessageActionItem>  getActionItems() {
+        return new ArrayList<>(item2Option.keySet());
+    }
+    
+    private void addMessageItem(MessageActionItem item, Object option) {
+        item2Option.put(item, option);
+        text2Option.put(item.getTitle(), option);
+    }
+    
+    @NbBundle.Messages({
+        "OPTION_Yes=Yes",
+        "OPTION_No=No",
+        "OPTION_OK=OK",
+        "OPTION_Cancel=Cancel",
+    })
+    private String mapDescriptorOption(Object option) {
+        if (option == NotifyDescriptor.CANCEL_OPTION) {
+            return Bundle.OPTION_Cancel();
+        } else if (option == NotifyDescriptor.NO_OPTION) {
+            return Bundle.OPTION_No();
+        } else if (option == NotifyDescriptor.YES_OPTION) {
+            return Bundle.OPTION_Yes();
+        } else if (option == NotifyDescriptor.OK_OPTION) {
+            return Bundle.OPTION_OK();
+        }
+        if (option instanceof Component) {
+            return null;
+        }
+        if (option != null) {
+            return option.toString();
+        }
+        return null;
+    }
+    
+    private void mapDescriptorOptions() {
+        Object[] options = descriptor.getOptions();
+        if (options == null) {
+            switch (descriptor.getOptionType()) {
+                case NotifyDescriptor.DEFAULT_OPTION:
+                case NotifyDescriptor.OK_CANCEL_OPTION:
+                    options = OK_CANCEL; break;
+                case NotifyDescriptor.YES_NO_CANCEL_OPTION:
+                    options = YES_NO_CANCEL; break;
+                case NotifyDescriptor.YES_NO_OPTION:
+                    options = YES_NO; break;
+                default:
+                    options = JUST_OK; 
+                    break;
+            }
+        }
+        for (Object o : options) {
+            String text;
+            
+            if (o instanceof JButton) {
+                text = addButtonItem((JButton)o);
+            } else if (o instanceof Icon) {
+                text = addIconItem((Icon)o);
+            } else {
+                text = mapDescriptorOption(o);
+            }
+            if (text != null) {
+                addMessageItem(new MessageActionItem(text), o);
+            } else {
+                reportUnknownOption(o);
+            }
+        }
+    }
+    
+    private void reportUnknownOption(Object o) {
+        Throwable t = new Throwable();
+        StackTraceElement[] stack = t.getStackTrace();
+        if (stack.length >= 7) {
+        String callerClass = stack[6].getClassName();
+            synchronized (warnedClasses) {
+                if (!warnedClasses.add(callerClass)) {
+                    return;
+                }
+            }
+        }
+        LOG.log(Level.WARNING, new Throwable(), 
+                () -> "Unhandled option " + o + " for descriptor: " + descriptor);
+    }
+    
+    private String addButtonItem(JButton button) {
+        if (!button.isVisible()) {
+            return null;
+        }
+        String t = button.getText();
+        List<ActionListener> ll = Arrays.asList(button.getActionListeners());
+        if (!ll.isEmpty()) {
+            optionListeners.put(button, ll);
+        }
+        return t;
+    }
+    
+    private String addIconItem(Icon icon) {
+        if (icon instanceof AccessibleIcon) {
+            return ((AccessibleIcon)icon).getAccessibleIconDescription();
+        } else {
+            return null;
+        }
+    }
+    
+    public Object clientNotify() {
+        try {
+            return clientNotifyLater().get();
+        } catch (InterruptedException | ExecutionException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+    
+    public CompletableFuture<Object> clientNotifyLater() {
+        ShowMessageRequestParams params = createShowMessageRequest();
+        if (params == null) {
+            CompletableFuture<Object> x = new CompletableFuture<>();
+            x.complete(NotifyDescriptor.CLOSED_OPTION);
+            return x;
+        }
+        CompletableFuture<MessageActionItem> resultItem =  client.showMessageRequest(request);
+        return resultItem /*.exceptionally(this::handleClientException) */.thenApply(this::processActivatedOption);
+    }
+    
+    MessageActionItem handleClientException(Throwable t) {
+        // TBD
+        return null;
+    }
+    
+    Object processActivatedOption(MessageActionItem item) {
+        Object option = selectActivatedOption(item);
+        List<ActionListener> ll = optionListeners.get(option);
+        if (ll != null) {
+            ActionEvent e = new ActionEvent(option, ActionEvent.ACTION_PERFORMED, item.getTitle());
+            for (ActionListener l : ll) {
+                try {
+                    l.actionPerformed(e);
+                } catch (RuntimeException ex) {
+                    LOG.log(Level.SEVERE, "Error occurred during actionListener dispatch", ex);
+                }
+            }
+        }
+        return option;
+    }
+    
+    Object selectActivatedOption(MessageActionItem item) {
+        if (item == null) {
+            return NotifyDescriptor.CLOSED_OPTION;
+        }
+        Object option = item2Option.get(item);
+        if (option == null) {
+            option = text2Option.get(item.getTitle());
+        }
+        if (option == null) {
+            LOG.log(Level.WARNING, "Unknown client response received: {0}, the valid options were: {1}", new Object[] {
+                item.getTitle(), 
+                item2Option.keySet().stream().map(MessageActionItem::getTitle).collect(Collectors.toList())
+            });
+            return NotifyDescriptor.CLOSED_OPTION;
+        }
+        
+        return option;
+    }
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/UIContext.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/UIContext.java
index 781d04e..d820f4e 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/UIContext.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ui/UIContext.java
@@ -20,17 +20,37 @@ package org.netbeans.modules.java.lsp.server.ui;
 
 import java.lang.ref.Reference;
 import java.lang.ref.WeakReference;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.MessageActionItem;
 import org.eclipse.lsp4j.MessageParams;
+import org.eclipse.lsp4j.ShowMessageRequestParams;
+import org.netbeans.api.annotations.common.NonNull;
 import org.netbeans.modules.java.lsp.server.protocol.ShowStatusMessageParams;
 import org.openide.awt.StatusDisplayer.Message;
 import org.openide.util.Lookup;
 
 public abstract class UIContext {
     private static Reference<UIContext> lastCtx = new WeakReference<>(null);
-
-    public static synchronized UIContext find() {
-        UIContext ctx = Lookup.getDefault().lookup(UIContext.class);
+    
+    /**
+     * Allows to pass Lookup as a context to locate UIContext implementation; can be useful for tests. If not found
+     * in the context `lkp', will be searched in the default Lookup (if lkp is not the default one).
+     * @param lkp context lookup
+     * @return UIContext.
+     */
+    @NonNull
+    public static synchronized UIContext find(Lookup lkp) {
+        UIContext ctx = lkp.lookup(UIContext.class);
+        if (ctx != null) {
+            return ctx;
+        }
+        Lookup def = Lookup.getDefault();
+        if (lkp != def) {
+            ctx = def.lookup(UIContext.class);
+        }
         if (ctx == null) {
+            // PENDING: better context transfer between threads is needed; this way the UIContext can remote to a bad
+            // LSP client window
             ctx = lastCtx.get();
             if (ctx != null && !ctx.isValid()) {
                 lastCtx.clear();
@@ -49,8 +69,14 @@ public abstract class UIContext {
         return ctx;
     }
 
+    @NonNull
+    public static synchronized UIContext find() {
+        return find(Lookup.getDefault());
+    }
+
     protected abstract boolean isValid();
     protected abstract void showMessage(MessageParams msg);
+    protected abstract CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams msg);
     protected abstract void logMessage(MessageParams msg);
     protected abstract Message showStatusMessage(ShowStatusMessageParams msg);
 
@@ -62,6 +88,13 @@ public abstract class UIContext {
         }
 
         @Override
+        protected CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams msg) {
+            System.err.println(msg.getType() + ": " + msg.getMessage());
+            CompletableFuture<MessageActionItem> ai = CompletableFuture.completedFuture(null);
+            return ai;
+        }
+
+        @Override
         protected void showMessage(MessageParams msg) {
             System.err.println(msg.getType() + ": " + msg.getMessage());
         }
diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/ui/AbstractDialogDisplayerTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/ui/AbstractDialogDisplayerTest.java
new file mode 100644
index 0000000..9d9ec81
--- /dev/null
+++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/ui/AbstractDialogDisplayerTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import javax.swing.JPanel;
+import static junit.framework.TestCase.fail;
+import org.eclipse.lsp4j.MessageActionItem;
+import org.eclipse.lsp4j.MessageParams;
+import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.ShowMessageRequestParams;
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.modules.java.lsp.server.protocol.ShowStatusMessageParams;
+import org.openide.NotifyDescriptor;
+import org.openide.awt.StatusDisplayer;
+
+/**
+ *
+ * @author sdedic
+ */
+public class AbstractDialogDisplayerTest extends NbTestCase {
+
+    public AbstractDialogDisplayerTest(String name) {
+        super(name);
+    }
+    
+    private static class MockUIContext extends UIContext {
+        @Override
+        protected boolean isValid() {
+            return true;
+        }
+
+        @Override
+        protected void showMessage(MessageParams msg) {
+        }
+
+        @Override
+        protected CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams msg) {
+            return CompletableFuture.completedFuture(null);
+        }
+
+        @Override
+        protected void logMessage(MessageParams msg) {
+        }
+
+        @Override
+        protected StatusDisplayer.Message showStatusMessage(ShowStatusMessageParams msg) {
+            return null;
+        }
+    }
+    
+    /**
+     * Checks that component-based dialogs will just return CLOSED.
+     * @throws Exception 
+     */
+    public void testUnsupportedDialogWithPanel() throws Exception {
+        MockUIContext client = new MockUIContext() {
+            @Override
+            public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams requestParams) {
+                fail();
+                return null;
+            }
+
+            @Override
+            public void showMessage(MessageParams messageParams) {
+                fail();
+            }
+        };
+        
+        NotifyDescriptor nd = new NotifyDescriptor(new JPanel(), "Unused", 
+                NotifyDescriptor.OK_CANCEL_OPTION, 
+                NotifyDescriptor.WARNING_MESSAGE,
+                null, null);
+        
+        NotifyDescriptorAdapter adapter = new NotifyDescriptorAdapter(nd, client);
+        assertSame(NotifyDescriptor.CLOSED_OPTION, adapter.clientNotify());
+    }
+    
+    private static final Map<Integer, MessageType>  MESSAGE_TYPES = new HashMap<>();
+    
+    {
+        MESSAGE_TYPES.put(NotifyDescriptor.PLAIN_MESSAGE, MessageType.Info);
+        MESSAGE_TYPES.put(NotifyDescriptor.QUESTION_MESSAGE, MessageType.Info);
+        MESSAGE_TYPES.put(NotifyDescriptor.INFORMATION_MESSAGE, MessageType.Info);
+        MESSAGE_TYPES.put(NotifyDescriptor.WARNING_MESSAGE, MessageType.Warning);
+        MESSAGE_TYPES.put(NotifyDescriptor.ERROR_MESSAGE, MessageType.Error);
+    }
+    
+    /**
+     * Checks that showMessage receives an appropriate message type, for different
+     * ND's {@code messageTy[e} value/
+     * @throws Exception 
+     */
+    public void testCheckMessageTypes() throws Exception {
+        for (int i : MESSAGE_TYPES.keySet()) {
+            MockUIContext cl = new MockUIContext() {
+                MessageType check = MESSAGE_TYPES.get(i);
+                
+                public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams requestParams) {
+                    assertEquals(check, requestParams.getType());
+                    CompletableFuture<MessageActionItem> x = new CompletableFuture<>();
+                    x.complete(null);
+                    return x;
+                }
+            };
+            NotifyDescriptor nd = new NotifyDescriptor("Hello, LSP client", "Unused", 
+                    NotifyDescriptor.OK_CANCEL_OPTION, 
+                    i,
+                    null, null);
+            NotifyDescriptorAdapter adapter = new NotifyDescriptorAdapter(nd, cl);
+            adapter.clientNotify();
+        }
+    }
+    
+    /**
+     * Checks that yes-no-cancel items will be presented at the client
+     */
+    public void testYesNoCancelItems() {
+        MockUIContext cl = new MockUIContext() { 
+            public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams requestParams) {
+                assertEquals(3, requestParams.getActions().size());
+                assertEquals("Yes", requestParams.getActions().get(0).getTitle());
+                assertEquals("No", requestParams.getActions().get(1).getTitle());
+                assertEquals("Cancel", requestParams.getActions().get(2).getTitle());
+
+                CompletableFuture<MessageActionItem> x = new CompletableFuture<>();
+                x.complete(requestParams.getActions().get(0));
+                return x;
+            }
+        };
+        NotifyDescriptor nd = new NotifyDescriptor("Hello, LSP client", "Unused", 
+                NotifyDescriptor.YES_NO_CANCEL_OPTION, 
+                NotifyDescriptor.QUESTION_MESSAGE,
+                null, null);
+        NotifyDescriptorAdapter adapter = new NotifyDescriptorAdapter(nd, cl);
+        assertEquals(NotifyDescriptor.YES_OPTION, adapter.clientNotify());
+    }
+}


---------------------------------------------------------------------
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