Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signature help #30

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/main/groovy/nextflow/lsp/NextflowLanguageServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.SetTraceParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SignatureHelpOptions;
import org.eclipse.lsp4j.SignatureHelpParams;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.eclipse.lsp4j.TextEdit;
Expand Down Expand Up @@ -163,6 +166,8 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
serverCapabilities.setHoverProvider(true);
serverCapabilities.setReferencesProvider(true);
serverCapabilities.setRenameProvider(true);
var signatureHelpOptions = new SignatureHelpOptions(List.of("(", ","));
serverCapabilities.setSignatureHelpProvider(signatureHelpOptions);
serverCapabilities.setWorkspaceSymbolProvider(true);

var initializeResult = new InitializeResult(serverCapabilities);
Expand Down Expand Up @@ -406,6 +411,20 @@ public CompletableFuture<WorkspaceEdit> rename(RenameParams params) {
});
}

@Override
public CompletableFuture<SignatureHelp> signatureHelp(SignatureHelpParams params) {
return CompletableFutures.computeAsync((cancelChecker) -> {
cancelChecker.checkCanceled();
var uri = params.getTextDocument().getUri();
var position = params.getPosition();
log.debug(String.format("textDocument/signatureHelp %s [ %d, %d ]", relativePath(uri), position.getLine(), position.getCharacter()));
var service = getLanguageService(uri);
if( service == null )
return null;
return service.signatureHelp(params);
});
}

// --- WorkspaceService

@Override
Expand Down
20 changes: 19 additions & 1 deletion src/main/groovy/nextflow/lsp/ast/ASTNodeStringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import groovy.lang.groovydoc.Groovydoc;
import nextflow.lsp.ast.ASTNodeCache;
import nextflow.lsp.ast.ASTUtils;
import nextflow.lsp.services.util.CustomFormattingOptions;
import nextflow.lsp.services.util.Formatter;
import nextflow.script.dsl.Constant;
import nextflow.script.dsl.DslType;
import nextflow.script.dsl.FeatureFlag;
Expand All @@ -46,7 +48,7 @@
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.runtime.StringGroovyMethods;

import static nextflow.script.v2.ASTHelpers.findAnnotation;
import static nextflow.script.v2.ASTHelpers.*;

/**
* Utility methods for retreiving text information for ast nodes.
Expand Down Expand Up @@ -93,13 +95,29 @@ public static String toString(MethodNode node, ASTNodeCache ast) {
var builder = new StringBuilder();
builder.append("workflow ");
builder.append(wn.isEntry() ? "<entry>" : wn.getName());
builder.append("(");
builder.append(
asBlockStatements(wn.takes).stream()
.map(take -> asVarX(take).getName())
.collect(Collectors.joining(", "))
);
builder.append(")");
return builder.toString();
}

if( node instanceof ProcessNode pn ) {
var builder = new StringBuilder();
builder.append("process ");
builder.append(pn.getName());
builder.append("\n\ninput:\n");
asDirectives(pn.inputs).forEach((call) -> {
var fmt = new Formatter(new CustomFormattingOptions(0, false, false));
fmt.append(call.getMethodAsString());
fmt.append(' ');
fmt.visitArguments(asMethodCallArguments(call), hasNamedArgs(call), false);
builder.append(fmt.toString());
builder.append('\n');
});
return builder.toString();
}

Expand Down
12 changes: 12 additions & 0 deletions src/main/groovy/nextflow/lsp/services/LanguageService.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SignatureHelpParams;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.WorkspaceEdit;
Expand Down Expand Up @@ -115,6 +117,7 @@ public LanguageService() {
protected LinkProvider getLinkProvider() { return null; }
protected ReferenceProvider getReferenceProvider() { return null; }
protected RenameProvider getRenameProvider() { return null; }
protected SignatureHelpProvider getSignatureHelpProvider() { return null; }
protected SymbolProvider getSymbolProvider() { return null; }

private volatile boolean initialized;
Expand Down Expand Up @@ -294,6 +297,15 @@ public WorkspaceEdit rename(RenameParams params) {
return provider.rename(params.getTextDocument(), params.getPosition(), params.getNewName());
}

public SignatureHelp signatureHelp(SignatureHelpParams params) {
var provider = getSignatureHelpProvider();
if( provider == null )
return null;

updateNow();
return provider.signatureHelp(params.getTextDocument(), params.getPosition(), params.getContext());
}

public List<? extends WorkspaceSymbol> symbol(WorkspaceSymbolParams params) {
var provider = getSymbolProvider();
if( provider == null )
Expand Down
27 changes: 27 additions & 0 deletions src/main/groovy/nextflow/lsp/services/SignatureHelpProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2024, Seqera Labs
*
* Licensed 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 nextflow.lsp.services;

import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SignatureHelpContext;
import org.eclipse.lsp4j.TextDocumentIdentifier;

public interface SignatureHelpProvider {

SignatureHelp signatureHelp(TextDocumentIdentifier textDocument, Position position, SignatureHelpContext context);

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import nextflow.lsp.services.LinkProvider;
import nextflow.lsp.services.ReferenceProvider;
import nextflow.lsp.services.RenameProvider;
import nextflow.lsp.services.SignatureHelpProvider;
import nextflow.lsp.services.SymbolProvider;

/**
Expand Down Expand Up @@ -95,6 +96,11 @@ protected RenameProvider getRenameProvider() {
return new ScriptReferenceProvider(astCache);
}

@Override
protected SignatureHelpProvider getSignatureHelpProvider() {
return new ScriptSignatureHelpProvider(astCache);
}

@Override
protected SymbolProvider getSymbolProvider() {
return new ScriptSymbolProvider(astCache);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright 2024, Seqera Labs
*
* Licensed 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 nextflow.lsp.services.script;

import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import nextflow.lsp.ast.ASTNodeStringUtils;
import nextflow.lsp.ast.ASTUtils;
import nextflow.lsp.services.SignatureHelpProvider;
import nextflow.lsp.services.util.CustomFormattingOptions;
import nextflow.lsp.services.util.Formatter;
import nextflow.lsp.util.Logger;
import nextflow.script.v2.ProcessNode;
import nextflow.script.v2.WorkflowNode;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.MethodCall;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.MarkupKind;
import org.eclipse.lsp4j.ParameterInformation;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SignatureHelpContext;
import org.eclipse.lsp4j.SignatureInformation;
import org.eclipse.lsp4j.TextDocumentIdentifier;

import static nextflow.script.v2.ASTHelpers.*;

public class ScriptSignatureHelpProvider implements SignatureHelpProvider {

private static Logger log = Logger.getInstance();

private ScriptAstCache ast;

public ScriptSignatureHelpProvider(ScriptAstCache ast) {
this.ast = ast;
}

@Override
public SignatureHelp signatureHelp(TextDocumentIdentifier textDocument, Position position, SignatureHelpContext context) {
if( ast == null ) {
log.error("ast cache is empty while providing signature help");
return null;
}

var uri = URI.create(textDocument.getUri());
var offsetNode = ast.getNodeAtLineAndColumn(uri, position.getLine(), position.getCharacter());
if( offsetNode == null )
return null;

var call = getMethodCall(offsetNode);
if( call == null )
return null;

var result = context.getActiveSignatureHelp();
if( result == null )
result = astNodeToSignatureHelp(call);

var activeParameter = getActiveParameter(offsetNode, call);
var activeSignature = getActiveSignature(call, activeParameter);
result.setActiveSignature(activeSignature);
result.setActiveParameter(activeParameter);
return result;
}

private MethodCall getMethodCall(ASTNode node) {
if( node instanceof MethodCall call ) {
return call;
}

if( node instanceof ArgumentListExpression ) {
return getMethodCall(ast.getParent(node));
}

ASTNode current = node;
while( current != null ) {
if( current instanceof MethodCall call )
return call;
current = ast.getParent(current);
}
return null;
}

private SignatureHelp astNodeToSignatureHelp(MethodCall call) {
var methods = ASTUtils.getMethodOverloadsFromCallExpression(call, ast);
var signatures = methods.stream()
.map((mn) -> {
var documentation = ASTNodeStringUtils.getDocumentation(mn);
var si = new SignatureInformation(
ASTNodeStringUtils.getLabel(mn, ast),
documentation != null ? new MarkupContent(MarkupKind.MARKDOWN, documentation) : null,
getParameterInfo(mn)
);
return si;
})
.collect(Collectors.toList());
return new SignatureHelp(signatures, 0, 0);
}

private List<ParameterInformation> getParameterInfo(MethodNode mn) {
if( mn instanceof ProcessNode pn ) {
return asDirectives(pn.inputs)
.map((call) -> {
var fmt = new Formatter(new CustomFormattingOptions(0, false, false));
fmt.append(call.getMethodAsString());
fmt.append(' ');
fmt.visitArguments(asMethodCallArguments(call), hasNamedArgs(call), false);
var label = fmt.toString();
var documentation = new MarkupContent(MarkupKind.MARKDOWN, "```groovy\n" + label + "\n```");
return new ParameterInformation(label, documentation);
})
.collect(Collectors.toList());
}

if( mn instanceof WorkflowNode wn ) {
return asBlockStatements(wn.takes).stream()
.map((take) -> {
var varX = asVarX(take);
return new ParameterInformation(varX.getName(), varX.getName());
})
.collect(Collectors.toList());
}

return Arrays.stream(mn.getParameters())
.map(p -> new ParameterInformation(ASTNodeStringUtils.toString(p, ast)))
.collect(Collectors.toList());
}

private int getActiveParameter(ASTNode node, MethodCall call) {
var args = asMethodCallArguments(call);
if( node == call || node == call.getArguments() ) {
return args.size();
}
for( int i = 0; i < args.size(); i++ ) {
if( contains(args.get(i), node) )
return i;
}
return -1;
}

private static boolean contains(ASTNode a, ASTNode b) {
return a.getLineNumber() <= b.getLineNumber()
&& a.getColumnNumber() <= b.getColumnNumber()
&& b.getLastLineNumber() <= a.getLastLineNumber()
&& b.getLastColumnNumber() <= a.getLastColumnNumber();
}

private int getActiveSignature(MethodCall call, int activeParameter) {
var methods = ASTUtils.getMethodOverloadsFromCallExpression(call, ast);
if( methods.size() == 1 )
return 0;
var best = ASTUtils.getMethodFromCallExpression(call, ast, activeParameter);
return methods.indexOf(best);
}

}