| /* |
| * 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.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Type; |
| 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.NoSuchElementException; |
| import java.util.Set; |
| import java.util.concurrent.CancellationException; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.function.Function; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import com.google.gson.InstanceCreator; |
| import com.google.gson.JsonObject; |
| import org.eclipse.lsp4j.CallHierarchyRegistrationOptions; |
| import org.eclipse.lsp4j.CodeActionKind; |
| import org.eclipse.lsp4j.CodeActionOptions; |
| import org.eclipse.lsp4j.CodeLensOptions; |
| import org.eclipse.lsp4j.CompletionOptions; |
| import org.eclipse.lsp4j.ConfigurationItem; |
| import org.eclipse.lsp4j.ConfigurationParams; |
| import org.eclipse.lsp4j.ExecuteCommandOptions; |
| import org.eclipse.lsp4j.FoldingRangeProviderOptions; |
| import org.eclipse.lsp4j.InitializeParams; |
| import org.eclipse.lsp4j.InitializeResult; |
| import org.eclipse.lsp4j.MessageActionItem; |
| import org.eclipse.lsp4j.MessageParams; |
| import org.eclipse.lsp4j.MessageType; |
| import org.eclipse.lsp4j.PublishDiagnosticsParams; |
| import org.eclipse.lsp4j.RenameOptions; |
| import org.eclipse.lsp4j.SemanticTokensCapabilities; |
| import org.eclipse.lsp4j.SemanticTokensParams; |
| import org.eclipse.lsp4j.ServerCapabilities; |
| import org.eclipse.lsp4j.ShowMessageRequestParams; |
| import org.eclipse.lsp4j.TextDocumentIdentifier; |
| import org.eclipse.lsp4j.TextDocumentSyncKind; |
| import org.eclipse.lsp4j.TextDocumentSyncOptions; |
| import org.eclipse.lsp4j.WorkDoneProgressCancelParams; |
| import org.eclipse.lsp4j.WorkDoneProgressParams; |
| import org.eclipse.lsp4j.WorkspaceFolder; |
| import org.eclipse.lsp4j.jsonrpc.Endpoint; |
| import org.eclipse.lsp4j.jsonrpc.JsonRpcException; |
| import org.eclipse.lsp4j.jsonrpc.Launcher; |
| import org.eclipse.lsp4j.jsonrpc.MessageConsumer; |
| import org.eclipse.lsp4j.jsonrpc.MessageIssueException; |
| import org.eclipse.lsp4j.jsonrpc.messages.Message; |
| import org.eclipse.lsp4j.jsonrpc.messages.NotificationMessage; |
| import org.eclipse.lsp4j.jsonrpc.messages.RequestMessage; |
| import org.eclipse.lsp4j.jsonrpc.services.JsonDelegate; |
| import org.eclipse.lsp4j.launch.LSPLauncher; |
| import org.eclipse.lsp4j.services.LanguageClient; |
| 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; |
| import org.netbeans.modules.java.lsp.server.LspSession; |
| import org.netbeans.modules.java.lsp.server.Utils; |
| import org.netbeans.modules.java.lsp.server.explorer.LspTreeViewServiceImpl; |
| import org.netbeans.modules.java.lsp.server.explorer.api.NodeChangedParams; |
| import org.netbeans.modules.java.lsp.server.explorer.api.TreeViewService; |
| import org.netbeans.modules.java.lsp.server.files.OpenedDocuments; |
| import org.netbeans.modules.java.lsp.server.progress.OperationContext; |
| import org.netbeans.modules.progress.spi.InternalHandle; |
| import org.netbeans.spi.project.ActionProgress; |
| import org.netbeans.spi.project.ActionProvider; |
| import org.openide.filesystems.FileObject; |
| import org.openide.filesystems.FileUtil; |
| import org.openide.util.Lookup; |
| import org.openide.util.NbBundle; |
| import org.openide.util.Pair; |
| import org.openide.util.RequestProcessor; |
| import org.openide.util.lookup.AbstractLookup; |
| import org.openide.util.lookup.InstanceContent; |
| import org.openide.util.lookup.Lookups; |
| import org.openide.util.lookup.ProxyLookup; |
| |
| /** |
| * |
| * @author lahvac |
| */ |
| public final class Server { |
| private static final Logger LOG = Logger.getLogger(Server.class.getName()); |
| |
| private Server() { |
| } |
| |
| public static NbCodeLanguageClient getStubClient() { |
| return STUB_CLIENT; |
| } |
| |
| public static boolean isClientResponseThread(NbCodeLanguageClient client) { |
| return client != null ? |
| DISPATCHERS.get() == client : |
| DISPATCHERS.get() != null; |
| } |
| |
| public static NbLspServer launchServer(Pair<InputStream, OutputStream> io, LspSession session) { |
| LanguageServerImpl server = new LanguageServerImpl(); |
| ConsumeWithLookup msgProcessor = new ConsumeWithLookup(server.getSessionLookup()); |
| Launcher<NbCodeLanguageClient> serverLauncher = createLauncher(server, io, msgProcessor::attachLookup); |
| NbCodeLanguageClient remote = serverLauncher.getRemoteProxy(); |
| ((LanguageClientAware) server).connect(remote); |
| msgProcessor.attachClient(server.client); |
| Future<Void> runningServer = serverLauncher.startListening(); |
| return new NbLspServer(server, runningServer); |
| } |
| |
| private static Launcher<NbCodeLanguageClient> createLauncher(LanguageServerImpl server, Pair<InputStream, OutputStream> io, |
| Function<MessageConsumer, MessageConsumer> processor) { |
| return new LSPLauncher.Builder<NbCodeLanguageClient>() |
| .setLocalService(server) |
| .setRemoteInterface(NbCodeLanguageClient.class) |
| .setInput(io.first()) |
| .setOutput(io.second()) |
| .wrapMessages(processor) |
| .configureGson(gb -> { |
| gb.registerTypeAdapter(SemanticTokensCapabilities.class, new InstanceCreator<SemanticTokensCapabilities>() { |
| @Override public SemanticTokensCapabilities createInstance(Type type) { |
| return new SemanticTokensCapabilities(null); |
| } |
| }); |
| gb.registerTypeAdapter(SemanticTokensParams.class, new InstanceCreator<SemanticTokensParams>() { |
| @Override public SemanticTokensParams createInstance(Type type) { |
| return new SemanticTokensParams(new TextDocumentIdentifier("")); |
| } |
| }); |
| }) |
| .create(); |
| } |
| |
| static final ThreadLocal<NbCodeLanguageClient> DISPATCHERS = new ThreadLocal<>(); |
| |
| /** |
| * Processes message while the default Lookup is set to |
| * {@link LanguageServerImpl#getSessionLookup()}. |
| */ |
| private static class ConsumeWithLookup { |
| private final Lookup sessionLookup; |
| private NbCodeLanguageClient client; |
| private OperationContext initialContext; |
| |
| public ConsumeWithLookup(Lookup sessionLookup) { |
| this.sessionLookup = sessionLookup; |
| } |
| |
| synchronized void attachClient(NbCodeLanguageClient client) { |
| this.client = client; |
| } |
| |
| public MessageConsumer attachLookup(MessageConsumer delegate) { |
| // PENDING: allow for message consumer wrappers to be registered to add pre/post processing for |
| // the request plus build the request's default Lookup contents. |
| if (!(delegate instanceof Endpoint)) { |
| return delegate; |
| } |
| return new MessageConsumer() { |
| @Override |
| public void consume(Message msg) throws MessageIssueException, JsonRpcException { |
| InstanceContent ic = new InstanceContent(); |
| ProxyLookup ll = new ProxyLookup(new AbstractLookup(ic), sessionLookup); |
| final OperationContext ctx; |
| |
| // Intercept client REQUESTS; take the progress token from them, if it is |
| // attached. |
| Runnable r; |
| InternalHandle toCancel = null; |
| if (msg instanceof RequestMessage) { |
| RequestMessage rq = (RequestMessage)msg; |
| Object p = rq.getParams(); |
| if (initialContext == null) { |
| initialContext = OperationContext.create(client); |
| ctx = initialContext; |
| } else { |
| ctx = initialContext.operationContext(); |
| } |
| // PENDING: this ought to be somehow registered, so different services |
| // may enrich lookup/pre/postprocess the processing, not just the progress support. |
| if (p instanceof WorkDoneProgressParams) { |
| ctx.setProgressToken(((WorkDoneProgressParams)p).getWorkDoneToken()); |
| } |
| } else if (msg instanceof NotificationMessage) { |
| NotificationMessage not = (NotificationMessage)msg; |
| Object p = not.getParams(); |
| OperationContext selected = null; |
| if (p instanceof WorkDoneProgressCancelParams && initialContext != null) { |
| WorkDoneProgressCancelParams wdc = (WorkDoneProgressCancelParams)p; |
| toCancel = initialContext.findActiveHandle(wdc.getToken()); |
| selected = OperationContext.getHandleContext(toCancel); |
| } |
| ctx = selected; |
| } else { |
| ctx = null; |
| } |
| if (ctx != null) { |
| ic.add(ctx); |
| } |
| final InternalHandle ftoCancel = toCancel; |
| try { |
| DISPATCHERS.set(client); |
| Lookups.executeWith(ll, () -> { |
| try { |
| delegate.consume(msg); |
| } finally { |
| // cancel while the OperationContext is still active. |
| if (ftoCancel != null) { |
| ftoCancel.requestCancel(); |
| } |
| if (ctx != null) { |
| // if initialized (for requests only), discards the token, |
| // as it becomes invalid at the end of this message. Further progresses |
| // must do their own processing. |
| ctx.stop(); |
| } |
| } |
| }); |
| } finally { |
| DISPATCHERS.remove(); |
| } |
| } |
| }; |
| } |
| } |
| |
| |
| /** |
| * 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; |
| } |
| }; |
| } |
| }; |
| } |
| |
| public static class LanguageServerImpl implements LanguageServer, LanguageClientAware, LspServerState, NbLanguageServer { |
| |
| private static final String NETBEANS_FORMAT = "netbeans.format"; |
| private static final String NETBEANS_JAVA_IMPORTS = "netbeans.java.imports"; |
| |
| // 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 TextDocumentServiceImpl textDocumentService = new TextDocumentServiceImpl(this); |
| private final WorkspaceServiceImpl workspaceService = new WorkspaceServiceImpl(this); |
| private final InstanceContent sessionServices = new InstanceContent(); |
| private final Lookup sessionLookup = new ProxyLookup( |
| new AbstractLookup(sessionServices), |
| Lookup.getDefault() |
| ); |
| |
| private final LspTreeViewServiceImpl treeService = new LspTreeViewServiceImpl(sessionLookup); |
| |
| /** |
| * 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<>(); |
| |
| /** |
| * 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(); |
| |
| /** |
| * Workspace folders (nonproject) accepted by the user; we do not ask for opening projects |
| * underneath these folders. |
| */ |
| // @GuardedBy(this) |
| private final List<FileObject> acceptedWorkspaceFolders = new ArrayList<>(); |
| |
| private final OpenedDocuments openedDocuments = new OpenedDocuments(); |
| |
| Lookup getSessionLookup() { |
| return sessionLookup; |
| } |
| |
| /** |
| * Open projects that own the `projectCandidates` files asynchronously. |
| * Returns immediately, results or errors are reported through the Future. |
| * |
| * @param projectCandidates files whose projects should be opened. |
| * @return future that yields the opened project instances. |
| */ |
| @Override |
| public CompletableFuture<Project[]> asyncOpenSelectedProjects(List<FileObject> projectCandidates, boolean addWorkspace) { |
| if (projectCandidates == null || projectCandidates.isEmpty()) { |
| return CompletableFuture.completedFuture(new Project[0]); |
| } |
| CompletableFuture<Project[]> f = new CompletableFuture<>(); |
| SERVER_INIT_RP.post(() -> { |
| asyncOpenSelectedProjects0(f, projectCandidates, addWorkspace, false); |
| }); |
| 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_AskOpenProject=To enable all features of project {0}, it 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) { |
| FileObject pdir = prj.getProjectDirectory(); |
| // accept projects in folders which were not recognized as project parts. |
| for (FileObject wf : acceptedWorkspaceFolders) { |
| if (wf.equals(pdir) || FileUtil.isParentOf(wf, pdir)) { |
| openImmediately = true; |
| } |
| } |
| } |
| } |
| if (openImmediately) { |
| // open without asking |
| SERVER_INIT_RP.post(() -> { |
| asyncOpenSelectedProjects0(f, Collections.singletonList(file), false, 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 (prj.getProjectDirectory() == file) { |
| smrp.setMessage(Bundle.PROMPT_AskOpenProject(dispName)); |
| } else { |
| smrp.setMessage(Bundle.PROMPT_AskOpenProjectForFile(file.getNameExt(), 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, false); |
| }); |
| }); |
| } |
| return f.thenApply(arr -> arr.length > 0 ? arr[0] : null); |
| }); |
| } |
| |
| public List<FileObject> getAcceptedWorkspaceFolders() { |
| return acceptedWorkspaceFolders; |
| } |
| |
| /** |
| * For diagnostic purposes |
| */ |
| private AtomicInteger openRequestId = new AtomicInteger(1); |
| |
| private void asyncOpenSelectedProjects0(CompletableFuture<Project[]> f, List<FileObject> projectCandidates, boolean asWorkspaceProjects, boolean validParents) { |
| List<Project> projects = new ArrayList<>(); |
| List<FileObject> nonProjects = new ArrayList<>(); |
| List<FileObject> haveProjects = new ArrayList<>(); |
| try { |
| if (projectCandidates != null) { |
| for (FileObject candidate : projectCandidates) { |
| Project prj = FileOwnerQuery.getOwner(candidate); |
| if (prj != null) { |
| projects.add(prj); |
| haveProjects.add(prj.getProjectDirectory()); |
| } else if (validParents && candidate.isFolder()) { |
| nonProjects.add(candidate); |
| } |
| } |
| |
| synchronized (this) { |
| boolean nwp = asWorkspaceProjects; |
| for (FileObject pd : haveProjects) { |
| for (FileObject wf : new ArrayList<>(acceptedWorkspaceFolders)) { |
| if (wf.equals(pd) || FileUtil.isParentOf(pd, wf)) { |
| LOG.log(Level.FINE, "Nonproject workspace folder turned to project: {0}", projectCandidates.get(0)); |
| acceptedWorkspaceFolders.remove(wf); |
| // we should call asyncOpenSelectedProjects1 twice, once to add to workspace, once to not add - |
| // but it should only happen with single-file open = just one project. |
| if (projectCandidates.size() == 1) { |
| nwp = true; |
| } |
| } |
| } |
| } |
| A: for (FileObject np : nonProjects) { |
| for (FileObject c : acceptedWorkspaceFolders) { |
| if (c.equals(np) || FileUtil.isParentOf(c, np)) { |
| continue A; |
| } |
| } |
| LOG.log(Level.FINE, "Not recognized as a project, but accepting as workspace : {0}", np); |
| acceptedWorkspaceFolders.add(np); |
| } |
| asWorkspaceProjects = nwp; |
| } |
| } |
| Project[] previouslyOpened; |
| try { |
| previouslyOpened = OpenProjects.getDefault().openProjects().get(); |
| if (previouslyOpened.length > 0) { |
| Level level = Level.FINEST; |
| assert (level = Level.CONFIG) != null; |
| for (Project p : previouslyOpened) { |
| LOG.log(level, "Previously opened project at {0}", p.getProjectDirectory()); |
| } |
| } |
| } catch (InterruptedException | ExecutionException ex) { |
| throw new IllegalStateException(ex); |
| |
| } |
| asyncOpenSelectedProjects1(f, previouslyOpened, projects, asWorkspaceProjects); |
| } catch (RuntimeException ex) { |
| f.completeExceptionally(ex); |
| } |
| } |
| |
| private void asyncOpenSelectedProjects1(CompletableFuture<Project[]> f, Project[] previouslyOpened, List<Project> projects, boolean addToWorkspace) { |
| int id = this.openRequestId.getAndIncrement(); |
| |
| List<CompletableFuture> primingBuilds = new ArrayList<>(); |
| List<Project> toOpen = new ArrayList<>(); |
| Map<Project, CompletableFuture<Void>> local = new HashMap<>(); |
| synchronized (this) { |
| LOG.log(Level.FINER, "{0}: Asked to open project(s): {1}", new Object[]{ id, Arrays.asList(projects) }); |
| for (Project p : projects) { |
| CompletableFuture<Void> pending = beingOpened.get(p); |
| if (pending != null) { |
| primingBuilds.add(pending); |
| } else { |
| toOpen.add(p); |
| local.put(p, new CompletableFuture<Void>()); |
| } |
| } |
| beingOpened.putAll(local); |
| } |
| |
| LOG.log(Level.FINER, id + ": Opening projects: {0}", Arrays.asList(toOpen)); |
| |
| // before the projects are officialy 'opened', try to prime the projects |
| for (Project p : toOpen) { |
| ActionProvider pap = p.getLookup().lookup(ActionProvider.class); |
| if (pap == null) { |
| LOG.log(Level.FINER, "{0}: No action provider at all !", id); |
| continue; |
| } |
| if (!Arrays.asList(pap.getSupportedActions()).contains(ActionProvider.COMMAND_PRIME)) { |
| LOG.log(Level.FINER, "{0}: No action provider gives PRIME", id); |
| // this may take some while; so better call outside of any locks. |
| continue; |
| } |
| 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 = new CompletableFuture<>(); |
| LOG.log(Level.FINER, "{0}: Found enabled Priming build for: {1}", new Object[]{id, p}); |
| ActionProgress progress = new ActionProgress() { |
| @Override |
| protected void started() {} |
| |
| @Override |
| public void finished(boolean success) { |
| LOG.log(Level.FINER, id + ": Priming build completed for project " + p); |
| primeF.complete(null); |
| } |
| }; |
| primingBuilds.add(primeF); |
| |
| pap.invokeAction(ActionProvider.COMMAND_PRIME, Lookups.fixed(progress)); |
| } |
| } |
| |
| // Wait for all priming builds, even those already pending, to finish: |
| CompletableFuture.allOf(primingBuilds.toArray(new CompletableFuture[primingBuilds.size()])).thenRun(() -> { |
| OpenProjects.getDefault().open(projects.toArray(new Project[0]), false); |
| try { |
| LOG.log(Level.FINER, "{0}: Calling openProjects() for : {1}", new Object[]{id, Arrays.asList(projects)}); |
| OpenProjects.getDefault().openProjects().get(); |
| } catch (InterruptedException | ExecutionException ex) { |
| throw new IllegalStateException(ex); |
| } |
| 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) { |
| openedProjects = projectSet; |
| if (addToWorkspace) { |
| Set<Project> ns = new HashSet<>(projects); |
| List<Project> current = Arrays.asList(workspaceProjects.getNow(new Project[0])); |
| int s = current.size(); |
| ns.addAll(current); |
| 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 -> { |
| f.completeExceptionally(e); |
| return null; |
| }); |
| } |
| |
| private JavaSource checkJavaSupport() { |
| final ClasspathInfo info = ClasspathInfo.create(ClassPath.EMPTY, ClassPath.EMPTY, ClassPath.EMPTY); |
| final JavaSource source = JavaSource.create(info); |
| if (source == null) { |
| SERVER_INIT_RP.post(() -> { |
| final String msg = NO_JAVA_SUPPORT + System.getProperty("java.version"); |
| showStatusBarMessage(MessageType.Error, msg, 5000); |
| }); |
| } |
| return source; |
| } |
| |
| @Override |
| public CompletableFuture<Project[]> openedProjects() { |
| return workspaceProjects; |
| } |
| |
| @Override |
| public OpenedDocuments getOpenedDocuments() { |
| return openedDocuments; |
| } |
| |
| private JavaSource showIndexingCompleted(Project[] opened) { |
| try { |
| final JavaSource source = checkJavaSupport(); |
| if (source != null) { |
| source.runWhenScanFinished(cc -> { |
| showStatusBarMessage(MessageType.Info, INDEXING_COMPLETED, 0); |
| }, true); |
| } |
| return source; |
| } catch (IOException ex) { |
| throw new IllegalStateException(ex); |
| } |
| } |
| |
| private void showStatusBarMessage(final MessageType type, final String msg, int timeout) { |
| if (client.getNbCodeCapabilities().hasStatusBarMessageSupport()) { |
| client.showStatusBarMessage(new ShowStatusMessageParams(type, msg, timeout)); |
| } else { |
| client.showMessage(new ShowStatusMessageParams(type, msg, timeout)); |
| } |
| } |
| |
| private InitializeResult constructInitResponse(InitializeParams init, JavaSource src) { |
| ServerCapabilities capabilities = new ServerCapabilities(); |
| if (src != null) { |
| TextDocumentSyncOptions textDocumentSyncOptions = new TextDocumentSyncOptions(); |
| textDocumentSyncOptions.setChange(TextDocumentSyncKind.Incremental); |
| textDocumentSyncOptions.setOpenClose(true); |
| textDocumentSyncOptions.setWillSaveWaitUntil(true); |
| capabilities.setTextDocumentSync(textDocumentSyncOptions); |
| CompletionOptions completionOptions = new CompletionOptions(); |
| completionOptions.setResolveProvider(true); |
| completionOptions.setTriggerCharacters(Arrays.asList(".", "#", "@", "*")); |
| capabilities.setCompletionProvider(completionOptions); |
| capabilities.setHoverProvider(true); |
| CodeActionOptions codeActionOptions = new CodeActionOptions(Arrays.asList(CodeActionKind.QuickFix, CodeActionKind.Source, CodeActionKind.SourceOrganizeImports, CodeActionKind.Refactor)); |
| codeActionOptions.setResolveProvider(true); |
| capabilities.setCodeActionProvider(codeActionOptions); |
| capabilities.setDocumentSymbolProvider(true); |
| capabilities.setDefinitionProvider(true); |
| capabilities.setTypeDefinitionProvider(true); |
| capabilities.setImplementationProvider(true); |
| capabilities.setDocumentHighlightProvider(true); |
| capabilities.setDocumentFormattingProvider(true); |
| capabilities.setDocumentRangeFormattingProvider(true); |
| capabilities.setReferencesProvider(true); |
| |
| CallHierarchyRegistrationOptions chOpts = new CallHierarchyRegistrationOptions(); |
| chOpts.setWorkDoneProgress(true); |
| capabilities.setCallHierarchyProvider(chOpts); |
| List<String> commands = new ArrayList<>(Arrays.asList(GRAALVM_PAUSE_SCRIPT, |
| JAVA_BUILD_WORKSPACE, |
| JAVA_CLEAN_WORKSPACE, |
| JAVA_RUN_PROJECT_ACTION, |
| JAVA_FIND_DEBUG_ATTACH_CONFIGURATIONS, |
| JAVA_FIND_DEBUG_PROCESS_TO_ATTACH, |
| JAVA_FIND_PROJECT_CONFIGURATIONS, |
| JAVA_GET_PROJECT_CLASSPATH, |
| JAVA_GET_PROJECT_PACKAGES, |
| JAVA_GET_PROJECT_SOURCE_ROOTS, |
| JAVA_LOAD_WORKSPACE_TESTS, |
| JAVA_NEW_FROM_TEMPLATE, |
| JAVA_NEW_PROJECT, |
| JAVA_PROJECT_CONFIGURATION_COMPLETION, |
| JAVA_PROJECT_RESOLVE_PROJECT_PROBLEMS, |
| JAVA_SUPER_IMPLEMENTATION, |
| JAVA_SOURCE_FOR, |
| JAVA_CLEAR_PROJECT_CACHES, |
| NATIVE_IMAGE_FIND_DEBUG_PROCESS_TO_ATTACH)); |
| for (CodeActionsProvider codeActionsProvider : Lookup.getDefault().lookupAll(CodeActionsProvider.class)) { |
| commands.addAll(codeActionsProvider.getCommands()); |
| } |
| capabilities.setExecuteCommandProvider(new ExecuteCommandOptions(commands)); |
| capabilities.setWorkspaceSymbolProvider(true); |
| capabilities.setCodeLensProvider(new CodeLensOptions(false)); |
| RenameOptions renOpt = new RenameOptions(); |
| renOpt.setPrepareProvider(true); |
| capabilities.setRenameProvider(renOpt); |
| FoldingRangeProviderOptions foldingOptions = new FoldingRangeProviderOptions(); |
| capabilities.setFoldingRangeProvider(foldingOptions); |
| textDocumentService.init(init.getCapabilities(), capabilities); |
| } |
| return new InitializeResult(capabilities); |
| } |
| |
| @Override |
| public CompletableFuture<InitializeResult> initialize(InitializeParams init) { |
| NbCodeClientCapabilities capa = NbCodeClientCapabilities.get(init); |
| client.setClientCaps(capa); |
| hackConfigureGroovySupport(capa); |
| List<FileObject> projectCandidates = new ArrayList<>(); |
| List<WorkspaceFolder> folders = init.getWorkspaceFolders(); |
| if (folders != null) { |
| for (WorkspaceFolder w : folders) { |
| try { |
| projectCandidates.add(Utils.fromUri(w.getUri())); |
| } catch (MalformedURLException ex) { |
| LOG.log(Level.FINE, null, ex); |
| } |
| } |
| } else { |
| String root = init.getRootUri(); |
| |
| if (root != null) { |
| try { |
| projectCandidates.add(Utils.fromUri(root)); |
| } catch (MalformedURLException ex) { |
| LOG.log(Level.FINE, null, ex); |
| } |
| } else { |
| //TODO: use getRootPath()? |
| } |
| } |
| CompletableFuture<Project[]> prjs = workspaceProjects; |
| SERVER_INIT_RP.post(() -> asyncOpenSelectedProjects0(prjs, projectCandidates, true, true)); |
| |
| // chain showIndexingComplete message after initial project open. |
| prjs. |
| thenApply(this::showIndexingCompleted); |
| |
| initializeOptions(); |
| |
| // but complete the InitializationRequest independently of the project initialization. |
| return CompletableFuture.completedFuture( |
| finishInitialization( |
| constructInitResponse(init, checkJavaSupport()) |
| ) |
| ); |
| } |
| |
| private void initializeOptions() { |
| getWorkspaceProjects().thenAccept(projects -> { |
| if (projects != null && projects.length > 0) { |
| ConfigurationItem item = new ConfigurationItem(); |
| FileObject fo = projects[0].getProjectDirectory(); |
| item.setScopeUri(Utils.toUri(fo)); |
| item.setSection(NETBEANS_FORMAT); |
| client.configuration(new ConfigurationParams(Collections.singletonList(item))).thenAccept(c -> { |
| if (c != null && !c.isEmpty() && c.get(0) instanceof JsonObject) { |
| workspaceService.updateJavaFormatPreferences(fo, (JsonObject) c.get(0)); |
| } |
| }); |
| item.setSection(NETBEANS_JAVA_IMPORTS); |
| client.configuration(new ConfigurationParams(Collections.singletonList(item))).thenAccept(c -> { |
| if (c != null && !c.isEmpty() && c.get(0) instanceof JsonObject) { |
| workspaceService.updateJavaImportPreferences(fo, (JsonObject) c.get(0)); |
| } |
| }); |
| } |
| }); |
| } |
| |
| public CompletableFuture<Project[]> getWorkspaceProjects() { |
| return workspaceProjects; |
| } |
| |
| public InitializeResult finishInitialization(InitializeResult res) { |
| OperationContext c = OperationContext.find(sessionLookup); |
| // discard the progress token as it is going to be invalid anyway. Further pending |
| // initializations need to create its own tokens. |
| c.acquireProgressToken(); |
| return res; |
| } |
| |
| @Override |
| public CompletableFuture<Object> shutdown() { |
| return CompletableFuture.completedFuture(null); |
| } |
| |
| @Override |
| public void exit() { |
| } |
| |
| @JsonDelegate |
| public TreeViewService getTreeViewService() { |
| return treeService; |
| } |
| |
| @Override |
| public TextDocumentService getTextDocumentService() { |
| return textDocumentService; |
| } |
| |
| @Override |
| public WorkspaceService getWorkspaceService() { |
| return workspaceService; |
| } |
| |
| @Override |
| public void cancelProgress(WorkDoneProgressCancelParams params) { |
| // handled in the interceptor, after the complete RPC call completes. |
| } |
| |
| @Override |
| public void connect(LanguageClient aClient) { |
| this.client = new NbCodeClientWrapper((NbCodeLanguageClient)aClient); |
| sessionServices.add(this); |
| sessionServices.add(client); |
| sessionServices.add(new WorkspaceIOContext() { |
| @Override |
| protected LanguageClient client() { |
| return client; |
| } |
| }); |
| sessionServices.add(new WorkspaceUIContext(client)); |
| sessionServices.add(treeService.getNodeRegistry()); |
| ((LanguageClientAware) getTextDocumentService()).connect(client); |
| ((LanguageClientAware) getWorkspaceService()).connect(client); |
| ((LanguageClientAware) treeService).connect(client); |
| } |
| } |
| |
| public static final String JAVA_BUILD_WORKSPACE = "java.build.workspace"; |
| public static final String JAVA_CLEAN_WORKSPACE = "java.clean.workspace"; |
| public static final String JAVA_NEW_FROM_TEMPLATE = "java.new.from.template"; |
| public static final String JAVA_NEW_PROJECT = "java.new.project"; |
| public static final String JAVA_GET_PROJECT_SOURCE_ROOTS = "java.get.project.source.roots"; |
| public static final String JAVA_GET_PROJECT_CLASSPATH = "java.get.project.classpath"; |
| public static final String JAVA_GET_PROJECT_PACKAGES = "java.get.project.packages"; |
| public static final String JAVA_LOAD_WORKSPACE_TESTS = "java.load.workspace.tests"; |
| public static final String JAVA_SUPER_IMPLEMENTATION = "java.super.implementation"; |
| public static final String JAVA_SOURCE_FOR = "java.source.for"; |
| public static final String GRAALVM_PAUSE_SCRIPT = "graalvm.pause.script"; |
| public static final String JAVA_RUN_PROJECT_ACTION = "java.project.run.action"; |
| |
| /** |
| * Enumerates project configurations. |
| */ |
| public static final String JAVA_FIND_PROJECT_CONFIGURATIONS = "java.project.configurations"; |
| /** |
| * Enumerates attach debugger configurations. |
| */ |
| public static final String JAVA_FIND_DEBUG_ATTACH_CONFIGURATIONS = "java.attachDebugger.configurations"; |
| /** |
| * Enumerates JVM processes eligible for debugger attach. |
| */ |
| public static final String JAVA_FIND_DEBUG_PROCESS_TO_ATTACH = "java.attachDebugger.pickProcess"; |
| /** |
| * Enumerates native processes eligible for debugger attach. |
| */ |
| public static final String NATIVE_IMAGE_FIND_DEBUG_PROCESS_TO_ATTACH = "nativeImage.attachDebugger.pickProcess"; |
| /** |
| * Provides code-completion of configurations. |
| */ |
| public static final String JAVA_PROJECT_CONFIGURATION_COMPLETION = "java.project.configuration.completion"; |
| /** |
| * Provides resolution of project problems. |
| */ |
| public static final String JAVA_PROJECT_RESOLVE_PROJECT_PROBLEMS = "java.project.resolveProjectProblems"; |
| |
| |
| /** |
| * Diagnostic / test command: clears NBLS internal project caches. Useful between testcases and after |
| * new project files were generated into workspace subtree. |
| */ |
| public static final String JAVA_CLEAR_PROJECT_CACHES = "java.clear.project.caches"; |
| |
| static final String INDEXING_COMPLETED = "Indexing completed."; |
| static final String NO_JAVA_SUPPORT = "Cannot initialize Java support on JDK "; |
| |
| static final NbCodeLanguageClient STUB_CLIENT = new NbCodeLanguageClient() { |
| private final NbCodeClientCapabilities caps = new NbCodeClientCapabilities(); |
| |
| private void logWarning(Object... args) { |
| LOG.log(Level.WARNING, "LSP Client called without proper context with param(s): {0}", |
| Arrays.asList(args)); |
| } |
| |
| @Override |
| public void showStatusBarMessage(ShowStatusMessageParams params) { |
| logWarning(params); |
| } |
| |
| @Override |
| public CompletableFuture<List<QuickPickItem>> showQuickPick(ShowQuickPickParams params) { |
| logWarning(params); |
| return CompletableFuture.completedFuture(params.getCanPickMany() || params.getItems().isEmpty() ? params.getItems() : Collections.singletonList(params.getItems().get(0))); |
| } |
| |
| @Override |
| public CompletableFuture<String> showInputBox(ShowInputBoxParams params) { |
| logWarning(params); |
| return CompletableFuture.completedFuture(params.getValue()); |
| } |
| |
| @Override |
| public void notifyTestProgress(TestProgressParams params) { |
| logWarning(params); |
| } |
| |
| @Override |
| public NbCodeClientCapabilities getNbCodeCapabilities() { |
| logWarning(); |
| return caps; |
| } |
| |
| @Override |
| public void telemetryEvent(Object object) { |
| logWarning(object); |
| } |
| |
| @Override |
| public void publishDiagnostics(PublishDiagnosticsParams diagnostics) { |
| logWarning(diagnostics); |
| } |
| |
| @Override |
| public void showMessage(MessageParams messageParams) { |
| logWarning(messageParams); |
| } |
| |
| @Override |
| public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams requestParams) { |
| logWarning(requestParams); |
| CompletableFuture<MessageActionItem> x = new CompletableFuture<>(); |
| x.complete(null); |
| return x; |
| } |
| |
| @Override |
| public CompletableFuture<String> createTextEditorDecoration(DecorationRenderOptions params) { |
| logWarning(params); |
| CompletableFuture<String> x = new CompletableFuture<>(); |
| x.complete(null); |
| return x; |
| } |
| |
| @Override |
| public void setTextEditorDecoration(SetTextEditorDecorationParams params) { |
| logWarning(params); |
| } |
| |
| @Override |
| public void disposeTextEditorDecoration(String params) { |
| logWarning(params); |
| } |
| |
| @Override |
| public void logMessage(MessageParams message) { |
| logWarning(message); |
| } |
| |
| @Override |
| public void notifyNodeChange(NodeChangedParams params) { |
| logWarning(params); |
| } |
| |
| @Override |
| public CompletableFuture<String> showHtmlPage(HtmlPageParams params) { |
| logWarning(params); |
| return CompletableFuture.completedFuture(null); |
| } |
| |
| @Override |
| public CompletableFuture<Void> configurationUpdate(UpdateConfigParams params) { |
| logWarning(params); |
| return CompletableFuture.completedFuture(null); |
| } |
| }; |
| |
| |
| private static boolean groovyClassWarningLogged; |
| |
| /** |
| * Hacky way to enable or disable Groovy support. Since it is hack, it will disable Groovy for the whole NBJLS, not just a specific client / project. Should |
| * be revisited after NetBeans 12.5, after Groovy parsing improves |
| * @param caps |
| */ |
| private static void hackConfigureGroovySupport(NbCodeClientCapabilities caps) { |
| boolean b = caps != null && caps.wantsGroovySupport(); |
| try { |
| Class clazz = Lookup.getDefault().lookup(ClassLoader.class).loadClass("org.netbeans.modules.groovy.editor.api.GroovyIndexer"); |
| Method m = clazz.getDeclaredMethod("setIndexingEnabled", Boolean.TYPE); |
| m.setAccessible(true); |
| m.invoke(null, b); |
| } catch (ReflectiveOperationException ex) { |
| if (!groovyClassWarningLogged) { |
| groovyClassWarningLogged = true; |
| LOG.log(Level.WARNING, "Unable to configure Groovy support", ex); |
| } |
| } |
| } |
| } |