From c7eda4fa3fdb670a012181f92a6e79df11811322 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 25 Jul 2024 06:07:06 +0200 Subject: [PATCH 1/2] Re-apply #7505 that was reverted by #7528 with additional resolved.getDocumentation() can be null --- .../bindings/AbstractCompletionItem.java | 220 ++++++++++++++++++ .../bindings/CompletionProviderImpl.java | 199 +--------------- .../modules/lsp/client/bindings/Icons.java | 2 +- .../client/bindings/LspCompletionItem.java | 142 +++++++++++ .../bindings/LspCompletionProviderImpl.java | 178 ++++++++++++++ .../client/bindings/NavigatorPanelImpl.java | 13 -- 6 files changed, 551 insertions(+), 203 deletions(-) create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/AbstractCompletionItem.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LspCompletionItem.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LspCompletionProviderImpl.java diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/AbstractCompletionItem.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/AbstractCompletionItem.java new file mode 100644 index 000000000000..09bacb648747 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/AbstractCompletionItem.java @@ -0,0 +1,220 @@ +/* + * 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.bindings; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.event.KeyEvent; +import java.net.URL; +import java.util.List; +import java.util.function.Supplier; +import javax.swing.Action; +import javax.swing.ImageIcon; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import javax.swing.text.StyledDocument; +import org.netbeans.api.editor.completion.Completion; +import org.netbeans.editor.BaseDocument; +import org.netbeans.editor.Utilities; +import org.netbeans.spi.editor.completion.CompletionDocumentation; +import org.netbeans.spi.editor.completion.CompletionItem; +import org.netbeans.spi.editor.completion.CompletionResultSet; +import org.netbeans.spi.editor.completion.CompletionTask; +import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery; +import org.netbeans.spi.editor.completion.support.AsyncCompletionTask; +import org.netbeans.spi.editor.completion.support.CompletionUtilities; +import org.openide.text.NbDocument; +import org.openide.util.Exceptions; + +abstract class AbstractCompletionItem implements CompletionItem { + private final Document doc; + private final int caretOffset; + private final String leftLabel; + private final String rightLabel; + private final ImageIcon icon; + private final String sortText; + private final String insert; + + AbstractCompletionItem( + Document doc, int caretOffset, String leftLabel, String rightLabel, + ImageIcon icon, String sortText, String insert + ) { + this.doc = doc; + this.caretOffset = caretOffset; + this.leftLabel = leftLabel; + this.rightLabel = rightLabel; + this.icon = icon; + this.sortText = sortText; + this.insert = insert; + } + + abstract Edit findEdit(boolean[] hideImmediately); + abstract int findStart(Document doc, Edit te); + abstract int findEnd(Document doc, Edit te); + abstract String findNewText(Edit te); + + abstract boolean isTextEdit(Edit te); + + abstract List getCommitCharacters(); + abstract boolean isTriggerCharacter(String ch); + abstract Supplier resolveDocumentation(Document doc, int caretOffset); + + @Override + public void defaultAction(JTextComponent jtc) { + commit(""); + } + + private void commit(String appendText) { + boolean[] hideNow = { false }; + Edit te = findEdit(hideNow); + if (hideNow[0]) { + Completion.get().hideDocumentation(); + Completion.get().hideCompletion(); + return; + } + NbDocument.runAtomic((StyledDocument) doc, () -> { + try { + int endPos; + if (isTextEdit(te)) { + int start = findStart(doc, te); + int end = findEnd(doc, te); + doc.remove(start, end - start); + String newText = findNewText(te); + doc.insertString(start, newText, null); + endPos = start + newText.length(); + } else { + String toAdd = insert; + int[] identSpan = Utilities.getIdentifierBlock((BaseDocument) doc, caretOffset); + if (identSpan != null) { + doc.remove(identSpan[0], identSpan[1] - identSpan[0]); + doc.insertString(identSpan[0], toAdd, null); + endPos = identSpan[0] + toAdd.length(); + } else { + doc.insertString(caretOffset, toAdd, null); + endPos = caretOffset + toAdd.length(); + } + } + doc.insertString(endPos, appendText, null); + } catch (BadLocationException ex) { + Exceptions.printStackTrace(ex); + } + }); + Completion.get().hideDocumentation(); + Completion.get().hideCompletion(); + } + + @Override + public void processKeyEvent(KeyEvent ke) { + if (ke.getID() == KeyEvent.KEY_TYPED) { + String commitText = String.valueOf(ke.getKeyChar()); + List commitCharacters = getCommitCharacters(); + if (commitCharacters != null && commitCharacters.contains(commitText)) { + commit(commitText); + ke.consume(); + if (isTriggerCharacter(commitText)) { + Completion.get().showCompletion(); + } + } + } + } + + @Override + public int getPreferredWidth(Graphics grphcs, Font font) { + return CompletionUtilities.getPreferredWidth(leftLabel, rightLabel, grphcs, font); + } + + @Override + public void render(Graphics grphcs, Font font, Color color, Color color1, int i, int i1, boolean bln) { + CompletionUtilities.renderHtml(icon, leftLabel, rightLabel, grphcs, font, color, i, i1, bln); + } + + @Override + public CompletionTask createDocumentationTask() { + return new AsyncCompletionTask(new AsyncCompletionQuery() { + @Override + protected void query(CompletionResultSet resultSet, Document doc, int caretOffset) { + Supplier resolved = resolveDocumentation(doc, caretOffset); + if (resolved != null) { + resultSet.setDocumentation(new CompletionDocumentation() { + @Override + public String getText() { + String[] both = resolved.get(); + String detail = both[0]; + String content = both[1]; + + StringBuilder documentation = new StringBuilder(); + documentation.append("\n"); + if (detail != null) { + documentation.append("").append(CompletionProviderImpl.escape(detail)).append(""); + documentation.append("\n

"); + } + if (content != null) { + documentation.append(content); + } + return documentation.toString(); + } + + @Override + public URL getURL() { + return null; + } + + @Override + public CompletionDocumentation resolveLink(String link) { + return null; + } + + @Override + public Action getGotoSourceAction() { + return null; + } + }); + } + resultSet.finish(); + } + }); + } + + @Override + public CompletionTask createToolTipTask() { + return null; + } + + @Override + public boolean instantSubstitution(JTextComponent jtc) { + return false; + } + + @Override + public int getSortPriority() { + return 100; + } + + @Override + public CharSequence getSortText() { + return sortText; + } + + @Override + public CharSequence getInsertPrefix() { + return insert; + } +} 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..8a6066485cb6 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 @@ -18,56 +18,38 @@ */ package org.netbeans.modules.lsp.client.bindings; -import com.vladsch.flexmark.html.HtmlRenderer; -import com.vladsch.flexmark.parser.Parser; -import java.awt.Color; -import java.awt.Font; -import java.awt.Graphics; -import java.awt.event.KeyEvent; -import java.net.URL; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; -import javax.swing.Action; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JToolTip; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.JTextComponent; -import javax.swing.text.StyledDocument; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionItemKind; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.CompletionOptions; import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.InitializeResult; -import org.eclipse.lsp4j.InsertReplaceEdit; -import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.ParameterInformation; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SignatureHelpParams; import org.eclipse.lsp4j.SignatureInformation; import org.eclipse.lsp4j.TextDocumentIdentifier; -import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.netbeans.api.editor.completion.Completion; import org.netbeans.api.editor.mimelookup.MimeRegistration; -import org.netbeans.editor.BaseDocument; -import org.netbeans.editor.Utilities; import org.netbeans.modules.editor.NbEditorUtilities; import org.netbeans.modules.lsp.client.LSPBindings; import org.netbeans.modules.lsp.client.Utils; -import org.netbeans.spi.editor.completion.CompletionDocumentation; import org.netbeans.spi.editor.completion.CompletionProvider; import org.netbeans.spi.editor.completion.CompletionResultSet; import org.netbeans.spi.editor.completion.CompletionTask; import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery; import org.netbeans.spi.editor.completion.support.AsyncCompletionTask; -import org.netbeans.spi.editor.completion.support.CompletionUtilities; import org.openide.filesystems.FileObject; -import org.openide.text.NbDocument; import org.openide.util.Exceptions; import org.openide.util.ImageUtilities; import org.openide.xml.XMLUtil; @@ -184,172 +166,9 @@ protected void query(CompletionResultSet resultSet, Document doc, int caretOffse CompletionItemKind kind = i.getKind(); Icon ic = Icons.getCompletionIcon(kind); ImageIcon icon = new ImageIcon(ImageUtilities.icon2Image(ic)); - resultSet.addItem(new org.netbeans.spi.editor.completion.CompletionItem() { - @Override - public void defaultAction(JTextComponent jtc) { - commit(""); - } - private void commit(String appendText) { - Either edit = i.getTextEdit(); - if (edit != null && edit.isRight()) { - //TODO: the NetBeans client does not current support InsertReplaceEdits, should not happen - Completion.get().hideDocumentation(); - Completion.get().hideCompletion(); - return ; - } - TextEdit te = edit != null ? edit.getLeft() : null; - NbDocument.runAtomic((StyledDocument) doc, () -> { - try { - int endPos; - if (te != null) { - int start = Utils.getOffset(doc, te.getRange().getStart()); - int end = Utils.getOffset(doc, te.getRange().getEnd()); - doc.remove(start, end - start); - doc.insertString(start, te.getNewText(), null); - endPos = start + te.getNewText().length(); - } else { - String toAdd = i.getInsertText(); - if (toAdd == null) { - toAdd = i.getLabel(); - } - int[] identSpan = Utilities.getIdentifierBlock((BaseDocument) doc, caretOffset); - if (identSpan != null) { - doc.remove(identSpan[0], identSpan[1] - identSpan[0]); - doc.insertString(identSpan[0], toAdd, null); - endPos = identSpan[0] + toAdd.length(); - } else { - doc.insertString(caretOffset, toAdd, null); - endPos = caretOffset + toAdd.length(); - } - } - doc.insertString(endPos, appendText, null); - } catch (BadLocationException ex) { - Exceptions.printStackTrace(ex); - } - }); - Completion.get().hideDocumentation(); - Completion.get().hideCompletion(); - } - - @Override - public void processKeyEvent(KeyEvent ke) { - if (ke.getID() == KeyEvent.KEY_TYPED) { - String commitText = String.valueOf(ke.getKeyChar()); - List commitCharacters = i.getCommitCharacters(); - - if (commitCharacters != null && commitCharacters.contains(commitText)) { - commit(commitText); - ke.consume(); - if (isTriggerCharacter(server, commitText)) { - Completion.get().showCompletion(); - } - } - } - } - - @Override - public int getPreferredWidth(Graphics grphcs, Font font) { - return CompletionUtilities.getPreferredWidth(leftLabel, rightLabel, grphcs, font); - } - - @Override - public void render(Graphics grphcs, Font font, Color color, Color color1, int i, int i1, boolean bln) { - CompletionUtilities.renderHtml(icon, leftLabel, rightLabel, grphcs, font, color, i, i1, bln); - } - - @Override - public CompletionTask createDocumentationTask() { - return new AsyncCompletionTask(new AsyncCompletionQuery() { - @Override - protected void query(CompletionResultSet resultSet, Document doc, int caretOffset) { - CompletionItem resolved; - if ((i.getDetail() == null || i.getDocumentation() == null) && hasCompletionResolve(server)) { - CompletionItem temp; - try { - temp = server.getTextDocumentService().resolveCompletionItem(i).get(); - } catch (InterruptedException | ExecutionException ex) { - Exceptions.printStackTrace(ex); - temp = i; - } - resolved = temp; - } else { - resolved = i; - } - if (resolved.getDocumentation() != null || resolved.getDetail() != null) { - resultSet.setDocumentation(new CompletionDocumentation() { - @Override - public String getText() { - StringBuilder documentation = new StringBuilder(); - documentation.append("\n"); - if (resolved.getDetail() != null) { - documentation.append("").append(escape(resolved.getDetail())).append(""); - documentation.append("\n

"); - } - if (resolved.getDocumentation() != null) { - MarkupContent content; - if (resolved.getDocumentation().isLeft()) { - content = new MarkupContent(); - content.setKind("plaintext"); - content.setValue(resolved.getDocumentation().getLeft()); - } else { - content = resolved.getDocumentation().getRight(); - } - switch (content.getKind()) { - 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; - } - } - return documentation.toString(); - } - @Override - public URL getURL() { - return null; - } - @Override - public CompletionDocumentation resolveLink(String link) { - return null; - } - @Override - public Action getGotoSourceAction() { - return null; - } - }); - } - resultSet.finish(); - } - }); - } - - @Override - public CompletionTask createToolTipTask() { - return null; - } - - @Override - public boolean instantSubstitution(JTextComponent jtc) { - return false; - } - - @Override - public int getSortPriority() { - return 100; - } - - @Override - public CharSequence getSortText() { - return sortText; - } - - @Override - public CharSequence getInsertPrefix() { - return insert; - } - }); + resultSet.addItem(new LspCompletionItem(i, doc, caretOffset, server, leftLabel, rightLabel, icon, sortText, insert, this, org.netbeans.modules.lsp.client.bindings.CompletionProviderImpl.this)); } - } catch (BadLocationException | InterruptedException ex) { - Exceptions.printStackTrace(ex); - } catch (ExecutionException ex) { + } catch (BadLocationException | InterruptedException | ExecutionException ex) { Exceptions.printStackTrace(ex); } finally { resultSet.finish(); @@ -357,17 +176,19 @@ public CharSequence getInsertPrefix() { } }, component); } - - private boolean hasCompletionResolve(LSPBindings server) { + + final boolean hasCompletionResolve(LSPBindings server) { ServerCapabilities capabilities = server.getInitResult().getCapabilities(); - if (capabilities == null) return false; + if (capabilities == null) { + return false; + } CompletionOptions completionProvider = capabilities.getCompletionProvider(); if (completionProvider == null) return false; Boolean resolveProvider = completionProvider.getResolveProvider(); return resolveProvider != null && resolveProvider; } - private static String escape(String s) { + static String escape(String s) { if (s != null) { try { return XMLUtil.toAttributeValue(s); @@ -393,8 +214,8 @@ public int getAutoQueryTypes(JTextComponent component, String typedText) { } return isTriggerCharacter(server, typedText) ? COMPLETION_QUERY_TYPE : 0; } - - private boolean isTriggerCharacter(LSPBindings server, String text) { + + boolean isTriggerCharacter(LSPBindings server, String text) { InitializeResult init = server.getInitResult(); if (init == null) return false; ServerCapabilities capabilities = init.getCapabilities(); diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/Icons.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/Icons.java index 68e68c5176a9..ef7e909403c8 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/Icons.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/Icons.java @@ -41,7 +41,7 @@ public final class Icons { private Icons() { } - public static Icon getCompletionIcon(CompletionItemKind completionKind) { + public static Icon getCompletionIcon(Enum completionKind) { Image img = null; if (completionKind != null) { diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LspCompletionItem.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LspCompletionItem.java new file mode 100644 index 000000000000..606d413faab5 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LspCompletionItem.java @@ -0,0 +1,142 @@ +/* + * 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.bindings; + +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; +import javax.swing.ImageIcon; +import javax.swing.text.Document; +import org.eclipse.lsp4j.InsertReplaceEdit; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.netbeans.modules.lsp.client.LSPBindings; +import org.netbeans.modules.lsp.client.Utils; +import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery; +import org.openide.util.Exceptions; + +final class LspCompletionItem extends AbstractCompletionItem> { + private final org.eclipse.lsp4j.CompletionItem i; + private final LSPBindings server; + private final AsyncCompletionQuery outer; + private final CompletionProviderImpl cp; + + public LspCompletionItem( + org.eclipse.lsp4j.CompletionItem i, + Document doc, int caretOffset, LSPBindings server, + String leftLabel, String rightLabel, ImageIcon icon, String sortText, + String insert, + AsyncCompletionQuery outer, CompletionProviderImpl cp + ) { + super(doc, caretOffset, leftLabel, rightLabel, icon, sortText, insert); + this.cp = cp; + this.outer = outer; + this.i = i; + this.server = server; + } + + @Override + Either findEdit(boolean[] hideImmediately) { + Either edit = i.getTextEdit(); + if (edit != null && edit.isRight()) { + //TODO: the NetBeans client does not currently support InsertReplaceEdits, should not happen + hideImmediately[0] = true; + return null; + } + return edit; + } + + @Override + boolean isTextEdit(Either edit) { + return edit != null && edit.getLeft() != null; + } + + @Override + int findStart(Document doc, Either te) { + return Utils.getOffset(doc, te.getLeft().getRange().getStart()); + } + + @Override + int findEnd(Document doc, Either te) { + return Utils.getOffset(doc, te.getLeft().getRange().getEnd()); + } + + @Override + String findNewText(Either te) { + return te.getLeft().getNewText(); + } + + @Override + List getCommitCharacters() { + return i.getCommitCharacters(); + } + + @Override + boolean isTriggerCharacter(String commitText) { + return cp.isTriggerCharacter(server, commitText); + } + + @Override + Supplier resolveDocumentation(Document doc, int caretOffset) { + org.eclipse.lsp4j.CompletionItem resolved; + if ((i.getDetail() == null || i.getDocumentation() == null) && cp.hasCompletionResolve(server)) { + org.eclipse.lsp4j.CompletionItem temp; + try { + temp = server.getTextDocumentService().resolveCompletionItem(i).get(); + } catch (InterruptedException | ExecutionException ex) { + Exceptions.printStackTrace(ex); + temp = i; + } + resolved = temp; + } else { + resolved = i; + } + if (resolved.getDocumentation() != null || resolved.getDetail() != null) { + return () -> { + String txt; + if (resolved.getDocumentation() != null) { + MarkupContent content; + if (resolved.getDocumentation().isLeft()) { + content = new MarkupContent(); + content.setKind("plaintext"); + content.setValue(resolved.getDocumentation().getLeft()); + } else { + content = resolved.getDocumentation().getRight(); + } + switch (content.getKind()) { + case "markdown": + txt = HtmlRenderer.builder().build().render(Parser.builder().build().parse(content.getValue())); + break; + default: + txt = "
\n" + content.getValue() + "\n
"; + break; + } + } else { + txt = null; + } + return new String[] { resolved.getDetail(), txt }; + }; + } else { + return null; + } + } +} diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LspCompletionProviderImpl.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LspCompletionProviderImpl.java new file mode 100644 index 000000000000..36444e3d5f19 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/LspCompletionProviderImpl.java @@ -0,0 +1,178 @@ +/** + * 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.bindings; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.api.lsp.TextEdit; +import org.netbeans.modules.editor.NbEditorUtilities; +import org.netbeans.spi.editor.completion.CompletionProvider; +import org.netbeans.spi.editor.completion.CompletionResultSet; +import org.netbeans.spi.editor.completion.CompletionTask; +import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery; +import org.netbeans.spi.editor.completion.support.AsyncCompletionTask; +import org.netbeans.spi.lsp.CompletionCollector; +import org.openide.filesystems.FileObject; +import org.openide.util.ImageUtilities; +import org.openide.util.Lookup; + +@MimeRegistration(mimeType = "", service = CompletionProvider.class) +public class LspCompletionProviderImpl implements CompletionProvider { + + @Override + public CompletionTask createTask(int queryType, JTextComponent component) { + return new AsyncCompletionTask(new AsyncCompletionQuery() { + @Override + protected void query(CompletionResultSet resultSet, Document doc, int caretOffset) { + FileObject file = NbEditorUtilities.getFileObject(doc); + if (file == null) { + //TODO: beep + resultSet.finish(); + return; + } + final String mime = file.getMIMEType(); + for (Lookup.Item item : MimeLookup.getLookup(mime).lookupResult(CompletionProvider.class).allItems()) { + String id = item.getId(); + if (id.startsWith("Editors/"+ mime)) { + // found real CompletionProvider - don't bridge LSP API + resultSet.finish(); + return; + } + } + Consumer consumer = (i) -> { + String insert = i.getInsertText() != null ? i.getInsertText() : i.getLabel(); + String leftLabel = encode(i.getLabel()); + String rightLabel = null; + try { + if (i.getDetail() != null) { + rightLabel = encode(i.getDetail().get()); + } + } catch (InterruptedException | ExecutionException interruptedException) { + // leave null + } + String sortText = i.getSortText() != null ? i.getSortText() : i.getLabel(); + org.netbeans.api.lsp.Completion.Kind kind = i.getKind(); + Icon ic = Icons.getCompletionIcon(kind); + ImageIcon icon = new ImageIcon(ImageUtilities.icon2Image(ic)); + resultSet.addItem(new LspApiCompletionItem(i, doc, caretOffset, leftLabel, rightLabel, icon, sortText, insert)); + }; + org.netbeans.api.lsp.Completion.Context context = new org.netbeans.api.lsp.Completion.Context(org.netbeans.api.lsp.Completion.TriggerKind.Invoked, null); + for (CompletionCollector cc : MimeLookup.getLookup(mime).lookupAll(CompletionCollector.class)) { + cc.collectCompletions(doc, caretOffset, context, consumer); + } + resultSet.finish(); + } + }, component); + } + + private String encode(String str) { + return str.replace("&", "&") + .replace("<", "<"); + } + + @Override + public int getAutoQueryTypes(JTextComponent component, String typedText) { + FileObject file = NbEditorUtilities.getFileObject(component.getDocument()); + if (file == null) { + return 0; + } + return 0; + } + + private static class LspApiCompletionItem extends AbstractCompletionItem { + + private final org.netbeans.api.lsp.Completion i; + + public LspApiCompletionItem(org.netbeans.api.lsp.Completion i, Document doc, int caretOffset, String leftLabel, String rightLabel, ImageIcon icon, String sortText, String insert) { + super(doc, caretOffset, leftLabel, rightLabel, icon, sortText, insert); + this.i = i; + } + + @Override + TextEdit findEdit(boolean[] hideNow) { + return i.getTextEdit(); + } + + @Override + boolean isTextEdit(TextEdit te) { + return te != null; + } + + @Override + int findStart(Document doc, TextEdit te) { + return te.getStartOffset(); + } + + @Override + int findEnd(Document doc, TextEdit te) { + return te.getEndOffset(); + } + + @Override + String findNewText(TextEdit te) { + return te.getNewText(); + } + + @Override + List getCommitCharacters() { + return i.getCommitCharacters().stream().map(String::valueOf).collect(Collectors.toList()); + } + + @Override + boolean isTriggerCharacter(String commitText) { + return false; + } + + @Override + Supplier resolveDocumentation(Document doc, int caretOffset) { + if (i.getDetail() == null && i.getDocumentation() == null) { + return null; + } else { + return () -> { + String detail = null; + String documentation = null; + try { + if (i.getDetail() != null) { + detail = i.getDetail().get(); + } + } catch (InterruptedException | ExecutionException interruptedException) { + // leave null + } + try { + if (i.getDocumentation() != null) { + documentation = i.getDocumentation().get(); + } + } catch (InterruptedException | ExecutionException interruptedException) { + // leave null + } + return new String[]{detail, documentation}; + }; + } + } + } +} diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/NavigatorPanelImpl.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/NavigatorPanelImpl.java index bde41feae455..c03162f7fbd9 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/NavigatorPanelImpl.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/NavigatorPanelImpl.java @@ -18,19 +18,13 @@ */ package org.netbeans.modules.lsp.client.bindings; -import java.awt.BorderLayout; import java.awt.event.ActionEvent; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.logging.Level; -import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.Action; -import javax.swing.JComponent; -import javax.swing.JPanel; -import javax.swing.SwingUtilities; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.Range; @@ -40,17 +34,10 @@ import org.netbeans.modules.lsp.client.LSPBindings; import org.netbeans.modules.lsp.client.LSPBindings.BackgroundTask; import org.netbeans.modules.lsp.client.Utils; -import org.netbeans.spi.navigator.NavigatorPanel; -import org.openide.explorer.ExplorerManager; -import org.openide.explorer.view.BeanTreeView; import org.openide.filesystems.FileObject; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Node; -import org.openide.util.Lookup; -import org.openide.util.LookupEvent; -import org.openide.util.LookupListener; -import org.openide.util.NbBundle.Messages; /** * From 557c823ba5244c6d67262eca3ce27d22bc07fc78 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 25 Jul 2024 06:08:08 +0200 Subject: [PATCH 2/2] ErrorProviderBridge exposes content of ErrorProviders to NetBeans IDE --- .../client/bindings/ErrorProviderBridge.java | 115 +++++++++++++++ ...xtDocumentSyncServerCapabilityHandler.java | 21 ++- .../bindings/ErrorProviderBridgeTest.java | 139 ++++++++++++++++++ .../hints/lsp/HintsDiagnosticsProvider.java | 6 +- .../infrastructure/JavaErrorProvider.java | 7 +- .../modules/nbcode/integration/layer.xml | 13 ++ .../protocol/TextDocumentServiceImpl.java | 5 +- .../unit/src/META-INF/generated-layer.xml | 10 ++ 8 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/ErrorProviderBridge.java create mode 100644 ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/ErrorProviderBridgeTest.java diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/ErrorProviderBridge.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/ErrorProviderBridge.java new file mode 100644 index 000000000000..cf6778f073d1 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/ErrorProviderBridge.java @@ -0,0 +1,115 @@ +/* + * 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.bindings; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; +import org.netbeans.api.lsp.Diagnostic; +import org.netbeans.spi.editor.hints.ErrorDescription; +import org.netbeans.spi.editor.hints.ErrorDescriptionFactory; +import org.netbeans.spi.editor.hints.HintsController; +import org.netbeans.spi.editor.hints.Severity; +import org.netbeans.spi.lsp.ErrorProvider; +import org.openide.filesystems.FileObject; +import org.openide.util.RequestProcessor; +import org.openide.util.WeakListeners; + +class ErrorProviderBridge implements Runnable, DocumentListener { + + private final FileObject file; + private final RequestProcessor.Task task; + private final Collection errorProviders; + private final DocumentListener listener; + + ErrorProviderBridge(Document doc, FileObject file, Collection errorProviders, RequestProcessor rp) { + this.file = file; + this.errorProviders = errorProviders; + this.task = rp.create(this); + this.listener = WeakListeners.create(DocumentListener.class, this, doc); + doc.addDocumentListener(listener); + } + + final void start() { + task.schedule(0); + } + + final void waitFinished() { + task.waitFinished(); + } + + @Override + public final void run() { + for (ErrorProvider p : errorProviders) { + computeHints(ErrorProvider.Kind.ERRORS, p, "lsp:errors"); + computeHints(ErrorProvider.Kind.HINTS, p, "lsp:hints"); + } + } + + private void computeHints(final ErrorProvider.Kind type, ErrorProvider p, final String prefix) { + List arr = new ArrayList<>(); + ErrorProvider.Context errorCtx = new ErrorProvider.Context(file, type); + List errors = p.computeErrors(errorCtx); + if (errors != null) { + for (Diagnostic e : errors) { + final Severity s; + switch(e.getSeverity()) { + case Error: + s = Severity.ERROR; break; + case Warning: + s = Severity.WARNING; break; + case Information: + case Hint: + default: + s = Severity.HINT; break; + } + ErrorDescription descr = ErrorDescriptionFactory.createErrorDescription(s, + e.getDescription(), + file, + e.getStartPosition().getOffset(), + e.getEndPosition().getOffset() + ); + arr.add(descr); + } + applyHints(prefix, p, arr); + } + } + + protected void applyHints(final String prefix, ErrorProvider p, List arr) { + HintsController.setErrors(file, prefix + ":" + p.getClass().getName(), arr); + } + + @Override + public final void insertUpdate(DocumentEvent e) { + start(); + } + + @Override + public final void removeUpdate(DocumentEvent e) { + start(); + } + + @Override + public final void changedUpdate(DocumentEvent e) { + start(); + } +} diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java index 2659afddcf67..496828357f67 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; @@ -47,16 +48,24 @@ import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.netbeans.api.editor.EditorRegistry; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.lsp.Diagnostic; import org.netbeans.editor.BaseDocumentEvent; import org.netbeans.lib.editor.util.swing.DocumentUtilities; import org.netbeans.modules.editor.*; import org.netbeans.modules.lsp.client.LSPBindings; import org.netbeans.modules.lsp.client.Utils; +import org.netbeans.spi.editor.hints.ErrorDescription; +import org.netbeans.spi.editor.hints.ErrorDescriptionFactory; +import org.netbeans.spi.editor.hints.HintsController; +import org.netbeans.spi.editor.hints.Severity; +import org.netbeans.spi.lsp.ErrorProvider; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.modules.OnStart; import org.openide.text.NbDocument; import org.openide.util.Exceptions; +import org.openide.util.Lookup; import org.openide.util.RequestProcessor; /** @@ -322,8 +331,16 @@ private void registerBackgroundTasks(JTextComponent c) { LSPBindings server = LSPBindings.getBindings(file); - if (server == null) - return ; //ignore + if (server == null) { + Lookup lkp = MimeLookup.getLookup(file.getMIMEType()); + Collection errorProviders = lkp.lookupAll(ErrorProvider.class); + if (!errorProviders.isEmpty()) { + ErrorProviderBridge b = new ErrorProviderBridge(doc, file, errorProviders, WORKER); + b.start(); + c.putClientProperty(ErrorProviderBridge.class, b); + } + return; + } SwingUtilities.invokeLater(() -> { if (c.getClientProperty(MarkOccurrences.class) == null) { diff --git a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/ErrorProviderBridgeTest.java b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/ErrorProviderBridgeTest.java new file mode 100644 index 000000000000..287ed3dde392 --- /dev/null +++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/ErrorProviderBridgeTest.java @@ -0,0 +1,139 @@ +/* + * 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.bindings; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.swing.JTextArea; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import org.junit.Test; +import static org.junit.Assert.*; +import org.netbeans.api.lsp.Diagnostic; +import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.editor.hints.ErrorDescription; +import org.netbeans.spi.lsp.ErrorProvider; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.RequestProcessor; + +public class ErrorProviderBridgeTest { + + public ErrorProviderBridgeTest() { + } + + @Test + public void testListeningAndLifecycle() throws Exception { + JTextComponent c = new JTextArea(); + FileObject file = FileUtil.createMemoryFileSystem().getRoot().createData("c.txt"); + RequestProcessor rp = new RequestProcessor("ErrorProviderBridgeTest"); + MockErrorProvider mp = new MockErrorProvider(c); + + List> appliedDescriptions = new ArrayList<>(); + ErrorProviderBridge b = new ErrorProviderBridge(c.getDocument(), file, Collections.singleton(mp), rp) { + @Override + protected void applyHints(String prefix, org.netbeans.spi.lsp.ErrorProvider p, List arr) { + if (prefix.equals("lsp:errors")) { + appliedDescriptions.add(arr); + } + } + }; + + b.start(); + b.waitFinished(); + + assertEquals("One set of errors reported", 1, appliedDescriptions.size()); + assertTrue("It is empty so far", appliedDescriptions.remove(0).isEmpty()); + + c.getDocument().insertString(0, "Ahoj\nERR\nOK", null); + b.waitFinished(); + + { + assertEquals("Another set of errors reported", 1, appliedDescriptions.size()); + final List errors = appliedDescriptions.remove(0); + assertEquals("There is one error", 1, errors.size()); + ErrorDescription descr = errors.get(0); + assertEquals("Starts right", 5, descr.getRange().getBegin().getOffset()); + assertEquals("Ends right", 8, descr.getRange().getEnd().getOffset()); + } + + c.getDocument().remove(0, 5); + b.waitFinished(); + + { + assertEquals("Yet another set of errors reported", 1, appliedDescriptions.size()); + final List errors = appliedDescriptions.remove(0); + assertEquals("There is one error", 1, errors.size()); + ErrorDescription descr = errors.get(0); + assertEquals("Starts right", 0, descr.getRange().getBegin().getOffset()); + assertEquals("Ends right", 3, descr.getRange().getEnd().getOffset()); + } + + Reference ref = new WeakReference<>(c); + c = null; + mp = null; + + NbTestCase.assertGC("Component can disappear", ref); + + ref = new WeakReference<>(b); + b = null; + NbTestCase.assertGC("Bridge can disappear", ref); + } + + private static final class MockErrorProvider implements ErrorProvider { + + private final Reference c; + + MockErrorProvider(JTextComponent c) { + this.c = new WeakReference<>(c); + } + + @Override + public List computeErrors(Context context) { + List arr = new ArrayList<>(); + JTextComponent tmp = c.get(); + if (tmp == null) { + return arr; + } + Document doc = tmp.getDocument(); + try { + String txt = doc.getText(0, doc.getLength()); + for (int from = -1;;) { + int at = txt.indexOf("ERR", from + 1); + if (at == -1) { + break; + } + Diagnostic d = Diagnostic.Builder.create(() -> at, () -> at + 3, "at" + at) + .setSeverity(Diagnostic.Severity.Error) + .build(); + arr.add(d); + from = at; + } + } catch (BadLocationException ex) { + throw new IllegalStateException(ex); + } + return arr; + } + + } +} 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..2ddfc5cfc3e8 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 @@ -22,7 +22,6 @@ import java.util.List; import javax.swing.text.Document; import org.netbeans.api.editor.document.LineDocument; -import org.netbeans.api.editor.mimelookup.MimeRegistration; import org.netbeans.api.lsp.Diagnostic; import org.netbeans.modules.editor.hints.AnnotationHolder; import org.netbeans.spi.editor.hints.ErrorDescription; @@ -34,14 +33,15 @@ /** * A simple implementation of {@link ErrorProvider} that converts errors + hints collected by * {@link HintsController} to LSP {@link Diagnostic}. The implementation does not support code actions yet. + * The implementation is registered by + * {@code java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/layer.xml}. *

* As {@link ErrorProvider}s are registered in MIME Lookup, this implementation is enumerated after those * possibly registered for specific MIME types. * * @author sdedic */ -@MimeRegistration(mimeType = "", service = ErrorProvider.class) -public class HintsDiagnosticsProvider implements ErrorProvider { +public final class HintsDiagnosticsProvider implements ErrorProvider { public HintsDiagnosticsProvider() { } diff --git a/java/java.hints/src/org/netbeans/modules/java/hints/infrastructure/JavaErrorProvider.java b/java/java.hints/src/org/netbeans/modules/java/hints/infrastructure/JavaErrorProvider.java index dbccee227fb5..e32e2aa4ae3a 100644 --- a/java/java.hints/src/org/netbeans/modules/java/hints/infrastructure/JavaErrorProvider.java +++ b/java/java.hints/src/org/netbeans/modules/java/hints/infrastructure/JavaErrorProvider.java @@ -21,6 +21,7 @@ import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.LineMap; import com.sun.source.util.TreePath; +import java.awt.GraphicsEnvironment; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -81,20 +82,18 @@ import org.openide.util.Exceptions; import org.openide.util.Union2; -/** +/** Provides errors and hints for VSCode. This class is abstract to + * be subclasses and registered by VSCode extension XML layer. * * @author lahvac */ -@MimeRegistration(mimeType="text/x-java", service=ErrorProvider.class) public class JavaErrorProvider implements ErrorProvider { - public static final String HINTS_TOOL_ID = "hints"; public static Consumer computeDiagsCallback; //for tests @Override public List computeErrors(Context context) { List result = new ArrayList<>(); - try { ParserManager.parse(Collections.singletonList(Source.create(context.file())), new UserTask() { @Override diff --git a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/layer.xml b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/layer.xml index bb577a6a8b10..8141315be67c 100644 --- a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/layer.xml +++ b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/layer.xml @@ -175,4 +175,17 @@ + + + + + + + + + + + + + diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java index ad0d82fa448e..deec280429ac 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java @@ -1032,7 +1032,10 @@ public CompletableFuture>> codeAction(CodeActio continue; } } - Optional diag = diagnostics.stream().filter(d -> entry.getKey().equals(d.getCode().getLeft())).findFirst(); + Optional diag = diagnostics.stream().filter(d -> { + String code = d.getCode() != null ? d.getCode().getLeft() : null; + return entry.getKey().equals(code); + }).findFirst(); org.netbeans.api.lsp.Diagnostic.LazyCodeActions actions = err.getActions(); if (actions != null) { for (org.netbeans.api.lsp.CodeAction inputAction : actions.computeCodeActions(ex -> client.logMessage(new MessageParams(MessageType.Error, ex.getMessage())))) { diff --git a/java/java.lsp.server/test/unit/src/META-INF/generated-layer.xml b/java/java.lsp.server/test/unit/src/META-INF/generated-layer.xml index 2f64f85e2fd5..84a2d34124f8 100644 --- a/java/java.lsp.server/test/unit/src/META-INF/generated-layer.xml +++ b/java/java.lsp.server/test/unit/src/META-INF/generated-layer.xml @@ -14,4 +14,14 @@ + + + + + + + + + +