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