Skip to content

Commit f7553c7

Browse files
committed
Add command to preview workspace
Signed-off-by: Ben Sherman <[email protected]>
1 parent 471b563 commit f7553c7

File tree

7 files changed

+183
-63
lines changed

7 files changed

+183
-63
lines changed

modules/language-server/src/main/java/nextflow/lsp/NextflowLanguageServer.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,11 @@ private static ServerCapabilities serverCapabilities() {
164164
var documentLinkOptions = new DocumentLinkOptions(false);
165165
result.setDocumentLinkProvider(documentLinkOptions);
166166
result.setDocumentSymbolProvider(true);
167-
var executeCommandOptions = new ExecuteCommandOptions(List.of("nextflow.server.previewDag"));
167+
var commands = List.of(
168+
"nextflow.server.previewDag",
169+
"nextflow.server.previewWorkspace"
170+
);
171+
var executeCommandOptions = new ExecuteCommandOptions(commands);
168172
result.setExecuteCommandProvider(executeCommandOptions);
169173
result.setHoverProvider(true);
170174
result.setReferencesProvider(true);
@@ -543,14 +547,21 @@ public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
543547
cancelChecker.checkCanceled();
544548
var command = params.getCommand();
545549
var arguments = params.getArguments();
546-
if( !"nextflow.server.previewDag".equals(command) || arguments.size() != 2 )
547-
return null;
548-
var uri = JsonUtils.getString(arguments.get(0));
549-
log.debug(String.format("textDocument/executeCommand %s %s", command, arguments.toString()));
550-
var service = getLanguageService(uri);
551-
if( service == null )
552-
return null;
553-
return service.executeCommand(command, arguments);
550+
if( "nextflow.server.previewDag".equals(command) && arguments.size() == 2 ) {
551+
log.debug(String.format("textDocument/previewDag %s", arguments.toString()));
552+
var uri = JsonUtils.getString(arguments.get(0));
553+
var service = getLanguageService(uri);
554+
if( service != null )
555+
return service.executeCommand(command, arguments);
556+
}
557+
if( "nextflow.server.previewWorkspace".equals(command) && arguments.size() == 1 ) {
558+
log.debug(String.format("textDocument/previewWorkspace %s", arguments.toString()));
559+
var name = JsonUtils.getString(arguments.get(0));
560+
var service = scriptServices.get(name);
561+
if( service != null )
562+
return service.executeCommand(command, arguments);
563+
}
564+
return null;
554565
});
555566
}
556567

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2024-2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package nextflow.lsp.services.script;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
21+
import nextflow.script.ast.ProcessNode;
22+
import nextflow.script.ast.WorkflowNode;
23+
import org.codehaus.groovy.ast.CodeVisitorSupport;
24+
import org.codehaus.groovy.ast.MethodNode;
25+
import org.codehaus.groovy.ast.expr.MethodCallExpression;
26+
27+
/**
28+
* Query the list of outgoing calls made by a workflow, process, or function.
29+
*
30+
* @author Ben Sherman <[email protected]>
31+
*/
32+
class OutgoingCallsVisitor extends CodeVisitorSupport {
33+
34+
private List<MethodCallExpression> outgoingCalls;
35+
36+
public List<MethodCallExpression> apply(MethodNode node) {
37+
outgoingCalls = new ArrayList<>();
38+
if( node instanceof ProcessNode pn )
39+
visit(pn.exec);
40+
else if( node instanceof WorkflowNode wn )
41+
visit(wn.main);
42+
else
43+
visit(node.getCode());
44+
return outgoingCalls;
45+
}
46+
47+
@Override
48+
public void visitMethodCallExpression(MethodCallExpression node) {
49+
visit(node.getObjectExpression());
50+
visit(node.getArguments());
51+
52+
if( node.isImplicitThis() )
53+
outgoingCalls.add(node);
54+
}
55+
}

modules/language-server/src/main/java/nextflow/lsp/services/script/ScriptCallHierarchyProvider.java

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@
2525
import nextflow.lsp.services.CallHierarchyProvider;
2626
import nextflow.lsp.util.LanguageServerUtils;
2727
import nextflow.lsp.util.Logger;
28-
import nextflow.script.ast.ProcessNode;
29-
import nextflow.script.ast.WorkflowNode;
3028
import org.codehaus.groovy.ast.ASTNode;
31-
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
3229
import org.codehaus.groovy.ast.MethodNode;
3330
import org.codehaus.groovy.ast.expr.ConstantExpression;
3431
import org.codehaus.groovy.ast.expr.MethodCallExpression;
@@ -188,12 +185,11 @@ public List<CallHierarchyOutgoingCall> outgoingCalls(CallHierarchyItem item) {
188185
return Collections.emptyList();
189186

190187
var sourceUnit = ast.getSourceUnit(uri);
191-
var visitor = new OutgoingCallsVisitor(sourceUnit);
192-
visitor.visit((MethodNode) fromNode);
188+
var calls = new OutgoingCallsVisitor().apply((MethodNode) fromNode);
193189

194190
var callsMap = new HashMap<String, MethodCallExpression>();
195191
var rangesMap = new HashMap<String, List<Range>>();
196-
for( var call : visitor.getOutgoingCalls() ) {
192+
for( var call : calls ) {
197193
var name = call.getMethodAsString();
198194
callsMap.put(name, call);
199195
if( !rangesMap.containsKey(name) )
@@ -221,42 +217,4 @@ public List<CallHierarchyOutgoingCall> outgoingCalls(CallHierarchyItem item) {
221217
return result;
222218
}
223219

224-
private static class OutgoingCallsVisitor extends ClassCodeVisitorSupport {
225-
226-
private SourceUnit sourceUnit;
227-
228-
private List<MethodCallExpression> outgoingCalls = new ArrayList<>();
229-
230-
public OutgoingCallsVisitor(SourceUnit sourceUnit) {
231-
this.sourceUnit = sourceUnit;
232-
}
233-
234-
@Override
235-
protected SourceUnit getSourceUnit() {
236-
return sourceUnit;
237-
}
238-
239-
public void visit(MethodNode node) {
240-
if( node instanceof ProcessNode pn )
241-
visit(pn.exec);
242-
else if( node instanceof WorkflowNode wn )
243-
visit(wn.main);
244-
else
245-
visit(node.getCode());
246-
}
247-
248-
@Override
249-
public void visitMethodCallExpression(MethodCallExpression node) {
250-
visit(node.getObjectExpression());
251-
visit(node.getArguments());
252-
253-
if( node.isImplicitThis() )
254-
outgoingCalls.add(node);
255-
}
256-
257-
public List<MethodCallExpression> getOutgoingCalls() {
258-
return outgoingCalls;
259-
}
260-
}
261-
262220
}

modules/language-server/src/main/java/nextflow/lsp/services/script/ScriptCodeLensProvider.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
import java.util.List;
2222
import java.util.Map;
2323

24-
import com.google.gson.JsonNull;
25-
import com.google.gson.JsonPrimitive;
2624
import nextflow.lsp.services.CodeLensProvider;
2725
import nextflow.lsp.services.script.dag.DataflowVisitor;
2826
import nextflow.lsp.services.script.dag.MermaidRenderer;
@@ -32,6 +30,8 @@
3230
import org.eclipse.lsp4j.Command;
3331
import org.eclipse.lsp4j.TextDocumentIdentifier;
3432

33+
import static nextflow.lsp.util.JsonUtils.asJson;
34+
3535
/**
3636
*
3737
* @author Ben Sherman <[email protected]>
@@ -62,8 +62,7 @@ public List<CodeLens> codeLens(TextDocumentIdentifier textDocument) {
6262
var range = LanguageServerUtils.astNodeToRange(wn);
6363
if( range == null )
6464
continue;
65-
var name = wn.isEntry() ? JsonNull.INSTANCE : new JsonPrimitive(wn.getName());
66-
var arguments = List.of(new JsonPrimitive(uri.toString()), (Object) name);
65+
var arguments = List.of(asJson(uri.toString()), asJson(wn.getName()));
6766
var command = new Command("Preview DAG", "nextflow.previewDag", arguments);
6867
result.add(new CodeLens(range, command, null));
6968
}

modules/language-server/src/main/java/nextflow/lsp/services/script/ScriptService.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,18 @@ protected SymbolProvider getSymbolProvider() {
117117

118118
@Override
119119
public Object executeCommand(String command, List<Object> arguments) {
120-
if( !"nextflow.server.previewDag".equals(command) || arguments.size() != 2 )
121-
return null;
122-
var uri = getJsonString(arguments.get(0));
123-
var name = getJsonString(arguments.get(1));
124-
var provider = new ScriptCodeLensProvider(astCache);
125-
return provider.previewDag(uri, name);
120+
updateNow();
121+
if( "nextflow.server.previewDag".equals(command) && arguments.size() == 2 ) {
122+
var uri = getJsonString(arguments.get(0));
123+
var name = getJsonString(arguments.get(1));
124+
var provider = new ScriptCodeLensProvider(astCache);
125+
return provider.previewDag(uri, name);
126+
}
127+
if( "nextflow.server.previewWorkspace".equals(command) ) {
128+
var provider = new WorkspacePreviewProvider(astCache);
129+
return provider.preview();
130+
}
131+
return null;
126132
}
127133

128134
private String getJsonString(Object json) {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2024-2025, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package nextflow.lsp.services.script;
17+
18+
import java.net.URI;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.stream.Stream;
22+
23+
import nextflow.lsp.ast.ASTUtils;
24+
import nextflow.script.ast.ProcessNode;
25+
import nextflow.script.ast.WorkflowNode;
26+
27+
/**
28+
*
29+
* @author Ben Sherman <[email protected]>
30+
*/
31+
public class WorkspacePreviewProvider {
32+
33+
private ScriptAstCache ast;
34+
35+
public WorkspacePreviewProvider(ScriptAstCache ast) {
36+
this.ast = ast;
37+
}
38+
39+
public Map<String,Object> preview() {
40+
var result = ast.getUris().stream()
41+
.filter(uri -> !ast.hasSyntaxErrors(uri))
42+
.flatMap(uri -> definitions(uri))
43+
.toList();
44+
return Map.of("result", result);
45+
}
46+
47+
private Stream<? extends Object> definitions(URI uri) {
48+
var processes = ast.getProcessNodes(uri).stream()
49+
.map(pn -> Map.of(
50+
"name", pn.getName(),
51+
"type", "process",
52+
"path", uri.getPath(),
53+
"line", pn.getLineNumber() - 1
54+
));
55+
var workflows = ast.getWorkflowNodes(uri).stream()
56+
.map(wn -> Map.of(
57+
"name", wn.isEntry() ? "<entry>" : wn.getName(),
58+
"type", "workflow",
59+
"path", uri.getPath(),
60+
"line", wn.getLineNumber() - 1,
61+
"children", children(wn)
62+
));
63+
return Stream.concat(processes, workflows);
64+
}
65+
66+
private List<? extends Object> children(WorkflowNode node) {
67+
return new OutgoingCallsVisitor().apply(node).stream()
68+
.map(call -> ASTUtils.getMethodFromCallExpression(call, ast))
69+
.filter(mn -> mn instanceof ProcessNode || mn instanceof WorkflowNode)
70+
.distinct()
71+
.map(mn -> Map.of(
72+
"name", mn.getName(),
73+
"path", ast.getURI(mn).getPath()
74+
))
75+
.toList();
76+
}
77+
78+
}

modules/language-server/src/main/java/nextflow/lsp/util/JsonUtils.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.List;
2020

2121
import com.google.gson.JsonElement;
22+
import com.google.gson.JsonNull;
2223
import com.google.gson.JsonObject;
2324
import com.google.gson.JsonPrimitive;
2425

@@ -28,6 +29,18 @@
2829
*/
2930
public class JsonUtils {
3031

32+
public static Object asJson(Object value) {
33+
if( value == null )
34+
return JsonNull.INSTANCE;
35+
if( value instanceof Boolean b )
36+
return new JsonPrimitive(b);
37+
if( value instanceof Number n )
38+
return new JsonPrimitive(n);
39+
if( value instanceof String s )
40+
return new JsonPrimitive(s);
41+
return value;
42+
}
43+
3144
public static List<String> getStringArray(Object json, String path) {
3245
var value = getObjectPath(json, path);
3346
if( value == null || !value.isJsonArray() )

0 commit comments

Comments
 (0)