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 <martin.entlic...@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-unsubscr...@netbeans.apache.org For additional commands, e-mail: commits-h...@netbeans.apache.org For further information about the NetBeans mailing lists, visit: https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists