| /* |
| * 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.net.MalformedURLException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.Future; |
| import java.util.function.Function; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import org.eclipse.lsp4j.CodeActionKind; |
| import org.eclipse.lsp4j.CodeActionOptions; |
| import org.eclipse.lsp4j.CodeLensOptions; |
| import org.eclipse.lsp4j.CompletionOptions; |
| 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.ServerCapabilities; |
| import org.eclipse.lsp4j.ShowMessageRequestParams; |
| import org.eclipse.lsp4j.TextDocumentSyncKind; |
| import org.eclipse.lsp4j.WorkDoneProgressCancelParams; |
| import org.eclipse.lsp4j.WorkDoneProgressParams; |
| import org.eclipse.lsp4j.WorkspaceFolder; |
| 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.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.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.ProjectUtils; |
| import org.netbeans.api.project.Sources; |
| import org.netbeans.api.project.ui.OpenProjects; |
| import org.netbeans.modules.java.lsp.server.Utils; |
| import org.netbeans.modules.java.lsp.server.progress.OperationContext; |
| import org.netbeans.modules.progress.spi.InternalHandle; |
| import org.openide.filesystems.FileObject; |
| import org.openide.util.Exceptions; |
| import org.openide.util.Lookup; |
| 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 void launchServer(InputStream in, OutputStream out) { |
| LanguageServerImpl server = new LanguageServerImpl(); |
| ConsumeWithLookup msgProcessor = new ConsumeWithLookup(server.getSessionLookup()); |
| Launcher<NbCodeLanguageClient> serverLauncher = createLauncher(server, in, out, msgProcessor::attachLookup); |
| NbCodeLanguageClient remote = serverLauncher.getRemoteProxy(); |
| ((LanguageClientAware) server).connect(remote); |
| msgProcessor.attachClient(server.client); |
| Future<Void> runningServer = serverLauncher.startListening(); |
| try { |
| runningServer.get(); |
| } catch (InterruptedException | ExecutionException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| |
| private static Launcher<NbCodeLanguageClient> createLauncher(LanguageServerImpl server, InputStream in, OutputStream out, |
| Function<MessageConsumer, MessageConsumer> processor) { |
| return new LSPLauncher.Builder<NbCodeLanguageClient>() |
| .setLocalService(server) |
| .setRemoteInterface(NbCodeLanguageClient.class) |
| .setInput(in) |
| .setOutput(out) |
| .wrapMessages(processor) |
| // .traceMessages(new java.io.PrintWriter(System.err)) |
| .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. |
| 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(); |
| } |
| } |
| }; |
| } |
| } |
| |
| // 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 class LanguageServerImpl implements LanguageServer, LanguageClientAware { |
| |
| private static final Logger LOG = Logger.getLogger(LanguageServerImpl.class.getName()); |
| private NbCodeClientWrapper client; |
| private final TextDocumentService textDocumentService = new TextDocumentServiceImpl(); |
| private final WorkspaceService workspaceService = new WorkspaceServiceImpl(); |
| private final InstanceContent sessionServices = new InstanceContent(); |
| private final Lookup sessionLookup = new ProxyLookup( |
| new AbstractLookup(sessionServices), |
| Lookup.getDefault() |
| ); |
| |
| Lookup getSessionLookup() { |
| return sessionLookup; |
| } |
| |
| private void asyncOpenSelectedProjects(CompletableFuture f, List<FileObject> projectCandidates) { |
| List<Project> projects = new ArrayList<>(); |
| try { |
| for (FileObject candidate : projectCandidates) { |
| Project prj = FileOwnerQuery.getOwner(candidate); |
| if (prj != null) { |
| projects.add(prj); |
| } |
| } |
| try { |
| Project[] 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); |
| } |
| OpenProjects.getDefault().open(projects.toArray(new Project[0]), false); |
| try { |
| 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); |
| } |
| Project[] prjs = projects.toArray(new Project[projects.size()]); |
| f.complete(prjs); |
| } catch (RuntimeException ex) { |
| f.completeExceptionally(ex); |
| } |
| } |
| |
| private JavaSource showIndexingCompleted(Project[] opened) { |
| try { |
| 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); |
| }); |
| } else { |
| 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(JavaSource src) { |
| ServerCapabilities capabilities = new ServerCapabilities(); |
| if (src != null) { |
| capabilities.setTextDocumentSync(TextDocumentSyncKind.Incremental); |
| CompletionOptions completionOptions = new CompletionOptions(); |
| completionOptions.setResolveProvider(true); |
| completionOptions.setTriggerCharacters(Collections.singletonList(".")); |
| capabilities.setCompletionProvider(completionOptions); |
| capabilities.setHoverProvider(true); |
| capabilities.setCodeActionProvider(new CodeActionOptions(Arrays.asList(CodeActionKind.QuickFix, CodeActionKind.Source))); |
| capabilities.setDocumentSymbolProvider(true); |
| capabilities.setDefinitionProvider(true); |
| capabilities.setDocumentHighlightProvider(true); |
| capabilities.setReferencesProvider(true); |
| List<String> commands = new ArrayList<>(Arrays.asList( |
| JAVA_BUILD_WORKSPACE, GRAALVM_PAUSE_SCRIPT, |
| JAVA_TEST_SINGLE_METHOD, JAVA_RUN_MAIN_METHOD)); |
| for (CodeGenerator codeGenerator : Lookup.getDefault().lookupAll(CodeGenerator.class)) { |
| commands.addAll(codeGenerator.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); |
| } |
| return new InitializeResult(capabilities); |
| } |
| |
| @Override |
| public CompletableFuture<InitializeResult> initialize(InitializeParams init) { |
| NbCodeClientCapabilities capa = NbCodeClientCapabilities.get(init); |
| client.setClientCaps(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[]> fProjects = new CompletableFuture<>(); |
| SERVER_INIT_RP.post(() -> asyncOpenSelectedProjects(fProjects, projectCandidates)); |
| |
| return fProjects. |
| thenApply(this::showIndexingCompleted). |
| thenApply(this::constructInitResponse). |
| thenApply(this::finishInitialization); |
| } |
| |
| 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() { |
| } |
| |
| @Override |
| public TextDocumentService getTextDocumentService() { |
| return textDocumentService; |
| } |
| |
| @Override |
| public WorkspaceService getWorkspaceService() { |
| return workspaceService; |
| } |
| |
| @Override |
| public void connect(LanguageClient aClient) { |
| this.client = new NbCodeClientWrapper((NbCodeLanguageClient)aClient); |
| sessionServices.add(client); |
| sessionServices.add(new WorkspaceIOContext() { |
| @Override |
| protected LanguageClient client() { |
| return client; |
| } |
| }); |
| sessionServices.add(new WorkspaceUIContext(client)); |
| ((LanguageClientAware) getTextDocumentService()).connect(aClient); |
| ((LanguageClientAware) getWorkspaceService()).connect(aClient); |
| } |
| } |
| |
| public static final String JAVA_BUILD_WORKSPACE = "java.build.workspace"; |
| public static final String JAVA_TEST_SINGLE_METHOD = "java.test.single.method"; |
| public static final String JAVA_RUN_MAIN_METHOD = "java.run.main.method"; |
| public static final String GRAALVM_PAUSE_SCRIPT = "graalvm.pause.script"; |
| 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 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 void logMessage(MessageParams message) { |
| logWarning(message); |
| } |
| }; |
| } |