You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by en...@apache.org on 2021/07/02 08:44:43 UTC

[netbeans] branch master updated: Completion provider for VS Code's launch.json.

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

entl 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 2ae14c0  Completion provider for VS Code's launch.json.
2ae14c0 is described below

commit 2ae14c0fb2e09816cb2fb9c1ef9aa18b98d3f446
Author: Martin Entlicher <ma...@oracle.com>
AuthorDate: Tue Jun 29 20:30:45 2021 +0200

    Completion provider for VS Code's launch.json.
---
 .../netbeans/modules/java/lsp/server/Utils.java    |  55 ++++++++
 .../attach/AttachConfigurationCompletion.java      | 141 +++++++++++++++++++
 .../debugging/attach/AttachConfigurations.java     | 119 +++++++---------
 .../debugging/attach/ConfigurationAttribute.java   |  50 +++++++
 .../debugging/attach/ConfigurationAttributes.java  | 141 +++++++++++++++++++
 .../debugging/attach/NbAttachRequestHandler.java   | 107 +++++----------
 .../protocol/LaunchConfigurationCompletion.java    |  68 ++++++++++
 .../protocol/ProjectConfigurationCompletion.java   | 137 +++++++++++++++++++
 .../modules/java/lsp/server/protocol/Server.java   |   5 +
 .../lsp/server/protocol/WorkspaceServiceImpl.java  |  61 +++++++++
 .../modules/java/lsp/server/UtilsTest.java         |  53 ++++++++
 java/java.lsp.server/vscode/package-lock.json      |   5 +
 java/java.lsp.server/vscode/package.json           |   1 +
 java/java.lsp.server/vscode/src/extension.ts       |  22 +--
 .../vscode/src/launchConfigurations.ts             | 150 +++++++++++++++++++++
 15 files changed, 962 insertions(+), 153 deletions(-)

diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
index 849c26f..248f642 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
@@ -18,6 +18,7 @@
  */
 package org.netbeans.modules.java.lsp.server;
 
+import com.google.gson.stream.JsonWriter;
 import com.sun.source.tree.CompilationUnitTree;
 import com.sun.source.tree.LineMap;
 import com.sun.source.tree.Tree;
@@ -27,6 +28,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.StringWriter;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -223,4 +225,57 @@ public class Utils {
     private static File getCacheDir() {
         return Places.getCacheSubfile("java-server");
     }
+
+    private static final char[] SNIPPET_ESCAPE_CHARS = new char[] { '\\', '$', '}' };
+    /**
+     * Escape special characters in a completion snippet. Characters '$' and '}'
+     * are escaped via backslash.
+     */
+    public static String escapeCompletionSnippetSpecialChars(String text) {
+        if (text.isEmpty()) {
+            return text;
+        }
+        for (char c : SNIPPET_ESCAPE_CHARS) {
+            StringBuilder replaced = null;
+            int lastPos = 0;
+            int i = 0;
+            while ((i = text.indexOf(c, i)) >= 0) {
+                if (replaced == null) {
+                    replaced = new StringBuilder(text.length() + 5); // Text length + some escapes
+                }
+                replaced.append(text.substring(lastPos, i));
+                replaced.append('\\');
+                lastPos = i;
+                i += 1;
+            }
+            if (replaced != null) {
+                replaced.append(text.substring(lastPos, text.length()));
+                text = replaced.toString();
+            }
+            replaced = null;
+        }
+        return text;
+    }
+
+    /**
+     * Encode a String value to a valid JSON value. Enclose into quotes explicitly when needed.
+     */
+    public static String encode2JSON(String value) {
+        if (value.isEmpty()) {
+            return value;
+        }
+        StringWriter sw = new StringWriter();
+        try (JsonWriter w = new JsonWriter(sw)) {
+            w.beginArray();
+            w.value(value);
+            w.endArray();
+            w.flush();
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        String encoded = sw.toString();
+        // We have ["value"], remove the array and quotes
+        return encoded.substring(2, encoded.length() - 2);
+    }
+
 }
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/AttachConfigurationCompletion.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/AttachConfigurationCompletion.java
new file mode 100644
index 0000000..e17c152
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/AttachConfigurationCompletion.java
@@ -0,0 +1,141 @@
+/*
+ * 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.debugging.attach;
+
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.eclipse.lsp4j.CompletionItem;
+import org.eclipse.lsp4j.InsertTextFormat;
+import org.netbeans.api.project.Project;
+import org.netbeans.modules.java.lsp.server.Utils;
+import org.netbeans.modules.java.lsp.server.protocol.LaunchConfigurationCompletion;
+import org.openide.util.Exceptions;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ * Completion of debugger attach configurations.
+ *
+ * @author Martin Entlicher
+ */
+@ServiceProvider(service = LaunchConfigurationCompletion.class, position = 200)
+public class AttachConfigurationCompletion implements LaunchConfigurationCompletion {
+
+    @Override
+    public CompletableFuture<List<CompletionItem>> configurations(Supplier<CompletableFuture<Project>> projectSupplier) {
+        return CompletableFuture.supplyAsync(() -> {
+            return createCompletion(AttachConfigurations.get());
+        }, AttachConfigurations.RP);
+    }
+
+    @Override
+    public CompletableFuture<List<CompletionItem>> attributes(Supplier<CompletableFuture<Project>> projectSupplier, Map<String, Object> currentAttributes) {
+        return CompletableFuture.supplyAsync(() -> {
+            return createAttributesCompletion(AttachConfigurations.get(), currentAttributes);
+        }, AttachConfigurations.RP);
+    }
+
+    @Override
+    public CompletableFuture<List<CompletionItem>> attributeValues(Supplier<CompletableFuture<Project>> projectSupplier, Map<String, Object> currentAttributes, String attribute) {
+        return CompletableFuture.completedFuture(Collections.emptyList());
+    }
+
+    private static List<CompletionItem> createCompletion(AttachConfigurations attachConfigurations) {
+        return attachConfigurations.getConfigurations().stream().map(configAttrs -> createCompletion(configAttrs)).collect(Collectors.toList());
+    }
+
+    private static CompletionItem createCompletion(ConfigurationAttributes configAttrs) {
+        CompletionItem ci = new CompletionItem("Java 8+: " + configAttrs.getName());    // NOI18N
+        StringWriter sw = new StringWriter();
+        try (JsonWriter w = new JsonWriter(sw)) {
+            w.setIndent("\t");                                              // NOI18N
+            w.beginObject();
+            w.name("name").jsonValue("\"${1:" + Utils.escapeCompletionSnippetSpecialChars(Utils.encode2JSON(configAttrs.getName())) + "}\""); // NOI18N
+            w.name("type").value(AttachConfigurations.CONFIG_TYPE);         // NOI18N
+            w.name("request").value(AttachConfigurations.CONFIG_REQUEST);   // NOI18N
+            int locationIndex = 2;
+            for (Map.Entry<String, ConfigurationAttribute> entry : configAttrs.getAttributes().entrySet()) {
+                ConfigurationAttribute ca = entry.getValue();
+                if (ca.isMustSpecify()) {
+                    String value = ca.getDefaultValue();
+                    if (value.startsWith("${command:")) { // Do not suggest to customize values provided by commands    // NOI18N
+                        value = Utils.escapeCompletionSnippetSpecialChars(Utils.encode2JSON(value));
+                    } else {
+                        value = "${" + (locationIndex++) + (value.isEmpty() ? "}" : ":" + Utils.escapeCompletionSnippetSpecialChars(Utils.encode2JSON(value)) + "}"); // NOI18N
+                    }
+                    // We have pre-encoded the value in order not to encode the completion snippet escape characters
+                    w.name(entry.getKey()).jsonValue("\"" + value + "\"");
+                }
+            }
+            w.endObject();
+            w.flush();
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        ci.setInsertText(sw.toString());
+        ci.setInsertTextFormat(InsertTextFormat.Snippet);
+        ci.setDocumentation(configAttrs.getDescription());
+        return ci;
+    }
+
+    private static List<CompletionItem> createAttributesCompletion(AttachConfigurations attachConfigurations, Map<String, Object> currentAttributes) {
+        List<CompletionItem> completionItems = null;
+        ConfigurationAttributes currentConfiguration = attachConfigurations.findConfiguration(currentAttributes);
+        if (currentConfiguration != null) {
+            Map<String, ConfigurationAttribute> attributes = currentConfiguration.getAttributes();
+            for (Map.Entry<String, ConfigurationAttribute> entry : attributes.entrySet()) {
+                String attrName = entry.getKey();
+                if (!currentAttributes.containsKey(attrName)) {
+                    StringWriter sw = new StringWriter();
+                    try (JsonWriter w = new JsonWriter(sw)) {
+                        w.beginObject();
+                        w.name(attrName).value(entry.getValue().getDefaultValue());
+                        w.endObject();
+                        w.flush();
+                    } catch (IOException ex) {
+                        Exceptions.printStackTrace(ex);
+                    }
+                    CompletionItem ci = new CompletionItem(attrName);
+                    String text = sw.toString();
+                    text = text.substring(1, text.length() - 1); // Remove { and }
+                    ci.setInsertText(text);
+                    ci.setDocumentation(entry.getValue().getDescription());
+                    if (completionItems == null) {
+                        completionItems = new ArrayList<>(3);
+                    }
+                    completionItems.add(ci);
+                }
+            }
+        }
+        if (completionItems != null) {
+            return completionItems;
+        } else {
+            return Collections.emptyList();
+        }
+    }
+
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/AttachConfigurations.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/AttachConfigurations.java
index 99951e7..b36b966 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/AttachConfigurations.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/AttachConfigurations.java
@@ -19,20 +19,18 @@
 package org.netbeans.modules.java.lsp.server.debugging.attach;
 
 import com.sun.jdi.Bootstrap;
-import com.sun.jdi.VirtualMachineManager;
 import com.sun.jdi.connect.AttachingConnector;
-import com.sun.jdi.connect.Connector;
 import com.sun.tools.attach.AttachNotSupportedException;
 import com.sun.tools.attach.VirtualMachine;
 import com.sun.tools.attach.VirtualMachineDescriptor;
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 
 import org.eclipse.lsp4j.MessageParams;
@@ -43,7 +41,6 @@ import org.netbeans.modules.java.lsp.server.debugging.utils.ErrorUtilities;
 import org.netbeans.modules.java.lsp.server.protocol.DebugConnector;
 import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
 import org.netbeans.modules.java.lsp.server.protocol.QuickPickItem;
-import org.netbeans.modules.java.lsp.server.protocol.Server;
 import org.netbeans.modules.java.lsp.server.protocol.ShowQuickPickParams;
 import org.openide.util.NbBundle.Messages;
 import org.openide.util.RequestProcessor;
@@ -55,95 +52,71 @@ import org.openide.util.RequestProcessor;
  */
 public final class AttachConfigurations {
 
-    static final String NAME_ATTACH_PROCESS = "Attach to Process";          // NOI18N
-    static final String NAME_ATTACH_SOCKET = "Attach to Port";              // NOI18N
-    static final String NAME_ATTACH_SHMEM = "Attach to Shared Memory";      // NOI18N
-    static final String NAME_ATTACH_BY = "Attach by ";                      // NOI18N
+    static final String CONFIG_TYPE = "java8+";     // NOI18N
+    static final String CONFIG_REQUEST = "attach";  // NOI18N
 
-    static final String CONNECTOR_PROCESS = "com.sun.jdi.ProcessAttach";    // NOI18N
-    static final String CONNECTOR_SOCKET = "com.sun.jdi.SocketAttach";      // NOI18N
-    static final String CONNECTOR_SHMEM = "com.sun.jdi.SharedMemoryAttach"; // NOI18N
+    static final RequestProcessor RP = new RequestProcessor(AttachConfigurations.class);
 
-    static final String PROCESS_ARG_PID = "processId";          // NOI18N
-    static final String SOCKET_ARG_HOST = "hostName";           // NOI18N
-    static final String SOCKET_ARG_PORT = "port";               // NOI18N
-    static final String SHMEM_ARG_NAME = "sharedMemoryName";    // NOI18N
+    private final List<ConfigurationAttributes> configurations;
 
-    private static final RequestProcessor RP = new RequestProcessor(AttachConfigurations.class);
+    private AttachConfigurations(List<AttachingConnector> attachingConnectors) {
+        List<ConfigurationAttributes> configs = new ArrayList<>(5);
+        for (AttachingConnector ac : attachingConnectors) {
+            configs.add(new ConfigurationAttributes(ac));
+        }
+        this.configurations = Collections.unmodifiableList(configs);
+    }
 
-    private AttachConfigurations() {}
+    public static AttachConfigurations get() {
+        return new AttachConfigurations(Bootstrap.virtualMachineManager().attachingConnectors());
+    }
 
     public static CompletableFuture<Object> findConnectors() {
         return CompletableFuture.supplyAsync(() -> {
-            return listAttachingConnectors();
+            return get().listAttachingConnectors();
         }, RP);
     }
 
-    @Messages({"DESC_HostName=Name of host machine to connect to", "DESC_Port=Port number to connect to",
-               "DESC_ShMem=Shared memory transport address at which the target VM is listening"})
-    private static List<DebugConnector> listAttachingConnectors() {
-        VirtualMachineManager vmm = Bootstrap.virtualMachineManager ();
-        List<AttachingConnector> attachingConnectors = vmm.attachingConnectors();
-        List<DebugConnector> connectors = new ArrayList<>(5);
-        String type = "java8+";             // NOI18N
-        for (AttachingConnector ac : attachingConnectors) {
-            String connectorName = ac.name();
-            Map<String, Connector.Argument> defaultArguments = ac.defaultArguments();
-            DebugConnector connector;
-            switch (connectorName) {
-                case CONNECTOR_PROCESS:
-                    connector = new DebugConnector(connectorName, NAME_ATTACH_PROCESS, type,
-                            Collections.singletonList(PROCESS_ARG_PID),
-                            Collections.singletonList("${command:" + Server.JAVA_FIND_DEBUG_PROCESS_TO_ATTACH + "}"),   // NOI18N
-                            Collections.singletonList(""));
-                    break;
-                case CONNECTOR_SOCKET: {
-                    String hostName = getArgumentOrDefault(defaultArguments.get("hostname"), "localhost");          // NOI18N
-                    String port = getArgumentOrDefault(defaultArguments.get("port"), "8000"); // NOI18N
-                    connector = new DebugConnector(connectorName, NAME_ATTACH_SOCKET, type,
-                            Arrays.asList(SOCKET_ARG_HOST, SOCKET_ARG_PORT),
-                            Arrays.asList(hostName, port),
-                            Arrays.asList(Bundle.DESC_HostName(), Bundle.DESC_Port()));
-                    break;
-                }
-                case CONNECTOR_SHMEM: {
-                    String name = getArgumentOrDefault(defaultArguments.get("name"), "");       // NOI18N
-                    connector = new DebugConnector(connectorName, NAME_ATTACH_SHMEM, type,
-                            Collections.singletonList(SHMEM_ARG_NAME),
-                            Collections.singletonList(name),
-                            Collections.singletonList(Bundle.DESC_ShMem()));
-                    break;
-                }
-                default: {
-                    List<String> names = new ArrayList<>();
-                    List<String> values = new ArrayList<>();
-                    List<String> descriptions = new ArrayList<>();
-                    for (Connector.Argument arg : defaultArguments.values()) {
-                        if (arg.mustSpecify()) {
-                            names.add(arg.name());
-                            String value = arg.value();
-                            values.add(value);
-                            descriptions.add(arg.description());
-                        }
-                    }
-                    connector = new DebugConnector(connectorName, NAME_ATTACH_BY + connectorName, type,
-                            names, values, descriptions);
+    List<ConfigurationAttributes> getConfigurations() {
+        return configurations;
+    }
+
+    private List<DebugConnector> listAttachingConnectors() {
+        List<DebugConnector> connectors = new ArrayList<>(configurations.size());
+        for (ConfigurationAttributes configAttributes : configurations) {
+            Map<String, ConfigurationAttribute> attributesMap = configAttributes.getAttributes();
+            List<String> names = new ArrayList<>(2);
+            List<String> values = new ArrayList<>(2);
+            List<String> descriptions = new ArrayList<>(2);
+            for (Map.Entry<String, ConfigurationAttribute> entry : attributesMap.entrySet()) {
+                ConfigurationAttribute ca = entry.getValue();
+                if (ca.isMustSpecify()) {
+                    names.add(entry.getKey());
+                    values.add(ca.getDefaultValue());
+                    descriptions.add(ca.getDescription());
                 }
             }
+            DebugConnector connector = new DebugConnector(configAttributes.getId(), configAttributes.getName(), CONFIG_TYPE,
+                            names, values, descriptions);
             connectors.add(connector);
         }
         connectors.sort((c1, c2) -> c1.getName().compareToIgnoreCase(c2.getName()));
         return connectors;
     }
 
-    private static String getArgumentOrDefault(Connector.Argument arg, String def) {
-        if (arg != null) {
-            String value = arg.value();
-            if (!value.isEmpty()) {
-                return value;
+    ConfigurationAttributes findConfiguration(Map<String, Object> attributes) {
+        if (!CONFIG_TYPE.equals(attributes.get("type")) ||              // NOI18N
+            !CONFIG_REQUEST.equals(attributes.get("request"))) {        // NOI18N
+
+            return null;
+        }
+        Set<String> names = attributes.keySet();
+        for (ConfigurationAttributes config : configurations) {
+            if (config.areMandatoryAttributesIn(names)) {
+                return config;
             }
         }
-        return def;
+        return null;
     }
 
     public static CompletableFuture<Object> findProcessAttachTo(NbCodeLanguageClient client) {
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/ConfigurationAttribute.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/ConfigurationAttribute.java
new file mode 100644
index 0000000..f4686c2
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/ConfigurationAttribute.java
@@ -0,0 +1,50 @@
+/*
+ * 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.debugging.attach;
+
+/**
+ * Representation of a single attribute of attach configuration.
+ *
+ * @author Martin Entlicher
+ */
+final class ConfigurationAttribute {
+
+    private final String defaultValue;
+    private final String description;
+    private final boolean mustSpecify;
+
+    public ConfigurationAttribute(String defaultValue, String description, boolean mustSpecify) {
+        this.defaultValue = defaultValue;
+        this.description = description;
+        this.mustSpecify = mustSpecify;
+    }
+
+    public String getDefaultValue() {
+        return defaultValue;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public boolean isMustSpecify() {
+        return mustSpecify;
+    }
+
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/ConfigurationAttributes.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/ConfigurationAttributes.java
new file mode 100644
index 0000000..82d4083
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/ConfigurationAttributes.java
@@ -0,0 +1,141 @@
+/*
+ * 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.debugging.attach;
+
+import com.sun.jdi.connect.AttachingConnector;
+import com.sun.jdi.connect.Connector;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.netbeans.modules.java.lsp.server.protocol.Server;
+import org.openide.util.NbBundle;
+
+/**
+ * Attributes of an attach configuration. Based on {@link AttachingConnector},
+ * we translate names and attributes of well known connectors for usability reasons.
+ *
+ * @author Martin Entlicher
+ */
+final class ConfigurationAttributes {
+
+    private static final String CONNECTOR_PROCESS = "com.sun.jdi.ProcessAttach";    // NOI18N
+    private static final String CONNECTOR_SOCKET = "com.sun.jdi.SocketAttach";      // NOI18N
+    private static final String CONNECTOR_SHMEM = "com.sun.jdi.SharedMemoryAttach"; // NOI18N
+
+    static final String PROCESS_ARG_PID = "processId";          // NOI18N
+    static final String SOCKET_ARG_HOST = "hostName";           // NOI18N
+    static final String SOCKET_ARG_PORT = "port";               // NOI18N
+    static final String SHMEM_ARG_NAME = "sharedMemoryName";    // NOI18N
+
+    private final AttachingConnector ac;
+    private final String id;
+    private final String name;
+    private final String description;
+    private final Map<String, ConfigurationAttribute> attributes = new LinkedHashMap<>();
+
+    @NbBundle.Messages({"LBL_AttachToProcess=Attach to Process",
+                        "LBL_AttachToPort=Attach to Port",
+                        "LBL_AttachToShmem=Attach to Shared Memory",
+                        "# {0} - connector name", "LBL_AttachBy=Attach by {0}",
+                        "DESC_Process=Process Id of the debuggee",
+                        "DESC_HostName=Name or IP address of the host machine to connect to",
+                        "DESC_Port=Port number to connect to",
+                        "DESC_ShMem=Shared memory transport address at which the target VM is listening"})
+    ConfigurationAttributes(AttachingConnector ac) {
+        this.ac = ac;
+        String connectorName = ac.name();
+        this.id = connectorName;
+        this.description = ac.description();
+        Map<String, Connector.Argument> defaultArguments = ac.defaultArguments();
+        switch (connectorName) {
+            case CONNECTOR_PROCESS:
+                this.name = Bundle.LBL_AttachToProcess();
+                attributes.put(PROCESS_ARG_PID, new ConfigurationAttribute("${command:" + Server.JAVA_FIND_DEBUG_PROCESS_TO_ATTACH + "}", "", true)); // NOI18N
+                break;
+            case CONNECTOR_SOCKET:
+                this.name = Bundle.LBL_AttachToPort();
+                String hostName = getArgumentOrDefault(defaultArguments.get("hostname"), "localhost"); // NOI18N
+                String port = getArgumentOrDefault(defaultArguments.get("port"), "8000"); // NOI18N
+                attributes.put(SOCKET_ARG_HOST, new ConfigurationAttribute(hostName, Bundle.DESC_HostName(), true));
+                attributes.put(SOCKET_ARG_PORT, new ConfigurationAttribute(port, Bundle.DESC_Port(), true));
+                break;
+            case CONNECTOR_SHMEM:
+                this.name = Bundle.LBL_AttachToShmem();
+                String shmName = getArgumentOrDefault(defaultArguments.get("name"), ""); // NOI18N
+                attributes.put(SHMEM_ARG_NAME, new ConfigurationAttribute(shmName, Bundle.DESC_ShMem(), true));
+                break;
+            default:
+                this.name = Bundle.LBL_AttachBy(connectorName);
+                for (Connector.Argument arg : defaultArguments.values()) {
+                    if (arg.mustSpecify()) {
+                        attributes.put(arg.name(), new ConfigurationAttribute(arg.value(), arg.description(), true));
+                    }
+                }
+        }
+        for (Connector.Argument arg : defaultArguments.values()) {
+            if (!arg.mustSpecify()) {
+                attributes.put(arg.name(), new ConfigurationAttribute(arg.value(), arg.description(), false));
+            }
+        }
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public AttachingConnector getConnector() {
+        return ac;
+    }
+
+    public Map<String, ConfigurationAttribute> getAttributes() {
+        return attributes;
+    }
+
+    private static String getArgumentOrDefault(Connector.Argument arg, String def) {
+        if (arg != null) {
+            String value = arg.value();
+            if (!value.isEmpty()) {
+                return value;
+            }
+        }
+        return def;
+    }
+
+    boolean areMandatoryAttributesIn(Set<String> names) {
+        for (Map.Entry<String, ConfigurationAttribute> entry : attributes.entrySet()) {
+            if (entry.getValue().isMustSpecify()) {
+                if (!names.contains(entry.getKey())) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/NbAttachRequestHandler.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/NbAttachRequestHandler.java
index 22e59a9..41b7691 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/NbAttachRequestHandler.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/NbAttachRequestHandler.java
@@ -33,6 +33,8 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.eclipse.lsp4j.MessageParams;
 import org.eclipse.lsp4j.MessageType;
@@ -63,8 +65,13 @@ public final class NbAttachRequestHandler {
     private static final String CONNECTOR_ARG_HOST = "hostname";    // NOI18N
     private static final String CONNECTOR_ARG_PORT = "port";        // NOI18N
     private static final String CONNECTOR_ARG_NAME = "name";        // NOI18N
-    // The default attributes of DebugConfiguration
-    private static final Set<String> CONFIG_ATTRIBUTES = new HashSet<>(Arrays.asList("type", "name", "request", "classPaths", "console"));   // NOI18N
+
+    private static final Map<String, String> ATTR_CONFIG_TO_CONNECTOR = Stream.of(new String[][] {
+        { ConfigurationAttributes.PROCESS_ARG_PID, CONNECTOR_ARG_PID },
+        { ConfigurationAttributes.SOCKET_ARG_HOST, CONNECTOR_ARG_HOST },
+        { ConfigurationAttributes.SOCKET_ARG_PORT, CONNECTOR_ARG_PORT },
+        { ConfigurationAttributes.SHMEM_ARG_NAME, CONNECTOR_ARG_NAME },
+    }).collect(Collectors.toMap(data -> data[0], data -> data[1]));
 
     private static final RequestProcessor RP = new RequestProcessor(AttachConfigurations.class);
 
@@ -87,84 +94,42 @@ public final class NbAttachRequestHandler {
 
     @Messages({"# {0} - connector name", "MSG_InvalidConnector=Invalid connector name: {0}"})
     private CompletableFuture<Void> attachToJVM(Map<String, Object> attachArguments, DebugAdapterContext context) {
-        String name = (String) attachArguments.get("name");     // NOI18N
-        AttachingDICookie attachingCookie;
-        String connectorName;
-        Map<String, String> translatedArguments = new HashMap<>();
         CompletableFuture<Void> resultFuture = new CompletableFuture<>();
-        switch (name) {
-            case AttachConfigurations.NAME_ATTACH_PROCESS:
-                Object pid = attachArguments.get(AttachConfigurations.PROCESS_ARG_PID);
-                connectorName = AttachConfigurations.CONNECTOR_PROCESS;
-                translatedArguments.put(AttachConfigurations.PROCESS_ARG_PID, CONNECTOR_ARG_PID);
-                break;
-            case AttachConfigurations.NAME_ATTACH_SOCKET:
-                connectorName = AttachConfigurations.CONNECTOR_SOCKET;
-                translatedArguments.put(AttachConfigurations.SOCKET_ARG_HOST, CONNECTOR_ARG_HOST);
-                translatedArguments.put(AttachConfigurations.SOCKET_ARG_PORT, CONNECTOR_ARG_PORT);
-                break;
-            case AttachConfigurations.NAME_ATTACH_SHMEM:
-                connectorName = AttachConfigurations.CONNECTOR_SHMEM;
-                translatedArguments.put(AttachConfigurations.SHMEM_ARG_NAME, CONNECTOR_ARG_NAME);
-                break;
-            default:
-                if (name.startsWith(AttachConfigurations.NAME_ATTACH_BY)) {
-                    connectorName = name.substring(AttachConfigurations.NAME_ATTACH_BY.length());
-                } else {
-                    ErrorUtilities.completeExceptionally(resultFuture,
-                            Bundle.MSG_InvalidConnector(name),
-                            ResponseErrorCode.serverErrorStart);
-                    connectorName = null;
-                }
-        }
-        if (connectorName != null) {
-            context.setDebugMode(true);
-            RP.post(() -> attachTo(connectorName, attachArguments, translatedArguments, context, resultFuture));
+        ConfigurationAttributes configurationAttributes = AttachConfigurations.get().findConfiguration(attachArguments);
+        if (configurationAttributes != null) {
+            AttachingConnector connector = configurationAttributes.getConnector();
+            RP.post(() -> attachTo(connector, attachArguments, context, resultFuture));
         } else {
-            assert resultFuture.isCompletedExceptionally();
+            context.setDebugMode(true);
+            String name = (String) attachArguments.get("name");     // NOI18N
+            ErrorUtilities.completeExceptionally(resultFuture,
+                    Bundle.MSG_InvalidConnector(name),
+                    ResponseErrorCode.serverErrorStart);
         }
         return resultFuture;
     }
 
-    @Messages({"# {0} - connector name", "# {1} - argument name", "MSG_ConnectorArgumentNotFound=Argument {0} of {1} was not found.",
-               "# {0} - argument name", "# {1} - value", "MSG_ConnectorInvalidValue=Invalid value of {0}: {1}",
-               "# {0} - connector name", "MSG_ConnectorNotFound=Connector {0} was not found."})
-    private void attachTo(String connectorName, Map<String, Object> arguments, Map<String, String> translatedArguments, DebugAdapterContext context, CompletableFuture<Void> resultFuture) {
-        VirtualMachineManager vmm = Bootstrap.virtualMachineManager ();
-        List<AttachingConnector> attachingConnectors = vmm.attachingConnectors();
-        for (AttachingConnector connector : attachingConnectors) {
-            if (connector.name().equals(connectorName)) {
-                Map<String, Argument> args = connector.defaultArguments();
-                for (String argName : arguments.keySet()) {
-                    if (CONFIG_ATTRIBUTES.contains(argName) || argName.startsWith("__")) {
-                        continue;
-                    }
-                    String argNameTranslated = translatedArguments.getOrDefault(argName, argName);
-                    Argument arg = args.get(argNameTranslated);
-                    if (arg == null) {
-                        ErrorUtilities.completeExceptionally(resultFuture,
-                            Bundle.MSG_ConnectorArgumentNotFound(connectorName, argNameTranslated),
-                            ResponseErrorCode.serverErrorStart);
-                        return ;
-                    }
-                    String value = arguments.get(argName).toString();
-                    if (!arg.isValid(value)) {
-                        ErrorUtilities.completeExceptionally(resultFuture,
-                            Bundle.MSG_ConnectorInvalidValue(argName, value),
-                            ResponseErrorCode.serverErrorStart);
-                        return ;
-                    }
-                    arg.setValue(value);
-                }
-                AttachingDICookie attachingCookie = AttachingDICookie.create(connector, args);
-                resultFuture.complete(null);
-                startAttaching(attachingCookie, context);
+    @Messages({"# {0} - argument name", "# {1} - value", "MSG_ConnectorInvalidValue=Invalid value of {0}: {1}"})
+    private void attachTo(AttachingConnector connector, Map<String, Object> arguments, DebugAdapterContext context, CompletableFuture<Void> resultFuture) {
+        Map<String, Argument> args = connector.defaultArguments();
+        for (String argName : arguments.keySet()) {
+            String argNameTranslated = ATTR_CONFIG_TO_CONNECTOR.getOrDefault(argName, argName);
+            Argument arg = args.get(argNameTranslated);
+            if (arg == null) {
+                continue;
+            }
+            String value = arguments.get(argName).toString();
+            if (!arg.isValid(value)) {
+                ErrorUtilities.completeExceptionally(resultFuture,
+                    Bundle.MSG_ConnectorInvalidValue(argName, value),
+                    ResponseErrorCode.serverErrorStart);
                 return ;
             }
+            arg.setValue(value);
         }
-        ErrorUtilities.completeExceptionally(resultFuture,
-                Bundle.MSG_ConnectorNotFound(connectorName),
-                ResponseErrorCode.serverErrorStart);
+        AttachingDICookie attachingCookie = AttachingDICookie.create(connector, args);
+        resultFuture.complete(null);
+        startAttaching(attachingCookie, context);
     }
 
     @Messages("MSG_FailedToAttach=Failed to attach.")
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LaunchConfigurationCompletion.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LaunchConfigurationCompletion.java
new file mode 100644
index 0000000..40ec50f
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LaunchConfigurationCompletion.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.protocol;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+
+import org.eclipse.lsp4j.CompletionItem;
+
+import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.api.project.Project;
+
+/**
+ * Provider of launch configurations completion. Run/Debug launch and Debugger
+ * attach configurations can be provided.
+ *
+ * @author Martin Entlicher
+ */
+public interface LaunchConfigurationCompletion {
+
+    /**
+     * Provide configurations of Run/Debug actions.
+     *
+     * @param projectSupplier Supplier of the relevant project
+     * @return a list of completion items, the list must not be <code>null</code>.
+     */
+    @NonNull
+    CompletableFuture<List<CompletionItem>> configurations(Supplier<CompletableFuture<Project>> projectSupplier);
+
+    /**
+     * Provide attributes to a specific configuration.
+     *
+     * @param projectSupplier Supplier of the relevant project
+     * @param attributes all attributes currently specified for the configuration
+     * @return a list of completion items, the list must not be <code>null</code>.
+     */
+    @NonNull
+    CompletableFuture<List<CompletionItem>> attributes(Supplier<CompletableFuture<Project>> projectSupplier, Map<String, Object> attributes);
+
+    /**
+     * Provide values of an attribute of a configuration.
+     *
+     * @param projectSupplier Supplier of the relevant project
+     * @param attributes all attributes currently specified for the configuration
+     * @param attributeName name of the attribute which values are to be provided
+     * @return a list of completion items, the list must not be <code>null</code>.
+     */
+    @NonNull
+    CompletableFuture<List<CompletionItem>> attributeValues(Supplier<CompletableFuture<Project>> projectSupplier, Map<String, Object> attributes, String attributeName);
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ProjectConfigurationCompletion.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ProjectConfigurationCompletion.java
new file mode 100644
index 0000000..5bd77c7
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/ProjectConfigurationCompletion.java
@@ -0,0 +1,137 @@
+/*
+ * 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.protocol;
+
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+
+import org.eclipse.lsp4j.CompletionItem;
+import org.netbeans.api.project.Project;
+import org.netbeans.modules.java.lsp.server.Utils;
+import org.netbeans.spi.project.ProjectConfiguration;
+import org.netbeans.spi.project.ProjectConfigurationProvider;
+import org.openide.util.Exceptions;
+import org.openide.util.NbBundle;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ * Completion of project configurations in launch.json.
+ *
+ * @author Martin Entlicher
+ */
+@ServiceProvider(service = LaunchConfigurationCompletion.class, position = 100)
+public class ProjectConfigurationCompletion implements LaunchConfigurationCompletion {
+
+    private static final String CONFIG_TYPE = "java8+";     // NOI18N
+
+    @Override
+    public CompletableFuture<List<CompletionItem>> configurations(Supplier<CompletableFuture<Project>> projectSupplier) {
+        return projectSupplier.get().thenApply(p -> createConfigurationsCompletion(p));
+    }
+
+    @Override
+    public CompletableFuture<List<CompletionItem>> attributes(Supplier<CompletableFuture<Project>> projectSupplier, Map<String, Object> currentAttributes) {
+        return CompletableFuture.completedFuture(Collections.emptyList());
+    }
+
+    @Override
+    public CompletableFuture<List<CompletionItem>> attributeValues(Supplier<CompletableFuture<Project>> projectSupplier, Map<String, Object> currentAttributes, String attribute) {
+        if ("launchConfiguration".equals(attribute)) {      // NOI18N
+            return projectSupplier.get().thenApply(p -> createLaunchConfigCompletion(p));
+        } else {
+            return CompletableFuture.completedFuture(Collections.emptyList());
+        }
+    }
+
+    @NbBundle.Messages({"# {0} - Configuration name", "LBL_LaunchJavaConfig=Launch Java: {0}",
+                        "# {0} - Configuration name", "LBL_LaunchJavaConfig_desc=Launch a Java 8+ application using {0}."})
+    private static List<CompletionItem> createConfigurationsCompletion(Project p) {
+        Collection<ProjectConfiguration> configurations = getConfigurations(p);
+        int size = configurations.size();
+        if (size <= 1) {
+            return Collections.emptyList();
+        }
+        List<CompletionItem> completionItems = new ArrayList<>(size - 1);
+        boolean skipFirst = true;
+        for (ProjectConfiguration c : configurations) {
+            if (skipFirst) {
+                skipFirst = false;
+                continue;
+            }
+            String configDisplayName = c.getDisplayName();
+            String launchName = Bundle.LBL_LaunchJavaConfig(configDisplayName);
+            CompletionItem ci = new CompletionItem("Java 8+: " + launchName);   // NOI18N
+            StringWriter sw = new StringWriter();
+            try (JsonWriter w = new JsonWriter(sw)) {
+                w.setIndent("\t");                                          // NOI18N
+                w.beginObject();
+                w.name("name").value(launchName);                           // NOI18N
+                w.name("type").value(CONFIG_TYPE);                          // NOI18N
+                w.name("request").value("launch");                          // NOI18N
+                w.name("mainClass").value("${file}");                       // NOI18N
+                w.name("launchConfiguration").value(configDisplayName);     // NOI18N
+                w.endObject();
+                w.flush();
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+            ci.setInsertText(sw.toString());
+            ci.setDocumentation(Bundle.LBL_LaunchJavaConfig_desc(configDisplayName));
+            completionItems.add(ci);
+        }
+        return completionItems;
+    }
+
+    private List<CompletionItem> createLaunchConfigCompletion(Project p) {
+        Collection<ProjectConfiguration> configurations = getConfigurations(p);
+        int size = configurations.size();
+        if (size <= 1) {
+            return Collections.emptyList();
+        }
+        List<CompletionItem> completionItems = new ArrayList<>(size - 1);
+        boolean skipFirst = true;
+        for (ProjectConfiguration c : configurations) {
+            if (skipFirst) {
+                skipFirst = false;
+                continue;
+            }
+            String configDisplayName = c.getDisplayName();
+            CompletionItem ci = new CompletionItem(configDisplayName);
+            ci.setInsertText("\"" + Utils.encode2JSON(configDisplayName) + "\"");
+            completionItems.add(ci);
+        }
+        return completionItems;
+    }
+
+    private static Collection<ProjectConfiguration> getConfigurations(Project p) {
+        ProjectConfigurationProvider<ProjectConfiguration> provider = p.getLookup().lookup(ProjectConfigurationProvider.class);
+        if (provider == null) {
+            return Collections.emptyList();
+        }
+        return provider.getConfigurations();
+    }
+}
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 9bbd50a..f92c682 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
@@ -623,6 +623,7 @@ public final class Server {
                         JAVA_LOAD_WORKSPACE_TESTS,
                         JAVA_NEW_FROM_TEMPLATE,
                         JAVA_NEW_PROJECT,
+                        JAVA_PROJECT_CONFIGURATION_COMPLETION,
                         JAVA_SUPER_IMPLEMENTATION));
                 for (CodeGenerator codeGenerator : Lookup.getDefault().lookupAll(CodeGenerator.class)) {
                     commands.addAll(codeGenerator.getCommands());
@@ -756,6 +757,10 @@ public final class Server {
      * Enumerates JVM processes eligible for debugger attach.
      */
     public static final String JAVA_FIND_DEBUG_PROCESS_TO_ATTACH = "java.attachDebugger.pickProcess";
+    /**
+     * Provides code-completion of configurations.
+     */
+    public static final String JAVA_PROJECT_CONFIGURATION_COMPLETION = "java.project.configuration.completion";
 
     static final String INDEXING_COMPLETED = "Indexing completed.";
     static final String NO_JAVA_SUPPORT = "Cannot initialize Java support on JDK ";
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java
index b42b9a5..aadb46c 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java
@@ -19,27 +19,33 @@
 package org.netbeans.modules.java.lsp.server.protocol;
 
 import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
 import com.sun.source.util.TreePath;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import javax.lang.model.element.Element;
 import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.TypeElement;
+import org.eclipse.lsp4j.CompletionItem;
 import org.eclipse.lsp4j.DidChangeConfigurationParams;
 import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
 import org.eclipse.lsp4j.ExecuteCommandParams;
@@ -238,6 +244,51 @@ public final class WorkspaceServiceImpl implements WorkspaceService, LanguageCli
             case Server.JAVA_FIND_DEBUG_PROCESS_TO_ATTACH: {
                 return AttachConfigurations.findProcessAttachTo(client);
             }
+            case Server.JAVA_PROJECT_CONFIGURATION_COMPLETION: {
+                // We expect one, two or three arguments.
+                // The first argument is always the URI of the launch.json file.
+                // When not more arguments are provided, all available configurations ought to be provided.
+                // When only a second argument is present, it's a map of the current attributes in a configuration,
+                // and additional attributes valid in that particular configuration ought to be provided.
+                // When a third argument is present, it's an attribute name whose possible values ought to be provided.
+                List<Object> arguments = params.getArguments();
+                Collection<? extends LaunchConfigurationCompletion> configurations = Lookup.getDefault().lookupAll(LaunchConfigurationCompletion.class);
+                List<CompletableFuture<List<CompletionItem>>> completionFutures;
+                String configUri = ((JsonPrimitive) arguments.get(0)).getAsString();
+                Supplier<CompletableFuture<Project>> projectSupplier = () -> {
+                    FileObject file;
+                    try {
+                        file = URLMapper.findFileObject(new URL(configUri));
+                    } catch (MalformedURLException ex) {
+                        Exceptions.printStackTrace(ex);
+                        return CompletableFuture.completedFuture(null);
+                    }
+                    return server.asyncOpenFileOwner(file);
+                };
+                switch (arguments.size()) {
+                    case 1:
+                        completionFutures = configurations.stream().map(c -> c.configurations(projectSupplier)).collect(Collectors.toList());
+                        break;
+                    case 2:
+                        Map<String, Object> attributes = attributesMap((JsonObject) arguments.get(1));
+                        completionFutures = configurations.stream().map(c -> c.attributes(projectSupplier, attributes)).collect(Collectors.toList());
+                        break;
+                    case 3:
+                        attributes = attributesMap((JsonObject) arguments.get(1));
+                        String attribute = ((JsonPrimitive) arguments.get(2)).getAsString();
+                        completionFutures = configurations.stream().map(c -> c.attributeValues(projectSupplier, attributes, attribute)).collect(Collectors.toList());
+                        break;
+                    default:
+                        StringBuilder classes = new StringBuilder();
+                        for (int i = 0; i < arguments.size(); i++) {
+                            classes.append(arguments.get(i).getClass().toString());
+                        }
+                        throw new IllegalStateException("Wrong arguments("+arguments.size()+"): " + arguments + ", classes = " + classes);  // NOI18N
+                }
+                CompletableFuture<List<CompletionItem>> joinedFuture = CompletableFuture.allOf(completionFutures.toArray(new CompletableFuture[0]))
+                        .thenApply(avoid -> completionFutures.stream().flatMap(c -> c.join().stream()).collect(Collectors.toList()));
+                return (CompletableFuture<Object>) (CompletableFuture<?>) joinedFuture;
+            }
             default:
                 for (CodeGenerator codeGenerator : Lookup.getDefault().lookupAll(CodeGenerator.class)) {
                     if (codeGenerator.getCommands().contains(command)) {
@@ -248,6 +299,16 @@ public final class WorkspaceServiceImpl implements WorkspaceService, LanguageCli
         throw new UnsupportedOperationException("Command not supported: " + params.getCommand());
     }
     
+    private static Map<String, Object> attributesMap(JsonObject json) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        for (Entry<String, JsonElement> entry : json.entrySet()) {
+            JsonPrimitive jp = (JsonPrimitive) entry.getValue();
+            Object value = jp.isBoolean() ? jp.getAsBoolean() : jp.isNumber() ? jp.getAsNumber() : jp.getAsString();
+            map.put(entry.getKey(), value);
+        }
+        return map;
+    }
+    
     private CompletableFuture<Object> findProjectConfigurations(FileObject ownedFile) {
         return server.asyncOpenFileOwner(ownedFile).thenApply(p -> {
             if (p == null) {
diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/UtilsTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/UtilsTest.java
new file mode 100644
index 0000000..ae22592
--- /dev/null
+++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/UtilsTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import org.junit.Test;
+
+/**
+ *
+ * @author Martin Entlicher
+ */
+public class UtilsTest {
+
+    @Test
+    public void testEscapeScompletionSnippetSpceialChars() {
+        assertEquals("", Utils.escapeCompletionSnippetSpecialChars(""));
+        assertEquals("a", Utils.escapeCompletionSnippetSpecialChars("a"));
+        assertEquals("\\$", Utils.escapeCompletionSnippetSpecialChars("$"));
+        assertEquals("{\\}", Utils.escapeCompletionSnippetSpecialChars("{}"));
+        assertEquals("\\${\\}", Utils.escapeCompletionSnippetSpecialChars("${}"));
+        assertEquals("\\}", Utils.escapeCompletionSnippetSpecialChars("}"));
+        assertEquals("\\$\\${{\\}\\}", Utils.escapeCompletionSnippetSpecialChars("$${{}}"));
+        assertEquals("a\\$\n\\}", Utils.escapeCompletionSnippetSpecialChars("a$\n}"));
+        assertEquals("\\\\a", Utils.escapeCompletionSnippetSpecialChars("\\a"));
+
+        String nonEscapedStringNotChanged = new String("abcdef");
+        assertSame(nonEscapedStringNotChanged, Utils.escapeCompletionSnippetSpecialChars(nonEscapedStringNotChanged));
+    }
+
+    @Test
+    public void testEncode2JSON() {
+        assertEquals("", Utils.encode2JSON(""));
+        assertEquals("abcd", Utils.encode2JSON("abcd"));
+        assertEquals("'\\\"\\b\\t\\n\\r\\\\", Utils.encode2JSON("'\"\b\t\n\r\\"));
+    }
+}
diff --git a/java/java.lsp.server/vscode/package-lock.json b/java/java.lsp.server/vscode/package-lock.json
index df54329..33e724a 100644
--- a/java/java.lsp.server/vscode/package-lock.json
+++ b/java/java.lsp.server/vscode/package-lock.json
@@ -542,6 +542,11 @@
 			"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
 			"dev": true
 		},
+		"jsonc-parser": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
+			"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA=="
+		},
 		"locate-path": {
 			"version": "3.0.0",
 			"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
diff --git a/java/java.lsp.server/vscode/package.json b/java/java.lsp.server/vscode/package.json
index 9a30fb4..c0b0b3b 100644
--- a/java/java.lsp.server/vscode/package.json
+++ b/java/java.lsp.server/vscode/package.json
@@ -367,6 +367,7 @@
 		"vscode-test": "^1.3.0"
 	},
 	"dependencies": {
+		"jsonc-parser": "3.0.0",
 		"vscode-debugadapter": "1.42.1",
 		"vscode-languageclient": "6.1.3",
 		"vscode-test-adapter-api": "^1.9.0",
diff --git a/java/java.lsp.server/vscode/src/extension.ts b/java/java.lsp.server/vscode/src/extension.ts
index f2a5255..5425bdd 100644
--- a/java/java.lsp.server/vscode/src/extension.ts
+++ b/java/java.lsp.server/vscode/src/extension.ts
@@ -42,6 +42,7 @@ import { TestAdapterRegistrar } from 'vscode-test-adapter-util';
 import * as launcher from './nbcode';
 import {NbTestAdapter} from './testAdapter';
 import { StatusMessageRequest, ShowStatusMessageParams, QuickPickRequest, InputBoxRequest, TestProgressNotification, DebugConnector } from './protocol';
+import * as launchConfigurations from './launchConfigurations';
 
 const API_VERSION : string = "1.0";
 let client: Promise<LanguageClient>;
@@ -318,16 +319,19 @@ export function activate(context: ExtensionContext): VSNetBeansAPI {
         await runDebug(false, false, uri, methodName, launchConfiguration);
     }));
 
-	// get the Test Explorer extension and register TestAdapter
-	const testExplorerExtension = vscode.extensions.getExtension<TestHub>(testExplorerExtensionId);
-	if (testExplorerExtension) {
-		const testHub = testExplorerExtension.exports;
+    // register completions:
+    launchConfigurations.registerCompletion(context);
+
+    // get the Test Explorer extension and register TestAdapter
+    const testExplorerExtension = vscode.extensions.getExtension<TestHub>(testExplorerExtensionId);
+    if (testExplorerExtension) {
+        const testHub = testExplorerExtension.exports;
         testAdapterRegistrar = new TestAdapterRegistrar(
-			testHub,
-			workspaceFolder => new NbTestAdapter(workspaceFolder, client)
-		);
-		context.subscriptions.push(testAdapterRegistrar);
-	}
+            testHub,
+            workspaceFolder => new NbTestAdapter(workspaceFolder, client)
+        );
+        context.subscriptions.push(testAdapterRegistrar);
+    }
 
     return Object.freeze({
         version : API_VERSION
diff --git a/java/java.lsp.server/vscode/src/launchConfigurations.ts b/java/java.lsp.server/vscode/src/launchConfigurations.ts
new file mode 100644
index 0000000..48006b5
--- /dev/null
+++ b/java/java.lsp.server/vscode/src/launchConfigurations.ts
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+'use strict';
+
+import { commands, CompletionItem, CompletionList, ExtensionContext, languages, ProviderResult, SnippetString } from 'vscode';
+import { InsertTextFormat } from 'vscode-languageclient';
+import * as jsoncp from 'jsonc-parser';
+
+export function registerCompletion(context: ExtensionContext) {
+   context.subscriptions.push(languages.registerCompletionItemProvider({ language: 'jsonc', pattern: '**/launch.json' }, {
+        provideCompletionItems(document, position, cancelToken) {
+            const sourceText = document.getText();
+            const root = jsoncp.parseTree(sourceText);
+            if (root) {
+                const offset = document.offsetAt(position);
+                const currentNode = jsoncp.findNodeAtOffset(root, offset);
+                if (currentNode) {
+                    const path = jsoncp.getNodePath(currentNode);
+                    if (path.length >= 1 && 'configurations' == path[0]) {
+                        const uri = document.uri.toString();
+                        let completionItems: ProviderResult<CompletionList<CompletionItem>> | CompletionItem[];
+                        if (path.length == 1) {
+                            // Get all configurations:
+                            completionItems = commands.executeCommand('java.project.configuration.completion', uri);
+                        } else {
+                            let node: jsoncp.Node = currentNode;
+                            if (currentNode.type == 'property' && currentNode.parent) {
+                                let propName = currentNode.children?.[0]?.value;
+                                if (!propName) { // Invalid node?
+                                    return new CompletionList();
+                                }
+                                node = currentNode.parent;
+                                let attributesMap = getAttributes(node);
+                                // Get possible values of property 'propName':
+                                completionItems = commands.executeCommand('java.project.configuration.completion', uri, attributesMap, propName);
+                            } else {
+                                let attributesMap = getAttributes(node);
+                                // Get additional possible attributes:
+                                completionItems = commands.executeCommand('java.project.configuration.completion', uri, attributesMap);
+                            }
+                        }
+
+
+			return (completionItems as Thenable<CompletionList<CompletionItem>>).then(itemsList => {
+                            let items = itemsList.items;
+			    if (!items) {
+                                items = ((itemsList as unknown) as CompletionItem[]);
+			    }
+                            addCommas(sourceText, offset, items);
+			    return new CompletionList(items);
+                        });
+                    }
+                }
+            }
+        }
+    }));
+}
+
+function getAttributesMap(node: jsoncp.Node) {
+    let attributes = new Map<string, object>();
+    if (node.children) {
+        for (let index in node.children) {
+            let ch = node.children[index];
+            let prop = ch.children;
+            if (prop) {
+                attributes.set(prop[0].value, prop[1].value);
+            }
+        }
+    }
+    return attributes;
+}
+
+function getAttributes(node: jsoncp.Node) {
+    let attributes: any = {};
+    if (node.children) {
+        for (let index in node.children) {
+            let ch = node.children[index];
+            let prop = ch.children;
+            if (prop) {
+                attributes[prop[0].value] = prop[1].value;
+            }
+        }
+    }
+    return attributes;
+}
+
+function addCommas(sourceText: string, offset: number, completionItems: CompletionItem[]) {
+    if (!completionItems) {
+        return ;
+    }
+    let prepend = false;
+    let o = offset - 1;
+    while (o >= 0) {
+        let c = sourceText.charAt(o);
+        if (!/\s/.test(c)) {
+            prepend = c != '[' && c != '{' && c != ',' && c != ':';
+            break;
+        }
+        o--;
+    }
+    let append = false;
+    o = offset + 1;
+    while (o < sourceText.length) {
+        let c = sourceText.charAt(o);
+        if (!/\s/.test(c)) {
+            append = c != ']' && c != '}' && c != ',';
+            break;
+        }
+        o++;
+    }
+    for (let index in completionItems) {
+        let ci = completionItems[index];
+        if (ci.insertText) {
+            if ((<any> ci).insertTextFormat === InsertTextFormat.Snippet) {
+                let snippet = new SnippetString(<string> ci.insertText);
+                ci.insertText = snippet;
+                if (prepend) {
+                    snippet.value = ',' + snippet.value;
+                }
+                if (append) {
+                    snippet.value = snippet.value + ',';
+                }
+	    } else {
+                if (prepend) {
+                    ci.insertText = ',' + ci.insertText;
+                }
+                if (append) {
+                    ci.insertText = ci.insertText + ',';
+                }
+            }
+        }
+    }
+}
+

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