This is an automated email from the ASF dual-hosted git repository. sdedic 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 5b0e52c Auto-open project owner of the opened file. new eff78c8 Merge pull request #2806 from sdedic/lsp/openImediateProjectOwner 5b0e52c is described below commit 5b0e52cb2b68424ac2c8869c3d991a252e1fd178 Author: Svata Dedic <svatopluk.de...@oracle.com> AuthorDate: Fri Mar 12 14:33:21 2021 +0100 Auto-open project owner of the opened file. --- .../modules/java/lsp/server/LspServerState.java | 18 ++ .../modules/java/lsp/server/protocol/Server.java | 205 +++++++++++++++++++-- .../server/protocol/TextDocumentServiceImpl.java | 6 +- 3 files changed, 211 insertions(+), 18 deletions(-) diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspServerState.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspServerState.java index 366099e..bb66851 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspServerState.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/LspServerState.java @@ -19,6 +19,7 @@ package org.netbeans.modules.java.lsp.server; import java.util.List; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.services.TextDocumentService; import org.netbeans.api.project.Project; @@ -51,6 +52,23 @@ public interface LspServerState { public CompletableFuture<Project[]> asyncOpenSelectedProjects(List<FileObject> fileCandidates); /** + * Opens project on behalf of a file. This makes the project 'second-class citizen' in LSP: it will be + * opened in OpenProjects to be reachable for all supports, but will track it separately from projects + * opened by {@link #asyncOpenSelectedProjects}. + * <p/> + * The user may be asked, if the opened project is not part of existing workspace projects or opened + * projects. If the user cancels, the returned future completes exceptionally with {@link CancellationException}. + * <p/> + * If the file is not owned by a project, or the project open fails, the returned future will return + * {@code null}. + * + * @param file file owned by a project + * @return future that completes when the project is opened, or opening cancelled. + * @see CancellationException + */ + public CompletableFuture<Project> asyncOpenFileOwner(FileObject file); + + /** * Accesses TextDocumentService instance. * @return TextDocumentService */ 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 5dab251..82e802c 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 @@ -24,10 +24,16 @@ import java.io.OutputStream; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -67,12 +73,15 @@ import org.eclipse.lsp4j.services.LanguageClientAware; import org.eclipse.lsp4j.services.LanguageServer; import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.WorkspaceService; +import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.java.classpath.ClassPath; import org.netbeans.api.java.source.ClasspathInfo; import org.netbeans.api.java.source.JavaSource; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; import org.netbeans.api.project.ProjectUtils; +import static org.netbeans.api.project.ProjectUtils.parentOf; import org.netbeans.api.project.Sources; import org.netbeans.api.project.ui.OpenProjects; import org.netbeans.modules.java.lsp.server.LspServerState; @@ -84,6 +93,7 @@ import org.netbeans.spi.project.ActionProvider; import org.openide.filesystems.FileObject; import org.openide.util.Exceptions; import org.openide.util.Lookup; +import org.openide.util.NbBundle; import org.openide.util.RequestProcessor; import org.openide.util.lookup.AbstractLookup; import org.openide.util.lookup.InstanceContent; @@ -227,12 +237,50 @@ public final class Server { } } - // change to a greater throughput if the initialization waits on more processes than just (serialized) project open. - private static final RequestProcessor SERVER_INIT_RP = new RequestProcessor(LanguageServerImpl.class.getName()); - + /** + * Returns a sequence of parents of the given project, leading to the {@link #rootOf} that + * project. If `{@code excludeSelf}` is true, the sequence does not contain the project itself. + * Note that if the project has no parent, then {@code excludeSelf = true} may return an + * empty sequence. + * <p> + * The sequence starts at the project (or its immediate parent, if excludeSelf is true), and + * iterate towards the root of the project. + * + * @param project inspected project + * @return path from the project to the root + * @since + */ + public static Iterable<Project> projectPath(@NonNull Project project, boolean excludeSelf) { + return new Iterable<Project>() { + @Override + public Iterator<Project> iterator() { + return new Iterator<Project>() { + Project next = excludeSelf ? project : parentOf(project); + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public Project next() { + if (next == null) { + throw new NoSuchElementException(); + } + Project r = next; + next = parentOf(r); + return r; + } + }; + } + }; + } + static class LanguageServerImpl implements LanguageServer, LanguageClientAware, LspServerState { + // change to a greater throughput if the initialization waits on more processes than just (serialized) project open. + private static final RequestProcessor SERVER_INIT_RP = new RequestProcessor(LanguageServerImpl.class.getName()); + private static final Logger LOG = Logger.getLogger(LanguageServerImpl.class.getName()); private NbCodeClientWrapper client; private final TextDocumentService textDocumentService = new TextDocumentServiceImpl(this); @@ -242,13 +290,34 @@ public final class Server { new AbstractLookup(sessionServices), Lookup.getDefault() ); - private final CompletableFuture<Project[]> workspaceProjects = new CompletableFuture<>(); /** - * Projects that are being opened and primed right now. + * Projects that are or were opened. After projects open, their CompletableFutures + * remain here to signal no further priming build is required. */ + // @GuardedBy(this) private final Map<Project, CompletableFuture<Void>> beingOpened = new HashMap<>(); - private final CompletableFuture<Project[]> initialOpenedProjects = new CompletableFuture<>(); + + /** + * Projects opened based on files. This registry avoids duplicate questions if + * more files are opened at the same time; the project question is displayed just for the + * first time. + */ + // @GuardedBy(this) + private final Map<Project, CompletableFuture<Project>> openingFileOwners = new HashMap<>(); + + /** + * Holds projects opened in the LSP workspace; these projects serve as root points for + * other projects opened behind the scenes. The value is initially uncompleted, but + * is replaced by a <b>completed</b> future at any time the set of workspace projects change. + */ + private volatile CompletableFuture<Project[]> workspaceProjects = new CompletableFuture<>(); + + /** + * All projects opened by this LSP server. The collection is replaced every time + * the set of opened projects change, collections are never modified. + */ + private volatile Collection<Project> openedProjects = Collections.emptyList(); Lookup getSessionLookup() { return sessionLookup; @@ -263,20 +332,100 @@ public final class Server { */ @Override public CompletableFuture<Project[]> asyncOpenSelectedProjects(List<FileObject> projectCandidates) { - System.err.println("Called asyncOpenProjects for " + projectCandidates); CompletableFuture<Project[]> f = new CompletableFuture<>(); SERVER_INIT_RP.post(() -> { - asyncOpenSelectedProjects0(f, projectCandidates); + asyncOpenSelectedProjects0(f, projectCandidates, true); }); return f; } + + @NbBundle.Messages({ + "PROMPT_AskOpenProjectForFile=File {0} belongs to project {1}. To enable all features, the project should be opened" + + " and initialized by the Language Server. Do you want to proceed ?", + "PROMPT_AskOpenProjectForFileNoName=File {0} belongs to a project. To enable all features, the project should be opened" + + " and initialized by the Language Server. Do you want to proceed ?", + "PROMPT_AskOpenProjectForFile_Yes=Open and initialize", + "PROMPT_AskOpenProjectForFile_No=No", + "PROMPT_AskOpenProjectForFile_Unnamed=(unnamed)" + }) + @Override + public CompletableFuture<Project> asyncOpenFileOwner(FileObject file) { + Project prj = FileOwnerQuery.getOwner(file); + if (prj == null) { + return CompletableFuture.completedFuture(null); + } + // first wait on the initial workspace open/init. + return workspaceProjects.thenCompose((wprj) -> { + CompletableFuture<Project[]> f = new CompletableFuture<>(); + CompletableFuture<Project> g = f.thenApply(arr -> arr.length > 0 ? arr[0] : null); + Collection<Project> prjs = Arrays.asList(wprj); + + boolean openImmediately = false; + synchronized (this) { + if (openedProjects.contains(prj)) { + // shortcut + return CompletableFuture.completedFuture(prj); + } + CompletableFuture<Void> h = beingOpened.get(prj); + if (h != null) { + // already being really opened + return h.thenApply((unused) -> prj); + } + // the project is already being asked for; otherwise leave + // a trace + flag so the project is not asked again. + CompletableFuture<Project> p = openingFileOwners.putIfAbsent(prj, g); + if (p != null) { + return p; + } + // if any of the parent projects is among the opened ones, + // then we are permitted + for (Project check : projectPath(prj, false)) { + if (prjs.contains(check)) { + openImmediately = true; + break; + } + } + } + if (openImmediately) { + // open without asking + SERVER_INIT_RP.post(() -> { + asyncOpenSelectedProjects0(f, Collections.singletonList(file), false); + }); + } else { + ProjectInformation pi = ProjectUtils.getInformation(prj); + String dispName = pi != null ? pi.getDisplayName() : Bundle.PROMPT_AskOpenProjectForFile_Unnamed(); + final MessageActionItem yes = new MessageActionItem(Bundle.PROMPT_AskOpenProjectForFile_Yes()); + ShowMessageRequestParams smrp = new ShowMessageRequestParams(Arrays.asList( + yes, + new MessageActionItem(Bundle.PROMPT_AskOpenProjectForFile_No()) + )); + if (dispName.equals(prj.getProjectDirectory().getPath())) { + smrp.setMessage(Bundle.PROMPT_AskOpenProjectForFileNoName(file.getPath())); + } else { + smrp.setMessage(Bundle.PROMPT_AskOpenProjectForFile(file.getPath(), dispName)); + } + smrp.setType(MessageType.Info); + + client.showMessageRequest(smrp).thenAccept(ai -> { + if (!yes.equals(ai)) { + f.completeExceptionally(new CancellationException()); + return; + } + SERVER_INIT_RP.post(() -> { + asyncOpenSelectedProjects0(f, Collections.singletonList(file), false); + }); + }); + } + return f.thenApply(arr -> arr.length > 0 ? arr[0] : null); + }); + } /** * For diagnostic purposes */ private AtomicInteger openRequestId = new AtomicInteger(1); - private void asyncOpenSelectedProjects0(CompletableFuture<Project[]> f, List<FileObject> projectCandidates) { + private void asyncOpenSelectedProjects0(CompletableFuture<Project[]> f, List<FileObject> projectCandidates, boolean asWorkspaceProjects) { List<Project> projects = new ArrayList<>(); try { for (FileObject candidate : projectCandidates) { @@ -299,13 +448,13 @@ public final class Server { throw new IllegalStateException(ex); } - asyncOpenSelectedProjects1(f, previouslyOpened, projects); + asyncOpenSelectedProjects1(f, previouslyOpened, projects, asWorkspaceProjects); } catch (RuntimeException ex) { f.completeExceptionally(ex); } } - private void asyncOpenSelectedProjects1(CompletableFuture<Project[]> f, Project[] previouslyOpened, List<Project> projects) { + private void asyncOpenSelectedProjects1(CompletableFuture<Project[]> f, Project[] previouslyOpened, List<Project> projects, boolean addToWorkspace) { int id = this.openRequestId.getAndIncrement(); List<CompletableFuture> primingBuilds = new ArrayList<>(); @@ -341,7 +490,7 @@ public final class Server { } LOG.log(Level.FINER, "{0}: Found Priming action: {1}", new Object[]{id, p}); if (pap.isActionEnabled(ActionProvider.COMMAND_PRIME, Lookup.EMPTY)) { - final CompletableFuture<Void> primeF = local.get(p); + final CompletableFuture<Void> primeF = new CompletableFuture<>(); LOG.log(Level.FINER, "{0}: Found enabled Priming build for: {1}", new Object[]{id, p}); ActionProgress progress = new ActionProgress() { @Override @@ -371,11 +520,32 @@ public final class Server { for (Project prj : projects) { //init source groups/FileOwnerQuery: ProjectUtils.getSources(prj).getSourceGroups(Sources.TYPE_GENERIC); + final CompletableFuture<Void> prjF = local.get(prj); + if (prjF != null) { + prjF.complete(null); + } } + Set<Project> projectSet = new HashSet<>(Arrays.asList(OpenProjects.getDefault().getOpenProjects())); + projectSet.retainAll(openedProjects); + projectSet.addAll(projects); + Project[] prjs = projects.toArray(new Project[projects.size()]); + LOG.log(Level.FINER, "{0}: Finished opening projects: {1}", new Object[]{id, Arrays.asList(projects)}); synchronized (this) { - LOG.log(Level.FINER, "{0}: Finished opening projects: {1}", new Object[]{id, Arrays.asList(projects)}); - beingOpened.keySet().removeAll(toOpen); + openedProjects = projectSet; + if (addToWorkspace) { + Set<Project> ns = new HashSet<>(projects); + int s = ns.size(); + ns.addAll(Arrays.asList(workspaceProjects.getNow(new Project[0]))); + if (s != ns.size()) { + prjs = ns.toArray(new Project[ns.size()]); + workspaceProjects = CompletableFuture.completedFuture(prjs); + } + } + for (Project p : prjs) { + // override flag in opening cache, no further questions asked. + openingFileOwners.put(p, f.thenApply(unused -> p)); + } } f.complete(prjs); }).exceptionally(e -> { @@ -398,7 +568,7 @@ public final class Server { @Override public CompletableFuture<Project[]> openedProjects() { - return initialOpenedProjects; + return workspaceProjects; } private JavaSource showIndexingCompleted(Project[] opened) { @@ -482,10 +652,11 @@ public final class Server { //TODO: use getRootPath()? } } - SERVER_INIT_RP.post(() -> asyncOpenSelectedProjects0(initialOpenedProjects, projectCandidates)); + CompletableFuture<Project[]> prjs = workspaceProjects; + SERVER_INIT_RP.post(() -> asyncOpenSelectedProjects0(prjs, projectCandidates, true)); // chain showIndexingComplete message after initial project open. - initialOpenedProjects. + prjs. thenApply(this::showIndexingCompleted); // but complete the InitializationRequest independently of the project initialization. diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java index 313ec35..db228e1 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java @@ -1784,7 +1784,11 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli client.logMessage(new MessageParams(MessageType.Error, ex.getMessage())); } openedDocuments.put(params.getTextDocument().getUri(), doc); - runDiagnoticTasks(params.getTextDocument().getUri()); + + // attempt to open the directly owning project, delay diagnostics after project open: + server.asyncOpenFileOwner(file).thenRun(() -> + runDiagnoticTasks(params.getTextDocument().getUri()) + ); } catch (IOException ex) { throw new IllegalStateException(ex); } finally { --------------------------------------------------------------------- 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