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 000000000000..4e267b68f7ff --- /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 cdcf5cf636fb..df866c4ccd71 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 @@ private InitializeResult constructInitResponse(InitializeParams init, JavaSource 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 @@ protected LanguageClient client() { * 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 352906751a1f..2060a4546a9b 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 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { } return (CompletableFuture) (CompletableFuture)result; } + + case Server.JAVA_PROJECT_INFO: { + final CompletableFuture result = new CompletableFuture<>(); + List 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 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)(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 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { throw new UnsupportedOperationException("Command not supported: " + params.getCommand()); } + private class ProjectInfoWorker { + final URL[] locations; + final boolean projectStructure; + final boolean recursive; + final boolean actions; + + Map infos = new HashMap<>(); + Set 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 process() { + List 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 children = ProjectUtils.getContainedProjects(p, false); + List 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 processProjects(Project[] prjs) { + for (Project p : prjs) { + fillProjectInfo(p); + } + if (toOpen.isEmpty()) { + return finalizeInfos(); + } + List dirs = new ArrayList<>(toOpen.size()); + for (Project p : toOpen) { + dirs.add(p.getProjectDirectory()); + } + toOpen.clear(); + return server.asyncOpenSelectedProjects(dirs).thenCompose(this::processProjects); + } + + CompletableFuture finalizeInfos() { + List 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>> testMethodsListener = new AtomicReference<>(); private final AtomicReference openProjectsListener = new AtomicReference<>();