This is an automated email from the ASF dual-hosted git repository. dbalek pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/netbeans.git
The following commit(s) were added to refs/heads/master by this push: new 68222f2649 LPS: SignatureHelp implemented. new e71774364f Merge pull request #6476 from dbalek/dbalek/lsp-signature-help 68222f2649 is described below commit 68222f26491c6b9ea886f18605e4f9443b45fc34 Author: Dusan Balek <dusan.ba...@oracle.com> AuthorDate: Thu Sep 21 16:01:46 2023 +0200 LPS: SignatureHelp implemented. --- ide/api.lsp/apichanges.xml | 15 ++ ide/api.lsp/manifest.mf | 2 +- .../org/netbeans/api/lsp/SignatureInformation.java | 248 +++++++++++++++++++++ .../modules/lsp/SignatureInformationAccessor.java | 57 +++++ .../spi/lsp/SignatureInformationCollector.java | 132 +++++++++++ .../netbeans/api/lsp/SignatureInformationTest.java | 128 +++++++++++ java/java.completion/nbproject/project.properties | 2 +- .../modules/java/completion/JavaTooltipTask.java | 78 +++++-- java/java.editor/nbproject/project.xml | 4 +- .../java/JavaSignatureInformationCollector.java | 66 ++++++ .../editor/java/MethodParamsTipPaintComponent.java | 7 + .../modules/java/lsp/server/protocol/Server.java | 5 + .../server/protocol/TextDocumentServiceImpl.java | 48 +++- .../java/lsp/server/protocol/ServerTest.java | 64 +++++- 14 files changed, 826 insertions(+), 30 deletions(-) diff --git a/ide/api.lsp/apichanges.xml b/ide/api.lsp/apichanges.xml index b14de6dd7b..3cbeb75231 100644 --- a/ide/api.lsp/apichanges.xml +++ b/ide/api.lsp/apichanges.xml @@ -51,6 +51,21 @@ <!-- ACTUAL CHANGES BEGIN HERE: --> <changes> + <change id="SignatureInformation"> + <api name="LSP_API"/> + <summary>Added SignatureInformation and SignatureInformationCollector</summary> + <version major="1" minor="20"/> + <date day="21" month="9" year="2023"/> + <author login="dbalek"/> + <compatibility binary="compatible" source="compatible" addition="yes" deletion="no"/> + <description> + A <a href="@TOP@/org/netbeans/api/lsp/SignatureInformation.html">SignatureInformation</a> class + and <a href="@TOP@/org/netbeans/api/lsp/SignatureInformationCollecto.html">SignatureInformationCollector</a> interface + introduced that allows to compute and collect signature information. + </description> + <class package="org.netbeans.api.lsp" name="SignatureInformation"/> + <class package="org.netbeans.spi.lsp" name="SignatureInformationCollector"/> + </change> <change id="LazyCodeAction"> <api name="LSP_API"/> <summary>Added CodeAction with lazy edit computation</summary> diff --git a/ide/api.lsp/manifest.mf b/ide/api.lsp/manifest.mf index dccfadd348..3bfd176abc 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.19 +OpenIDE-Module-Specification-Version: 1.20 AutoUpdate-Show-In-Client: false diff --git a/ide/api.lsp/src/org/netbeans/api/lsp/SignatureInformation.java b/ide/api.lsp/src/org/netbeans/api/lsp/SignatureInformation.java new file mode 100644 index 0000000000..a5e384da01 --- /dev/null +++ b/ide/api.lsp/src/org/netbeans/api/lsp/SignatureInformation.java @@ -0,0 +1,248 @@ +/* + * 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; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import javax.swing.text.Document; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.NullAllowed; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.lib.editor.util.swing.DocumentUtilities; +import org.netbeans.modules.lsp.SignatureInformationAccessor; +import org.netbeans.spi.lsp.SignatureInformationCollector; + +/** + * Represents the signature of something callable. A signature can have a label, + * like a function-name, a doc-comment, and a set of parameters. + * + * @author Dusan Balek + * @since 1.20 + */ +public final class SignatureInformation { + + static { + SignatureInformationAccessor.setDefault(new SignatureInformationAccessor() { + @Override + public SignatureInformation createSignatureInformation(String label, List<ParameterInformation> params, boolean isActive, String documentation) { + return new SignatureInformation(label, params, isActive, documentation); + } + + @Override + public ParameterInformation createParameterInformation(String label, boolean isActive, String documentation) { + return new ParameterInformation(label, isActive, documentation); + } + }); + } + + private final String label; + private final List<ParameterInformation> params; + private final boolean isActive; + private final String documentation; + + private SignatureInformation(String label, List<ParameterInformation> params, boolean isActive, String documentation) { + this.label = label; + this.params = params; + this.isActive = isActive; + this.documentation = documentation; + } + + /** + * The label of this signature information. + * + * @since 1.20 + */ + @NonNull + public String getLabel() { + return label; + } + + /** + * The parameters of this signature. + * + * @since 1.20 + */ + @NonNull + public List<ParameterInformation> getParameters() { + return Collections.unmodifiableList(params); + } + + /** + * Returns true if the signature is active. + * + * @since 1.20 + */ + public boolean isActive() { + return isActive; + } + + /** + * A human-readable string that represents a doc-comment. An HTML format is + * supported. + * + * @since 1.20 + */ + @CheckForNull + public String getDocumentation() { + return documentation; + } + + /** + * Computes and collects signature information for a document at a given offset. Example + * usage can be illustrated by: + * {@snippet file="org/netbeans/api/lsp/SignatureInformationTest.java" region="testSignatureInformationCollect"} + * + * @param doc a text document + * @param offset an offset inside the text document + * @param context an optional signature help context + * @param consumer an operation accepting collected signature information + * + * @since 1.20 + */ + public static void collect(@NonNull Document doc, int offset, @NullAllowed Context context, @NonNull Consumer<SignatureInformation> consumer) { + MimePath mimePath = MimePath.parse(DocumentUtilities.getMimeType(doc)); + for (SignatureInformationCollector collector : MimeLookup.getLookup(mimePath).lookupAll(SignatureInformationCollector.class)) { + collector.collectSignatureInformation(doc, offset, context, consumer); + } + } + + /** + * Represents a parameter of a callable-signature. A parameter can + * have a label and a doc-comment. + * + * @since 1.20 + */ + public static final class ParameterInformation { + + private final String label; + private final boolean isActive; + private final String documentation; + + private ParameterInformation(String label, boolean isActive, String documentation) { + this.label = label; + this.isActive = isActive; + this.documentation = documentation; + } + + /** + * The label of this parameter information. + * <p> + * <i>Note</i>: a label should be a substring of its containing + * signature label. Its intended use case is to highlight the parameter + * label part in the {@code SignatureInformation.label}. + * + * @since 1.20 + */ + @NonNull + public String getLabel() { + return label; + } + + /** + * Returns true if the parameter is active. + * + * @since 1.20 + */ + public boolean isActive() { + return isActive; + } + + /** + * A human-readable string that represents a doc-comment. An HTML format is + * supported. + * + * @since 1.20 + */ + @CheckForNull + public String getDocumentation() { + return documentation; + } + } + + /** + * Additional information about the context in which a signature help request + * was triggered. + * + * @since 1.20 + */ + public static final class Context { + + private final TriggerKind triggerKind; + private final Character triggerCharacter; + + public Context(@NonNull TriggerKind triggerKind, @NullAllowed Character triggerCharacter) { + this.triggerKind = triggerKind; + this.triggerCharacter = triggerCharacter; + } + + /** + * Action that caused signature help to be triggered. + * + * @since 1.20 + */ + @NonNull + public TriggerKind getTriggerKind() { + return triggerKind; + } + + /** + * Character that caused signature help to be triggered. + * Is undefined if {@code triggerKind != TriggerKind.TriggerCharacter}. + * + * @since 1.20 + */ + @CheckForNull + public Character getTriggerCharacter() { + return triggerCharacter; + } + } + + /** + * Specifies how a signature help was triggered. + * + * @since 1.20 + */ + public enum TriggerKind { + + /** + * Signature help was invoked manually by the user or by a command. + * + * @since 1.20 + */ + Invoked, + + /** + * Signature help was triggered by a trigger character. + * + * @since 1.20 + */ + TriggerCharacter, + + /** + * Signature help was triggered by the cursor moving or by the document + * content changing. + * + * @since 1.20 + */ + ContentChange + } +} diff --git a/ide/api.lsp/src/org/netbeans/modules/lsp/SignatureInformationAccessor.java b/ide/api.lsp/src/org/netbeans/modules/lsp/SignatureInformationAccessor.java new file mode 100644 index 0000000000..be0e9f87c9 --- /dev/null +++ b/ide/api.lsp/src/org/netbeans/modules/lsp/SignatureInformationAccessor.java @@ -0,0 +1,57 @@ +/* + * 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; + +import java.util.List; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.lsp.SignatureInformation; +import org.openide.util.Exceptions; +import org.openide.util.Parameters; + + +public abstract class SignatureInformationAccessor { + + private static volatile SignatureInformationAccessor DEFAULT; + + public static synchronized SignatureInformationAccessor getDefault() { + SignatureInformationAccessor instance = DEFAULT; + if (instance == null) { + Class<?> c = SignatureInformation.class; + try { + Class.forName(c.getName(), true, c.getClassLoader()); + instance = DEFAULT; + assert instance != null; + } catch (Exception ex) { + Exceptions.printStackTrace(ex); + } + } + return instance; + } + + public static void setDefault(@NonNull final SignatureInformationAccessor accessor) { + Parameters.notNull("accessor", accessor); //NOI18N + if (DEFAULT != null) { + throw new IllegalStateException("Accessor already initialized"); + } + DEFAULT = accessor; + } + + public abstract SignatureInformation createSignatureInformation(String label, List<SignatureInformation.ParameterInformation> params, boolean isActive, String documentation); + public abstract SignatureInformation.ParameterInformation createParameterInformation(String label, boolean isActive, String documentation); +} diff --git a/ide/api.lsp/src/org/netbeans/spi/lsp/SignatureInformationCollector.java b/ide/api.lsp/src/org/netbeans/spi/lsp/SignatureInformationCollector.java new file mode 100644 index 0000000000..3205202425 --- /dev/null +++ b/ide/api.lsp/src/org/netbeans/spi/lsp/SignatureInformationCollector.java @@ -0,0 +1,132 @@ +/* + * 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.spi.lsp; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import javax.swing.text.Document; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.NullAllowed; +import org.netbeans.api.lsp.SignatureInformation; +import org.netbeans.modules.lsp.SignatureInformationAccessor; +import org.netbeans.spi.editor.mimelookup.MimeLocation; + +/** + * Interface for computing and collecting signature information. Clients can use + * this interface to collect signature information and send them for presentation + * outside of NetBeans using the Language Server Protocol. Implementations of the + * interface should be registered in MimeLookup. + * {@snippet : + * + * {@code @}MimeRegistration(mimeType = "text/foo", service = SignatureInformationCollector.class) + * public class FooSignatureInformationCollector implements SignatureInformationCollector { + * ... + * } + * } + * + * @author Dusan Balek + * @since 1.20 + */ +@MimeLocation(subfolderName = "SignatureHelpProviders") +public interface SignatureInformationCollector { + + /** + * Computes and collects signature information for a document at a given offset. + * + * @param doc a text document + * @param offset an offset inside the text document + * @param context an optional signature help context + * @param consumer an operation accepting collected signature information + * + * + * @since 1.0 + */ + public void collectSignatureInformation(@NonNull Document doc, int offset, @NullAllowed SignatureInformation.Context context, @NonNull Consumer<SignatureInformation> consumer); + + /** + * Creates a builder for {@link SignatureInformation} instances. + * + * @param label the label of the signature information + * @param isActive true if the signature is active + * @return newly created builder + * + * @since 1.20 + */ + public static Builder newBuilder(@NonNull String label, boolean isActive) { + return new Builder(label, isActive); + } + + /** + * Builder for {@link SignatureInformation} instances. Its usage can be illustrated by: + * {@snippet file="org/netbeans/api/lsp/SignatureInformationTest.java" region="builder"} + * + * @since 1.20 + */ + public static final class Builder { + + private final String label; + private final List<SignatureInformation.ParameterInformation> params; + private final boolean isActive; + private String documentation; + + private Builder(@NonNull String label, boolean isActive) { + this.label = label; + this.isActive = isActive; + this.params = new ArrayList<>(); + } + + /** + * A human-readable string that represents a doc-comment. An HTML format + * is supported. + * + * @since 1.20 + */ + @NonNull + public Builder documentation(@NonNull String documentation) { + this.documentation = documentation; + return this; + } + + /** + * Adds parameter information to this signature. + * + * @param label label of the parameter information + * @param isActive true if the the parameter is active + * @param documentation an optional doc-comment of the parameter + * + * @since 1.20 + */ + @NonNull + public Builder addParameter(@NonNull String label, boolean isActive, @NullAllowed String documentation) { + this.params.add(SignatureInformationAccessor.getDefault().createParameterInformation(label, isActive, documentation)); + return this; + } + + /** + * Builds signature information. + * + * @since 1.20 + */ + @NonNull + public SignatureInformation build() { + return SignatureInformationAccessor.getDefault().createSignatureInformation(label, params, isActive, documentation); + } + } +} diff --git a/ide/api.lsp/test/unit/src/org/netbeans/api/lsp/SignatureInformationTest.java b/ide/api.lsp/test/unit/src/org/netbeans/api/lsp/SignatureInformationTest.java new file mode 100644 index 0000000000..21ebf4fe4e --- /dev/null +++ b/ide/api.lsp/test/unit/src/org/netbeans/api/lsp/SignatureInformationTest.java @@ -0,0 +1,128 @@ +/* + * 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; + +import java.util.List; +import java.util.function.Consumer; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultStyledDocument; +import javax.swing.text.Document; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.api.editor.mimelookup.test.MockMimeLookup; +import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.lsp.SignatureInformationCollector; + +/** + * + * @author Dusan Balek + */ +public class SignatureInformationTest extends NbTestCase { + + public SignatureInformationTest(String name) { + super(name); + } + + @Override + public void setUp () throws Exception { + super.setUp(); + clearWorkDir(); + MockMimeLookup.setInstances (MimePath.get ("text/foo"), new FooSignatureInformationCollector()); + } + + public void testSignatureInformationCollect() { + Document doc = createDocument("text/foo", ""); + int offset = 0; + // @start region="testSignatureInformationCollect" + + // Compute and collect signature information for a document at a given offset + SignatureInformation.collect(doc, offset, null, signature -> { + + // signature should never be 'null' + assertNotNull(signature); + + // getting signature 'label' + String label = signature.getLabel(); + assertEquals("label", label); + + // check if the signature is active + assertTrue(signature.isActive()); + + // getting signature 'parameters' + List<SignatureInformation.ParameterInformation> params = signature.getParameters(); + // check number of parameters + assertEquals(2, params.size()); + for (int i = 0; i < params.size(); i++) { + SignatureInformation.ParameterInformation param = params.get(i); + // getting parameter 'label' + String paramLabel = param.getLabel(); + assertEquals("param" + i, paramLabel); + // check if the parameter is active + if (i == 1) { + assertTrue(param.isActive()); + } else { + assertFalse(param.isActive()); + } + // getting optional parameter 'documentation' + String paramDocumentation = param.getDocumentation(); + assertEquals("param" + i + " documentation", paramDocumentation); + } + + // getting optional signature 'documentation' + String documentation = signature.getDocumentation(); + assertEquals("documentation", documentation); + }); + + // @end region="testSignatureInformationCollect" + } + + private Document createDocument(String mimeType, String contents) { + Document doc = new DefaultStyledDocument(); + doc.putProperty("mimeType", mimeType); + try { + doc.insertString(0, contents, null); + return doc; + } catch (BadLocationException ble) { + throw new IllegalStateException(ble); + } + } + + private static class FooSignatureInformationCollector implements SignatureInformationCollector { + + @Override + public void collectSignatureInformation(Document doc, int offset, SignatureInformation.Context context, Consumer<SignatureInformation> consumer) { + // @start region="builder" + + // Create a builder for creating 'SignatureInformation' instance providing its 'label' and 'isActive' flag + SignatureInformation si = SignatureInformationCollector.newBuilder("label", true) + + // add signature parameters + .addParameter("param0", false, "param0 documentation") + .addParameter("param1", true, "param1 documentation") + + // set signature documentation + .documentation("documentation") + + // create a new 'SignatureInformation' instance + .build(); + + // @end region="builder" + consumer.accept(si); + } + } +} diff --git a/java/java.completion/nbproject/project.properties b/java/java.completion/nbproject/project.properties index a59952644c..4540e613f7 100644 --- a/java/java.completion/nbproject/project.properties +++ b/java/java.completion/nbproject/project.properties @@ -17,7 +17,7 @@ is.autoload=true javac.source=1.8 javac.compilerargs=-Xlint -Xlint:-serial -spec.version.base=2.7.0 +spec.version.base=2.8.0 #test configs test.config.jet-main.includes=\ diff --git a/java/java.completion/src/org/netbeans/modules/java/completion/JavaTooltipTask.java b/java/java.completion/src/org/netbeans/modules/java/completion/JavaTooltipTask.java index 10149c3d09..0ee74aa066 100644 --- a/java/java.completion/src/org/netbeans/modules/java/completion/JavaTooltipTask.java +++ b/java/java.completion/src/org/netbeans/modules/java/completion/JavaTooltipTask.java @@ -21,7 +21,6 @@ package org.netbeans.modules.java.completion; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.Callable; @@ -42,7 +41,6 @@ import com.sun.source.util.Trees; import org.netbeans.api.annotations.common.NullAllowed; import org.netbeans.api.java.source.*; -import org.openide.util.NbBundle; /** * @@ -60,7 +58,9 @@ public final class JavaTooltipTask extends BaseTask { private int anchorOffset; private List<List<String>> toolTipData; + private List<String> toolTipSignatures; private int toolTipIndex; + private int activeSignatureIndex; private int toolTipOffset; private JavaTooltipTask(final int caretOffset, final Callable<Boolean> cancel) { @@ -71,10 +71,18 @@ public final class JavaTooltipTask extends BaseTask { return toolTipData; } + public List<String> getTooltipSignatures() { + return toolTipSignatures; + } + public int getTooltipIndex() { return toolTipIndex; } + public int getActiveSignatureIndex() { + return activeSignatureIndex; + } + public int getAnchorOffset() { return anchorOffset; } @@ -85,7 +93,7 @@ public final class JavaTooltipTask extends BaseTask { @Override protected void resolve(CompilationController controller) throws IOException { - Env env = getCompletionEnvironment(controller, true); + Env env = getCompletionEnvironment(controller, false); if (env == null) { return; } @@ -102,18 +110,19 @@ public final class JavaTooltipTask extends BaseTask { List<Tree> argTypes = getArgumentsUpToPos(env, mi.getArguments(), (int) sourcePositions.getEndPosition(root, mi.getMethodSelect()), startPos, false); if (argTypes != null) { controller.toPhase(JavaSource.Phase.RESOLVED); + final Trees trees = controller.getTrees(); TypeMirror[] types = new TypeMirror[argTypes.size()]; int j = 0; for (Tree t : argTypes) { - types[j++] = controller.getTrees().getTypeMirror(new TreePath(path, t)); + types[j++] = trees.getTypeMirror(new TreePath(path, t)); } - Tree mid = mi.getMethodSelect(); + final Tree mid = mi.getMethodSelect(); + final Element activeElement = trees.getElement(path); path = new TreePath(path, mid); switch (mid.getKind()) { case MEMBER_SELECT: { ExpressionTree exp = ((MemberSelectTree) mid).getExpression(); path = new TreePath(path, exp); - final Trees trees = controller.getTrees(); final TypeMirror type = trees.getTypeMirror(path); final Element element = trees.getElement(path); final boolean isStatic = element != null && (element.getKind().isClass() || element.getKind().isInterface() || element.getKind() == TYPE_PARAMETER); @@ -127,13 +136,12 @@ public final class JavaTooltipTask extends BaseTask { return (!isStatic || e.getModifiers().contains(STATIC) || e.getKind() == CONSTRUCTOR) && (t.getKind() != TypeKind.DECLARED || trees.isAccessible(scope, e, (DeclaredType) (isSuperCall && enclType != null ? enclType : t))); } }; - toolTipData = getMatchingParams(controller, type, controller.getElementUtilities().getMembers(type, acceptor), ((MemberSelectTree) mid).getIdentifier().toString(), types, controller.getTypes()); + handleMatchingParams(controller, type, activeElement, controller.getElementUtilities().getMembers(type, acceptor), ((MemberSelectTree) mid).getIdentifier().toString(), types); break; } case IDENTIFIER: { final Scope scope = env.getScope(); final TreeUtilities tu = controller.getTreeUtilities(); - final Trees trees = controller.getTrees(); final TypeElement enclClass = scope.getEnclosingClass(); final boolean isStatic = enclClass != null ? (tu.isStaticContext(scope) || (env.getPath().getLeaf().getKind() == Tree.Kind.BLOCK && ((BlockTree) env.getPath().getLeaf()).isStatic())) : false; ElementUtilities.ElementAcceptor acceptor = new ElementUtilities.ElementAcceptor() { @@ -152,12 +160,12 @@ public final class JavaTooltipTask extends BaseTask { String name = ((IdentifierTree) mid).getName().toString(); if (SUPER_KEYWORD.equals(name) && enclClass != null) { TypeMirror superclass = enclClass.getSuperclass(); - toolTipData = getMatchingParams(controller, superclass, controller.getElementUtilities().getMembers(superclass, acceptor), INIT, types, controller.getTypes()); + handleMatchingParams(controller, superclass, activeElement, controller.getElementUtilities().getMembers(superclass, acceptor), INIT, types); } else if (THIS_KEYWORD.equals(name) && enclClass != null) { TypeMirror thisclass = enclClass.asType(); - toolTipData = getMatchingParams(controller, thisclass, controller.getElementUtilities().getMembers(thisclass, acceptor), INIT, types, controller.getTypes()); + handleMatchingParams(controller, thisclass, activeElement, controller.getElementUtilities().getMembers(thisclass, acceptor), INIT, types); } else { - toolTipData = getMatchingParams(controller, enclClass != null ? enclClass.asType() : null, controller.getElementUtilities().getLocalMembersAndVars(scope, acceptor), name, types, controller.getTypes()); + handleMatchingParams(controller, enclClass != null ? enclClass.asType() : null, activeElement, controller.getElementUtilities().getLocalMembersAndVars(scope, acceptor), name, types); } break; } @@ -183,13 +191,14 @@ public final class JavaTooltipTask extends BaseTask { List<Tree> argTypes = getArgumentsUpToPos(env, nc.getArguments(), pos, startPos, false); if (argTypes != null) { controller.toPhase(JavaSource.Phase.RESOLVED); + final Trees trees = controller.getTrees(); TypeMirror[] types = new TypeMirror[argTypes.size()]; int j = 0; for (Tree t : argTypes) { - types[j++] = controller.getTrees().getTypeMirror(new TreePath(path, t)); + types[j++] = trees.getTypeMirror(new TreePath(path, t)); } + final Element activeElement = trees.getElement(path); path = new TreePath(path, nc.getIdentifier()); - final Trees trees = controller.getTrees(); TypeMirror type = trees.getTypeMirror(path); if (type != null && type.getKind() == TypeKind.ERROR && path.getLeaf().getKind() == Tree.Kind.PARAMETERIZED_TYPE) { path = new TreePath(path, ((ParameterizedTypeTree) path.getLeaf()).getType()); @@ -204,7 +213,7 @@ public final class JavaTooltipTask extends BaseTask { return e.getKind() == CONSTRUCTOR && (trees.isAccessible(scope, e, (DeclaredType) t) || isAnonymous && e.getModifiers().contains(PROTECTED)); } }; - toolTipData = getMatchingParams(controller, type, controller.getElementUtilities().getMembers(type, acceptor), INIT, types, controller.getTypes()); + handleMatchingParams(controller, type, activeElement, controller.getElementUtilities().getMembers(type, acceptor), INIT, types); toolTipIndex = types.length; if (pos < 0) { path = path.getParentPath(); @@ -226,9 +235,12 @@ public final class JavaTooltipTask extends BaseTask { } } - private List<List<String>> getMatchingParams(CompilationInfo info, TypeMirror type, Iterable<? extends Element> elements, String name, TypeMirror[] argTypes, Types types) { - List<List<String>> ret = new ArrayList<>(); + private void handleMatchingParams(CompilationInfo info, TypeMirror type, Element activeElement, Iterable<? extends Element> elements, String name, TypeMirror[] argTypes) { + List<List<String>> data = new ArrayList<>(); + List<String> signatures = new ArrayList<>(); + Types types = info.getTypes(); TypeUtilities tu = info.getTypeUtilities(); + activeSignatureIndex = 0; for (Element e : elements) { if ((e.getKind() == CONSTRUCTOR || e.getKind() == METHOD) && name.contentEquals(e.getSimpleName())) { List<? extends VariableElement> params = ((ExecutableElement) e).getParameters(); @@ -237,10 +249,19 @@ public final class JavaTooltipTask extends BaseTask { if (!varArgs && (parSize < argTypes.length)) { continue; } + if (e == activeElement) { + activeSignatureIndex = signatures.size(); + } + ExecutableType eType = (ExecutableType) asMemberOf(e, type, types); + StringBuilder sig = new StringBuilder(INIT.equals(name) && type != null && type.getKind() == TypeKind.DECLARED ? ((DeclaredType) type).asElement().getSimpleName() : name).append('('); if (parSize == 0) { - ret.add(Collections.<String>singletonList(NbBundle.getMessage(JavaCompletionTask.class, "JCP-no-parameters"))); + data.add(new ArrayList<>()); + sig.append(')'); + if (e.getKind() == METHOD) { + sig.append(" : ").append(tu.getTypeName(eType.getReturnType())); + } + signatures.add(sig.toString()); } else { - ExecutableType eType = (ExecutableType) asMemberOf(e, type, types); Iterator<? extends TypeMirror> parIt = eType.getParameterTypes().iterator(); TypeMirror param = null; for (int i = 0; i <= argTypes.length; i++) { @@ -258,21 +279,29 @@ public final class JavaTooltipTask extends BaseTask { for (Iterator<? extends VariableElement> it = params.iterator(); it.hasNext();) { VariableElement ve = it.next(); StringBuilder sb = new StringBuilder(); - sb.append(tu.getTypeName(tIt.next())); + CharSequence typeName = tu.getTypeName(tIt.next()); + sb.append(typeName); + sig.append(typeName); if (varArgs && !tIt.hasNext()) { sb.delete(sb.length() - 2, sb.length()).append("..."); //NOI18N + sig.delete(sig.length() - 2, sig.length()).append("..."); //NOI18N } CharSequence veName = ve.getSimpleName(); if (veName != null && veName.length() > 0) { - sb.append(" "); // NOI18N - sb.append(veName); + sb.append(" ").append(veName); // NOI18N + sig.append(" ").append(veName); // NOI18N } if (it.hasNext()) { - sb.append(", "); // NOI18N + sig.append(", "); // NOI18N } paramStrings.add(sb.toString()); } - ret.add(paramStrings); + data.add(paramStrings); + sig.append(')'); + if (e.getKind() == METHOD) { + sig.append(" : ").append(tu.getTypeName(eType.getReturnType())); + } + signatures.add(sig.toString()); break; } if (argTypes[i] == null || argTypes[i].getKind() != TypeKind.ERROR && !isAssignable(types, argTypes[i], param)) { @@ -282,7 +311,8 @@ public final class JavaTooltipTask extends BaseTask { } } } - return ret.isEmpty() ? null : ret; + toolTipData = data.isEmpty() ? null : data; + toolTipSignatures = signatures.isEmpty() ? null : signatures; } private static boolean isAssignable(Types types, TypeMirror arg, TypeMirror parameter) { diff --git a/java/java.editor/nbproject/project.xml b/java/java.editor/nbproject/project.xml index 132d3c9ffe..e3b37e4895 100644 --- a/java/java.editor/nbproject/project.xml +++ b/java/java.editor/nbproject/project.xml @@ -58,7 +58,7 @@ <compile-dependency/> <run-dependency> <release-version>1</release-version> - <specification-version>1.17</specification-version> + <specification-version>1.20</specification-version> </run-dependency> </dependency> <dependency> @@ -207,7 +207,7 @@ <build-prerequisite/> <compile-dependency/> <run-dependency> - <specification-version>1.8</specification-version> + <specification-version>2.8</specification-version> </run-dependency> </dependency> <dependency> diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/JavaSignatureInformationCollector.java b/java/java.editor/src/org/netbeans/modules/editor/java/JavaSignatureInformationCollector.java new file mode 100644 index 0000000000..943b14f0eb --- /dev/null +++ b/java/java.editor/src/org/netbeans/modules/editor/java/JavaSignatureInformationCollector.java @@ -0,0 +1,66 @@ +/* + * 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.editor.java; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; +import javax.swing.text.Document; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.api.lsp.SignatureInformation; +import org.netbeans.modules.java.completion.JavaTooltipTask; +import org.netbeans.modules.parsing.api.ParserManager; +import org.netbeans.modules.parsing.api.Source; +import org.netbeans.modules.parsing.spi.ParseException; +import org.netbeans.spi.lsp.SignatureInformationCollector; +import org.openide.util.Exceptions; + +/** + * + * @author Dusan Balek + */ +@MimeRegistration(mimeType = "text/x-java", service = SignatureInformationCollector.class) +public final class JavaSignatureInformationCollector implements SignatureInformationCollector { + + @Override + public void collectSignatureInformation(Document doc, int offset, SignatureInformation.Context context, Consumer<SignatureInformation> consumer) { + if (context == null || context.getTriggerKind() != SignatureInformation.TriggerKind.TriggerCharacter || context.getTriggerCharacter() == '(') { + try { + JavaTooltipTask task = JavaTooltipTask.create(offset, () -> false); + ParserManager.parse(Collections.singletonList(Source.create(doc)), task); + if (task.getTooltipData() != null && task.getTooltipSignatures() != null) { + Iterator<List<String>> it = task.getTooltipData().iterator(); + for (int i = 0; i < task.getTooltipSignatures().size() && it.hasNext(); i++) { + List<String> params = it.next(); + String signature = task.getTooltipSignatures().get(i); + Builder builder = SignatureInformationCollector.newBuilder(signature, i == task.getActiveSignatureIndex()); + for (int j = 0; j < params.size(); j++) { + String param = params.get(j); + builder.addParameter(param, j == task.getTooltipIndex(), null); + } + consumer.accept(builder.build()); + } + } + } catch (ParseException ex) { + Exceptions.printStackTrace(ex); + } + } + } +} diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/MethodParamsTipPaintComponent.java b/java/java.editor/src/org/netbeans/modules/editor/java/MethodParamsTipPaintComponent.java index 565375de5d..2db9c3338c 100644 --- a/java/java.editor/src/org/netbeans/modules/editor/java/MethodParamsTipPaintComponent.java +++ b/java/java.editor/src/org/netbeans/modules/editor/java/MethodParamsTipPaintComponent.java @@ -24,6 +24,7 @@ import java.util.List; import javax.swing.*; import javax.swing.text.JTextComponent; import org.openide.awt.GraphicsUtils; +import org.openide.util.NbBundle; /** * @@ -91,8 +92,14 @@ public class MethodParamsTipPaintComponent extends JToolTip { if (params != null) { for (List<String> p : params) { int i = 0; + if (p.isEmpty()) { + p.add(NbBundle.getMessage(MethodParamsTipPaintComponent.class, "JCP-no-parameters")); + } int plen = p.size() - 1; for (String s : p) { + if (i < plen) { + s += ", "; //NOI18N + } if (getWidth(s, i == idx || i == plen && idx > plen ? getDrawFont().deriveFont(Font.BOLD) : getDrawFont()) + drawX > screenWidth) { drawY += fontHeight; drawX = startX + getWidth(" ", drawFont); //NOI18N diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java index ba01d4f530..f846fdb952 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java @@ -71,6 +71,7 @@ import org.eclipse.lsp4j.SemanticTokensParams; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.SetTraceParams; import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.SignatureHelpOptions; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentSyncKind; import org.eclipse.lsp4j.TextDocumentSyncOptions; @@ -783,6 +784,10 @@ public final class Server { completionOptions.setResolveProvider(true); completionOptions.setTriggerCharacters(Arrays.asList(".", "#", "@", "*")); capabilities.setCompletionProvider(completionOptions); + SignatureHelpOptions signatureHelpOptions = new SignatureHelpOptions(); + signatureHelpOptions.setTriggerCharacters(Arrays.asList("(")); + signatureHelpOptions.setRetriggerCharacters(Arrays.asList(",")); + capabilities.setSignatureHelpProvider(signatureHelpOptions); capabilities.setHoverProvider(true); CodeActionOptions codeActionOptions = new CodeActionOptions(Arrays.asList(CodeActionKind.QuickFix, CodeActionKind.Source, CodeActionKind.SourceOrganizeImports, CodeActionKind.Refactor)); codeActionOptions.setResolveProvider(true); 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 7401da39a2..b6c4a4b9f2 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 @@ -126,6 +126,7 @@ import org.eclipse.lsp4j.LocationLink; import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.MessageParams; import org.eclipse.lsp4j.MessageType; +import org.eclipse.lsp4j.ParameterInformation; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.PrepareRenameParams; import org.eclipse.lsp4j.PrepareRenameResult; @@ -144,6 +145,7 @@ import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SignatureHelpParams; +import org.eclipse.lsp4j.SignatureInformation; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.TextDocumentContentChangeEvent; import org.eclipse.lsp4j.TextDocumentEdit; @@ -580,7 +582,51 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli @Override public CompletableFuture<SignatureHelp> signatureHelp(SignatureHelpParams params) { - throw new UnsupportedOperationException("Not supported yet."); + // shortcut: if the projects are not yet initialized, return empty: + if (server.openedProjects().getNow(null) == null) { + return CompletableFuture.completedFuture(null); + } + String uri = params.getTextDocument().getUri(); + FileObject file = fromURI(uri); + Document rawDoc = server.getOpenedDocuments().getDocument(uri); + if (file == null || !(rawDoc instanceof StyledDocument)) { + return CompletableFuture.completedFuture(null); + } + StyledDocument doc = (StyledDocument) rawDoc; + List<SignatureInformation> signatures = new ArrayList<>(); + AtomicInteger activeSignature = new AtomicInteger(-1); + AtomicInteger activeParameter = new AtomicInteger(-1); + org.netbeans.api.lsp.SignatureInformation.collect(doc, Utils.getOffset(doc, params.getPosition()), null, signature -> { + SignatureInformation signatureInformation = new SignatureInformation(signature.getLabel()); + List<ParameterInformation> parameters = new ArrayList<>(signature.getParameters().size()); + for (int i = 0; i < signature.getParameters().size(); i++) { + org.netbeans.api.lsp.SignatureInformation.ParameterInformation parameter = signature.getParameters().get(i); + ParameterInformation parameterInformation = new ParameterInformation(parameter.getLabel()); + if (parameter.getDocumentation() != null) { + MarkupContent markup = new MarkupContent(); + markup.setKind("markdown"); + markup.setValue(html2MD(parameter.getDocumentation())); + parameterInformation.setDocumentation(markup); + } + parameters.add(parameterInformation); + if (signatureInformation.getActiveParameter() == null && parameter.isActive()) { + signatureInformation.setActiveParameter(i); + } + } + if (signature.getDocumentation() != null) { + MarkupContent markup = new MarkupContent(); + markup.setKind("markdown"); + markup.setValue(html2MD(signature.getDocumentation())); + signatureInformation.setDocumentation(markup); + } + signatureInformation.setParameters(parameters); + if (activeSignature.get() < 0 && signature.isActive()) { + activeSignature.set(signatures.size()); + activeParameter.set(signatureInformation.getActiveParameter()); + } + signatures.add(signatureInformation); + }); + return CompletableFuture.completedFuture(signatures.isEmpty() ? null : new SignatureHelp(signatures, activeSignature.get(), activeParameter.get())); } @Override diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java index 84867cfa97..8f657a9b2b 100644 --- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java +++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java @@ -53,7 +53,6 @@ import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -109,6 +108,7 @@ import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.MessageActionItem; import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.ParameterInformation; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.ProgressParams; import org.eclipse.lsp4j.PublishDiagnosticsParams; @@ -126,6 +126,9 @@ import org.eclipse.lsp4j.ResourceOperationKind; import org.eclipse.lsp4j.ShowDocumentParams; import org.eclipse.lsp4j.ShowDocumentResult; import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.SignatureHelp; +import org.eclipse.lsp4j.SignatureHelpParams; +import org.eclipse.lsp4j.SignatureInformation; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.TextDocumentClientCapabilities; import org.eclipse.lsp4j.TextDocumentContentChangeEvent; @@ -1391,6 +1394,65 @@ public class ServerTest extends NbTestCase { "\n"); } + public void testSignatureHelp() throws Exception { + File src = new File(getWorkDir(), "Test.java"); + src.getParentFile().mkdirs(); + String code = "/**\n" + + " * This is a test class with Javadoc.\n" + + " */\n" + + "public class Test {\n" + + " public static void main(String[] args) {\n" + + " System.out.println(\"len: \" + args.length);\n" + + " }\n" + + "}\n"; + try (Writer w = new FileWriter(src)) { + w.write(code); + } + FileUtil.refreshFor(getWorkDir()); + Launcher<LanguageServer> serverLauncher = createClientLauncherWithLogging(new LspClient() { + @Override + public void telemetryEvent(Object arg0) { + } + + @Override + public void publishDiagnostics(PublishDiagnosticsParams params) { + } + + @Override + public void showMessage(MessageParams arg0) { + } + + @Override + public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams arg0) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void logMessage(MessageParams arg0) { + throw new UnsupportedOperationException("Not supported yet."); + } + }, client.getInputStream(), client.getOutputStream()); + serverLauncher.startListening(); + LanguageServer server = serverLauncher.getRemoteProxy(); + InitializeResult result = server.initialize(new InitializeParams()).get(); + assertNotNull(result.getCapabilities().getSignatureHelpProvider()); + server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(toURI(src), "java", 0, code))); + SignatureHelp help = server.getTextDocumentService().signatureHelp(new SignatureHelpParams(new TextDocumentIdentifier(toURI(src)), new Position(5, 30))).get(); + assertNotNull(help); + List<SignatureInformation> signatures = help.getSignatures(); + assertNotNull(signatures); + SignatureInformation sInfo = signatures.stream().filter(si -> "println(String x) : void".equals(si.getLabel())).findFirst().get(); + assertNotNull(sInfo); + assertEquals(signatures.indexOf(sInfo), help.getActiveSignature().intValue()); + assertEquals(0, help.getActiveParameter().intValue()); + List<ParameterInformation> params = sInfo.getParameters(); + assertNotNull(params); + assertEquals(1, params.size()); + assertTrue(params.get(0).getLabel().isLeft()); + assertEquals("String x", params.get(0).getLabel().getLeft()); + assertEquals(0, sInfo.getActiveParameter().intValue()); + } + public void testAdvancedCompletion1() throws Exception { String javaVersion = System.getProperty("java.specification.version"); File src = new File(getWorkDir(), "Test.java"); --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@netbeans.apache.org For additional commands, e-mail: commits-h...@netbeans.apache.org For further information about the NetBeans mailing lists, visit: https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists