From dd251608f5584574acb29a548d30c72458ac0c9d Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 9 Oct 2024 15:57:25 +0200 Subject: [PATCH] Adding opt-in to use the api.lsp services to fill in language features for a given mime-type. --- ide/api.lsp/apichanges.xml | 14 + ide/api.lsp/manifest.mf | 2 +- ide/api.lsp/nbproject/project.xml | 1 + .../api/lsp/bridge/RegisterLSPServices.java | 41 ++ .../bridge/RegisterLSPServicesProcessor.java | 70 +++ ide/lsp.client/nbproject/project.properties | 2 +- .../modules/lsp/client/LSPBindings.java | 102 +++-- .../LanguageServerProviderAccessor.java | 4 + .../bindings/CompletionProviderImpl.java | 15 +- .../client/bridge/BridgingLanguageServer.java | 415 ++++++++++++++++++ .../BridgingLanguageServerProvider.java | 36 ++ .../client/spi/LanguageServerProvider.java | 21 +- .../hints/lsp/HintsDiagnosticsProvider.java | 1 + 13 files changed, 670 insertions(+), 54 deletions(-) create mode 100644 ide/api.lsp/src/org/netbeans/api/lsp/bridge/RegisterLSPServices.java create mode 100644 ide/api.lsp/src/org/netbeans/modules/lsp/bridge/RegisterLSPServicesProcessor.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/bridge/BridgingLanguageServer.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/bridge/BridgingLanguageServerProvider.java diff --git a/ide/api.lsp/apichanges.xml b/ide/api.lsp/apichanges.xml index 290b3137fc95..80fd7d1766f0 100644 --- a/ide/api.lsp/apichanges.xml +++ b/ide/api.lsp/apichanges.xml @@ -51,6 +51,20 @@ + + + Adding RegisterLSPServices annotation + + + + + + A RegisterLSPServices annotation is + introduced that allows to use the implementation of the services defined in this + module inside the IDE. + + + Adding CodeActionProvider.getSupportedCodeActionKinds method diff --git a/ide/api.lsp/manifest.mf b/ide/api.lsp/manifest.mf index e11e4f0b21b3..2db50f88cbd1 100644 --- a/ide/api.lsp/manifest.mf +++ b/ide/api.lsp/manifest.mf @@ -1,5 +1,5 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.api.lsp/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/api/lsp/Bundle.properties -OpenIDE-Module-Specification-Version: 1.29 +OpenIDE-Module-Specification-Version: 1.30 AutoUpdate-Show-In-Client: false diff --git a/ide/api.lsp/nbproject/project.xml b/ide/api.lsp/nbproject/project.xml index 33244b33fb14..f4a590c3f07e 100644 --- a/ide/api.lsp/nbproject/project.xml +++ b/ide/api.lsp/nbproject/project.xml @@ -108,6 +108,7 @@ org.netbeans.api.lsp + org.netbeans.api.lsp.bridge org.netbeans.spi.lsp diff --git a/ide/api.lsp/src/org/netbeans/api/lsp/bridge/RegisterLSPServices.java b/ide/api.lsp/src/org/netbeans/api/lsp/bridge/RegisterLSPServices.java new file mode 100644 index 000000000000..7787351da52a --- /dev/null +++ b/ide/api.lsp/src/org/netbeans/api/lsp/bridge/RegisterLSPServices.java @@ -0,0 +1,41 @@ +/* + * 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.api.lsp.bridge; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * For every mime-type specified in this annotation, the implementations of the + * services defined in this module will be looked up and used to provide + * the IDE features inside the IDE. + * + * @since 1.30 + */ +@Target(ElementType.PACKAGE) +public @interface RegisterLSPServices { + + /** + * The mime-types for which the implementations of this module's services + * should be used. + * + * @return the mime-types + */ + public String[] mimeTypes(); +} diff --git a/ide/api.lsp/src/org/netbeans/modules/lsp/bridge/RegisterLSPServicesProcessor.java b/ide/api.lsp/src/org/netbeans/modules/lsp/bridge/RegisterLSPServicesProcessor.java new file mode 100644 index 000000000000..a1cc606a3a8b --- /dev/null +++ b/ide/api.lsp/src/org/netbeans/modules/lsp/bridge/RegisterLSPServicesProcessor.java @@ -0,0 +1,70 @@ +/* + * 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.lsp.bridge; + +import java.util.HashSet; +import java.util.Set; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import org.netbeans.api.lsp.bridge.RegisterLSPServices; +import org.openide.filesystems.annotations.LayerBuilder; +import org.openide.filesystems.annotations.LayerBuilder.File; +import org.openide.filesystems.annotations.LayerGeneratingProcessor; +import org.openide.filesystems.annotations.LayerGenerationException; +import org.openide.util.lookup.ServiceProvider; + +@ServiceProvider(service=Processor.class) +public final class RegisterLSPServicesProcessor extends LayerGeneratingProcessor { + + @Override + public Set getSupportedAnnotationTypes() { + Set hash = new HashSet(); + hash.add(RegisterLSPServices.class.getCanonicalName()); + return hash; + } + + @Override + protected boolean handleProcess( + Set annotations, RoundEnvironment roundEnv + ) throws LayerGenerationException { + for (Element e : roundEnv.getElementsAnnotatedWith(RegisterLSPServices.class)) { + RegisterLSPServices services = (RegisterLSPServices) e.getAnnotation(RegisterLSPServices.class); + if (services == null) { + continue; + } + LayerBuilder builder = layer(e); + for (String mimeType : services.mimeTypes()) { + File provider = builder.file("Editors/" + mimeType + "/org-netbeans-modules-lsp-client-bridge-BridgingLanguageServerProvider.instance"); + provider.stringvalue("instanceOf", "org.netbeans.modules.lsp.client.spi.LanguageServerProvider"); + provider.write(); + File breadcrumbs = builder.file("Editors/" + mimeType + "/SideBar/breadcrumbs.instance"); + breadcrumbs.stringvalue("location", "South"); + breadcrumbs.intvalue("position", 5237); + breadcrumbs.boolvalue("scrollable", false); + breadcrumbs.methodvalue("instanceCreate", "org.netbeans.modules.editor.breadcrumbs.spi.BreadcrumbsController", "createSideBarFactory"); + breadcrumbs.write(); + } + } + return true; + } + +} diff --git a/ide/lsp.client/nbproject/project.properties b/ide/lsp.client/nbproject/project.properties index a4e606ad33a7..1a90e0b6e615 100644 --- a/ide/lsp.client/nbproject/project.properties +++ b/ide/lsp.client/nbproject/project.properties @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -javac.source=1.8 +javac.release=17 javac.compilerargs=-Xlint -Xlint:-serial javadoc.arch=${basedir}/arch.xml release.external/org.eclipse.lsp4j-0.13.0.jar=modules/ext/org.eclipse.lsp4j-0.13.0.jar diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java index b7be9b7a2fc5..b7d56e6108d4 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java @@ -71,6 +71,7 @@ import org.eclipse.lsp4j.WorkspaceEditCapabilities; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.launch.LSPLauncher; +import org.eclipse.lsp4j.services.LanguageClientAware; import org.eclipse.lsp4j.services.LanguageServer; import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.WorkspaceService; @@ -303,55 +304,64 @@ private static LSPBindings buildBindings(ServerDescription inDescription, Projec foundServer = true; try { LanguageClientImpl lci = new LanguageClientImpl(); - InputStream in = LanguageServerProviderAccessor.getINSTANCE().getInputStream(desc); - OutputStream out = LanguageServerProviderAccessor.getINSTANCE().getOutputStream(desc); - Process p = LanguageServerProviderAccessor.getINSTANCE().getProcess(desc); - Launcher.Builder launcherBuilder = new LSPLauncher.Builder() - .setLocalService(lci) - .setRemoteInterface(LanguageServer.class) - .setInput(in) - .setOutput(out) - .configureGson(gson -> { - gson.registerTypeAdapter(SemanticTokensLegend.class, new InstanceCreator() { - @Override - public SemanticTokensLegend createInstance(Type type) { - return new SemanticTokensLegend(Collections.emptyList(), Collections.emptyList()); - } + LanguageServer server = LanguageServerProviderAccessor.getINSTANCE().getServer(desc); + Process process; + if (server == null) { + InputStream in = LanguageServerProviderAccessor.getINSTANCE().getInputStream(desc); + OutputStream out = LanguageServerProviderAccessor.getINSTANCE().getOutputStream(desc); + process = LanguageServerProviderAccessor.getINSTANCE().getProcess(desc); + Launcher.Builder launcherBuilder = new LSPLauncher.Builder() + .setLocalService(lci) + .setRemoteInterface(LanguageServer.class) + .setInput(in) + .setOutput(out) + .configureGson(gson -> { + gson.registerTypeAdapter(SemanticTokensLegend.class, new InstanceCreator() { + @Override + public SemanticTokensLegend createInstance(Type type) { + return new SemanticTokensLegend(Collections.emptyList(), Collections.emptyList()); + } + }); + gson.registerTypeAdapter(SemanticTokens.class, new InstanceCreator() { + @Override + public SemanticTokens createInstance(Type type) { + return new SemanticTokens(Collections.emptyList()); + } + }); }); - gson.registerTypeAdapter(SemanticTokens.class, new InstanceCreator() { - @Override - public SemanticTokens createInstance(Type type) { - return new SemanticTokens(Collections.emptyList()); - } - }); - }); - if (LOG.isLoggable(Level.FINER)) { - PrintWriter pw = new PrintWriter(new Writer() { - StringBuffer sb = new StringBuffer(); - - @Override - public void write(char[] cbuf, int off, int len) throws IOException { - sb.append(cbuf, off, len); - } - - @Override - public void flush() throws IOException { - LOG.finer(sb.toString()); - } - - @Override - public void close() throws IOException { - sb.setLength(0); - sb.trimToSize(); - } - }); - launcherBuilder.traceMessages(pw); + if (LOG.isLoggable(Level.FINER)) { + PrintWriter pw = new PrintWriter(new Writer() { + StringBuffer sb = new StringBuffer(); + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + sb.append(cbuf, off, len); + } + + @Override + public void flush() throws IOException { + LOG.finer(sb.toString()); + } + + @Override + public void close() throws IOException { + sb.setLength(0); + sb.trimToSize(); + } + }); + launcherBuilder.traceMessages(pw); + } + Launcher launcher = launcherBuilder.create(); + launcher.startListening(); + server = launcher.getRemoteProxy(); + } else { + process = null; + if (server instanceof LanguageClientAware aware) { + aware.connect(lci); + } } - Launcher launcher = launcherBuilder.create(); - launcher.startListening(); - LanguageServer server = launcher.getRemoteProxy(); - InitializeResult result = initServer(p, server, dir); //XXX: what if a different root is expected???? + InitializeResult result = initServer(process, server, dir); //XXX: what if a different root is expected???? server.initialized(new InitializedParams()); b = new LSPBindings(server, result, LanguageServerProviderAccessor.getINSTANCE().getProcess(desc)); // Register cleanup via LSPReference#run diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/LanguageServerProviderAccessor.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/LanguageServerProviderAccessor.java index e62d4e619aa4..9fab198d7a19 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/LanguageServerProviderAccessor.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/LanguageServerProviderAccessor.java @@ -20,6 +20,8 @@ import java.io.InputStream; import java.io.OutputStream; +import org.eclipse.lsp4j.services.LanguageServer; +import org.netbeans.api.annotations.common.NonNull; import org.netbeans.modules.lsp.client.spi.LanguageServerProvider.LanguageServerDescription; import org.openide.util.Exceptions; @@ -51,6 +53,8 @@ public static void setINSTANCE (LanguageServerProviderAccessor instance) { public abstract InputStream getInputStream(LanguageServerDescription desc); public abstract OutputStream getOutputStream(LanguageServerDescription desc); public abstract Process getProcess(LanguageServerDescription desc); + public abstract LanguageServer getServer(LanguageServerDescription desc); public abstract LSPBindings getBindings(LanguageServerDescription desc); public abstract void setBindings(LanguageServerDescription desc, LSPBindings bindings); + public abstract LanguageServerDescription createLanguageServerDescription(@NonNull LanguageServer server); } diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/CompletionProviderImpl.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/CompletionProviderImpl.java index d4eb07445fc7..6385e802bc70 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/CompletionProviderImpl.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/CompletionProviderImpl.java @@ -173,12 +173,18 @@ protected void query(CompletionResultSet resultSet, Document doc, int caretOffse } for (CompletionItem i : items) { String insert = i.getInsertText() != null ? i.getInsertText() : i.getLabel(); - String leftLabel = encode(i.getLabel()); + String leftLabel; String rightLabel; - if (i.getDetail() != null) { - rightLabel = encode(i.getDetail()); + if (i.getLabelDetails() != null) { + leftLabel = encode(i.getLabel() + (i.getLabelDetails().getDetail() != null ? i.getLabelDetails().getDetail() : "")); + rightLabel = encode(i.getLabelDetails().getDescription()); } else { - rightLabel = null; + leftLabel = encode(i.getLabel()); + if (i.getDetail() != null) { + rightLabel = encode(i.getDetail()); + } else { + rightLabel = null; + } } String sortText = i.getSortText() != null ? i.getSortText() : i.getLabel(); CompletionItemKind kind = i.getKind(); @@ -298,6 +304,7 @@ public String getText() { default: case "plaintext": documentation.append("
\n").append(content.getValue()).append("\n
"); break; case "markdown": documentation.append(HtmlRenderer.builder().build().render(Parser.builder().build().parse(content.getValue()))); break; + case "html": documentation.append(content.getValue()); break; } } return documentation.toString(); diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bridge/BridgingLanguageServer.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bridge/BridgingLanguageServer.java new file mode 100644 index 000000000000..700d723228b6 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bridge/BridgingLanguageServer.java @@ -0,0 +1,415 @@ +/* + * 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.lsp.client.bridge; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.CompletionItemLabelDetails; +import org.eclipse.lsp4j.CompletionItemTag; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.CompletionTriggerKind; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.DiagnosticSeverity; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.InsertTextFormat; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.ServerInfo; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.SymbolTag; +import org.eclipse.lsp4j.TextDocumentSyncKind; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +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.editor.document.LineDocument; +import org.netbeans.api.editor.document.LineDocumentUtils; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.lsp.Completion; +import org.netbeans.api.lsp.Completion.Context; +import org.netbeans.api.lsp.Completion.TriggerKind; +import org.netbeans.api.lsp.StructureElement; +import org.netbeans.modules.lsp.client.Utils; +import org.netbeans.spi.lsp.ErrorProvider; +import org.netbeans.spi.lsp.StructureProvider; +import org.openide.cookies.EditorCookie; +import org.openide.filesystems.FileObject; +import org.openide.util.RequestProcessor; +import org.openide.util.RequestProcessor.Task; + +public class BridgingLanguageServer implements LanguageServer, LanguageClientAware { + + private static final int DIAGNOSTIC_DELAY = 500; + private static final RequestProcessor WORKER = new RequestProcessor(BridgingLanguageServer.class.getName() + "-worker", 1, false, false); + private static final RequestProcessor BACKGROUND = new RequestProcessor(BridgingLanguageServer.class.getName() + "-background", 1, false, false); + + private final Map runDiagnostics = new WeakHashMap<>(); + private LanguageClient client; + + @Override + public CompletableFuture initialize(InitializeParams params) { + ServerCapabilities serverCaps = new ServerCapabilities(); + serverCaps.setTextDocumentSync(TextDocumentSyncKind.Incremental); + ServerInfo serverInfo = new ServerInfo(); + InitializeResult initResult = new InitializeResult(serverCaps, serverInfo); + CompletableFuture result = new CompletableFuture<>(); + + result.complete(initResult); + + return result; + } + + @Override + public CompletableFuture shutdown() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public void exit() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + private void reRunDiagnostics(FileObject file) { + runDiagnostics.computeIfAbsent(file, x -> BACKGROUND.create(() -> { + try { + if (client == null) { + return ; + } + + EditorCookie ec = file.getLookup().lookup(EditorCookie.class); + Document doc = ec.openDocument(); + List diagnostics = new ArrayList<>(); + + for (ErrorProvider.Kind kind : ErrorProvider.Kind.values()) { + ErrorProvider.Context ctx = new ErrorProvider.Context(file, kind); + + for (ErrorProvider provider : MimeLookup.getLookup(file.getMIMEType()).lookupAll(ErrorProvider.class)) { + for (var diag : provider.computeErrors(ctx)) { + if (diag.getSeverity() == null) { + System.err.println("!!!!"); + } + DiagnosticSeverity severity = diag.getSeverity() != null ? DiagnosticSeverity.valueOf(diag.getSeverity().name()) : null; + diagnostics.add(new Diagnostic(new Range(Utils.createPosition(doc, diag.getStartPosition().getOffset()), Utils.createPosition(doc, diag.getEndPosition().getOffset())), diag.getDescription(), severity,/*XXX*/ null, diag.getCode())); + } + } + } + client.publishDiagnostics(new PublishDiagnosticsParams(Utils.toURI(file), diagnostics)); + } catch (Exception ex) { + //TODO: + throw new IllegalStateException(ex); + } + })).schedule(DIAGNOSTIC_DELAY); + } + + @Override + public TextDocumentService getTextDocumentService() { + return new TextDocumentService() { + @Override + public void didOpen(DidOpenTextDocumentParams params) { + FileObject file = Utils.fromURI(params.getTextDocument().getUri()); + reRunDiagnostics(file); + } + @Override + public void didChange(DidChangeTextDocumentParams params) { + FileObject file = Utils.fromURI(params.getTextDocument().getUri()); + reRunDiagnostics(file); + } + @Override + public void didClose(DidCloseTextDocumentParams params) { + } + @Override + public void didSave(DidSaveTextDocumentParams params) { + } + @Override + public CompletableFuture, CompletionList>> completion(CompletionParams position) { + CompletableFuture, CompletionList>> result = new CompletableFuture<>(); + try { + FileObject file = Utils.fromURI(position.getTextDocument().getUri()); + EditorCookie ec = file.getLookup().lookup(EditorCookie.class); + Document doc = ec.openDocument(); + List items = new ArrayList<>(); + TriggerKind triggerKind = TriggerKind.Invoked; //XXX?? + Character triggerCharacter = null; + if (position.getContext() != null) { + triggerKind = TriggerKind.valueOf(position.getContext().getTriggerKind().name()); + triggerCharacter = position.getContext().getTriggerKind() == CompletionTriggerKind.TriggerCharacter ? position.getContext().getTriggerCharacter().charAt(0) : null; + } + boolean complete = Completion.collect(doc, Utils.getOffset(doc, position.getPosition()), new Context(triggerKind, triggerCharacter), completion -> { + CompletionItem item = convertCompletionItem(doc, completion); + items.add(item); + }); + CompletionList resultValue = new CompletionList(!complete, items); + result.complete(Either.forRight(resultValue)); + } catch (IOException ex) { + result.completeExceptionally(ex); + } + return result; + } + + @Override + public CompletableFuture resolveCompletionItem(CompletionItem unresolved) { + CompletableFuture result = new CompletableFuture<>(); + CompletionResolutionData completionData = (CompletionResolutionData) unresolved.getData(); + if (completionData != null) { + Completion completion = completionData.completion(); + Document doc = completionData.doc(); + WORKER.post(() -> { + if (completion.getDetail() != null) { + try { + String detail = completion.getDetail().get(); + if (detail != null) { + unresolved.setDetail(detail); + } + } catch (Exception ex) { + } + } + if (completion.getAdditionalTextEdits() != null) { + try { + List additionalTextEdits = completion.getAdditionalTextEdits().get(); + if (additionalTextEdits != null && !additionalTextEdits.isEmpty()) { + unresolved.setAdditionalTextEdits(additionalTextEdits.stream().map(ed -> { + return new TextEdit(new Range(createPosition(doc, ed.getStartOffset()), createPosition(doc, ed.getEndOffset())), ed.getNewText()); + }).collect(Collectors.toList())); + } + } catch (Exception ex) { + } + } + if (completion.getDocumentation() != null) { + try { + String documentation = completion.getDocumentation().getNow(null); + if (documentation != null) { + MarkupContent markup = new MarkupContent(); + markup.setKind("html"); + markup.setValue(documentation); + unresolved.setDocumentation(markup); + } + } catch (Exception ex) { + } + } + result.complete(unresolved); + }); + } else { + result.complete(unresolved); + } + return result; + } + + @Override + public CompletableFuture>> documentSymbol(DocumentSymbolParams params) { + CompletableFuture>> result = new CompletableFuture<>(); + try { + FileObject file = Utils.fromURI(params.getTextDocument().getUri()); + EditorCookie ec = file.getLookup().lookup(EditorCookie.class); + Document doc = ec.openDocument(); + List items = new ArrayList<>(); + List> symbols = new ArrayList<>(); + for (StructureProvider structure : MimeLookup.getLookup(file.getMIMEType()).lookupAll(StructureProvider.class)) { + for (StructureElement el : structure.getStructure(doc)) { + symbols.add(Either.forRight(structureElement2DocumentSymbol(doc, el))); + } + } + result.complete(symbols); + } catch (IOException | BadLocationException ex) { + result.completeExceptionally(ex); + } + return result; + } + }; + } + + private CompletionItem convertCompletionItem(Document doc, Completion completion) { + CompletionItem item = new CompletionItem(completion.getLabel()); + if (completion.getLabelDetail() != null || completion.getLabelDescription() != null) { + CompletionItemLabelDetails labelDetails = new CompletionItemLabelDetails(); + labelDetails.setDetail(completion.getLabelDetail()); + labelDetails.setDescription(completion.getLabelDescription()); + item.setLabelDetails(labelDetails); + } + if (completion.getKind() != null) { + item.setKind(CompletionItemKind.valueOf(completion.getKind().name())); + } + if (completion.getTags() != null) { + item.setTags(completion.getTags().stream().map(tag -> CompletionItemTag.valueOf(tag.name())).collect(Collectors.toList())); + } + if (completion.getDetail() != null && completion.getDetail().isDone()) { + item.setDetail(completion.getDetail().getNow(null)); + } + if (completion.getDocumentation() != null && completion.getDocumentation().isDone()) { + String documentation = completion.getDocumentation().getNow(null); + if (documentation != null) { + MarkupContent markup = new MarkupContent(); + markup.setKind("html"); + markup.setValue(documentation); + item.setDocumentation(markup); + } + } + if (completion.isPreselect()) { + item.setPreselect(true); + } + item.setSortText(completion.getSortText()); + item.setFilterText(completion.getFilterText()); + item.setInsertText(completion.getInsertText()); + if (completion.getInsertTextFormat() != null) { + item.setInsertTextFormat(InsertTextFormat.valueOf(completion.getInsertTextFormat().name())); + } + org.netbeans.api.lsp.TextEdit edit = completion.getTextEdit(); + if (edit != null) { + item.setTextEdit(Either.forLeft(new TextEdit(new Range(createPosition(doc, edit.getStartOffset()), createPosition(doc, edit.getEndOffset())), edit.getNewText()))); + } + org.netbeans.api.lsp.Command command = completion.getCommand(); + if (command != null) { + item.setCommand(new Command(command.getTitle(), command.getCommand(), command.getArguments())); + } + if (completion.getAdditionalTextEdits() != null && completion.getAdditionalTextEdits().isDone()) { + List additionalTextEdits = completion.getAdditionalTextEdits().getNow(null); + if (additionalTextEdits != null && !additionalTextEdits.isEmpty()) { + item.setAdditionalTextEdits(additionalTextEdits.stream().map(ed -> { + return new TextEdit(new Range(createPosition(doc, ed.getStartOffset()), createPosition(doc, ed.getEndOffset())), ed.getNewText()); + }).collect(Collectors.toList())); + } + } + if (completion.getCommitCharacters() != null) { + item.setCommitCharacters(completion.getCommitCharacters().stream().map(ch -> ch.toString()).collect(Collectors.toList())); + } + item.setData(new CompletionResolutionData(doc, completion)); + return item; + } + + private static DocumentSymbol structureElement2DocumentSymbol(Document doc, StructureElement el) throws BadLocationException { + Position selectionStartPos = Utils.createPosition(doc, el.getSelectionStartOffset()); + Position selectionEndPos = Utils.createPosition(doc, el.getSelectionEndOffset()); + Range selectionRange = new Range(selectionStartPos, selectionEndPos); + Position enclosedStartPos = Utils.createPosition(doc, el.getExpandedStartOffset()); + Position enclosedEndPos = Utils.createPosition(doc, el.getExpandedEndOffset()); + Range expandedRange = new Range(enclosedStartPos, enclosedEndPos); + DocumentSymbol ds; + if (el.getChildren() != null && !el.getChildren().isEmpty()) { + List children = new ArrayList<>(); + for (StructureElement child: el.getChildren()) { + ds = structureElement2DocumentSymbol(doc, child); + if (ds != null) { + children.add(ds); + } + } + ds = new DocumentSymbol(el.getName(), structureElementKind2SymbolKind(el.getKind()), expandedRange, selectionRange, el.getDetail(), children); + ds.setTags(elementTags2SymbolTags(el.getTags())); + return ds; + } + ds = new DocumentSymbol(el.getName(), structureElementKind2SymbolKind(el.getKind()), expandedRange, selectionRange, el.getDetail()); + ds.setTags(elementTags2SymbolTags(el.getTags())); + return ds; + } + + private static SymbolKind structureElementKind2SymbolKind (StructureElement.Kind kind) { + switch (kind) { + case Array : return SymbolKind.Array; + case Boolean: return SymbolKind.Boolean; + case Class: return SymbolKind.Class; + case Constant: return SymbolKind.Constant; + case Constructor: return SymbolKind.Constructor; + case Enum: return SymbolKind.Enum; + case EnumMember: return SymbolKind.EnumMember; + case Event: return SymbolKind.Event; + case Field: return SymbolKind.Field; + case File: return SymbolKind.File; + case Function: return SymbolKind.Function; + case Interface: return SymbolKind.Interface; + case Key: return SymbolKind.Key; + case Method: return SymbolKind.Method; + case Module: return SymbolKind.Module; + case Namespace: return SymbolKind.Namespace; + case Null: return SymbolKind.Null; + case Number: return SymbolKind.Number; + case Object: return SymbolKind.Object; + case Operator: return SymbolKind.Operator; + case Package: return SymbolKind.Package; + case Property: return SymbolKind.Property; + case String: return SymbolKind.String; + case Struct: return SymbolKind.Struct; + case TypeParameter: return SymbolKind.TypeParameter; + case Variable: return SymbolKind.Variable; + } + return SymbolKind.Object; + } + + private static List elementTags2SymbolTags (Set tags) { + if (tags != null) { + // we now have only deprecated tag + return Collections.singletonList(SymbolTag.Deprecated); + } + return null; + } + + @Override + public WorkspaceService getWorkspaceService() { + return new WorkspaceService() { + @Override + public void didChangeConfiguration(DidChangeConfigurationParams params) { + } + @Override + public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { + } + }; + } + + public static Position createPosition(Document doc, int offset) { + try { + return new Position(LineDocumentUtils.getLineIndex((LineDocument) doc, offset), + offset - LineDocumentUtils.getLineStart((LineDocument) doc, offset)); + } catch (BadLocationException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public void connect(LanguageClient client) { + this.client = client; + } + + record CompletionResolutionData(Document doc, Completion completion) {} +} diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bridge/BridgingLanguageServerProvider.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bridge/BridgingLanguageServerProvider.java new file mode 100644 index 000000000000..8e735b9c7aea --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bridge/BridgingLanguageServerProvider.java @@ -0,0 +1,36 @@ +/* + * 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.lsp.client.bridge; + +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.modules.lsp.client.LanguageServerProviderAccessor; +import org.netbeans.modules.lsp.client.spi.LanguageServerProvider; +import org.openide.util.Lookup; + +public class BridgingLanguageServerProvider implements LanguageServerProvider { + + private static final LanguageServerDescription GLOBAL = + LanguageServerProviderAccessor.getINSTANCE() + .createLanguageServerDescription(new BridgingLanguageServer()); + + @Override + public LanguageServerDescription startServer(Lookup lookup) { + return GLOBAL; + } +} diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageServerProvider.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageServerProvider.java index 6ad31d21f9f9..eea786f3fe34 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageServerProvider.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageServerProvider.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.io.OutputStream; +import org.eclipse.lsp4j.services.LanguageServer; import org.netbeans.api.annotations.common.CheckForNull; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.annotations.common.NullAllowed; @@ -57,18 +58,24 @@ public static final class LanguageServerDescription { * @return an instance of LanguageServerDescription */ public static @NonNull LanguageServerDescription create(@NonNull InputStream in, @NonNull OutputStream out, @NullAllowed Process process) { - return new LanguageServerDescription(in, out, process); + return new LanguageServerDescription(in, out, process, null); + } + + static @NonNull LanguageServerDescription create(@NonNull LanguageServer server) { + return new LanguageServerDescription(null, null, null, server); } private final InputStream in; private final OutputStream out; private final Process process; + private final LanguageServer server; private LSPBindings bindings; - private LanguageServerDescription(InputStream in, OutputStream out, Process process) { + private LanguageServerDescription(InputStream in, OutputStream out, Process process, LanguageServer server) { this.in = in; this.out = out; this.process = process; + this.server = server; } static { @@ -88,6 +95,11 @@ public Process getProcess(LanguageServerDescription desc) { return desc.process; } + @Override + public LanguageServer getServer(LanguageServerDescription desc) { + return desc.server; + } + @Override public LSPBindings getBindings(LanguageServerDescription desc) { return desc.bindings; @@ -97,6 +109,11 @@ public LSPBindings getBindings(LanguageServerDescription desc) { public void setBindings(LanguageServerDescription desc, LSPBindings bindings) { desc.bindings = bindings; } + + @Override + public LanguageServerDescription createLanguageServerDescription(LanguageServer server) { + return LanguageServerDescription.create(server); + } }); } diff --git a/ide/spi.editor.hints/src/org/netbeans/modules/editor/hints/lsp/HintsDiagnosticsProvider.java b/ide/spi.editor.hints/src/org/netbeans/modules/editor/hints/lsp/HintsDiagnosticsProvider.java index bb44116320d2..fe12813992f8 100644 --- a/ide/spi.editor.hints/src/org/netbeans/modules/editor/hints/lsp/HintsDiagnosticsProvider.java +++ b/ide/spi.editor.hints/src/org/netbeans/modules/editor/hints/lsp/HintsDiagnosticsProvider.java @@ -95,6 +95,7 @@ public List computeErrors(Context context) { break; } + b.setSeverity(s); result.add(b.build()); } return result;