Merge pull request #4595 from sdedic/lsp/projectInfo
Project info exported through LSP
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/LspProjectInfo.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/LspProjectInfo.java
new file mode 100644
index 0000000..4e267b6
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/project/LspProjectInfo.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.java.lsp.server.project;
+
+import java.net.URI;
+
+/**
+ *
+ * @author sdedic
+ */
+public class LspProjectInfo {
+ /**
+ * Project's directory
+ */
+ public URI projectDirectory;
+
+ /**
+ * Project's name.
+ */
+ public String name;
+
+ /**
+ * Project's display name as defined in project file(s)
+ */
+ public String displayName;
+
+ /**
+ * The build system / project type. Ant, Gradle, Maven.
+ */
+ public String projectType;
+
+ /**
+ * URIs of subprojects. Usually children of the project's own directory.
+ */
+ public URI[] subprojects;
+
+ /**
+ * If part of a reactor or multi-project, the URI of the root project.
+ */
+ public URI rootProject;
+
+ /**
+ * Supported project actions. Names.
+ */
+ public String[] projectActionNames;
+}
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 cdcf5cf..df866c4 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
@@ -749,7 +749,9 @@
JAVA_SUPER_IMPLEMENTATION,
JAVA_SOURCE_FOR,
JAVA_CLEAR_PROJECT_CACHES,
- NATIVE_IMAGE_FIND_DEBUG_PROCESS_TO_ATTACH));
+ NATIVE_IMAGE_FIND_DEBUG_PROCESS_TO_ATTACH,
+ JAVA_PROJECT_INFO
+ ));
for (CodeActionsProvider codeActionsProvider : Lookup.getDefault().lookupAll(CodeActionsProvider.class)) {
commands.addAll(codeActionsProvider.getCommands());
}
@@ -944,6 +946,12 @@
* new project files were generated into workspace subtree.
*/
public static final String JAVA_CLEAR_PROJECT_CACHES = "java.clear.project.caches";
+
+ /**
+ * For a project directory, returns basic project information and structure.
+ * Syntax: nbls.project.info(locations : String | String[], options? : { projectStructure? : boolean; actions? : boolean; recursive? : boolean }) : LspProjectInfo
+ */
+ public static final String JAVA_PROJECT_INFO = "nbls.project.info";
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 3529067..2060a45 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,6 +19,7 @@
package org.netbeans.modules.java.lsp.server.protocol;
import com.google.gson.Gson;
+import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
@@ -29,6 +30,7 @@
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
+import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
@@ -48,6 +50,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -99,6 +102,7 @@
import org.netbeans.api.java.source.ui.ElementOpen;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectInformation;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
@@ -111,6 +115,7 @@
import org.netbeans.modules.java.lsp.server.Utils;
import org.netbeans.modules.java.lsp.server.debugging.attach.AttachConfigurations;
import org.netbeans.modules.java.lsp.server.debugging.attach.AttachNativeConfigurations;
+import org.netbeans.modules.java.lsp.server.project.LspProjectInfo;
import org.netbeans.modules.java.source.ElementHandleAccessor;
import org.netbeans.modules.java.source.ui.JavaSymbolProvider;
import org.netbeans.modules.java.source.ui.JavaTypeProvider;
@@ -139,8 +144,11 @@
* @author lahvac
*/
public final class WorkspaceServiceImpl implements WorkspaceService, LanguageClientAware {
+
+ private static final Logger LOG = Logger.getLogger(WorkspaceServiceImpl.class.getName());
private static final RequestProcessor WORKER = new RequestProcessor(WorkspaceServiceImpl.class.getName(), 1, false, false);
+ private static final RequestProcessor PROJECT_WORKER = new RequestProcessor(WorkspaceServiceImpl.class.getName(), 5, false, false);
private final Gson gson = new Gson();
private final LspServerState server;
@@ -578,6 +586,56 @@
}
return (CompletableFuture<Object>) (CompletableFuture<?>)result;
}
+
+ case Server.JAVA_PROJECT_INFO: {
+ final CompletableFuture<Object> result = new CompletableFuture<>();
+ List<Object> arguments = params.getArguments();
+ if (arguments.size() < 1) {
+ result.completeExceptionally(new IllegalArgumentException("Expecting URL or URL[] as an argument to " + command));
+ return result;
+ }
+ Object o = arguments.get(0);
+ URL[] locations = null;
+ if (o instanceof JsonArray) {
+ List<URL> locs = new ArrayList<>();
+ JsonArray a = (JsonArray)o;
+ a.forEach((e) -> {
+ if (e instanceof JsonPrimitive) {
+ String s = ((JsonPrimitive)e).getAsString();
+ try {
+ locs.add(new URL(s));
+ } catch (MalformedURLException ex) {
+ throw new IllegalArgumentException("Illegal location: " + s);
+ }
+ }
+ });
+ } else if (o instanceof JsonPrimitive) {
+ String s = ((JsonPrimitive)o).getAsString();
+ try {
+ locations = new URL[] { new URL(s) };
+ } catch (MalformedURLException ex) {
+ throw new IllegalArgumentException("Illegal location: " + s);
+ }
+ }
+ if (locations == null || locations.length == 0) {
+ result.completeExceptionally(new IllegalArgumentException("Expecting URL or URL[] as an argument to " + command));
+ return result;
+ }
+ boolean projectStructure = false;
+ boolean actions = false;
+ boolean recursive = false;
+
+ if (arguments.size() > 1) {
+ Object a2 = arguments.get(1);
+ if (a2 instanceof JsonObject) {
+ JsonObject options = (JsonObject)a2;
+ projectStructure = getOption(options, "projectStructure", false); // NOI18N
+ actions = getOption(options, "actions", false); // NOI18N
+ recursive = getOption(options, "recursive", false); // NOI18N
+ }
+ }
+ return (CompletableFuture<Object>)(CompletableFuture<?>)new ProjectInfoWorker(locations, projectStructure, recursive, actions).process();
+ }
default:
for (CodeActionsProvider codeActionsProvider : Lookup.getDefault().lookupAll(CodeActionsProvider.class)) {
if (codeActionsProvider.getCommands().contains(command)) {
@@ -588,6 +646,133 @@
throw new UnsupportedOperationException("Command not supported: " + params.getCommand());
}
+ private class ProjectInfoWorker {
+ final URL[] locations;
+ final boolean projectStructure;
+ final boolean recursive;
+ final boolean actions;
+
+ Map<FileObject, LspProjectInfo> infos = new HashMap<>();
+ Set<Project> toOpen = new HashSet<>();
+
+ public ProjectInfoWorker(URL[] locations, boolean projectStructure, boolean recursive, boolean actions) {
+ this.locations = locations;
+ this.projectStructure = projectStructure;
+ this.recursive = recursive;
+ this.actions = actions;
+ }
+
+ public CompletableFuture<LspProjectInfo[]> process() {
+ List<FileObject> files = new ArrayList();
+ for (URL u : locations) {
+ FileObject f = URLMapper.findFileObject(u);
+ if (f != null) {
+ files.add(f);
+ }
+ }
+ return server.asyncOpenSelectedProjects(files, false).thenCompose(this::processProjects);
+ }
+
+ LspProjectInfo fillProjectInfo(Project p) {
+ LspProjectInfo info = infos.get(p.getProjectDirectory());
+ if (info != null) {
+ return info;
+ }
+ info = new LspProjectInfo();
+
+ ProjectInformation pi = ProjectUtils.getInformation(p);
+ URL projectURL = URLMapper.findURL(p.getProjectDirectory(), URLMapper.EXTERNAL);
+ if (projectURL != null) {
+ try {
+ info.projectDirectory = projectURL.toURI();
+ } catch (URISyntaxException ex) {
+ // should not happen
+ }
+ }
+ info.name = pi.getName();
+ info.displayName = pi.getDisplayName();
+
+ // attempt to determine the project type
+ ProjectManager.Result r = ProjectManager.getDefault().isProject2(p.getProjectDirectory());
+ info.projectType = r.getProjectType();
+
+ if (actions) {
+ ActionProvider ap = p.getLookup().lookup(ActionProvider.class);
+ if (ap != null) {
+ info.projectActionNames = ap.getSupportedActions();
+ }
+ }
+
+ if (projectStructure) {
+ Set<Project> children = ProjectUtils.getContainedProjects(p, false);
+ List<URI> subprojectDirs = new ArrayList<>();
+ for (Project c : children) {
+ try {
+ subprojectDirs.add(URLMapper.findURL(c.getProjectDirectory(), URLMapper.EXTERNAL).toURI());
+ } catch (URISyntaxException ex) {
+ // should not happen
+ }
+ }
+ info.subprojects = subprojectDirs.toArray(new URI[subprojectDirs.size()]);
+ Project root = ProjectUtils.rootOf(p);
+ if (root != null) {
+ try {
+ info.rootProject = URLMapper.findURL(root.getProjectDirectory(), URLMapper.EXTERNAL).toURI();
+ } catch (URISyntaxException ex) {
+ // should not happen
+ }
+ }
+ if (recursive) {
+ toOpen.addAll(children);
+ }
+ }
+ infos.put(p.getProjectDirectory(), info);
+ return info;
+ }
+
+ CompletableFuture<LspProjectInfo[]> processProjects(Project[] prjs) {
+ for (Project p : prjs) {
+ fillProjectInfo(p);
+ }
+ if (toOpen.isEmpty()) {
+ return finalizeInfos();
+ }
+ List<FileObject> dirs = new ArrayList<>(toOpen.size());
+ for (Project p : toOpen) {
+ dirs.add(p.getProjectDirectory());
+ }
+ toOpen.clear();
+ return server.asyncOpenSelectedProjects(dirs).thenCompose(this::processProjects);
+ }
+
+ CompletableFuture<LspProjectInfo[]> finalizeInfos() {
+ List<LspProjectInfo> list = new ArrayList();
+ for (URL u : locations) {
+ FileObject f = URLMapper.findFileObject(u);
+ Project owner = FileOwnerQuery.getOwner(f);
+ if (owner != null) {
+ list.add(infos.remove(owner.getProjectDirectory()));
+ } else {
+ list.add(null);
+ }
+ }
+ list.addAll(infos.values());
+ LspProjectInfo[] toArray = list.toArray(new LspProjectInfo[list.size()]);
+ return CompletableFuture.completedFuture(toArray);
+ }
+ }
+
+ private static boolean getOption(JsonObject opts, String member, boolean def) {
+ if (!opts.has(member)) {
+ return def;
+ }
+ Object o = opts.get(member);
+ if (!(o instanceof JsonPrimitive)) {
+ return false;
+ }
+ return ((JsonPrimitive)o).getAsBoolean();
+ }
+
private final AtomicReference<BiConsumer<FileObject, Collection<TestMethodController.TestMethod>>> testMethodsListener = new AtomicReference<>();
private final AtomicReference<PropertyChangeListener> openProjectsListener = new AtomicReference<>();