├── .gitignore ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── Makefile ├── .github └── workflows │ ├── build.yml │ └── generate-send-dependencies.yml ├── src ├── main │ └── java │ │ └── nextflow │ │ ├── lsp │ │ ├── spec │ │ │ ├── PluginRef.java │ │ │ ├── PluginSpec.java │ │ │ ├── ScriptSpecFactory.java │ │ │ ├── PluginSpecCache.java │ │ │ └── ConfigSpecFactory.java │ │ ├── services │ │ │ ├── CodeLensProvider.java │ │ │ ├── HoverProvider.java │ │ │ ├── LinkProvider.java │ │ │ ├── SemanticTokensProvider.java │ │ │ ├── RenameProvider.java │ │ │ ├── FormattingProvider.java │ │ │ ├── ReferenceProvider.java │ │ │ ├── DefinitionProvider.java │ │ │ ├── SymbolProvider.java │ │ │ ├── ErrorReportingMode.java │ │ │ ├── CallHierarchyProvider.java │ │ │ ├── CompletionProvider.java │ │ │ ├── LanguageServerConfiguration.java │ │ │ ├── config │ │ │ │ ├── CachingResolveIncludeVisitor.java │ │ │ │ ├── ConfigFormattingProvider.java │ │ │ │ ├── ConfigService.java │ │ │ │ ├── ConfigSemanticTokensProvider.java │ │ │ │ ├── ConfigAstParentVisitor.java │ │ │ │ ├── ConfigLinkProvider.java │ │ │ │ └── ConfigAstCache.java │ │ │ └── script │ │ │ │ ├── OutgoingCallsVisitor.java │ │ │ │ ├── ScriptDefinitionProvider.java │ │ │ │ ├── WorkspacePreviewProvider.java │ │ │ │ ├── ScriptLinkProvider.java │ │ │ │ ├── ScriptFormattingProvider.java │ │ │ │ ├── ResolvePluginIncludeVisitor.java │ │ │ │ ├── dag │ │ │ │ ├── Graph.java │ │ │ │ └── VariableContext.java │ │ │ │ ├── ScriptHoverProvider.java │ │ │ │ ├── ScriptSymbolProvider.java │ │ │ │ └── ScriptService.java │ │ ├── compiler │ │ │ ├── LanguageServerErrorCollector.java │ │ │ └── LanguageServerCompiler.java │ │ ├── util │ │ │ ├── ProgressNotification.java │ │ │ ├── DebouncingExecutor.java │ │ │ ├── Logger.java │ │ │ ├── LanguageServerUtils.java │ │ │ ├── Positions.java │ │ │ └── JsonUtils.java │ │ ├── ast │ │ │ ├── CompletionUtils.java │ │ │ └── LanguageServerASTUtils.java │ │ └── file │ │ │ └── FileCache.java │ │ └── script │ │ ├── types │ │ └── shim │ │ │ ├── Bag.java │ │ │ ├── Set.java │ │ │ ├── Float.java │ │ │ ├── Duration.java │ │ │ ├── MemoryUnit.java │ │ │ ├── Map.java │ │ │ ├── Integer.java │ │ │ └── List.java │ │ └── dsl │ │ ├── TupleComponents.java │ │ └── Ops.java ├── test │ └── groovy │ │ └── nextflow │ │ ├── lsp │ │ ├── spec │ │ │ └── PluginSpecCacheTest.groovy │ │ ├── TestLanguageClient.groovy │ │ ├── compiler │ │ │ └── LanguageServerErrorCollectorTest.groovy │ │ ├── util │ │ │ ├── PositionsTest.groovy │ │ │ └── DebouncingExecutorTest.groovy │ │ ├── file │ │ │ └── FileCacheTest.groovy │ │ ├── services │ │ │ ├── script │ │ │ │ ├── ScriptFormattingTest.groovy │ │ │ │ ├── ScriptDefinitionTest.groovy │ │ │ │ ├── ScriptReferencesTest.groovy │ │ │ │ └── ScriptCompletionTest.groovy │ │ │ └── config │ │ │ │ ├── ConfigFormattingTest.groovy │ │ │ │ └── ConfigHoverTest.groovy │ │ └── TestUtils.groovy │ │ └── script │ │ └── types │ │ └── TypesTest.groovy └── spec │ └── groovy │ └── nextflow │ └── SpecWriter.groovy ├── settings.gradle ├── README.md └── gradlew.bat /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.configuration-cache=true 3 | org.gradle.parallel=true 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextflow-io/language-server/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | compile: 2 | (cd ../nextflow ; ./gradlew publishToMavenLocal) 3 | ./gradlew shadowJar 4 | 5 | test: 6 | ifndef class 7 | ./gradlew test 8 | else 9 | ./gradlew test --tests ${class} 10 | endif 11 | 12 | install: 13 | ./gradlew build publishToMavenLocal 14 | 15 | clean: 16 | ./gradlew clean 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Nextflow language server CI 2 | # read more here: https://help.github.com/en/articles/workflow-syntax-for-github-actions#on 3 | 4 | on: 5 | push: 6 | branches: 7 | - 'main' 8 | pull_request: 9 | types: [opened, reopened, synchronize] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | name: Build 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 30 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 1 22 | 23 | - name: Compile 24 | run: | 25 | git clone --depth 1 https://github.com/nextflow-io/nextflow ../nextflow 26 | make 27 | 28 | - name: Test 29 | run: | 30 | make test 31 | -------------------------------------------------------------------------------- /.github/workflows/generate-send-dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Generate and submit dependency graph for language-server 2 | on: 3 | push: 4 | branches: ['main'] 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | dependency-submission: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-java@v4 16 | with: 17 | distribution: temurin 18 | java-version: 17 19 | 20 | - name: Generate and submit dependency graph for language-server 21 | uses: gradle/actions/dependency-submission@v4 22 | with: 23 | dependency-resolution-task: "dependencies" 24 | additional-arguments: "--configuration runtimeClasspath" 25 | dependency-graph: generate-and-submit 26 | gradle-version: 8.10.2 27 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/spec/PluginRef.java: -------------------------------------------------------------------------------- 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.spec; 17 | 18 | public record PluginRef( 19 | String name, 20 | String version 21 | ) {} 22 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/CodeLensProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import java.util.List; 19 | 20 | import org.eclipse.lsp4j.CodeLens; 21 | import org.eclipse.lsp4j.TextDocumentIdentifier; 22 | 23 | public interface CodeLensProvider { 24 | 25 | List codeLens(TextDocumentIdentifier textDocument); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/HoverProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import org.eclipse.lsp4j.Hover; 19 | import org.eclipse.lsp4j.Position; 20 | import org.eclipse.lsp4j.TextDocumentIdentifier; 21 | 22 | public interface HoverProvider { 23 | 24 | Hover hover(TextDocumentIdentifier textDocument, Position position); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/LinkProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import java.util.List; 19 | 20 | import org.eclipse.lsp4j.DocumentLink; 21 | import org.eclipse.lsp4j.TextDocumentIdentifier; 22 | 23 | public interface LinkProvider { 24 | 25 | List documentLink(TextDocumentIdentifier textDocument); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 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 | 17 | plugins { 18 | // required to download the toolchain (jdk) from a remote repository 19 | // https://github.com/gradle/foojay-toolchains 20 | // https://docs.gradle.org/current/userguide/toolchains.html#sub:download_repositories 21 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" 22 | } 23 | 24 | rootProject.name = 'language-server' 25 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/SemanticTokensProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import java.util.List; 19 | 20 | import org.eclipse.lsp4j.SemanticTokens; 21 | import org.eclipse.lsp4j.TextDocumentIdentifier; 22 | 23 | public interface SemanticTokensProvider { 24 | 25 | SemanticTokens semanticTokensFull(TextDocumentIdentifier textDocument); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/RenameProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import org.eclipse.lsp4j.Position; 19 | import org.eclipse.lsp4j.TextDocumentIdentifier; 20 | import org.eclipse.lsp4j.WorkspaceEdit; 21 | 22 | public interface RenameProvider { 23 | 24 | WorkspaceEdit rename(TextDocumentIdentifier textDocument, Position position, String newName); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/FormattingProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import java.net.URI; 19 | import java.util.List; 20 | 21 | import nextflow.script.formatter.FormattingOptions; 22 | import org.eclipse.lsp4j.TextEdit; 23 | 24 | public interface FormattingProvider { 25 | 26 | List formatting(URI uri, FormattingOptions options); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/spec/PluginSpec.java: -------------------------------------------------------------------------------- 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.spec; 17 | 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | import nextflow.config.spec.SpecNode; 22 | import org.codehaus.groovy.ast.MethodNode; 23 | 24 | public record PluginSpec( 25 | Map configScopes, 26 | List factories, 27 | List functions, 28 | List operators 29 | ) {} 30 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/ReferenceProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import java.util.List; 19 | 20 | import org.eclipse.lsp4j.Location; 21 | import org.eclipse.lsp4j.Position; 22 | import org.eclipse.lsp4j.TextDocumentIdentifier; 23 | 24 | public interface ReferenceProvider { 25 | 26 | List references(TextDocumentIdentifier textDocument, Position position, boolean includeDeclaration); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/nextflow/script/types/shim/Bag.java: -------------------------------------------------------------------------------- 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.script.types.shim; 17 | 18 | import nextflow.script.dsl.Description; 19 | import nextflow.script.dsl.Ops; 20 | 21 | @Description(""" 22 | A bag is an unordered collection. 23 | 24 | [Read more](https://nextflow.io/docs/latest/reference/stdlib.html#bag-e) 25 | """) 26 | @Ops(BagOps.class) 27 | public interface Bag extends Iterable { 28 | } 29 | 30 | interface BagOps { 31 | 32 | Boolean isCase(E a, Bag b); 33 | 34 | Bag plus(Bag a, Bag b); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/DefinitionProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import java.util.List; 19 | 20 | import org.eclipse.lsp4j.Location; 21 | import org.eclipse.lsp4j.LocationLink; 22 | import org.eclipse.lsp4j.Position; 23 | import org.eclipse.lsp4j.TextDocumentIdentifier; 24 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 25 | 26 | public interface DefinitionProvider { 27 | 28 | Either, List> definition(TextDocumentIdentifier textDocument, Position position); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/nextflow/script/dsl/TupleComponents.java: -------------------------------------------------------------------------------- 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.script.dsl; 17 | 18 | import java.lang.annotation.ElementType; 19 | import java.lang.annotation.Retention; 20 | import java.lang.annotation.RetentionPolicy; 21 | import java.lang.annotation.Target; 22 | 23 | /** 24 | * Annotation for defining the component types of a tuple. 25 | * 26 | * @author Ben Sherman 27 | */ 28 | @Retention(RetentionPolicy.RUNTIME) 29 | @Target(ElementType.TYPE_USE) 30 | public @interface TupleComponents { 31 | Class[] value(); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/nextflow/script/dsl/Ops.java: -------------------------------------------------------------------------------- 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.script.dsl; 17 | 18 | import java.lang.annotation.ElementType; 19 | import java.lang.annotation.Retention; 20 | import java.lang.annotation.RetentionPolicy; 21 | import java.lang.annotation.Target; 22 | 23 | /** 24 | * Annotation for defining operators for a type. 25 | * 26 | * See also: https://groovy-lang.org/operators.html#Operator-Overloading 27 | * 28 | * @author Ben Sherman 29 | */ 30 | @Retention(RetentionPolicy.RUNTIME) 31 | @Target(ElementType.TYPE) 32 | public @interface Ops { 33 | Class value(); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/SymbolProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import java.util.List; 19 | 20 | import org.eclipse.lsp4j.DocumentSymbol; 21 | import org.eclipse.lsp4j.SymbolInformation; 22 | import org.eclipse.lsp4j.TextDocumentIdentifier; 23 | import org.eclipse.lsp4j.WorkspaceSymbol; 24 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 25 | 26 | public interface SymbolProvider { 27 | 28 | List> documentSymbol(TextDocumentIdentifier textDocument); 29 | 30 | List symbol(String query); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/spec/PluginSpecCacheTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.spec 18 | 19 | import spock.lang.Specification 20 | 21 | /** 22 | * 23 | * @author Ben Sherman 24 | */ 25 | class PluginSpecCacheTest extends Specification { 26 | 27 | def 'should fetch a plugin spec' () { 28 | given: 29 | def pluginSpecCache = new PluginSpecCache('https://registry.nextflow.io/api/') 30 | 31 | when: 32 | def spec = pluginSpecCache.get('nf-prov', '1.6.0') 33 | then: 34 | spec.configScopes().containsKey('prov') 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/ErrorReportingMode.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import nextflow.script.control.ParanoidWarning; 19 | import org.codehaus.groovy.control.messages.WarningMessage; 20 | import org.codehaus.groovy.syntax.SyntaxException; 21 | 22 | public enum ErrorReportingMode { 23 | OFF, 24 | ERRORS, 25 | WARNINGS, 26 | PARANOID; 27 | 28 | public boolean isRelevant(SyntaxException error) { 29 | return this != OFF; 30 | } 31 | 32 | public boolean isRelevant(WarningMessage warning) { 33 | if( warning instanceof ParanoidWarning ) 34 | return this == PARANOID; 35 | return compareTo(WARNINGS) >= 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/CallHierarchyProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import java.util.List; 19 | 20 | import org.eclipse.lsp4j.CallHierarchyIncomingCall; 21 | import org.eclipse.lsp4j.CallHierarchyItem; 22 | import org.eclipse.lsp4j.CallHierarchyOutgoingCall; 23 | import org.eclipse.lsp4j.Position; 24 | import org.eclipse.lsp4j.TextDocumentIdentifier; 25 | 26 | public interface CallHierarchyProvider { 27 | 28 | List prepare(TextDocumentIdentifier textDocument, Position position); 29 | 30 | List incomingCalls(CallHierarchyItem item); 31 | 32 | List outgoingCalls(CallHierarchyItem item); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/nextflow/script/types/shim/Set.java: -------------------------------------------------------------------------------- 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.script.types.shim; 17 | 18 | import nextflow.script.dsl.Description; 19 | import nextflow.script.dsl.Ops; 20 | 21 | @Description(""" 22 | A set is an unordered collection that cannot contain duplicate elements. 23 | 24 | [Read more](https://nextflow.io/docs/latest/reference/stdlib.html#sete) 25 | """) 26 | @Ops(SetOps.class) 27 | public interface Set extends Iterable { 28 | 29 | @Description(""" 30 | Returns the intersection of the set and the given iterable. 31 | """) 32 | Set intersect(Iterable right); 33 | } 34 | 35 | interface SetOps { 36 | 37 | Boolean isCase(E a, Set b); 38 | 39 | Set minus(Set a, Iterable b); 40 | 41 | Set plus(Set a, Iterable b); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/CompletionProvider.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import java.util.List; 19 | 20 | import org.eclipse.lsp4j.CompletionItem; 21 | import org.eclipse.lsp4j.CompletionList; 22 | import org.eclipse.lsp4j.Position; 23 | import org.eclipse.lsp4j.TextDocumentIdentifier; 24 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 25 | 26 | public interface CompletionProvider { 27 | 28 | /** 29 | * Get a list of completions for a given completion context. An 30 | * incomplete list may be returned if the full list exceeds the 31 | * maximum size. 32 | * 33 | * @param textDocument 34 | * @param position 35 | */ 36 | Either, CompletionList> completion(TextDocumentIdentifier textDocument, Position position); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/nextflow/script/types/shim/Float.java: -------------------------------------------------------------------------------- 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.script.types.shim; 17 | 18 | import nextflow.script.dsl.Description; 19 | import nextflow.script.dsl.Ops; 20 | 21 | @Description(""" 22 | A float is a floating-point number (i.e. real number) that can be positive or negative. 23 | 24 | [Read more](https://nextflow.io/docs/latest/reference/stdlib.html#float) 25 | """) 26 | @Ops(FloatOps.class) 27 | public interface Float { 28 | } 29 | 30 | interface FloatOps { 31 | 32 | Float and(Float a, Float b); 33 | 34 | Boolean compareTo(Float a, Float b); 35 | 36 | Boolean compareTo(Float a, Integer b); 37 | 38 | Float div(Float a, Float b); 39 | 40 | Float minus(Float a, Float b); 41 | 42 | Float multiply(Float a, Float b); 43 | 44 | Float negative(); 45 | 46 | Float ofType(Integer n); 47 | 48 | Float plus(Float a, Float b); 49 | 50 | Float positive(); 51 | 52 | Float power(Float a, Float b); 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/LanguageServerConfiguration.java: -------------------------------------------------------------------------------- 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; 17 | 18 | import java.util.Collections; 19 | import java.util.List; 20 | 21 | public record LanguageServerConfiguration( 22 | String dagDirection, 23 | boolean dagVerbose, 24 | ErrorReportingMode errorReportingMode, 25 | List excludePatterns, 26 | boolean extendedCompletion, 27 | boolean harshilAlignment, 28 | boolean maheshForm, 29 | int maxCompletionItems, 30 | String pluginRegistryUrl, 31 | boolean sortDeclarations, 32 | boolean typeChecking 33 | ) { 34 | 35 | public static LanguageServerConfiguration defaults() { 36 | return new LanguageServerConfiguration( 37 | "TB", 38 | false, 39 | ErrorReportingMode.WARNINGS, 40 | Collections.emptyList(), 41 | false, 42 | false, 43 | false, 44 | 100, 45 | "https://registry.nextflow.io/api/", 46 | false, 47 | false 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/TestLanguageClient.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp 18 | 19 | import java.util.concurrent.CompletableFuture 20 | 21 | import org.eclipse.lsp4j.MessageActionItem 22 | import org.eclipse.lsp4j.MessageParams 23 | import org.eclipse.lsp4j.PublishDiagnosticsParams 24 | import org.eclipse.lsp4j.ShowMessageRequestParams 25 | import org.eclipse.lsp4j.services.LanguageClient 26 | 27 | /** 28 | * 29 | * @author Ben Sherman 30 | */ 31 | class TestLanguageClient implements LanguageClient { 32 | 33 | @Override 34 | public void telemetryEvent(Object object) { 35 | } 36 | 37 | @Override 38 | public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) { 39 | return null 40 | } 41 | 42 | @Override 43 | public void showMessage(MessageParams messageParams) { 44 | } 45 | 46 | @Override 47 | public void publishDiagnostics(PublishDiagnosticsParams diagnostics) { 48 | } 49 | 50 | @Override 51 | public void logMessage(MessageParams message) { 52 | System.err.println(message.getMessage()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/nextflow/script/types/shim/Duration.java: -------------------------------------------------------------------------------- 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.script.types.shim; 17 | 18 | import nextflow.script.dsl.Description; 19 | import nextflow.script.dsl.Ops; 20 | 21 | @Description(""" 22 | A `Duration` represents a duration of time. 23 | 24 | [Read more](https://nextflow.io/docs/latest/reference/stdlib.html#duration) 25 | """) 26 | @Ops(DurationOps.class) 27 | public interface Duration { 28 | 29 | @Description(""" 30 | Get the duration value in days (rounded down). 31 | """) 32 | long toDays(); 33 | 34 | @Description(""" 35 | Get the duration value in hours (rounded down). 36 | """) 37 | long toHours(); 38 | 39 | @Description(""" 40 | Get the duration value in milliseconds. 41 | """) 42 | long toMillis(); 43 | 44 | @Description(""" 45 | Get the duration value in minutes (rounded down). 46 | """) 47 | long toMinutes(); 48 | 49 | @Description(""" 50 | Get the duration value in seconds (rounded down). 51 | """) 52 | long toSeconds(); 53 | 54 | } 55 | 56 | interface DurationOps { 57 | 58 | Boolean compareTo(Duration a, Duration b); 59 | 60 | Duration div(Duration a, Float b); 61 | 62 | Duration minus(Duration a, Duration b); 63 | 64 | Duration multiply(Duration a, Float b); 65 | 66 | Duration ofType(Integer n); 67 | 68 | Duration ofType(String s); 69 | 70 | Duration plus(Duration a, Duration b); 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/nextflow/script/types/shim/MemoryUnit.java: -------------------------------------------------------------------------------- 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.script.types.shim; 17 | 18 | import nextflow.script.dsl.Description; 19 | import nextflow.script.dsl.Ops; 20 | 21 | @Description(""" 22 | A `MemoryUnit` represents a quantity of bytes. 23 | 24 | [Read more](https://nextflow.io/docs/latest/reference/stdlib.html#memoryunit) 25 | """) 26 | @Ops(MemoryUnitOps.class) 27 | public interface MemoryUnit { 28 | 29 | @Description(""" 30 | Get the memory value in bytes (B). 31 | """) 32 | long toBytes(); 33 | 34 | @Description(""" 35 | Get the memory value in gigabytes (rounded down). 36 | """) 37 | long toGiga(); 38 | 39 | @Description(""" 40 | Get the memory value in kilobytes (rounded down). 41 | """) 42 | long toKilo(); 43 | 44 | @Description(""" 45 | Get the memory value in megabytes (rounded down). 46 | """) 47 | long toMega(); 48 | 49 | @Description(""" 50 | Get the memory value in terms of a given unit (rounded down). 51 | """) 52 | long toUnit(String unit); 53 | 54 | } 55 | 56 | interface MemoryUnitOps { 57 | 58 | Boolean compareTo(MemoryUnit a, MemoryUnit b); 59 | 60 | MemoryUnit div(MemoryUnit a, Float b); 61 | 62 | MemoryUnit minus(MemoryUnit a, MemoryUnit b); 63 | 64 | MemoryUnit multiply(MemoryUnit a, Float b); 65 | 66 | MemoryUnit ofType(Integer n); 67 | 68 | MemoryUnit ofType(String s); 69 | 70 | MemoryUnit plus(MemoryUnit a, MemoryUnit b); 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/config/CachingResolveIncludeVisitor.java: -------------------------------------------------------------------------------- 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.config; 17 | 18 | import java.net.URI; 19 | import java.util.Set; 20 | 21 | import nextflow.config.ast.ConfigIncludeNode; 22 | import nextflow.config.control.ResolveIncludeVisitor; 23 | import org.codehaus.groovy.ast.expr.ConstantExpression; 24 | import org.codehaus.groovy.control.SourceUnit; 25 | 26 | /** 27 | * 28 | * @author Ben Sherman 29 | */ 30 | public class CachingResolveIncludeVisitor extends ResolveIncludeVisitor { 31 | 32 | private Set changedUris; 33 | 34 | private boolean changed; 35 | 36 | public CachingResolveIncludeVisitor(SourceUnit sourceUnit, Set changedUris) { 37 | super(sourceUnit); 38 | this.changedUris = changedUris; 39 | } 40 | 41 | private URI uri() { 42 | return getSourceUnit().getSource().getURI(); 43 | } 44 | 45 | @Override 46 | public void visitConfigInclude(ConfigIncludeNode node) { 47 | if( !(node.source instanceof ConstantExpression) ) 48 | return; 49 | var source = node.source.getText(); 50 | var includeUri = getIncludeUri(uri(), source); 51 | if( !isIncludeLocal(includeUri) || !isIncludeStale(includeUri) ) 52 | return; 53 | changed = true; 54 | super.visitConfigInclude(node); 55 | } 56 | 57 | protected boolean isIncludeStale(URI includeUri) { 58 | return changedUris.contains(uri()) || changedUris.contains(includeUri); 59 | } 60 | 61 | public boolean isChanged() { 62 | return changed; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/compiler/LanguageServerErrorCollector.java: -------------------------------------------------------------------------------- 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.compiler; 17 | 18 | import java.util.List; 19 | 20 | import nextflow.script.control.LazyErrorCollector; 21 | import nextflow.script.control.PhaseAware; 22 | import org.codehaus.groovy.control.CompilerConfiguration; 23 | import org.codehaus.groovy.control.messages.Message; 24 | import org.codehaus.groovy.control.messages.SyntaxErrorMessage; 25 | 26 | /** 27 | * Error collector that defers error reporting and can 28 | * be updated on re-compilation. 29 | * 30 | * @author Ben Sherman 31 | */ 32 | public class LanguageServerErrorCollector extends LazyErrorCollector { 33 | 34 | public LanguageServerErrorCollector(CompilerConfiguration configuration) { 35 | super(configuration); 36 | } 37 | 38 | /** 39 | * Remove all errors on or after a given phase and replace them 40 | * with new errors. 41 | * 42 | * @param phase 43 | * @param newErrors 44 | */ 45 | public void updatePhase(int phase, List newErrors) { 46 | // var oldErrors = errors; 47 | // errors = null; 48 | if( errors != null ) 49 | errors.removeIf(message -> isErrorPhase(message, phase)); 50 | for( var errorMessage : newErrors ) 51 | addErrorAndContinue(errorMessage); 52 | } 53 | 54 | private static boolean isErrorPhase(Message message, int phase) { 55 | return message instanceof SyntaxErrorMessage sem 56 | && sem.getCause() instanceof PhaseAware pa 57 | && pa.getPhase() >= phase; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/util/ProgressNotification.java: -------------------------------------------------------------------------------- 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.util; 17 | 18 | import org.eclipse.lsp4j.ProgressParams; 19 | import org.eclipse.lsp4j.WorkDoneProgressBegin; 20 | import org.eclipse.lsp4j.WorkDoneProgressCreateParams; 21 | import org.eclipse.lsp4j.WorkDoneProgressEnd; 22 | import org.eclipse.lsp4j.WorkDoneProgressReport; 23 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 24 | import org.eclipse.lsp4j.services.LanguageClient; 25 | 26 | /** 27 | * 28 | * @author Ben Sherman 29 | */ 30 | public class ProgressNotification { 31 | 32 | private LanguageClient client; 33 | 34 | private String token; 35 | 36 | public ProgressNotification(LanguageClient client, String token) { 37 | this.client = client; 38 | this.token = token; 39 | } 40 | 41 | public void create() { 42 | client.createProgress(new WorkDoneProgressCreateParams(Either.forLeft(token))); 43 | } 44 | 45 | public void begin(String message) { 46 | var progress = new WorkDoneProgressBegin(); 47 | progress.setMessage(message); 48 | progress.setPercentage(0); 49 | client.notifyProgress(new ProgressParams(Either.forLeft(token), Either.forLeft(progress))); 50 | } 51 | 52 | public void update(String message, int percentage) { 53 | var progress = new WorkDoneProgressReport(); 54 | progress.setMessage(message); 55 | progress.setPercentage(percentage); 56 | client.notifyProgress(new ProgressParams(Either.forLeft(token), Either.forLeft(progress))); 57 | } 58 | 59 | public void end() { 60 | var progress = new WorkDoneProgressEnd(); 61 | client.notifyProgress(new ProgressParams(Either.forLeft(token), Either.forLeft(progress))); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/OutgoingCallsVisitor.java: -------------------------------------------------------------------------------- 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.ProcessNodeV1; 22 | import nextflow.script.ast.ProcessNodeV2; 23 | import nextflow.script.ast.WorkflowNode; 24 | import org.codehaus.groovy.ast.CodeVisitorSupport; 25 | import org.codehaus.groovy.ast.MethodNode; 26 | import org.codehaus.groovy.ast.expr.ElvisOperatorExpression; 27 | import org.codehaus.groovy.ast.expr.MethodCallExpression; 28 | 29 | /** 30 | * Query the list of outgoing calls made by a workflow, process, or function. 31 | * 32 | * @author Ben Sherman 33 | */ 34 | class OutgoingCallsVisitor extends CodeVisitorSupport { 35 | 36 | private List outgoingCalls; 37 | 38 | public List apply(MethodNode node) { 39 | outgoingCalls = new ArrayList<>(); 40 | if( node instanceof ProcessNodeV2 pn ) 41 | visit(pn.exec); 42 | else if( node instanceof ProcessNodeV1 pn ) 43 | visit(pn.exec); 44 | else if( node instanceof WorkflowNode wn ) 45 | visit(wn.main); 46 | else 47 | visit(node.getCode()); 48 | return outgoingCalls; 49 | } 50 | 51 | @Override 52 | public void visitMethodCallExpression(MethodCallExpression node) { 53 | visit(node.getObjectExpression()); 54 | visit(node.getArguments()); 55 | 56 | if( node.isImplicitThis() ) 57 | outgoingCalls.add(node); 58 | } 59 | 60 | @Override 61 | public void visitShortTernaryExpression(ElvisOperatorExpression node) { 62 | visit(node.getTrueExpression()); 63 | visit(node.getFalseExpression()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/compiler/LanguageServerErrorCollectorTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.compiler 18 | 19 | import nextflow.script.control.PhaseAware 20 | import nextflow.script.control.Phases 21 | import org.codehaus.groovy.ast.expr.EmptyExpression 22 | import org.codehaus.groovy.control.CompilerConfiguration 23 | import org.codehaus.groovy.control.SourceUnit 24 | import org.codehaus.groovy.control.messages.SyntaxErrorMessage 25 | import org.codehaus.groovy.syntax.SyntaxException 26 | import spock.lang.Specification 27 | 28 | /** 29 | * 30 | * @author Ben Sherman 31 | */ 32 | class LanguageServerErrorCollectorTest extends Specification { 33 | 34 | def 'should update errors after a given phase' () { 35 | given: 36 | def collector = new LanguageServerErrorCollector(new CompilerConfiguration()) 37 | def newError = makeErrorWithPhase(Phases.INCLUDE_RESOLUTION) 38 | 39 | when: 40 | collector.addErrorAndContinue(makeErrorWithPhase(Phases.SYNTAX)) 41 | collector.addErrorAndContinue(makeErrorWithPhase(Phases.INCLUDE_RESOLUTION)) 42 | collector.addErrorAndContinue(makeErrorWithPhase(Phases.NAME_RESOLUTION)) 43 | collector.addErrorAndContinue(makeErrorWithPhase(Phases.TYPE_CHECKING)) 44 | and: 45 | collector.updatePhase(Phases.INCLUDE_RESOLUTION, [ newError ]) 46 | then: 47 | collector.getErrors().size() == 2 48 | collector.getErrors()[0].cause.phase == Phases.SYNTAX 49 | collector.getErrors()[1] === newError 50 | } 51 | 52 | def makeErrorWithPhase(phase) { 53 | return new SyntaxErrorMessage(new MockError(phase), Mock(SourceUnit)) 54 | } 55 | 56 | class MockError extends SyntaxException implements PhaseAware { 57 | 58 | int phase 59 | 60 | MockError(int phase) { 61 | super('', EmptyExpression.INSTANCE) 62 | this.phase = phase 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/util/PositionsTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.util 18 | 19 | import org.eclipse.lsp4j.Position 20 | import spock.lang.Specification 21 | 22 | /** 23 | * 24 | * @author Ben Sherman 25 | */ 26 | class PositionsTest extends Specification { 27 | 28 | int sign(int n) { 29 | if( n > 0 ) 30 | return 1 31 | if( n < 0 ) 32 | return -1 33 | return 0 34 | } 35 | 36 | def 'should compare positions' () { 37 | given: 38 | def a = new Position(a1, a2) 39 | def b = new Position(b1, b2) 40 | 41 | expect: 42 | result == sign(Positions.COMPARATOR.compare(a, b)) 43 | 44 | where: 45 | a1 | a2 | b1 | b2 | result 46 | 0 | 0 | 0 | 0 | 0 47 | 1 | 0 | 0 | 1 | 1 48 | 0 | 1 | 1 | 0 | -1 49 | } 50 | 51 | def 'should convert position to offset' () { 52 | given: 53 | def script = ''' 54 | workflow { 55 | println 'Hello World!' 56 | } 57 | '''.stripIndent(true).trim() 58 | 59 | expect: 60 | Positions.getOffset(script, new Position(0, 0)) == 0 61 | Positions.getOffset(script, new Position(1, 0)) == 11 62 | Positions.getOffset(script, new Position(2, 0)) == 38 63 | Positions.getOffset(script, new Position(3, 0)) == -1 64 | } 65 | 66 | def 'should convert offset to position' () { 67 | given: 68 | def script = ''' 69 | workflow { 70 | println 'Hello World!' 71 | } 72 | '''.stripIndent(true).trim() 73 | 74 | expect: 75 | Positions.getPosition(script, 0) == new Position(0, 0) 76 | Positions.getPosition(script, 11) == new Position(1, 0) 77 | Positions.getPosition(script, 38) == new Position(2, 0) 78 | Positions.getPosition(script, 50) == new Position(-1, -1) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/spec/groovy/nextflow/SpecWriter.groovy: -------------------------------------------------------------------------------- 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 17 | 18 | import groovy.json.JsonOutput 19 | import groovy.transform.TypeChecked 20 | import nextflow.config.spec.ConfigScope 21 | import nextflow.config.spec.MarkdownRenderer 22 | import nextflow.config.spec.ScopeName 23 | import nextflow.config.spec.SpecNode 24 | import nextflow.plugin.Plugins 25 | import nextflow.plugin.spec.ConfigSpec 26 | import nextflow.script.dsl.Description 27 | 28 | @TypeChecked 29 | class SpecWriter { 30 | 31 | static void main(String[] args) { 32 | if (args.length == 0) { 33 | System.err.println("Missing output path") 34 | System.exit(1) 35 | } 36 | 37 | final outputPath = args[0] 38 | final file = new File(outputPath) 39 | file.parentFile.mkdirs() 40 | file.text = JsonOutput.toJson(getDefinitions()) 41 | 42 | println "Rendered core definitions to $file" 43 | 44 | final docsFile = new File("${file.parent}/config.md") 45 | docsFile.text = new MarkdownRenderer().render() 46 | 47 | println "Rendered Markdown docs to $docsFile" 48 | } 49 | 50 | private static List getDefinitions() { 51 | final result = new ArrayList() 52 | for( final scope : Plugins.getExtensions(ConfigScope) ) { 53 | final clazz = scope.getClass() 54 | final scopeName = clazz.getAnnotation(ScopeName)?.value() 55 | final description = clazz.getAnnotation(Description)?.value() 56 | if( scopeName == '' ) { 57 | SpecNode.Scope.of(clazz, '').children().each { name, node -> 58 | result.add(ConfigSpec.of(node, name)) 59 | } 60 | continue 61 | } 62 | if( !scopeName ) 63 | continue 64 | final node = SpecNode.Scope.of(clazz, description) 65 | result.add(ConfigSpec.of(node, scopeName)) 66 | } 67 | return result 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/util/DebouncingExecutor.java: -------------------------------------------------------------------------------- 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.util; 17 | 18 | import java.util.concurrent.Executors; 19 | import java.util.concurrent.ScheduledExecutorService; 20 | import java.util.concurrent.ScheduledFuture; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | /** 25 | * Executor service that debounces incoming tasks, so 26 | * that a task is executed only after not being triggered 27 | * for a given delay. 28 | * 29 | * @author Ben Sherman 30 | */ 31 | public class DebouncingExecutor { 32 | private final long delayMillis; 33 | private final Runnable action; 34 | private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); 35 | private final AtomicReference> futureRef = new AtomicReference<>(); 36 | 37 | public DebouncingExecutor(long delayMillis, Runnable action) { 38 | this.delayMillis = delayMillis; 39 | this.action = action; 40 | } 41 | 42 | /** 43 | * Schedule the action after the configured delay, cancelling 44 | * the currently scheduled task if present. 45 | */ 46 | public synchronized void executeLater() { 47 | cancelExisting(); 48 | 49 | var future = scheduler.schedule(() -> { 50 | action.run(); 51 | futureRef.set(null); 52 | }, delayMillis, TimeUnit.MILLISECONDS); 53 | 54 | futureRef.set(future); 55 | } 56 | 57 | /** 58 | * Execute the action immediately, cancelling the currently 59 | * scheduled task if present. 60 | */ 61 | public synchronized void executeNow() { 62 | cancelExisting(); 63 | action.run(); 64 | } 65 | 66 | private void cancelExisting() { 67 | var existing = futureRef.getAndSet(null); 68 | if( existing != null && !existing.isDone() ) 69 | existing.cancel(false); 70 | } 71 | 72 | /** 73 | * Call this method to shut down the executor when no longer needed. 74 | */ 75 | public void shutdown() { 76 | scheduler.shutdownNow(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/file/FileCacheTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.file 18 | 19 | import org.eclipse.lsp4j.DidChangeTextDocumentParams 20 | import org.eclipse.lsp4j.DidOpenTextDocumentParams 21 | import org.eclipse.lsp4j.Position 22 | import org.eclipse.lsp4j.Range 23 | import org.eclipse.lsp4j.TextDocumentContentChangeEvent 24 | import org.eclipse.lsp4j.TextDocumentItem 25 | import org.eclipse.lsp4j.VersionedTextDocumentIdentifier 26 | import spock.lang.Specification 27 | 28 | /** 29 | * 30 | * @author Ben Sherman 31 | */ 32 | class FileCacheTest extends Specification { 33 | 34 | def test() { 35 | given: 36 | def fileCache = new FileCache() 37 | 38 | when: 'should open a file' 39 | fileCache.didOpen(new DidOpenTextDocumentParams( 40 | new TextDocumentItem('file.txt', 'plaintext', 1, 'hello world') 41 | )) 42 | then: 43 | 'hello world' == fileCache.getContents(URI.create('file.txt')) 44 | 45 | when: 'should change an entire file' 46 | fileCache.didChange(new DidChangeTextDocumentParams( 47 | new VersionedTextDocumentIdentifier('file.txt', 2), 48 | Collections.singletonList( 49 | new TextDocumentContentChangeEvent('hi there') 50 | ) 51 | )) 52 | then: 53 | 'hi there' == fileCache.getContents(URI.create('file.txt')) 54 | 55 | when: 'should update a file with incremental changes' 56 | fileCache.didChange(new DidChangeTextDocumentParams( 57 | new VersionedTextDocumentIdentifier('file.txt', 3), 58 | List.of( 59 | new TextDocumentContentChangeEvent( 60 | new Range(new Position(0, 0), new Position(0, 2)), 61 | 'hello' 62 | ), 63 | new TextDocumentContentChangeEvent( 64 | new Range(new Position(0, 11), new Position(0, 11)), 65 | ', friend' 66 | ) 67 | ) 68 | )) 69 | then: 70 | 'hello there, friend' == fileCache.getContents(URI.create('file.txt')) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nextflow Language Server 2 | 3 | The official [language server](https://microsoft.github.io/language-server-protocol/) for [Nextflow](https://nextflow.io/) scripts and config files. 4 | 5 | The following language features are currently supported: 6 | 7 | - code navigation (outline, go to definition, find references) 8 | - completion 9 | - diagnostics (errors, warnings) 10 | - formatting 11 | - hover hints 12 | - rename 13 | - semantic highlighting 14 | - DAG preview for workflows 15 | 16 | ## Requirements 17 | 18 | The language server requires Java 17 or later. 19 | 20 | ## Configuration 21 | 22 | The language server exposes a set of configuration settings that can be controlled through the `workspace/didChangeConfiguration` event. See the [Nextflow VS Code extension](https://github.com/nextflow-io/vscode-language-nextflow/blob/master/package.json) for an example of how to set up these configuration settings with sensible defaults. 23 | 24 | ## Development 25 | 26 | To build from the command line: 27 | 28 | ```sh 29 | # clone Nextflow repository 30 | git clone https://github.com/nextflow-io/nextflow ../nextflow 31 | 32 | # build language server (with nf-lang module) 33 | make 34 | ``` 35 | 36 | To run unit tests: 37 | 38 | ```sh 39 | make test 40 | ``` 41 | 42 | To run the language server: 43 | 44 | ```sh 45 | java -jar build/libs/language-server-all.jar 46 | ``` 47 | 48 | Protocol messages are exchanged using standard input/output. 49 | 50 | ## Releasing 51 | 52 | The Nextflow language server follows the versioning scheme of Nextflow. For each stable release of Nextflow, there is a corresponding stable release of the language server. There is no correlation between patch releases of Nextflow and the language server -- they are patched independently of each other. 53 | 54 | A separate branch is maintained for each stable release, starting with `STABLE-24.10.x`. The `main` branch corresponds to the upcoming stable release. Updates to `main` should be backported as needed to maintain a consistent user experience, for example, when a new configuration option is added. 55 | 56 | To make a new release of the language server: 57 | 58 | 1. Build the language server locally. 59 | 2. Create a new GitHub release with the language server JAR and a list of notable changes. 60 | 61 | ## Troubleshooting 62 | 63 | Sometimes the language server might crash or get out of sync with your workspace. If this happens, you can restart the server from the command palette. You can also view the server logs from the "Output" tab under "Nextflow Language Server". 64 | 65 | When reporting an issue, please include a minimal code snippet that triggers the issue as well as any error traces from the server logs. 66 | 67 | ## Credits 68 | 69 | Based on [GroovyLanguageServer/groovy-language-server](https://github.com/GroovyLanguageServer/groovy-language-server). 70 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/ScriptDefinitionProvider.java: -------------------------------------------------------------------------------- 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.Collections; 20 | import java.util.List; 21 | 22 | import nextflow.lsp.ast.LanguageServerASTUtils; 23 | import nextflow.lsp.services.DefinitionProvider; 24 | import nextflow.lsp.util.LanguageServerUtils; 25 | import nextflow.lsp.util.Logger; 26 | import org.eclipse.lsp4j.Location; 27 | import org.eclipse.lsp4j.LocationLink; 28 | import org.eclipse.lsp4j.Position; 29 | import org.eclipse.lsp4j.TextDocumentIdentifier; 30 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 31 | 32 | /** 33 | * Get the location of the definition of a symbol. 34 | * 35 | * @author Ben Sherman 36 | */ 37 | public class ScriptDefinitionProvider implements DefinitionProvider { 38 | 39 | private static Logger log = Logger.getInstance(); 40 | 41 | private ScriptAstCache ast; 42 | 43 | public ScriptDefinitionProvider(ScriptAstCache ast) { 44 | this.ast = ast; 45 | } 46 | 47 | @Override 48 | public Either, List> definition(TextDocumentIdentifier textDocument, Position position) { 49 | if( ast == null ) { 50 | log.error("ast cache is empty while providing definition"); 51 | return Either.forLeft(Collections.emptyList()); 52 | } 53 | 54 | var uri = URI.create(textDocument.getUri()); 55 | var offsetNode = ast.getNodeAtPosition(uri, position); 56 | if( offsetNode == null ) 57 | return Either.forLeft(Collections.emptyList()); 58 | 59 | var defNode = LanguageServerASTUtils.getDefinition(offsetNode); 60 | if( defNode == null ) 61 | return Either.forLeft(Collections.emptyList()); 62 | 63 | var defUri = ast.getURI(defNode); 64 | if( defUri == null ) 65 | defUri = uri; 66 | var location = LanguageServerUtils.astNodeToLocation(defNode, defUri); 67 | if( location == null ) 68 | return Either.forLeft(Collections.emptyList()); 69 | 70 | return Either.forLeft(Collections.singletonList(location)); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/util/DebouncingExecutorTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.util 18 | 19 | import java.util.concurrent.CountDownLatch 20 | import java.util.concurrent.TimeUnit 21 | 22 | import spock.lang.Specification 23 | 24 | /** 25 | * 26 | * @author Ben Sherman 27 | */ 28 | class DebouncingExecutorSpec extends Specification { 29 | 30 | def "debouncer delays and deduplicates rapid submissions"() { 31 | given: 32 | def counter = 0 33 | def latch = new CountDownLatch(1) 34 | def debouncer = new DebouncingExecutor(200, { 35 | counter++ 36 | latch.countDown() 37 | }) 38 | 39 | when: "executeLater is called rapidly multiple times" 40 | debouncer.executeLater() 41 | Thread.sleep(50) 42 | debouncer.executeLater() 43 | Thread.sleep(50) 44 | debouncer.executeLater() 45 | 46 | then: "action is only run once after the last delay" 47 | latch.await(1, TimeUnit.SECONDS) 48 | counter == 1 49 | 50 | cleanup: 51 | debouncer.shutdown() 52 | } 53 | 54 | def "executeNow runs the action immediately and cancels pending task"() { 55 | given: 56 | def executed = false 57 | def latch = new CountDownLatch(1) 58 | def debouncer = new DebouncingExecutor(300, { 59 | executed = true 60 | latch.countDown() 61 | }) 62 | 63 | when: 64 | debouncer.executeLater() 65 | Thread.sleep(100) 66 | debouncer.executeNow() 67 | 68 | then: "action runs immediately" 69 | latch.await(500, TimeUnit.MILLISECONDS) 70 | executed 71 | 72 | cleanup: 73 | debouncer.shutdown() 74 | } 75 | 76 | def "multiple executeNow calls run the action each time"() { 77 | given: 78 | def executions = 0 79 | def debouncer = new DebouncingExecutor(100, { executions++ }) 80 | 81 | when: 82 | debouncer.executeNow() 83 | debouncer.executeNow() 84 | debouncer.executeNow() 85 | 86 | then: 87 | executions == 3 88 | 89 | cleanup: 90 | debouncer.shutdown() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/util/Logger.java: -------------------------------------------------------------------------------- 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.util; 17 | 18 | import org.eclipse.lsp4j.MessageParams; 19 | import org.eclipse.lsp4j.MessageType; 20 | import org.eclipse.lsp4j.services.LanguageClient; 21 | 22 | /** 23 | * Log messages to the client using the language server protocol. 24 | * 25 | * NOTE: The logger must be used instead of printing directly 26 | * to standard output (i.e. System.out), because the language 27 | * server itself uses standard output to send protocol messages. 28 | * Printing directly to standard output will cause client errors. 29 | * 30 | * @author Ben Sherman 31 | */ 32 | public class Logger { 33 | 34 | private static Logger instance; 35 | private static boolean debugEnabled; 36 | 37 | private LanguageClient client; 38 | private boolean initialized; 39 | 40 | public static boolean isDebugEnabled() { 41 | return debugEnabled; 42 | } 43 | 44 | public static void setDebugEnabled(boolean value) { 45 | debugEnabled = value; 46 | } 47 | 48 | private Logger() { 49 | } 50 | 51 | public void initialize(LanguageClient client) { 52 | if( !initialized ) 53 | this.client = client; 54 | initialized = true; 55 | } 56 | 57 | public static Logger getInstance() { 58 | if( instance == null ) 59 | instance = new Logger(); 60 | return instance; 61 | } 62 | 63 | public void debug(String message) { 64 | if( !initialized || !isDebugEnabled() ) 65 | return; 66 | client.logMessage(new MessageParams(MessageType.Log, message)); 67 | } 68 | 69 | public void error(String message) { 70 | if( !initialized ) 71 | return; 72 | client.logMessage(new MessageParams(MessageType.Error, message)); 73 | } 74 | 75 | public void showError(String message) { 76 | if( !initialized ) 77 | return; 78 | client.showMessage(new MessageParams(MessageType.Error, message)); 79 | } 80 | 81 | public void info(String message) { 82 | if( !initialized ) 83 | return; 84 | client.logMessage(new MessageParams(MessageType.Info, message)); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/WorkspacePreviewProvider.java: -------------------------------------------------------------------------------- 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.script.ast.ProcessNode; 24 | import nextflow.script.ast.WorkflowNode; 25 | 26 | import static nextflow.script.types.TypeCheckingUtils.*; 27 | 28 | /** 29 | * 30 | * @author Ben Sherman 31 | */ 32 | public class WorkspacePreviewProvider { 33 | 34 | private ScriptAstCache ast; 35 | 36 | public WorkspacePreviewProvider(ScriptAstCache ast) { 37 | this.ast = ast; 38 | } 39 | 40 | public Map preview() { 41 | var result = ast.getUris().stream() 42 | .filter(uri -> !ast.hasSyntaxErrors(uri)) 43 | .flatMap(uri -> definitions(uri)) 44 | .toList(); 45 | return Map.of("result", result); 46 | } 47 | 48 | private Stream definitions(URI uri) { 49 | var processes = ast.getProcessNodes(uri).stream() 50 | .map(pn -> Map.of( 51 | "name", pn.getName(), 52 | "type", "process", 53 | "path", uri.getPath(), 54 | "line", pn.getLineNumber() - 1 55 | )); 56 | var workflows = ast.getWorkflowNodes(uri).stream() 57 | .map(wn -> Map.of( 58 | "name", wn.isEntry() ? "" : wn.getName(), 59 | "type", "workflow", 60 | "path", uri.getPath(), 61 | "line", wn.getLineNumber() - 1, 62 | "children", children(wn) 63 | )); 64 | return Stream.concat(processes, workflows); 65 | } 66 | 67 | private List children(WorkflowNode node) { 68 | return new OutgoingCallsVisitor().apply(node).stream() 69 | .map(call -> resolveMethodCall(call)) 70 | .filter(mn -> mn instanceof ProcessNode || mn instanceof WorkflowNode) 71 | .distinct() 72 | .map(mn -> Map.of( 73 | "name", mn.getName(), 74 | "path", ast.getURI(mn).getPath() 75 | )) 76 | .toList(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/services/script/ScriptFormattingTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.services.script 18 | 19 | import nextflow.script.formatter.FormattingOptions 20 | import spock.lang.Specification 21 | 22 | import static nextflow.lsp.TestUtils.* 23 | 24 | /** 25 | * 26 | * @author Ben Sherman 27 | */ 28 | class ScriptFormattingTest extends Specification { 29 | 30 | boolean checkFormat(ScriptService service, String uri, String before, String after) { 31 | open(service, uri, before) 32 | def textEdits = service.formatting(URI.create(uri), new FormattingOptions(4, true)) 33 | assert textEdits.first().getNewText() == after.stripIndent() 34 | return true 35 | } 36 | 37 | def 'should format a script' () { 38 | given: 39 | def service = getScriptService() 40 | def uri = getUri('main.nf') 41 | 42 | expect: 43 | checkFormat(service, uri, 44 | '''\ 45 | workflow { println 'Hello!' } 46 | ''', 47 | '''\ 48 | workflow { 49 | println('Hello!') 50 | } 51 | ''' 52 | ) 53 | checkFormat(service, uri, 54 | '''\ 55 | workflow { 56 | println('Hello!') 57 | } 58 | ''', 59 | '''\ 60 | workflow { 61 | println('Hello!') 62 | } 63 | ''' 64 | ) 65 | } 66 | 67 | def 'should format an include declaration' () { 68 | given: 69 | def service = getScriptService() 70 | def uri = getUri('main.nf') 71 | 72 | expect: 73 | checkFormat(service, uri, 74 | '''\ 75 | include{foo;bar}from'./foobar.nf' 76 | ''', 77 | '''\ 78 | include { foo ; bar } from './foobar.nf' 79 | ''' 80 | ) 81 | checkFormat(service, uri, 82 | '''\ 83 | include{ 84 | foo;bar 85 | }from'./foobar.nf' 86 | ''', 87 | '''\ 88 | include { 89 | foo ; 90 | bar 91 | } from './foobar.nf' 92 | ''' 93 | ) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/ScriptLinkProvider.java: -------------------------------------------------------------------------------- 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.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.util.ArrayList; 22 | import java.util.Collections; 23 | import java.util.List; 24 | 25 | import nextflow.lsp.services.LinkProvider; 26 | import nextflow.lsp.util.Logger; 27 | import nextflow.lsp.util.LanguageServerUtils; 28 | import org.eclipse.lsp4j.DocumentLink; 29 | import org.eclipse.lsp4j.TextDocumentIdentifier; 30 | 31 | /** 32 | * Provide the locations of links in a document. 33 | * 34 | * @author Ben Sherman 35 | */ 36 | public class ScriptLinkProvider implements LinkProvider { 37 | 38 | private static Logger log = Logger.getInstance(); 39 | 40 | private ScriptAstCache ast; 41 | 42 | public ScriptLinkProvider(ScriptAstCache ast) { 43 | this.ast = ast; 44 | } 45 | 46 | @Override 47 | public List documentLink(TextDocumentIdentifier textDocument) { 48 | if( ast == null ) { 49 | log.error("ast cache is empty while providing document links"); 50 | return Collections.emptyList(); 51 | } 52 | 53 | var uri = URI.create(textDocument.getUri()); 54 | if( !ast.hasAST(uri) ) 55 | return Collections.emptyList(); 56 | 57 | var result = new ArrayList(); 58 | for( var node : ast.getIncludeNodes(uri) ) { 59 | var source = node.source.getText(); 60 | if( source.startsWith("plugin/") ) 61 | continue; 62 | var range = LanguageServerUtils.astNodeToRange(node.source); 63 | var target = getIncludeUri(uri, source).toString(); 64 | result.add(new DocumentLink(range, target)); 65 | } 66 | 67 | return result; 68 | } 69 | 70 | protected static URI getIncludeUri(URI uri, String source) { 71 | Path includePath = Path.of(uri).getParent().resolve(source); 72 | if( Files.isDirectory(includePath) ) 73 | includePath = includePath.resolve("main.nf"); 74 | else if( !source.endsWith(".nf") ) 75 | includePath = Path.of(includePath.toString() + ".nf"); 76 | return includePath.normalize().toUri(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/ScriptFormattingProvider.java: -------------------------------------------------------------------------------- 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.io.IOException; 19 | import java.net.URI; 20 | import java.util.Collections; 21 | import java.util.List; 22 | 23 | import nextflow.lsp.services.FormattingProvider; 24 | import nextflow.lsp.util.Logger; 25 | import nextflow.lsp.util.Positions; 26 | import nextflow.script.formatter.FormattingOptions; 27 | import nextflow.script.formatter.ScriptFormattingVisitor; 28 | import org.codehaus.groovy.runtime.IOGroovyMethods; 29 | import org.eclipse.lsp4j.Position; 30 | import org.eclipse.lsp4j.Range; 31 | import org.eclipse.lsp4j.TextEdit; 32 | 33 | /** 34 | * Provide formatting for a script. 35 | * 36 | * @author Ben Sherman 37 | */ 38 | public class ScriptFormattingProvider implements FormattingProvider { 39 | 40 | private static Logger log = Logger.getInstance(); 41 | 42 | private ScriptAstCache ast; 43 | 44 | public ScriptFormattingProvider(ScriptAstCache ast) { 45 | this.ast = ast; 46 | } 47 | 48 | @Override 49 | public List formatting(URI uri, FormattingOptions options) { 50 | if( ast == null ) { 51 | log.error("ast cache is empty while providing formatting"); 52 | return Collections.emptyList(); 53 | } 54 | 55 | if( !ast.hasAST(uri) || ast.hasSyntaxErrors(uri) ) { 56 | log.showError("Script could not be formatted because it has syntax errors: " + uri); 57 | return Collections.emptyList(); 58 | } 59 | 60 | var sourceUnit = ast.getSourceUnit(uri); 61 | String oldText; 62 | try { 63 | oldText = IOGroovyMethods.getText(sourceUnit.getSource().getReader()); 64 | } 65 | catch( IOException e ) { 66 | log.error("Failed to read source file: " + uri + " -- cause: " + e.toString()); 67 | return Collections.emptyList(); 68 | } 69 | 70 | var range = new Range(new Position(0, 0), Positions.getPosition(oldText, oldText.length())); 71 | var visitor = new ScriptFormattingVisitor(sourceUnit, options); 72 | visitor.visit(); 73 | var newText = visitor.toString(); 74 | 75 | return List.of( new TextEdit(range, newText) ); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/config/ConfigFormattingProvider.java: -------------------------------------------------------------------------------- 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.config; 17 | 18 | import java.io.IOException; 19 | import java.net.URI; 20 | import java.util.Collections; 21 | import java.util.List; 22 | 23 | import nextflow.config.formatter.ConfigFormattingVisitor; 24 | import nextflow.lsp.services.FormattingProvider; 25 | import nextflow.lsp.util.Logger; 26 | import nextflow.lsp.util.Positions; 27 | import nextflow.script.formatter.FormattingOptions; 28 | import org.codehaus.groovy.runtime.IOGroovyMethods; 29 | import org.eclipse.lsp4j.Position; 30 | import org.eclipse.lsp4j.Range; 31 | import org.eclipse.lsp4j.TextEdit; 32 | 33 | /** 34 | * Provide formatting for a config file. 35 | * 36 | * @author Ben Sherman 37 | */ 38 | public class ConfigFormattingProvider implements FormattingProvider { 39 | 40 | private static Logger log = Logger.getInstance(); 41 | 42 | private ConfigAstCache ast; 43 | 44 | public ConfigFormattingProvider(ConfigAstCache ast) { 45 | this.ast = ast; 46 | } 47 | 48 | @Override 49 | public List formatting(URI uri, FormattingOptions options) { 50 | if( ast == null ) { 51 | log.error("ast cache is empty while providing formatting"); 52 | return Collections.emptyList(); 53 | } 54 | 55 | if( !ast.hasAST(uri) || ast.hasSyntaxErrors(uri) ) { 56 | log.showError("Config file could not be formatted because it has syntax errors: " + uri); 57 | return Collections.emptyList(); 58 | } 59 | 60 | var sourceUnit = ast.getSourceUnit(uri); 61 | String oldText; 62 | try { 63 | oldText = IOGroovyMethods.getText(sourceUnit.getSource().getReader()); 64 | } 65 | catch( IOException e ) { 66 | log.error("Failed to read source file: " + uri + " -- cause: " + e.toString()); 67 | return Collections.emptyList(); 68 | } 69 | 70 | var range = new Range(new Position(0, 0), Positions.getPosition(oldText, oldText.length())); 71 | var visitor = new ConfigFormattingVisitor(sourceUnit, options); 72 | visitor.visit(); 73 | var newText = visitor.toString(); 74 | 75 | return List.of( new TextEdit(range, newText) ); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/services/config/ConfigFormattingTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.services.config 18 | 19 | import java.nio.file.Files 20 | import java.nio.file.Path 21 | 22 | import nextflow.lsp.TestLanguageClient 23 | import nextflow.lsp.services.LanguageServerConfiguration 24 | import nextflow.script.formatter.FormattingOptions 25 | import org.eclipse.lsp4j.DidOpenTextDocumentParams 26 | import org.eclipse.lsp4j.Position 27 | import org.eclipse.lsp4j.TextDocumentItem 28 | import spock.lang.Specification 29 | 30 | /** 31 | * 32 | * @author Ben Sherman 33 | */ 34 | class ConfigFormattingTest extends Specification { 35 | 36 | String openAndFormat(ConfigService service, Path filePath, String contents) { 37 | def uri = filePath.toUri() 38 | def textDocumentItem = new TextDocumentItem(uri.toString(), 'nextflow-config', 1, contents) 39 | service.didOpen(new DidOpenTextDocumentParams(textDocumentItem)) 40 | def textEdits = service.formatting(uri, new FormattingOptions(4, true)) 41 | return textEdits.first().getNewText() 42 | } 43 | 44 | def 'should format a config file' () { 45 | given: 46 | def workspaceRoot = Path.of(System.getProperty('user.dir')).resolve('build/test_workspace/') 47 | if( !Files.exists(workspaceRoot) ) 48 | workspaceRoot.toFile().mkdirs() 49 | 50 | def service = new ConfigService(workspaceRoot.toUri().toString()) 51 | def configuration = LanguageServerConfiguration.defaults() 52 | service.connect(new TestLanguageClient()) 53 | service.initialize(configuration) 54 | 55 | when: 56 | def filePath = workspaceRoot.resolve('nextflow.config') 57 | def contents = '''\ 58 | process.cpus = 2 ; process.memory = 8.GB 59 | '''.stripIndent() 60 | then: 61 | openAndFormat(service, filePath, contents) == '''\ 62 | process.cpus = 2 63 | process.memory = 8.GB 64 | '''.stripIndent() 65 | 66 | when: 67 | contents = '''\ 68 | process.cpus = 2 69 | process.memory = 8.GB 70 | '''.stripIndent() 71 | then: 72 | openAndFormat(service, filePath, contents) == '''\ 73 | process.cpus = 2 74 | process.memory = 8.GB 75 | '''.stripIndent() 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/services/script/ScriptDefinitionTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.services.script 18 | 19 | import org.eclipse.lsp4j.DefinitionParams 20 | import org.eclipse.lsp4j.Location 21 | import org.eclipse.lsp4j.Position 22 | import org.eclipse.lsp4j.TextDocumentIdentifier 23 | import spock.lang.Specification 24 | 25 | import static nextflow.lsp.TestUtils.* 26 | 27 | /** 28 | * 29 | * @author Ben Sherman 30 | */ 31 | class ScriptDefinitionTest extends Specification { 32 | 33 | Location getDefinition(ScriptService service, String uri, Position position) { 34 | def locations = service 35 | .definition(new DefinitionParams(new TextDocumentIdentifier(uri), position)) 36 | .getLeft() 37 | return locations.size() > 0 ? locations.first() : null 38 | } 39 | 40 | def 'should get the definition of a workflow in the same file' () { 41 | given: 42 | def service = getScriptService() 43 | def uri = getUri('main.nf') 44 | 45 | when: 46 | def contents = '''\ 47 | workflow HELLO { 48 | } 49 | 50 | workflow { 51 | HELLO() 52 | } 53 | ''' 54 | open(service, uri, contents) 55 | service.updateNow() 56 | def location = getDefinition(service, uri, new Position(4, 4)) 57 | then: 58 | location != null 59 | location.getUri() == uri 60 | location.getRange().getStart() == new Position(0, 0) 61 | location.getRange().getEnd() == new Position(1, 1) 62 | } 63 | 64 | def 'should get the definition of a workflow in a different file' () { 65 | given: 66 | def service = getScriptService() 67 | def mainUri = getUri('main.nf') 68 | def moduleUri = getUri('module.nf') 69 | 70 | when: 71 | open(service, moduleUri, '''\ 72 | workflow HELLO { 73 | take: 74 | target 75 | 76 | main: 77 | println "Hello, ${target}!" 78 | } 79 | ''') 80 | open(service, mainUri, '''\ 81 | include { HELLO } from './module.nf' 82 | 83 | workflow { 84 | HELLO('World') 85 | } 86 | ''') 87 | service.updateNow() 88 | def location = getDefinition(service, mainUri, new Position(3, 4)) 89 | then: 90 | location != null 91 | location.getUri() == moduleUri 92 | location.getRange().getStart() == new Position(0, 0) 93 | location.getRange().getEnd() == new Position(6, 1) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/config/ConfigService.java: -------------------------------------------------------------------------------- 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.config; 17 | 18 | import nextflow.lsp.ast.ASTNodeCache; 19 | import nextflow.lsp.services.CompletionProvider; 20 | import nextflow.lsp.services.FormattingProvider; 21 | import nextflow.lsp.services.HoverProvider; 22 | import nextflow.lsp.services.LanguageServerConfiguration; 23 | import nextflow.lsp.services.LanguageService; 24 | import nextflow.lsp.services.LinkProvider; 25 | import nextflow.lsp.services.SemanticTokensProvider; 26 | import nextflow.lsp.spec.PluginSpecCache; 27 | 28 | /** 29 | * Implementation of language services for Nextflow config files. 30 | * 31 | * @author Ben Sherman 32 | */ 33 | public class ConfigService extends LanguageService { 34 | 35 | private PluginSpecCache pluginSpecCache; 36 | 37 | private ConfigAstCache astCache; 38 | 39 | public ConfigService(String rootUri) { 40 | super(rootUri); 41 | astCache = new ConfigAstCache(); 42 | } 43 | 44 | @Override 45 | public boolean matchesFile(String uri) { 46 | return uri.endsWith(".config") && !uri.endsWith("nf-test.config"); 47 | } 48 | 49 | @Override 50 | public void initialize(LanguageServerConfiguration configuration) { 51 | initialize(configuration, new PluginSpecCache(configuration.pluginRegistryUrl())); 52 | } 53 | 54 | public void initialize(LanguageServerConfiguration configuration, PluginSpecCache pluginSpecCache) { 55 | synchronized (this) { 56 | this.pluginSpecCache = pluginSpecCache; 57 | astCache.initialize(configuration, pluginSpecCache); 58 | } 59 | super.initialize(configuration); 60 | } 61 | 62 | public PluginSpecCache getPluginSpecCache() { 63 | return pluginSpecCache; 64 | } 65 | 66 | @Override 67 | protected ASTNodeCache getAstCache() { 68 | return astCache; 69 | } 70 | 71 | @Override 72 | protected CompletionProvider getCompletionProvider(int maxItems, boolean extended) { 73 | return new ConfigCompletionProvider(astCache, maxItems); 74 | } 75 | 76 | @Override 77 | protected FormattingProvider getFormattingProvider() { 78 | return new ConfigFormattingProvider(astCache); 79 | } 80 | 81 | @Override 82 | protected HoverProvider getHoverProvider() { 83 | return new ConfigHoverProvider(astCache); 84 | } 85 | 86 | @Override 87 | protected LinkProvider getLinkProvider() { 88 | return new ConfigLinkProvider(astCache); 89 | } 90 | 91 | @Override 92 | protected SemanticTokensProvider getSemanticTokensProvider() { 93 | return new ConfigSemanticTokensProvider(astCache); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/services/config/ConfigHoverTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.services.config 18 | 19 | import nextflow.config.spec.SpecNode 20 | import nextflow.lsp.TestLanguageClient 21 | import nextflow.lsp.services.LanguageServerConfiguration 22 | import nextflow.lsp.spec.PluginSpec 23 | import nextflow.lsp.spec.PluginSpecCache 24 | import org.eclipse.lsp4j.Hover 25 | import org.eclipse.lsp4j.HoverParams 26 | import org.eclipse.lsp4j.Position 27 | import org.eclipse.lsp4j.TextDocumentIdentifier 28 | import spock.lang.Specification 29 | 30 | import static nextflow.lsp.TestUtils.* 31 | 32 | /** 33 | * 34 | * @author Ben Sherman 35 | */ 36 | class ConfigHoverTest extends Specification { 37 | 38 | String getHoverHint(ConfigService service, String uri, Position position) { 39 | def hover = service.hover(new HoverParams(new TextDocumentIdentifier(uri), position)) 40 | return hover 41 | ? hover.getContents().getRight().getValue() 42 | : null 43 | } 44 | 45 | def 'should get hover hint for a config scope' () { 46 | given: 47 | def service = getConfigService() 48 | def uri = getUri('nextflow.config') 49 | 50 | when: 51 | open(service, uri, '''\ 52 | executor { 53 | } 54 | ''') 55 | service.updateNow() 56 | def value = getHoverHint(service, uri, new Position(1, 0)) 57 | then: 58 | value == 'The `executor` scope controls various executor behaviors.\n' 59 | } 60 | 61 | def 'should get hover hint for a plugin config scope' () { 62 | given: 63 | def uri = getUri('nextflow.config') 64 | def service = new ConfigService(workspaceRoot().toUri().toString()) 65 | def configuration = LanguageServerConfiguration.defaults() 66 | def pluginSpecCache = Spy(new PluginSpecCache(configuration.pluginRegistryUrl())) 67 | pluginSpecCache.get('nf-prov', '1.6.0') >> new PluginSpec( 68 | [ 69 | 'prov': new SpecNode.Scope('The `prov` scope allows you to configure the `nf-prov` plugin.', [:]) 70 | ], 71 | [], [], [] 72 | ) 73 | service.connect(new TestLanguageClient()) 74 | service.initialize(configuration, pluginSpecCache) 75 | 76 | when: 77 | open(service, uri, '''\ 78 | plugins { 79 | id 'nf-prov@1.6.0' 80 | } 81 | 82 | prov { 83 | } 84 | ''') 85 | service.updateNow() 86 | def value = getHoverHint(service, uri, new Position(4, 0)) 87 | then: 88 | value == 'The `prov` scope allows you to configure the `nf-prov` plugin.\n' 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/script/types/TypesTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.script.types 18 | 19 | import org.codehaus.groovy.ast.ClassHelper 20 | import org.codehaus.groovy.ast.ClassNode 21 | import org.codehaus.groovy.ast.GenericsType 22 | import spock.lang.Specification 23 | 24 | /** 25 | * 26 | * @author Ben Sherman 27 | */ 28 | class TypesTest extends Specification { 29 | 30 | def 'should render a type' () { 31 | when: 32 | def cn = ClassHelper.MAP_TYPE 33 | then: 34 | Types.getName(cn) == 'Map' 35 | 36 | when: 37 | cn = new ClassNode(Map.class) 38 | then: 39 | Types.getName(cn) == 'Map' 40 | 41 | when: 42 | cn.setGenericsTypes(new GenericsType[] { 43 | new GenericsType(ClassHelper.STRING_TYPE), 44 | new GenericsType(ClassHelper.STRING_TYPE) 45 | }) 46 | then: 47 | Types.getName(cn) == 'Map' 48 | } 49 | 50 | def 'should render the return type of a method' () { 51 | when: 52 | def cn = ClassHelper.makeCached(TaskConfig) 53 | def returnType = cn.getDeclaredMethods('getExt')[0].getReturnType() 54 | then: 55 | Types.getName(returnType) == 'Map' 56 | } 57 | 58 | def 'should render a functional type' () { 59 | given: 60 | def cn = ClassHelper.makeCached(Channel) 61 | 62 | when: 63 | def param = cn.getDeclaredMethods('filter')[0].getParameters()[0] 64 | then: 65 | TypesEx.getName(param.getType()) == '(E) -> Boolean' 66 | 67 | when: 68 | param = cn.getDeclaredMethods('map')[0].getParameters()[0] 69 | then: 70 | TypesEx.getName(param.getType()) == '(E) -> R' 71 | 72 | when: 73 | param = cn.getDeclaredMethods('reduce')[0].getParameters().last() 74 | then: 75 | TypesEx.getName(param.getType()) == '(R, E) -> R' 76 | } 77 | 78 | def 'should determine whether a type is assignable to another type' () { 79 | expect: 80 | TypesEx.isAssignableFrom(ClassHelper.makeCached(TARGET), ClassHelper.makeCached(SOURCE)) == RESULT 81 | 82 | where: 83 | TARGET | SOURCE | RESULT 84 | 85 | Integer | Integer | true 86 | Integer | Float | false 87 | Float | Integer | true 88 | Float | Float | true 89 | 90 | String | String | true 91 | String | Integer | false 92 | Integer | String | false 93 | 94 | Iterable | Bag | true 95 | Iterable | List | true 96 | Iterable | Set | true 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/compiler/LanguageServerCompiler.java: -------------------------------------------------------------------------------- 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.compiler; 17 | 18 | import java.io.File; 19 | import java.net.URI; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | 23 | import groovy.lang.GroovyClassLoader; 24 | import nextflow.config.control.StringReaderSourceWithURI; 25 | import nextflow.lsp.file.FileCache; 26 | import nextflow.script.control.Compiler; 27 | import org.codehaus.groovy.GroovyBugError; 28 | import org.codehaus.groovy.control.CompilerConfiguration; 29 | import org.codehaus.groovy.control.ErrorCollector; 30 | import org.codehaus.groovy.control.SourceUnit; 31 | 32 | /** 33 | * Compiler that can load source files from the file cache 34 | * and defers errors to the language server. 35 | * 36 | * @author Ben Sherman 37 | */ 38 | public class LanguageServerCompiler extends Compiler { 39 | 40 | public LanguageServerCompiler(CompilerConfiguration configuration, GroovyClassLoader classLoader) { 41 | super(configuration, classLoader); 42 | } 43 | 44 | /** 45 | * Prepare a source file from the open cache, or the filesystem 46 | * if it is not open. 47 | * 48 | * If the file is not open and doesn't exist in the filesystem, 49 | * it has been renamed. Return null so that the file is removed 50 | * from any downstream caches. 51 | * 52 | * @param uri 53 | */ 54 | public SourceUnit createSourceUnit(URI uri, FileCache fileCache) { 55 | if( fileCache.isOpen(uri) ) { 56 | var contents = fileCache.getContents(uri); 57 | return new SourceUnit( 58 | uri.toString(), 59 | new StringReaderSourceWithURI(contents, uri, configuration()), 60 | configuration(), 61 | classLoader(), 62 | createErrorCollector()); 63 | } 64 | if( Files.exists(Path.of(uri)) ) { 65 | return super.createSourceUnit(new File(uri)); 66 | } 67 | return null; 68 | } 69 | 70 | @Override 71 | protected ErrorCollector createErrorCollector() { 72 | return new LanguageServerErrorCollector(configuration()); 73 | } 74 | 75 | /** 76 | * Compile a source file enough to build the AST, and defer 77 | * any errors to be handled later. 78 | * 79 | * @param sourceUnit 80 | */ 81 | @Override 82 | public void compile(SourceUnit sourceUnit) { 83 | try { 84 | super.compile(sourceUnit); 85 | } 86 | catch( GroovyBugError | Exception e ) { 87 | var uri = sourceUnit.getSource().getURI(); 88 | System.err.println("Unexpected exception while compiling " + uri.getPath() + ": " + e.toString()); 89 | e.printStackTrace(); 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/util/LanguageServerUtils.java: -------------------------------------------------------------------------------- 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.util; 17 | 18 | import java.net.URI; 19 | 20 | import org.codehaus.groovy.ast.ASTNode; 21 | import org.codehaus.groovy.control.messages.WarningMessage; 22 | import org.codehaus.groovy.syntax.SyntaxException; 23 | import org.eclipse.lsp4j.Location; 24 | import org.eclipse.lsp4j.Position; 25 | import org.eclipse.lsp4j.Range; 26 | 27 | /** 28 | * Utility methods for mapping compiler data structures 29 | * to LSP data structures. 30 | * 31 | * @author Ben Sherman 32 | */ 33 | public class LanguageServerUtils { 34 | 35 | public static int groovyToLspLine(int groovyLine) { 36 | return groovyLine != -1 ? groovyLine - 1 : -1; 37 | } 38 | 39 | public static int groovyToLspCharacter(int groovyColumn) { 40 | return groovyColumn > 0 ? groovyColumn - 1 : 0; 41 | } 42 | 43 | public static Position groovyToLspPosition(int groovyLine, int groovyColumn) { 44 | int lspLine = groovyToLspLine(groovyLine); 45 | if( lspLine == -1 ) 46 | return null; 47 | return new Position( 48 | lspLine, 49 | groovyToLspCharacter(groovyColumn)); 50 | } 51 | 52 | public static Range errorToRange(SyntaxException error) { 53 | var start = groovyToLspPosition(error.getStartLine(), error.getStartColumn()); 54 | if( start == null ) 55 | return null; 56 | var end = groovyToLspPosition(error.getEndLine(), error.getEndColumn()); 57 | if( end == null ) 58 | end = start; 59 | return new Range(start, end); 60 | } 61 | 62 | public static Range warningToRange(WarningMessage warning) { 63 | var token = warning.getContext().getRoot(); 64 | var start = groovyToLspPosition(token.getStartLine(), token.getStartColumn()); 65 | if( start == null ) 66 | return null; 67 | var end = groovyToLspPosition(token.getStartLine(), token.getStartColumn() + token.getText().length()); 68 | if( end == null ) 69 | end = start; 70 | return new Range(start, end); 71 | } 72 | 73 | public static Range astNodeToRange(ASTNode node) { 74 | var start = groovyToLspPosition(node.getLineNumber(), node.getColumnNumber()); 75 | if( start == null ) 76 | return null; 77 | var end = groovyToLspPosition(node.getLastLineNumber(), node.getLastColumnNumber()); 78 | if( end == null ) 79 | end = start; 80 | return new Range(start, end); 81 | } 82 | 83 | public static Location astNodeToLocation(ASTNode node, URI uri) { 84 | var range = astNodeToRange(node); 85 | if( range == null ) 86 | return null; 87 | return new Location(uri.toString(), range); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/config/ConfigSemanticTokensProvider.java: -------------------------------------------------------------------------------- 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.config; 17 | 18 | import java.net.URI; 19 | import java.util.List; 20 | 21 | import nextflow.config.ast.ConfigAssignNode; 22 | import nextflow.config.ast.ConfigIncludeNode; 23 | import nextflow.config.ast.ConfigNode; 24 | import nextflow.config.ast.ConfigVisitorSupport; 25 | import nextflow.lsp.services.SemanticTokensProvider; 26 | import nextflow.lsp.services.SemanticTokensVisitor; 27 | import nextflow.lsp.util.Logger; 28 | import org.codehaus.groovy.control.SourceUnit; 29 | import org.eclipse.lsp4j.SemanticTokens; 30 | import org.eclipse.lsp4j.TextDocumentIdentifier; 31 | 32 | /** 33 | * 34 | * @author Ben Sherman 35 | */ 36 | public class ConfigSemanticTokensProvider implements SemanticTokensProvider { 37 | 38 | private static Logger log = Logger.getInstance(); 39 | 40 | private ConfigAstCache ast; 41 | 42 | public ConfigSemanticTokensProvider(ConfigAstCache ast) { 43 | this.ast = ast; 44 | } 45 | 46 | @Override 47 | public SemanticTokens semanticTokensFull(TextDocumentIdentifier textDocument) { 48 | if( ast == null ) { 49 | log.error("ast cache is empty while providing semantic tokens"); 50 | return null; 51 | } 52 | 53 | var uri = URI.create(textDocument.getUri()); 54 | if( !ast.hasAST(uri) ) 55 | return null; 56 | 57 | var sourceUnit = ast.getSourceUnit(uri); 58 | var visitor = new Visitor(sourceUnit); 59 | visitor.visit(); 60 | return visitor.getTokens(); 61 | } 62 | 63 | private static class Visitor extends ConfigVisitorSupport { 64 | 65 | private SourceUnit sourceUnit; 66 | 67 | private SemanticTokensVisitor tok; 68 | 69 | public Visitor(SourceUnit sourceUnit) { 70 | this.sourceUnit = sourceUnit; 71 | this.tok = new SemanticTokensVisitor(); 72 | } 73 | 74 | @Override 75 | protected SourceUnit getSourceUnit() { 76 | return sourceUnit; 77 | } 78 | 79 | public void visit() { 80 | var moduleNode = sourceUnit.getAST(); 81 | if( moduleNode instanceof ConfigNode cn ) 82 | super.visit(cn); 83 | } 84 | 85 | public SemanticTokens getTokens() { 86 | return tok.getTokens(); 87 | } 88 | 89 | // config statements 90 | 91 | @Override 92 | public void visitConfigAssign(ConfigAssignNode node) { 93 | tok.visit(node.value); 94 | } 95 | 96 | @Override 97 | public void visitConfigInclude(ConfigIncludeNode node) { 98 | tok.visit(node.source); 99 | } 100 | 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/TestUtils.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp 18 | 19 | import java.nio.file.Files 20 | import java.nio.file.Path 21 | 22 | import nextflow.lsp.services.LanguageServerConfiguration 23 | import nextflow.lsp.services.LanguageService 24 | import nextflow.lsp.services.config.ConfigService 25 | import nextflow.lsp.services.script.ScriptService 26 | import nextflow.lsp.spec.PluginSpecCache 27 | import org.eclipse.lsp4j.DidOpenTextDocumentParams 28 | import org.eclipse.lsp4j.TextDocumentItem 29 | 30 | /** 31 | * 32 | * @author Ben Sherman 33 | */ 34 | class TestUtils { 35 | 36 | private static final Path workspaceRoot = workspaceRoot() 37 | 38 | private static Path workspaceRoot() { 39 | def workspaceRoot = Path.of(System.getProperty('user.dir')).resolve('build/test_workspace/') 40 | if( !Files.exists(workspaceRoot) ) 41 | workspaceRoot.toFile().mkdirs() 42 | return workspaceRoot 43 | } 44 | 45 | /** 46 | * Get a language service instance for Nextflow config files. 47 | */ 48 | static ConfigService getConfigService() { 49 | def service = new ConfigService(workspaceRoot.toUri().toString()) 50 | def configuration = LanguageServerConfiguration.defaults() 51 | service.connect(new TestLanguageClient()) 52 | service.initialize(configuration) 53 | // skip workspace scan 54 | open(service, getUri('nextflow.config'), '') 55 | service.updateNow() 56 | return service 57 | } 58 | 59 | /** 60 | * Get a language service instance for Nextflow scripts. 61 | */ 62 | static ScriptService getScriptService() { 63 | def service = new ScriptService(workspaceRoot.toUri().toString()) 64 | def configuration = LanguageServerConfiguration.defaults() 65 | def pluginSpecCache = new PluginSpecCache(configuration.pluginRegistryUrl()) 66 | service.connect(new TestLanguageClient()) 67 | service.initialize(configuration, pluginSpecCache) 68 | // skip workspace scan 69 | open(service, getUri('main.nf'), '') 70 | service.updateNow() 71 | return service 72 | } 73 | 74 | /** 75 | * Get the URI for a relative path. 76 | * 77 | * @param path 78 | */ 79 | static String getUri(String path) { 80 | return workspaceRoot.resolve(path).toUri().toString() 81 | } 82 | 83 | /** 84 | * Open a file. 85 | * 86 | * NOTE: this operation is asynchronous due to debouncing 87 | * 88 | * @param service 89 | * @param uri 90 | * @param contents 91 | */ 92 | static void open(LanguageService service, String uri, String contents) { 93 | def textDocumentItem = new TextDocumentItem(uri, 'nextflow', 1, contents.stripIndent()) 94 | service.didOpen(new DidOpenTextDocumentParams(textDocumentItem)) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/nextflow/script/types/shim/Map.java: -------------------------------------------------------------------------------- 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.script.types.shim; 17 | 18 | import java.util.function.BiConsumer; 19 | import java.util.function.BiPredicate; 20 | 21 | import nextflow.script.dsl.Description; 22 | import nextflow.script.dsl.Ops; 23 | 24 | @Description(""" 25 | A map associates or "maps" keys to values. Each key can map to at most one value -- a map cannot contain duplicate keys. 26 | 27 | [Read more](https://nextflow.io/docs/latest/reference/stdlib.html#map-k-v) 28 | """) 29 | @Ops(MapOps.class) 30 | public interface Map { 31 | 32 | @Description(""" 33 | Returns `true` if any key-value pair in the map satisfies the given condition. The closure should accept two parameters corresponding to the key and value of an entry. 34 | """) 35 | boolean any(BiPredicate condition); 36 | 37 | @Description(""" 38 | Returns `true` if the map contains a mapping for the given key. 39 | """) 40 | boolean containsKey(K key); 41 | 42 | @Description(""" 43 | Returns `true` if the map maps one or more keys to the given value. 44 | """) 45 | boolean containsValue(V value); 46 | 47 | @Description(""" 48 | Invoke the given closure for each key-value pair in the map. The closure should accept two parameters corresponding to the key and value of an entry. 49 | """) 50 | void each(BiConsumer action); 51 | 52 | @Description(""" 53 | Returns a set of the key-value pairs in the map. 54 | """) 55 | Set> entrySet(); 56 | 57 | @Description(""" 58 | Returns `true` if every key-value pair in the map satisfies the given condition. The closure should accept two parameters corresponding to the key and value of an entry. 59 | """) 60 | boolean every(BiPredicate condition); 61 | 62 | @Description(""" 63 | Returns `true` if the map is empty. 64 | """) 65 | boolean isEmpty(); 66 | 67 | @Description(""" 68 | Returns a set of the keys in the map. 69 | """) 70 | Set keySet(); 71 | 72 | @Description(""" 73 | Returns the number of key-value pairs in the map. 74 | """) 75 | int size(); 76 | 77 | @Description(""" 78 | Returns a sub-map containing the given keys. 79 | """) 80 | Map subMap(Iterable keys); 81 | 82 | @Description(""" 83 | Returns a collection of the values in the map. 84 | """) 85 | Bag values(); 86 | 87 | @Description(""" 88 | A map entry is a key-value pair. 89 | """) 90 | interface Entry { 91 | K getKey(); 92 | V getValue(); 93 | } 94 | 95 | } 96 | 97 | interface MapOps { 98 | 99 | V getAt(Map a, K b); 100 | 101 | Boolean isCase(K a, Map b); 102 | 103 | Map plus(Map a, Map b); 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/nextflow/script/types/shim/Integer.java: -------------------------------------------------------------------------------- 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.script.types.shim; 17 | 18 | import nextflow.script.dsl.Constant; 19 | import nextflow.script.dsl.Description; 20 | import nextflow.script.dsl.Ops; 21 | import nextflow.script.types.Duration; 22 | import nextflow.script.types.MemoryUnit; 23 | 24 | @Description(""" 25 | An integer is a whole number that can be positive or negative. 26 | 27 | [Read more](https://nextflow.io/docs/latest/reference/stdlib.html#integer) 28 | """) 29 | @Ops(IntegerOps.class) 30 | public interface Integer { 31 | 32 | Integer intdiv(Integer b); 33 | 34 | // Duration 35 | 36 | @Constant("d") 37 | @Description(""" 38 | Returns the equivalent Duration value in days. 39 | """) 40 | Duration d(); 41 | 42 | @Constant("h") 43 | @Description(""" 44 | Returns the equivalent Duration value in hours. 45 | """) 46 | Duration h(); 47 | 48 | @Constant("m") 49 | @Description(""" 50 | Returns the equivalent Duration value in minutes. 51 | """) 52 | Duration m(); 53 | 54 | @Constant("ms") 55 | @Description(""" 56 | Returns the equivalent Duration value in milliseconds. 57 | """) 58 | Duration ms(); 59 | 60 | @Constant("s") 61 | @Description(""" 62 | Returns the equivalent Duration value in seconds. 63 | """) 64 | Duration s(); 65 | 66 | // MemoryUnit 67 | 68 | @Constant("B") 69 | @Description(""" 70 | Returns the equivalent MemoryUnit value in bytes. 71 | """) 72 | MemoryUnit B(); 73 | 74 | @Constant("KB") 75 | @Description(""" 76 | Returns the equivalent MemoryUnit value in KB. 77 | """) 78 | MemoryUnit KB(); 79 | 80 | @Constant("MB") 81 | @Description(""" 82 | Returns the equivalent MemoryUnit value in MB. 83 | """) 84 | MemoryUnit MB(); 85 | 86 | @Constant("GB") 87 | @Description(""" 88 | Returns the equivalent MemoryUnit value in GB. 89 | """) 90 | MemoryUnit GB(); 91 | 92 | } 93 | 94 | interface IntegerOps { 95 | 96 | Integer and(Integer a, Integer b); 97 | 98 | Integer bitwiseNegate(); 99 | 100 | Boolean compareTo(Integer a, Integer b); 101 | 102 | Boolean compareTo(Integer a, Float b); 103 | 104 | Integer div(Integer a, Integer b); 105 | 106 | Integer leftShift(Integer a, Integer b); 107 | 108 | Integer minus(Integer a, Integer b); 109 | 110 | Integer mod(Integer a, Integer b); 111 | 112 | Integer multiply(Integer a, Integer b); 113 | 114 | Integer negative(); 115 | 116 | Integer or(Integer a, Integer b); 117 | 118 | Integer plus(Integer a, Integer b); 119 | 120 | Integer positive(); 121 | 122 | Integer power(Integer a, Integer b); 123 | 124 | Integer rightShift(Integer a, Integer b); 125 | 126 | Integer xor(Integer a, Integer b); 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/config/ConfigAstParentVisitor.java: -------------------------------------------------------------------------------- 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.config; 17 | 18 | import java.util.Map; 19 | 20 | import nextflow.config.ast.ConfigApplyNode; 21 | import nextflow.config.ast.ConfigApplyBlockNode; 22 | import nextflow.config.ast.ConfigAssignNode; 23 | import nextflow.config.ast.ConfigBlockNode; 24 | import nextflow.config.ast.ConfigIncludeNode; 25 | import nextflow.config.ast.ConfigIncompleteNode; 26 | import nextflow.config.ast.ConfigNode; 27 | import nextflow.config.ast.ConfigVisitorSupport; 28 | import nextflow.lsp.ast.ASTParentVisitor; 29 | import org.codehaus.groovy.ast.ASTNode; 30 | import org.codehaus.groovy.control.SourceUnit; 31 | 32 | /** 33 | * 34 | * @author Ben Sherman 35 | */ 36 | class ConfigAstParentVisitor extends ConfigVisitorSupport { 37 | 38 | private SourceUnit sourceUnit; 39 | 40 | private ASTParentVisitor lookup = new ASTParentVisitor(); 41 | 42 | public ConfigAstParentVisitor(SourceUnit sourceUnit) { 43 | this.sourceUnit = sourceUnit; 44 | } 45 | 46 | @Override 47 | protected SourceUnit getSourceUnit() { 48 | return sourceUnit; 49 | } 50 | 51 | public void visit() { 52 | var moduleNode = sourceUnit.getAST(); 53 | if( moduleNode instanceof ConfigNode cn ) 54 | super.visit(cn); 55 | } 56 | 57 | public Map getParents() { 58 | return lookup.getParents(); 59 | } 60 | 61 | @Override 62 | public void visitConfigApplyBlock(ConfigApplyBlockNode node) { 63 | lookup.push(node); 64 | try { 65 | super.visitConfigApplyBlock(node); 66 | } 67 | finally { 68 | lookup.pop(); 69 | } 70 | } 71 | 72 | @Override 73 | public void visitConfigApply(ConfigApplyNode node) { 74 | lookup.visitMethodCallExpression(node); 75 | } 76 | 77 | @Override 78 | public void visitConfigAssign(ConfigAssignNode node) { 79 | lookup.push(node); 80 | try { 81 | lookup.visit(node.value); 82 | } 83 | finally { 84 | lookup.pop(); 85 | } 86 | } 87 | 88 | @Override 89 | public void visitConfigBlock(ConfigBlockNode node) { 90 | lookup.push(node); 91 | try { 92 | super.visitConfigBlock(node); 93 | } 94 | finally { 95 | lookup.pop(); 96 | } 97 | } 98 | 99 | @Override 100 | public void visitConfigInclude(ConfigIncludeNode node) { 101 | lookup.push(node); 102 | try { 103 | lookup.visit(node.source); 104 | } 105 | finally { 106 | lookup.pop(); 107 | } 108 | } 109 | 110 | @Override 111 | public void visitConfigIncomplete(ConfigIncompleteNode node) { 112 | lookup.push(node); 113 | try { 114 | super.visitConfigIncomplete(node); 115 | } 116 | finally { 117 | lookup.pop(); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/config/ConfigLinkProvider.java: -------------------------------------------------------------------------------- 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.config; 17 | 18 | import java.net.URI; 19 | import java.nio.file.Path; 20 | import java.util.ArrayList; 21 | import java.util.Collections; 22 | import java.util.List; 23 | 24 | import nextflow.config.ast.ConfigIncludeNode; 25 | import nextflow.config.ast.ConfigNode; 26 | import nextflow.config.ast.ConfigVisitorSupport; 27 | import nextflow.lsp.services.LinkProvider; 28 | import nextflow.lsp.util.Logger; 29 | import nextflow.lsp.util.LanguageServerUtils; 30 | import org.codehaus.groovy.ast.expr.ConstantExpression; 31 | import org.codehaus.groovy.control.SourceUnit; 32 | import org.eclipse.lsp4j.DocumentLink; 33 | import org.eclipse.lsp4j.TextDocumentIdentifier; 34 | 35 | /** 36 | * Provide the locations of links in a document. 37 | * 38 | * @author Ben Sherman 39 | */ 40 | public class ConfigLinkProvider implements LinkProvider { 41 | 42 | private static Logger log = Logger.getInstance(); 43 | 44 | private ConfigAstCache ast; 45 | 46 | public ConfigLinkProvider(ConfigAstCache ast) { 47 | this.ast = ast; 48 | } 49 | 50 | @Override 51 | public List documentLink(TextDocumentIdentifier textDocument) { 52 | if( ast == null ) { 53 | log.error("ast cache is empty while providing document links"); 54 | return Collections.emptyList(); 55 | } 56 | 57 | var uri = URI.create(textDocument.getUri()); 58 | if( !ast.hasAST(uri) ) 59 | return Collections.emptyList(); 60 | 61 | var sourceUnit = ast.getSourceUnit(uri); 62 | var visitor = new ConfigLinkVisitor(sourceUnit); 63 | visitor.visit(); 64 | 65 | return visitor.getLinks(); 66 | } 67 | 68 | } 69 | 70 | class ConfigLinkVisitor extends ConfigVisitorSupport { 71 | 72 | private SourceUnit sourceUnit; 73 | 74 | private URI uri; 75 | 76 | private List links = new ArrayList<>(); 77 | 78 | public ConfigLinkVisitor(SourceUnit sourceUnit) { 79 | this.sourceUnit = sourceUnit; 80 | this.uri = sourceUnit.getSource().getURI(); 81 | } 82 | 83 | @Override 84 | protected SourceUnit getSourceUnit() { 85 | return sourceUnit; 86 | } 87 | 88 | public void visit() { 89 | var moduleNode = sourceUnit.getAST(); 90 | if( moduleNode instanceof ConfigNode cn ) 91 | super.visit(cn); 92 | } 93 | 94 | @Override 95 | public void visitConfigInclude(ConfigIncludeNode node) { 96 | if( !(node.source instanceof ConstantExpression) ) 97 | return; 98 | var source = node.source.getText(); 99 | var range = LanguageServerUtils.astNodeToRange(node.source); 100 | var target = getIncludeUri(uri, source).toString(); 101 | links.add(new DocumentLink(range, target)); 102 | } 103 | 104 | protected static URI getIncludeUri(URI uri, String source) { 105 | return Path.of(uri).getParent().resolve(source).normalize().toUri(); 106 | } 107 | 108 | public List getLinks() { 109 | return links; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/util/Positions.java: -------------------------------------------------------------------------------- 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.util; 17 | 18 | import java.io.BufferedReader; 19 | import java.io.IOException; 20 | import java.io.StringReader; 21 | import java.util.Comparator; 22 | 23 | import org.eclipse.lsp4j.Position; 24 | 25 | public class Positions { 26 | 27 | public static final Comparator COMPARATOR = (Position a, Position b) -> { 28 | return a.getLine() != b.getLine() 29 | ? a.getLine() - b.getLine() 30 | : a.getCharacter() - b.getCharacter(); 31 | }; 32 | 33 | /** 34 | * Map a two-dimensional position (line and character) to a 35 | * one-dimensional index in a string buffer. 36 | * 37 | * @param string 38 | * @param position 39 | */ 40 | public static int getOffset(String string, Position position) { 41 | int line = position.getLine(); 42 | int character = position.getCharacter(); 43 | int currentIndex = 0; 44 | if( line > 0 ) { 45 | var reader = new BufferedReader(new StringReader(string)); 46 | try { 47 | int readLines = 0; 48 | while( true ) { 49 | var b = reader.read(); 50 | if( b == -1 ) 51 | return -1; 52 | currentIndex++; 53 | if( (char) b == '\n' ) { 54 | readLines++; 55 | if( readLines == line ) 56 | break; 57 | } 58 | } 59 | } 60 | catch( IOException e ) { 61 | return -1; 62 | } 63 | 64 | try { 65 | reader.close(); 66 | } 67 | catch( IOException e ) { 68 | } 69 | } 70 | return currentIndex + character; 71 | } 72 | 73 | /** 74 | * Map a one-dimensional index in a string buffer to a 75 | * two-dimensional position (line and character). 76 | * 77 | * @param string 78 | * @param offset 79 | */ 80 | public static Position getPosition(String string, int offset) { 81 | int line = 0; 82 | int character = 0; 83 | if( offset > 0 ) { 84 | var reader = new BufferedReader(new StringReader(string)); 85 | try { 86 | while( true ) { 87 | var b = reader.read(); 88 | if( b == -1 ) 89 | return new Position(-1, -1); 90 | offset--; 91 | character++; 92 | if( (char) b == '\n' ) { 93 | line++; 94 | character = 0; 95 | } 96 | if( offset == 0 ) 97 | break; 98 | } 99 | } 100 | catch( IOException e ) { 101 | return new Position(-1, -1); 102 | } 103 | 104 | try { 105 | reader.close(); 106 | } 107 | catch( IOException e ) { 108 | } 109 | } 110 | return new Position(line, character); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/services/script/ScriptReferencesTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.services.script 18 | 19 | import nextflow.lsp.util.Positions 20 | import org.eclipse.lsp4j.Location 21 | import org.eclipse.lsp4j.Position 22 | import org.eclipse.lsp4j.ReferenceContext 23 | import org.eclipse.lsp4j.ReferenceParams 24 | import org.eclipse.lsp4j.TextDocumentIdentifier 25 | import spock.lang.Specification 26 | 27 | import static nextflow.lsp.TestUtils.* 28 | 29 | /** 30 | * 31 | * @author Ben Sherman 32 | */ 33 | class ScriptReferencesTest extends Specification { 34 | 35 | Map getReferences(ScriptService service, String uri, Position position) { 36 | def locations = service.references(new ReferenceParams(new TextDocumentIdentifier(uri), position, new ReferenceContext(true))) 37 | def result = [:] 38 | for( def location : locations ) { 39 | def key = location.getUri() 40 | def ranges = result.computeIfAbsent(key, (k) -> []) 41 | ranges.add(location.getRange()) 42 | } 43 | for( def key : result.keySet() ) 44 | result[key].sort((a, b) -> Positions.COMPARATOR.compare(a.getStart(), b.getStart())) 45 | return result 46 | } 47 | 48 | def 'should get the references of a workflow definition' () { 49 | given: 50 | def service = getScriptService() 51 | def uri = getUri('main.nf') 52 | 53 | when: 54 | def contents = '''\ 55 | workflow HELLO { 56 | } 57 | 58 | workflow { 59 | HELLO() 60 | } 61 | ''' 62 | open(service, uri, contents) 63 | service.updateNow() 64 | def references = getReferences(service, uri, new Position(0, 9)) 65 | then: 66 | references[uri].size() == 2 67 | references[uri][0].getStart() == new Position(0, 0) 68 | references[uri][0].getEnd() == new Position(1, 1) 69 | references[uri][1].getStart() == new Position(4, 4) 70 | references[uri][1].getEnd() == new Position(4, 11) 71 | } 72 | 73 | def 'should get the references of a workflow definition in a different file' () { 74 | given: 75 | def service = getScriptService() 76 | def mainUri = getUri('main.nf') 77 | def moduleUri = getUri('module.nf') 78 | 79 | when: 80 | open(service, moduleUri, '''\ 81 | workflow HELLO { 82 | } 83 | ''') 84 | open(service, mainUri, '''\ 85 | include { HELLO } from './module.nf' 86 | 87 | workflow { 88 | HELLO() 89 | } 90 | ''') 91 | service.updateNow() 92 | def references = getReferences(service, moduleUri, new Position(0, 9)) 93 | then: 94 | references[moduleUri].size() == 1 95 | references[moduleUri][0].getStart() == new Position(0, 0) 96 | references[moduleUri][0].getEnd() == new Position(1, 1) 97 | references[mainUri].size() == 2 98 | references[mainUri][0].getStart() == new Position(0, 10) 99 | references[mainUri][0].getEnd() == new Position(0, 15) 100 | references[mainUri][1].getStart() == new Position(3, 4) 101 | references[mainUri][1].getEnd() == new Position(3, 11) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/util/JsonUtils.java: -------------------------------------------------------------------------------- 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.util; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import com.google.gson.JsonElement; 22 | import com.google.gson.JsonNull; 23 | import com.google.gson.JsonObject; 24 | import com.google.gson.JsonPrimitive; 25 | 26 | /** 27 | * 28 | * @author Ben Sherman 29 | */ 30 | public class JsonUtils { 31 | 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 | 44 | public static List getStringArray(Object json, String path) { 45 | var value = getObjectPath(json, path); 46 | if( value == null || !value.isJsonArray() ) 47 | return null; 48 | var result = new ArrayList(); 49 | for( var el : value.getAsJsonArray() ) { 50 | try { 51 | result.add(el.getAsString()); 52 | } 53 | catch( ClassCastException e ) { 54 | continue; 55 | } 56 | } 57 | return result; 58 | } 59 | 60 | public static Boolean getBoolean(Object json, String path) { 61 | var value = getObjectPath(json, path); 62 | if( value == null ) 63 | return null; 64 | try { 65 | return value.getAsBoolean(); 66 | } 67 | catch( ClassCastException e ) { 68 | return null; 69 | } 70 | } 71 | 72 | public static Integer getInteger(Object json, String path) { 73 | var value = getObjectPath(json, path); 74 | if( value == null ) 75 | return null; 76 | try { 77 | return value.getAsInt(); 78 | } 79 | catch( ClassCastException e ) { 80 | return null; 81 | } 82 | } 83 | 84 | public static String getString(Object json, String path) { 85 | var value = getObjectPath(json, path); 86 | if( value == null ) 87 | return null; 88 | try { 89 | return value.getAsString(); 90 | } 91 | catch( ClassCastException e ) { 92 | return null; 93 | } 94 | } 95 | 96 | public static String getString(Object json) { 97 | return json instanceof JsonPrimitive jp ? jp.getAsString() : null; 98 | } 99 | 100 | private static JsonElement getObjectPath(Object json, String path) { 101 | if( !(json instanceof JsonObject) ) 102 | return null; 103 | 104 | JsonObject object = (JsonObject) json; 105 | var names = path.split("\\."); 106 | for( int i = 0; i < names.length - 1; i++ ) { 107 | var scope = names[i]; 108 | if( !object.has(scope) || !object.get(scope).isJsonObject() ) 109 | return null; 110 | object = object.get(scope).getAsJsonObject(); 111 | } 112 | 113 | var property = names[names.length - 1]; 114 | if( !object.has(property) ) 115 | return null; 116 | return object.get(property); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/test/groovy/nextflow/lsp/services/script/ScriptCompletionTest.groovy: -------------------------------------------------------------------------------- 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 | 17 | package nextflow.lsp.services.script 18 | 19 | import org.eclipse.lsp4j.CompletionItem 20 | import org.eclipse.lsp4j.CompletionItemKind 21 | import org.eclipse.lsp4j.CompletionParams 22 | import org.eclipse.lsp4j.Position 23 | import org.eclipse.lsp4j.TextDocumentIdentifier 24 | import spock.lang.Specification 25 | 26 | import static nextflow.lsp.TestUtils.* 27 | 28 | /** 29 | * 30 | * @author Ben Sherman 31 | */ 32 | class ScriptCompletionTest extends Specification { 33 | 34 | List getCompletions(ScriptService service, String uri, Position position) { 35 | return service 36 | .completion(new CompletionParams(new TextDocumentIdentifier(uri), position), 100, false) 37 | .getLeft() 38 | } 39 | 40 | def 'should get completion proposals for a property expression' () { 41 | given: 42 | def service = getScriptService() 43 | def uri = getUri('main.nf') 44 | 45 | when: 46 | open(service, uri, '''\ 47 | workflow { 48 | workflow.f 49 | } 50 | ''') 51 | def completions = getCompletions(service, uri, new Position(1, 14)) 52 | then: 53 | completions.size() == 2 54 | completions[0].getLabel() == "failOnIgnore" 55 | completions[0].getLabelDetails().getDescription() == "Boolean" 56 | completions[0].getKind() == CompletionItemKind.Constant 57 | completions[0].getDetail() == "failOnIgnore: Boolean" 58 | completions[1].getLabel() == "fusion" 59 | completions[1].getLabelDetails().getDescription() == "namespace" 60 | completions[1].getKind() == CompletionItemKind.Module 61 | completions[1].getDetail() == "(namespace) fusion" 62 | 63 | when: 64 | open(service, uri, '''\ 65 | workflow { 66 | workflow.config 67 | } 68 | ''') 69 | completions = getCompletions(service, uri, new Position(1, 19)) 70 | then: 71 | completions.size() == 1 72 | completions[0].getLabel() == "configFiles" 73 | completions[0].getLabelDetails().getDescription() == "List" 74 | completions[0].getKind() == CompletionItemKind.Constant 75 | completions[0].getDetail() == "configFiles: List" 76 | 77 | when: 78 | open(service, uri, '''\ 79 | workflow { 80 | log. 81 | } 82 | ''') 83 | completions = getCompletions(service, uri, new Position(1, 8)) 84 | then: 85 | completions.size() == 3 86 | completions[0].getLabel() == "info" 87 | completions[0].getLabelDetails().getDetail() == "(message: String)" 88 | completions[0].getKind() == CompletionItemKind.Function 89 | completions[0].getDetail() == "info(message: String)" 90 | completions[1].getLabel() == "error" 91 | completions[1].getLabelDetails().getDetail() == "(message: String)" 92 | completions[1].getKind() == CompletionItemKind.Function 93 | completions[1].getDetail() == "error(message: String)" 94 | completions[2].getLabel() == "warn" 95 | completions[2].getLabelDetails().getDetail() == "(message: String)" 96 | completions[2].getKind() == CompletionItemKind.Function 97 | completions[2].getDetail() == "warn(message: String)" 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/ResolvePluginIncludeVisitor.java: -------------------------------------------------------------------------------- 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.List; 19 | 20 | import nextflow.lsp.spec.PluginSpecCache; 21 | import nextflow.script.ast.IncludeNode; 22 | import nextflow.script.ast.ScriptNode; 23 | import nextflow.script.ast.ScriptVisitorSupport; 24 | import nextflow.script.control.PhaseAware; 25 | import nextflow.script.control.Phases; 26 | import org.codehaus.groovy.ast.ASTNode; 27 | import org.codehaus.groovy.ast.MethodNode; 28 | import org.codehaus.groovy.control.SourceUnit; 29 | import org.codehaus.groovy.control.messages.SyntaxErrorMessage; 30 | import org.codehaus.groovy.syntax.SyntaxException; 31 | 32 | /** 33 | * Resolve plugin includes against plugin specs. 34 | * 35 | * @author Ben Sherman 36 | */ 37 | public class ResolvePluginIncludeVisitor extends ScriptVisitorSupport { 38 | 39 | private SourceUnit sourceUnit; 40 | 41 | private PluginSpecCache pluginSpecCache; 42 | 43 | public ResolvePluginIncludeVisitor(SourceUnit sourceUnit, PluginSpecCache pluginSpecCache) { 44 | this.sourceUnit = sourceUnit; 45 | this.pluginSpecCache = pluginSpecCache; 46 | } 47 | 48 | @Override 49 | protected SourceUnit getSourceUnit() { 50 | return sourceUnit; 51 | } 52 | 53 | public void visit() { 54 | var moduleNode = sourceUnit.getAST(); 55 | if( moduleNode instanceof ScriptNode sn ) 56 | super.visit(sn); 57 | } 58 | 59 | @Override 60 | public void visitInclude(IncludeNode node) { 61 | var source = node.source.getText(); 62 | if( !source.startsWith("plugin/") ) 63 | return; 64 | var pluginName = source.split("/")[1]; 65 | var spec = pluginSpecCache.getCurrent(pluginName); 66 | if( spec == null ) { 67 | addError("Plugin '" + pluginName + "' does not exist or is not specified in the configuration file", node); 68 | return; 69 | } 70 | for( var entry : node.entries ) { 71 | var entryName = entry.name; 72 | var mn = findMethod(spec.functions(), entryName); 73 | if( mn != null ) { 74 | entry.setTarget(mn); 75 | continue; 76 | } 77 | if( findMethod(spec.factories(), entryName) != null ) 78 | continue; 79 | if( findMethod(spec.operators(), entryName) != null ) 80 | continue; 81 | addError("Included name '" + entryName + "' is not defined in plugin '" + pluginName + "'", node); 82 | } 83 | } 84 | 85 | private static MethodNode findMethod(List methods, String name) { 86 | return methods.stream() 87 | .filter(mn -> mn.getName().equals(name)) 88 | .findFirst().orElse(null); 89 | } 90 | 91 | @Override 92 | public void addError(String message, ASTNode node) { 93 | var cause = new ResolveIncludeError(message, node); 94 | var errorMessage = new SyntaxErrorMessage(cause, sourceUnit); 95 | sourceUnit.getErrorCollector().addErrorAndContinue(errorMessage); 96 | } 97 | 98 | private class ResolveIncludeError extends SyntaxException implements PhaseAware { 99 | 100 | public ResolveIncludeError(String message, ASTNode node) { 101 | super(message, node); 102 | } 103 | 104 | @Override 105 | public int getPhase() { 106 | return Phases.INCLUDE_RESOLUTION; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/dag/Graph.java: -------------------------------------------------------------------------------- 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.dag; 17 | 18 | import java.net.URI; 19 | import java.util.ArrayList; 20 | import java.util.Collections; 21 | import java.util.HashMap; 22 | import java.util.HashSet; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.Set; 26 | import java.util.Stack; 27 | 28 | /** 29 | * 30 | * @author Ben Sherman 31 | * @author Erik Danielsson 32 | */ 33 | class Graph { 34 | 35 | public final Map inputs = new HashMap<>(); 36 | 37 | public final Map nodes = new HashMap<>(); 38 | 39 | public final Map outputs = new HashMap<>(); 40 | 41 | private Stack subgraphs = new Stack<>(); 42 | 43 | private int nextSubgraphId = 0; 44 | 45 | public Graph() { 46 | pushSubgraph(); 47 | } 48 | 49 | public Subgraph peekSubgraph() { 50 | return subgraphs.peek(); 51 | } 52 | 53 | public void pushSubgraph() { 54 | subgraphs.push(new Subgraph(nextSubgraphId, null)); 55 | nextSubgraphId += 1; 56 | } 57 | 58 | public void pushSubgraph(Node dn) { 59 | subgraphs.push(new Subgraph(nextSubgraphId, dn)); 60 | nextSubgraphId += 1; 61 | } 62 | 63 | public Subgraph popSubgraph() { 64 | var result = subgraphs.pop(); 65 | subgraphs.peek().subgraphs.add(result); 66 | return result; 67 | } 68 | 69 | public Node addNode(String label, Node.Type type, URI uri, Set preds) { 70 | var id = nodes.size(); 71 | var dn = new Node(id, label, type, uri, preds); 72 | nodes.put(id, dn); 73 | subgraphs.peek().nodes.add(dn); 74 | return dn; 75 | } 76 | } 77 | 78 | 79 | class Subgraph { 80 | 81 | public final int id; 82 | 83 | public final Node pred; 84 | 85 | public final List subgraphs = new ArrayList<>(); 86 | 87 | public final Set nodes = new HashSet<>(); 88 | 89 | public Subgraph(int id, Node pred) { 90 | this.id = id; 91 | this.pred = pred; 92 | } 93 | 94 | public boolean isVerbose() { 95 | return nodes.stream().allMatch(n -> n.verbose); 96 | } 97 | 98 | @Override 99 | public boolean equals(Object other) { 100 | return other instanceof Subgraph s && this.id == s.id; 101 | } 102 | 103 | @Override 104 | public int hashCode() { 105 | return id; 106 | } 107 | } 108 | 109 | 110 | class Node { 111 | public enum Type { 112 | NAME, 113 | OPERATOR, 114 | CONTROL 115 | } 116 | 117 | public final int id; 118 | public final String label; 119 | public final Type type; 120 | public final URI uri; 121 | public final Set preds; 122 | 123 | public boolean verbose; 124 | 125 | public Node(int id, String label, Type type, URI uri, Set preds) { 126 | this.id = id; 127 | this.label = label; 128 | this.type = type; 129 | this.uri = uri; 130 | this.preds = preds; 131 | this.verbose = (type == Type.NAME); 132 | } 133 | 134 | public void addPredecessors(Set preds) { 135 | this.preds.addAll(preds); 136 | } 137 | 138 | @Override 139 | public boolean equals(Object other) { 140 | return other instanceof Node n && this.id == n.id; 141 | } 142 | 143 | @Override 144 | public int hashCode() { 145 | return id; 146 | } 147 | 148 | @Override 149 | public String toString() { 150 | var predIds = preds.stream().map(p -> p.id).toList(); 151 | return String.format("id=%s,label='%s',type=%s,preds=%s", id, label, type, predIds); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/nextflow/script/types/shim/List.java: -------------------------------------------------------------------------------- 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.script.types.shim; 17 | 18 | import java.util.function.Predicate; 19 | 20 | import groovy.transform.stc.FirstParam; 21 | import nextflow.script.dsl.Description; 22 | import nextflow.script.dsl.Ops; 23 | import nextflow.script.dsl.TupleComponents; 24 | import nextflow.script.types.Tuple; 25 | 26 | @Description(""" 27 | A list is an unordered collection of elements. 28 | 29 | [Read more](https://nextflow.io/docs/latest/reference/stdlib.html#list-e) 30 | """) 31 | @Ops(ListOps.class) 32 | public interface List extends Iterable { 33 | 34 | @Description(""" 35 | Collates the list into a list of sub-lists of length `size`. If `keepRemainder` is `true`, any remaining elements are included as a partial sub-list, otherwise they are excluded. 36 | """) 37 | List> collate(int size, boolean keepRemainder); 38 | List> collate(int size); 39 | 40 | @Description(""" 41 | Collates the list into a list of sub-lists of length `size`, stepping through the list `step` elements for each sub-list. If `keepRemainder` is `true`, any remaining elements are included as a partial sub-list, otherwise they are excluded. 42 | """) 43 | List> collate(int size, int step, boolean keepRemainder); 44 | List> collate(int size, int step); 45 | 46 | @Description(""" 47 | Returns the first value in the list that satisfies the given condition. 48 | """) 49 | E find(Predicate condition); 50 | 51 | @Description(""" 52 | Returns the first element in the list. Raises an error if the list is empty. 53 | """) 54 | E first(); 55 | 56 | @Description(""" 57 | Returns the list of integers from 0 to *n - 1*, where *n* is the number of elements in the list. 58 | """) 59 | List getIndices(); 60 | 61 | @Description(""" 62 | Equivalent to `first()`. 63 | """) 64 | E head(); 65 | 66 | @Description(""" 67 | Returns the index of the first occurrence of the given value in the list, or -1 if the list does not contain the value. 68 | """) 69 | int indexOf(E value); 70 | 71 | @Description(""" 72 | Returns a shallow copy of the list with the last element excluded. 73 | """) 74 | List init(); 75 | 76 | @Description(""" 77 | Returns the last element in the list. Raises an error if the list is empty. 78 | """) 79 | E last(); 80 | 81 | @Description(""" 82 | Returns a shallow copy of the list with the elements reversed. 83 | """) 84 | List reverse(); 85 | 86 | @Description(""" 87 | Returns the portion of the list between the given `fromIndex` (inclusive) and `toIndex` (exclusive). 88 | """) 89 | List subList(int fromIndex, int toIndex); 90 | 91 | @Description(""" 92 | Returns a shallow copy of the list with the first element excluded. 93 | """) 94 | List tail(); 95 | 96 | @Description(""" 97 | Returns the first *n* elements of the list. 98 | """) 99 | List take(int n); 100 | 101 | @Description(""" 102 | Returns the longest prefix of the list where each element satisfies the given condition. 103 | """) 104 | List takeWhile(Predicate condition); 105 | 106 | @Description(""" 107 | Returns a list of 2-tuples corresponding to the value and index of each element in the list. 108 | """) 109 | List<@TupleComponents({ FirstParam.class, Integer.class }) Tuple> withIndex(); 110 | 111 | } 112 | 113 | interface ListOps { 114 | 115 | Boolean compareTo(List a, List b); 116 | 117 | E getAt(List a, Integer b); 118 | 119 | Boolean isCase(E a, List b); 120 | 121 | List multiply(List a, Integer b); 122 | 123 | List plus(List a, List b); 124 | 125 | void putAt(List a, Integer b, E c); 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/spec/ScriptSpecFactory.java: -------------------------------------------------------------------------------- 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.spec; 17 | 18 | import java.lang.reflect.Modifier; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | import nextflow.script.dsl.Description; 23 | import nextflow.script.types.Duration; 24 | import nextflow.script.types.MemoryUnit; 25 | import org.codehaus.groovy.ast.AnnotationNode; 26 | import org.codehaus.groovy.ast.ClassHelper; 27 | import org.codehaus.groovy.ast.ClassNode; 28 | import org.codehaus.groovy.ast.MethodNode; 29 | import org.codehaus.groovy.ast.Parameter; 30 | import org.codehaus.groovy.ast.expr.ConstantExpression; 31 | import org.codehaus.groovy.ast.stmt.EmptyStatement; 32 | 33 | /** 34 | * Load script definitions from plugin specs. 35 | * 36 | * @author Ben Sherman 37 | */ 38 | public class ScriptSpecFactory { 39 | 40 | public static List fromDefinitions(List definitions, String type) { 41 | return definitions.stream() 42 | .filter(node -> type.equals(node.get("type"))) 43 | .map((node) -> { 44 | var spec = (Map) node.get("spec"); 45 | var name = (String) spec.get("name"); 46 | return fromMethod(spec); 47 | }) 48 | .toList(); 49 | } 50 | 51 | private static MethodNode fromMethod(Map spec) { 52 | var name = (String) spec.get("name"); 53 | var description = (String) spec.get("description"); 54 | var returnType = fromType(spec.get("returnType")); 55 | var parameters = fromParameters((List) spec.get("parameters")); 56 | var method = new MethodNode(name, Modifier.PUBLIC, returnType, parameters, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE); 57 | method.setHasNoRealSourcePosition(true); 58 | method.setDeclaringClass(ClassHelper.dynamicType()); 59 | method.setSynthetic(true); 60 | if( description != null ) { 61 | var an = new AnnotationNode(ClassHelper.makeCached(Description.class)); 62 | an.addMember("value", new ConstantExpression(description)); 63 | method.addAnnotation(an); 64 | } 65 | return method; 66 | } 67 | 68 | private static Parameter[] fromParameters(List parameters) { 69 | return parameters.stream() 70 | .map((param) -> { 71 | var name = (String) param.get("name"); 72 | var type = fromType(param.get("type")); 73 | return new Parameter(type, name); 74 | }) 75 | .toArray(Parameter[]::new); 76 | } 77 | 78 | private static final Map STANDARD_TYPES = Map.ofEntries( 79 | Map.entry("Boolean", ClassHelper.Boolean_TYPE), 80 | Map.entry("boolean", ClassHelper.Boolean_TYPE), 81 | Map.entry("Closure", ClassHelper.CLOSURE_TYPE), 82 | Map.entry("Duration", ClassHelper.makeCached(Duration.class)), 83 | Map.entry("Float", ClassHelper.Float_TYPE), 84 | Map.entry("float", ClassHelper.Float_TYPE), 85 | Map.entry("Integer", ClassHelper.Integer_TYPE), 86 | Map.entry("int", ClassHelper.Integer_TYPE), 87 | Map.entry("List", ClassHelper.LIST_TYPE), 88 | Map.entry("MemoryUnit", ClassHelper.makeCached(MemoryUnit.class)), 89 | Map.entry("Set", ClassHelper.SET_TYPE), 90 | Map.entry("String", ClassHelper.STRING_TYPE) 91 | ); 92 | 93 | private static ClassNode fromType(Object type) { 94 | if( type instanceof String s ) { 95 | return STANDARD_TYPES.getOrDefault(s, ClassHelper.dynamicType()); 96 | } 97 | if( type instanceof Map m ) { 98 | var name = (String) m.get("name"); 99 | // TODO: type arguments 100 | return STANDARD_TYPES.getOrDefault(name, ClassHelper.dynamicType()); 101 | } 102 | throw new IllegalStateException(); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/ast/CompletionUtils.java: -------------------------------------------------------------------------------- 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.ast; 17 | 18 | import java.util.List; 19 | 20 | import nextflow.script.ast.ProcessNode; 21 | import nextflow.script.ast.WorkflowNode; 22 | import nextflow.script.dsl.OutputDsl; 23 | import nextflow.script.dsl.ProcessDsl; 24 | import nextflow.script.types.TypesEx; 25 | import org.codehaus.groovy.ast.ASTNode; 26 | import org.codehaus.groovy.ast.ClassNode; 27 | import org.codehaus.groovy.ast.FieldNode; 28 | import org.codehaus.groovy.ast.MethodNode; 29 | import org.codehaus.groovy.ast.Variable; 30 | import org.codehaus.groovy.ast.VariableScope; 31 | import org.codehaus.groovy.ast.stmt.BlockStatement; 32 | import org.eclipse.lsp4j.CompletionItemKind; 33 | import org.eclipse.lsp4j.CompletionItemLabelDetails; 34 | import org.eclipse.lsp4j.MarkupContent; 35 | import org.eclipse.lsp4j.MarkupKind; 36 | 37 | import static nextflow.script.types.TypeCheckingUtils.*; 38 | 39 | /** 40 | * Utility methods for retreiving completion information for ast nodes. 41 | * 42 | * @author Ben Sherman 43 | */ 44 | public class CompletionUtils { 45 | 46 | public static VariableScope getVariableScope(List nodeStack) { 47 | for( var node : nodeStack ) { 48 | if( node instanceof BlockStatement block ) 49 | return block.getVariableScope(); 50 | } 51 | return null; 52 | } 53 | 54 | public static String astNodeToItemDetail(ASTNode node) { 55 | return ASTNodeStringUtils.getLabel(node); 56 | } 57 | 58 | public static MarkupContent astNodeToItemDocumentation(ASTNode node) { 59 | var documentation = ASTNodeStringUtils.getDocumentation(node); 60 | return documentation != null 61 | ? new MarkupContent(MarkupKind.MARKDOWN, documentation) 62 | : null; 63 | } 64 | 65 | public static CompletionItemKind astNodeToItemKind(ASTNode node) { 66 | if( node instanceof ClassNode cn ) { 67 | return cn.isEnum() 68 | ? CompletionItemKind.Enum 69 | : CompletionItemKind.Class; 70 | } 71 | if( node instanceof MethodNode ) { 72 | return CompletionItemKind.Method; 73 | } 74 | if( node instanceof Variable ) { 75 | return node instanceof FieldNode 76 | ? CompletionItemKind.Field 77 | : CompletionItemKind.Variable; 78 | } 79 | return CompletionItemKind.Property; 80 | } 81 | 82 | public static CompletionItemLabelDetails astNodeToItemLabelDetails(Object node) { 83 | var result = new CompletionItemLabelDetails(); 84 | if( node instanceof ProcessNode pn ) { 85 | result.setDescription("process"); 86 | } 87 | else if( node instanceof WorkflowNode pn ) { 88 | result.setDescription("workflow"); 89 | } 90 | else if( node instanceof MethodNode mn && TypesEx.isNamespace(mn) ) { 91 | result.setDescription("namespace"); 92 | } 93 | else if( node instanceof MethodNode mn ) { 94 | result.setDetail("(" + ASTNodeStringUtils.parametersToLabel(mn.getParameters()) + ")"); 95 | result.setDescription(methodDescription(mn)); 96 | } 97 | else if( node instanceof Variable variable ) { 98 | var type = getType(variable); 99 | result.setDescription(TypesEx.getName(type)); 100 | } 101 | return result; 102 | } 103 | 104 | private static String methodDescription(MethodNode mn) { 105 | if( TypesEx.hasReturnType(mn) ) 106 | return TypesEx.getName(mn.getReturnType()); 107 | var cn = mn.getDeclaringClass(); 108 | if( cn.isPrimaryClassNode() ) 109 | return null; 110 | var type = cn.getTypeClass(); 111 | if( type == ProcessDsl.DirectiveDsl.class ) 112 | return "process directive"; 113 | if( type == ProcessDsl.StageDsl.class ) 114 | return "stage directive"; 115 | if( type == OutputDsl.class ) 116 | return "output directive"; 117 | if( type == OutputDsl.IndexDsl.class ) 118 | return "output index directive"; 119 | return null; 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/dag/VariableContext.java: -------------------------------------------------------------------------------- 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.dag; 17 | 18 | import java.util.Collections; 19 | import java.util.HashMap; 20 | import java.util.HashSet; 21 | import java.util.Map; 22 | import java.util.Set; 23 | import java.util.Stack; 24 | 25 | /** 26 | * 27 | * @author Ben Sherman 28 | * @author Erik Danielsson 29 | */ 30 | class VariableContext { 31 | 32 | private Stack> scopes = new Stack<>(); 33 | 34 | public VariableContext() { 35 | scopes.push(new HashMap<>()); 36 | } 37 | 38 | /** 39 | * Get the current scope. 40 | */ 41 | public Map peekScope() { 42 | return scopes.peek(); 43 | } 44 | 45 | /** 46 | * Enter a new scope, inheriting all symbols defined 47 | * in the parent scope. 48 | */ 49 | public void pushScope() { 50 | var newScope = new HashMap(); 51 | scopes.push(newScope); 52 | } 53 | 54 | /** 55 | * Exit the current scope. 56 | */ 57 | public Map popScope() { 58 | return scopes.pop(); 59 | } 60 | 61 | /** 62 | * Get the active instance of a given symbol. 63 | * 64 | * @param name 65 | */ 66 | public Variable getSymbol(String name) { 67 | for( var scope : scopes ) { 68 | if( scope.containsKey(name) ) 69 | return scope.get(name); 70 | } 71 | return null; 72 | } 73 | 74 | /** 75 | * Get the current predecessors of a given symbol. 76 | * 77 | * @param name 78 | */ 79 | public Set getSymbolPreds(String name) { 80 | var variable = getSymbol(name); 81 | return variable != null 82 | ? variable.preds 83 | : Collections.emptySet(); 84 | } 85 | 86 | /** 87 | * Put a symbol into the current scope. 88 | * 89 | * @param name 90 | * @param dn 91 | * @param isLocal 92 | */ 93 | public void putSymbol(String name, Node dn, boolean isLocal) { 94 | var variable = getSymbol(name); 95 | var depth = variable != null 96 | ? variable.depth 97 | : isLocal ? currentDepth() : 1; 98 | var preds = Set.of(dn); 99 | scopes.peek().put(name, new Variable(depth, preds)); 100 | } 101 | 102 | public void putSymbol(String name, Node dn) { 103 | putSymbol(name, dn, false); 104 | } 105 | 106 | /** 107 | * Merge two conditional scopes into the current scope. 108 | * 109 | * @param ifScope 110 | * @param elseScope 111 | */ 112 | public Set mergeConditionalScopes(Map ifScope, Map elseScope) { 113 | var allSymbols = new HashMap(); 114 | 115 | // add symbols from if scope 116 | ifScope.forEach((name, variable) -> { 117 | if( variable.depth > currentDepth() ) 118 | return; 119 | 120 | // propagate symbols that are definitely assigned 121 | var other = elseScope.get(name); 122 | if( other != null ) 123 | allSymbols.put(name, variable.union(other)); 124 | 125 | // TODO: variables assigned in if but not else (or outside scope) are not definitely assigned 126 | }); 127 | 128 | // TODO: variables assigned in else but not if are not definitely assigned 129 | 130 | // add merged symbols to current scope 131 | scopes.peek().putAll(allSymbols); 132 | 133 | // return the set of merged symbols 134 | return allSymbols.keySet(); 135 | } 136 | 137 | private int currentDepth() { 138 | return scopes.size(); 139 | } 140 | 141 | } 142 | 143 | 144 | class Variable { 145 | 146 | public final int depth; 147 | 148 | public final Set preds; 149 | 150 | Variable(int depth, Set preds) { 151 | this.depth = depth; 152 | this.preds = preds; 153 | } 154 | 155 | public Variable union(Variable other) { 156 | var allPreds = new HashSet(preds); 157 | allPreds.addAll(other.preds); 158 | return new Variable(depth, allPreds); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/config/ConfigAstCache.java: -------------------------------------------------------------------------------- 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.config; 17 | 18 | import java.net.URI; 19 | import java.util.HashSet; 20 | import java.util.Map; 21 | import java.util.Set; 22 | 23 | import groovy.lang.GroovyClassLoader; 24 | import nextflow.config.ast.ConfigNode; 25 | import nextflow.config.control.ConfigResolveVisitor; 26 | import nextflow.config.control.ResolveIncludeVisitor; 27 | import nextflow.config.parser.ConfigParserPluginFactory; 28 | import nextflow.config.spec.SpecNode; 29 | import nextflow.lsp.ast.ASTNodeCache; 30 | import nextflow.lsp.compiler.LanguageServerCompiler; 31 | import nextflow.lsp.compiler.LanguageServerErrorCollector; 32 | import nextflow.lsp.file.FileCache; 33 | import nextflow.lsp.services.LanguageServerConfiguration; 34 | import nextflow.lsp.spec.PluginSpecCache; 35 | import nextflow.script.control.PhaseAware; 36 | import nextflow.script.control.Phases; 37 | import nextflow.script.types.Types; 38 | import org.codehaus.groovy.ast.ASTNode; 39 | import org.codehaus.groovy.control.CompilerConfiguration; 40 | import org.codehaus.groovy.control.SourceUnit; 41 | import org.codehaus.groovy.control.messages.WarningMessage; 42 | 43 | /** 44 | * 45 | * @author Ben Sherman 46 | */ 47 | public class ConfigAstCache extends ASTNodeCache { 48 | 49 | private LanguageServerConfiguration configuration; 50 | 51 | private PluginSpecCache pluginSpecCache; 52 | 53 | public ConfigAstCache() { 54 | super(createCompiler()); 55 | } 56 | 57 | private static LanguageServerCompiler createCompiler() { 58 | var config = createConfiguration(); 59 | var classLoader = new GroovyClassLoader(); 60 | return new LanguageServerCompiler(config, classLoader); 61 | } 62 | 63 | private static CompilerConfiguration createConfiguration() { 64 | var config = new CompilerConfiguration(); 65 | config.setPluginFactory(new ConfigParserPluginFactory()); 66 | config.setWarningLevel(WarningMessage.POSSIBLE_ERRORS); 67 | return config; 68 | } 69 | 70 | public void initialize(LanguageServerConfiguration configuration, PluginSpecCache pluginSpecCache) { 71 | this.configuration = configuration; 72 | this.pluginSpecCache = pluginSpecCache; 73 | } 74 | 75 | @Override 76 | protected Set analyze(Set uris, FileCache fileCache) { 77 | // phase 2: include checking 78 | var changedUris = new HashSet<>(uris); 79 | 80 | for( var sourceUnit : getSourceUnits() ) { 81 | var visitor = new CachingResolveIncludeVisitor(sourceUnit, changedUris); 82 | visitor.visit(); 83 | 84 | var uri = sourceUnit.getSource().getURI(); 85 | if( visitor.isChanged() ) { 86 | var errorCollector = (LanguageServerErrorCollector) sourceUnit.getErrorCollector(); 87 | errorCollector.updatePhase(Phases.INCLUDE_RESOLUTION, visitor.getErrors()); 88 | changedUris.add(uri); 89 | } 90 | } 91 | 92 | for( var uri : changedUris ) { 93 | var sourceUnit = getSourceUnit(uri); 94 | if( sourceUnit == null ) 95 | continue; 96 | // phase 3: name checking 97 | new ConfigResolveVisitor(sourceUnit, compiler().compilationUnit(), Types.DEFAULT_CONFIG_IMPORTS).visit(); 98 | new ConfigSpecVisitor(sourceUnit, pluginSpecCache, configuration.typeChecking()).visit(); 99 | if( sourceUnit.getErrorCollector().hasErrors() ) 100 | continue; 101 | // phase 4: type checking 102 | // TODO 103 | } 104 | 105 | return changedUris; 106 | } 107 | 108 | @Override 109 | protected Map visitParents(SourceUnit sourceUnit) { 110 | var visitor = new ConfigAstParentVisitor(sourceUnit); 111 | visitor.visit(); 112 | return visitor.getParents(); 113 | } 114 | 115 | /** 116 | * Check whether a source file has any errors. 117 | * 118 | * @param uri 119 | */ 120 | public boolean hasSyntaxErrors(URI uri) { 121 | return getErrors(uri).stream() 122 | .filter(error -> error instanceof PhaseAware pa ? pa.getPhase() == Phases.SYNTAX : true) 123 | .findFirst() 124 | .isPresent(); 125 | } 126 | 127 | public ConfigNode getConfigNode(URI uri) { 128 | return (ConfigNode) getSourceUnit(uri).getAST(); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/ast/LanguageServerASTUtils.java: -------------------------------------------------------------------------------- 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.ast; 17 | 18 | import java.util.Collections; 19 | import java.util.Iterator; 20 | 21 | import nextflow.script.ast.ASTNodeMarker; 22 | import nextflow.script.ast.FeatureFlagNode; 23 | import nextflow.script.ast.IncludeEntryNode; 24 | import nextflow.script.ast.ProcessNode; 25 | import nextflow.script.ast.WorkflowNode; 26 | import nextflow.script.types.Types; 27 | import org.codehaus.groovy.ast.ASTNode; 28 | import org.codehaus.groovy.ast.ClassNode; 29 | import org.codehaus.groovy.ast.FieldNode; 30 | import org.codehaus.groovy.ast.MethodNode; 31 | import org.codehaus.groovy.ast.Parameter; 32 | import org.codehaus.groovy.ast.Variable; 33 | import org.codehaus.groovy.ast.expr.ClassExpression; 34 | import org.codehaus.groovy.ast.expr.ConstructorCallExpression; 35 | import org.codehaus.groovy.ast.expr.MapEntryExpression; 36 | import org.codehaus.groovy.ast.expr.MethodCallExpression; 37 | import org.codehaus.groovy.ast.expr.PropertyExpression; 38 | import org.codehaus.groovy.ast.expr.VariableExpression; 39 | 40 | import static nextflow.script.ast.ASTUtils.*; 41 | import static nextflow.script.types.TypeCheckingUtils.*; 42 | 43 | /** 44 | * Utility methods for querying an AST. 45 | * 46 | * @author Ben Sherman 47 | */ 48 | public class LanguageServerASTUtils { 49 | 50 | /** 51 | * Get the ast node corresponding to the definition of a given 52 | * class, method, or variable. 53 | * 54 | * @param node 55 | */ 56 | public static ASTNode getDefinition(ASTNode node) { 57 | if( node instanceof VariableExpression ve ) 58 | return getDefinitionFromVariable(ve.getAccessedVariable()); 59 | 60 | if( node instanceof MethodCallExpression mce ) { 61 | var mn = (MethodNode) mce.getNodeMetaData(ASTNodeMarker.METHOD_TARGET); 62 | if( mn != null ) 63 | return mn; 64 | return resolveMethodCall(mce); 65 | } 66 | 67 | if( node instanceof PropertyExpression pe ) { 68 | var fn = (FieldNode) pe.getNodeMetaData(ASTNodeMarker.PROPERTY_TARGET); 69 | if( fn != null ) 70 | return fn; 71 | return resolveProperty(pe); 72 | } 73 | 74 | if( node instanceof ClassExpression ce ) 75 | return ce.getType().redirect(); 76 | 77 | if( node instanceof ConstructorCallExpression cce ) 78 | return cce.getType().redirect(); 79 | 80 | if( node instanceof MapEntryExpression ) { 81 | var namedParam = (Parameter) node.getNodeMetaData("_NAMED_PARAM"); 82 | if( namedParam != null ) 83 | return namedParam; 84 | } 85 | 86 | if( node instanceof FeatureFlagNode ffn ) 87 | return ffn.target != null ? ffn : null; 88 | 89 | if( node instanceof IncludeEntryNode entry ) 90 | return entry.getTarget(); 91 | 92 | if( node instanceof ClassNode cn ) 93 | return node; 94 | 95 | if( node instanceof MethodNode ) 96 | return node; 97 | 98 | if( node instanceof Variable ) 99 | return node; 100 | 101 | return null; 102 | } 103 | 104 | private static ASTNode getDefinitionFromVariable(Variable variable) { 105 | // built-in variable or workflow/process as variable 106 | var mn = asMethodVariable(variable); 107 | if( mn != null ) 108 | return mn; 109 | // local variable 110 | if( variable instanceof ASTNode node ) 111 | return node; 112 | return null; 113 | } 114 | 115 | /** 116 | * Get the ast nodes corresponding to references of a node. 117 | * 118 | * @param node 119 | * @param ast 120 | * @param includeDeclaration 121 | */ 122 | public static Iterator getReferences(ASTNode node, ASTNodeCache ast, boolean includeDeclaration) { 123 | var defNode = getDefinition(node); 124 | if( defNode == null ) 125 | return Collections.emptyIterator(); 126 | return ast.getNodes().stream() 127 | .filter((otherNode) -> { 128 | if( otherNode.getLineNumber() == -1 || otherNode.getColumnNumber() == -1 ) 129 | return false; 130 | if( defNode == otherNode ) 131 | return includeDeclaration; 132 | return defNode == getDefinition(otherNode); 133 | }) 134 | .iterator(); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/ScriptHoverProvider.java: -------------------------------------------------------------------------------- 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 | 20 | import nextflow.lsp.ast.ASTNodeStringUtils; 21 | import nextflow.lsp.ast.LanguageServerASTUtils; 22 | import nextflow.lsp.services.HoverProvider; 23 | import nextflow.lsp.util.Logger; 24 | import nextflow.script.ast.FunctionNode; 25 | import nextflow.script.ast.ProcessNode; 26 | import nextflow.script.ast.WorkflowNode; 27 | import nextflow.script.types.Types; 28 | import org.codehaus.groovy.ast.ASTNode; 29 | import org.codehaus.groovy.ast.MethodNode; 30 | import org.codehaus.groovy.ast.Variable; 31 | import org.codehaus.groovy.ast.expr.Expression; 32 | import org.codehaus.groovy.ast.expr.VariableExpression; 33 | import org.codehaus.groovy.ast.stmt.BlockStatement; 34 | import org.codehaus.groovy.ast.stmt.Statement; 35 | import org.eclipse.lsp4j.Hover; 36 | import org.eclipse.lsp4j.MarkupContent; 37 | import org.eclipse.lsp4j.MarkupKind; 38 | import org.eclipse.lsp4j.Position; 39 | import org.eclipse.lsp4j.TextDocumentIdentifier; 40 | 41 | import static nextflow.script.types.TypeCheckingUtils.*; 42 | 43 | /** 44 | * Provide hints for an expression or statement when hovered 45 | * based on available definitions and Groovydoc comments. 46 | * 47 | * @author Ben Sherman 48 | */ 49 | public class ScriptHoverProvider implements HoverProvider { 50 | 51 | private static Logger log = Logger.getInstance(); 52 | 53 | private ScriptAstCache ast; 54 | 55 | public ScriptHoverProvider(ScriptAstCache ast) { 56 | this.ast = ast; 57 | } 58 | 59 | @Override 60 | public Hover hover(TextDocumentIdentifier textDocument, Position position) { 61 | if( ast == null ) { 62 | log.error("ast cache is empty while providing hover hint"); 63 | return null; 64 | } 65 | 66 | var uri = URI.create(textDocument.getUri()); 67 | var nodeStack = ast.getNodesAtPosition(uri, position); 68 | if( nodeStack.isEmpty() ) 69 | return null; 70 | 71 | var offsetNode = nodeStack.get(0); 72 | var defNode = LanguageServerASTUtils.getDefinition(offsetNode); 73 | 74 | // don't show definition hover hint in the definition's own body 75 | if( defNode == offsetNode ) { 76 | if( position.getLine() != defNode.getLineNumber() - 1 ) 77 | defNode = null; 78 | } 79 | 80 | var builder = new StringBuilder(); 81 | 82 | var label = ASTNodeStringUtils.getLabel(defNode); 83 | if( label != null ) { 84 | builder.append("```nextflow\n"); 85 | builder.append(label); 86 | builder.append("\n```"); 87 | } 88 | 89 | var documentation = ASTNodeStringUtils.getDocumentation(defNode); 90 | if( documentation != null ) { 91 | builder.append("\n\n---\n\n"); 92 | builder.append(documentation); 93 | } 94 | 95 | if( Logger.isDebugEnabled() ) { 96 | builder.append("\n\n---\n\n"); 97 | builder.append("```\n"); 98 | for( int i = 0; i < nodeStack.size(); i++ ) { 99 | var node = nodeStack.get(nodeStack.size() - 1 - i); 100 | builder.append(" ".repeat(i)); 101 | builder.append(node.getClass().getSimpleName()); 102 | builder.append(String.format("(%d:%d-%d:%d)", node.getLineNumber(), node.getColumnNumber(), node.getLastLineNumber(), node.getLastColumnNumber() - 1)); 103 | var scope = 104 | node instanceof BlockStatement block ? block.getVariableScope() : 105 | node instanceof MethodNode mn ? mn.getVariableScope() : 106 | null; 107 | if( scope != null && scope.isClassScope() ) { 108 | builder.append(" ["); 109 | builder.append(scope.getClassScope().getNameWithoutPackage()); 110 | builder.append(']'); 111 | } 112 | if( node instanceof Expression exp ) { 113 | builder.append(": "); 114 | builder.append(Types.getName(getType(exp))); 115 | } 116 | builder.append('\n'); 117 | } 118 | builder.append("\n```"); 119 | } 120 | 121 | var value = builder.toString(); 122 | if( value.isEmpty() ) 123 | return null; 124 | return new Hover(new MarkupContent(MarkupKind.MARKDOWN, value)); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/ScriptSymbolProvider.java: -------------------------------------------------------------------------------- 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.ArrayList; 20 | import java.util.Collections; 21 | import java.util.List; 22 | 23 | import nextflow.lsp.services.SymbolProvider; 24 | import nextflow.lsp.util.LanguageServerUtils; 25 | import nextflow.lsp.util.Logger; 26 | import nextflow.script.ast.FunctionNode; 27 | import nextflow.script.ast.ProcessNode; 28 | import nextflow.script.ast.WorkflowNode; 29 | import org.codehaus.groovy.ast.ASTNode; 30 | import org.codehaus.groovy.ast.ClassNode; 31 | import org.codehaus.groovy.ast.MethodNode; 32 | import org.eclipse.lsp4j.DocumentSymbol; 33 | import org.eclipse.lsp4j.SymbolInformation; 34 | import org.eclipse.lsp4j.SymbolKind; 35 | import org.eclipse.lsp4j.TextDocumentIdentifier; 36 | import org.eclipse.lsp4j.WorkspaceSymbol; 37 | import org.eclipse.lsp4j.jsonrpc.messages.Either; 38 | 39 | /** 40 | * Provide the set of document symbols for a source file, 41 | * which can be used for efficient lookup and traversal. 42 | * 43 | * @author Ben Sherman 44 | */ 45 | public class ScriptSymbolProvider implements SymbolProvider { 46 | 47 | private static Logger log = Logger.getInstance(); 48 | 49 | private ScriptAstCache ast; 50 | 51 | public ScriptSymbolProvider(ScriptAstCache ast) { 52 | this.ast = ast; 53 | } 54 | 55 | @Override 56 | public List> documentSymbol(TextDocumentIdentifier textDocument) { 57 | if( ast == null ) { 58 | log.error("ast cache is empty while providing document symbols"); 59 | return Collections.emptyList(); 60 | } 61 | 62 | var uri = URI.create(textDocument.getUri()); 63 | if( !ast.hasAST(uri) ) 64 | return Collections.emptyList(); 65 | 66 | var result = new ArrayList>(); 67 | for( var node : ast.getTypeNodes(uri) ) 68 | addDocumentSymbol(node, result); 69 | for( var node : ast.getDefinitions(uri) ) 70 | addDocumentSymbol(node, result); 71 | 72 | return result; 73 | } 74 | 75 | private void addDocumentSymbol(ASTNode node, List> result) { 76 | var name = getSymbolName(node); 77 | var range = LanguageServerUtils.astNodeToRange(node); 78 | if( range == null ) 79 | return; 80 | result.add(Either.forRight(new DocumentSymbol(name, getSymbolKind(node), range, range))); 81 | } 82 | 83 | @Override 84 | public List symbol(String query) { 85 | if( ast == null ) { 86 | log.error("ast cache is empty while providing workspace symbols"); 87 | return Collections.emptyList(); 88 | } 89 | 90 | var result = new ArrayList(); 91 | for( var node : ast.getTypeNodes() ) 92 | addWorkspaceSymbol(node, query, result); 93 | for( var node : ast.getDefinitions() ) 94 | addWorkspaceSymbol(node, query, result); 95 | 96 | return result; 97 | } 98 | 99 | private void addWorkspaceSymbol(ASTNode node, String query, List result) { 100 | var name = getSymbolName(node); 101 | if( name == null || !name.toLowerCase().contains(query.toLowerCase()) ) 102 | return; 103 | var uri = ast.getURI(node); 104 | var location = LanguageServerUtils.astNodeToLocation(node, uri); 105 | if( location == null ) 106 | return; 107 | result.add(new WorkspaceSymbol(name, getSymbolKind(node), Either.forLeft(location))); 108 | } 109 | 110 | private static String getSymbolName(ASTNode node) { 111 | if( node instanceof ClassNode cn && cn.isEnum() ) 112 | return "enum " + cn.getName(); 113 | if( node instanceof FunctionNode fn ) 114 | return "function " + fn.getName(); 115 | if( node instanceof ProcessNode pn ) 116 | return "process " + pn.getName(); 117 | if( node instanceof WorkflowNode wn ) 118 | return wn.isEntry() 119 | ? "workflow " 120 | : "workflow " + wn.getName(); 121 | return null; 122 | } 123 | 124 | private static SymbolKind getSymbolKind(ASTNode node) { 125 | if( node instanceof ClassNode cn && cn.isEnum() ) 126 | return SymbolKind.Enum; 127 | if( node instanceof MethodNode mn ) 128 | return SymbolKind.Function; 129 | return null; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/file/FileCache.java: -------------------------------------------------------------------------------- 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.file; 17 | 18 | import java.io.BufferedReader; 19 | import java.io.IOException; 20 | import java.net.URI; 21 | import java.nio.file.Files; 22 | import java.nio.file.Paths; 23 | import java.util.ArrayList; 24 | import java.util.HashMap; 25 | import java.util.HashSet; 26 | import java.util.Map; 27 | import java.util.Set; 28 | 29 | import nextflow.lsp.util.Positions; 30 | import org.eclipse.lsp4j.DidChangeTextDocumentParams; 31 | import org.eclipse.lsp4j.DidCloseTextDocumentParams; 32 | import org.eclipse.lsp4j.DidOpenTextDocumentParams; 33 | 34 | /** 35 | * Cache the contents of open files and track changed files 36 | * (files that need to be recompiled). 37 | * 38 | * @author Ben Sherman 39 | */ 40 | public class FileCache { 41 | 42 | private Map openFiles = new HashMap<>(); 43 | private Set changedFiles = new HashSet<>(); 44 | 45 | public Set getOpenFiles() { 46 | return openFiles.keySet(); 47 | } 48 | 49 | public Set removeChangedFiles() { 50 | var result = changedFiles; 51 | changedFiles = new HashSet<>(); 52 | return result; 53 | } 54 | 55 | public void markChanged(String filename) { 56 | changedFiles.add(URI.create(filename)); 57 | } 58 | 59 | public void markChanged(URI uri) { 60 | changedFiles.add(uri); 61 | } 62 | 63 | public boolean isOpen(URI uri) { 64 | return openFiles.containsKey(uri); 65 | } 66 | 67 | /** 68 | * When a file is opened, add it to the cache and mark it as 69 | * changed. 70 | * 71 | * @param params 72 | */ 73 | public void didOpen(DidOpenTextDocumentParams params) { 74 | var uri = URI.create(params.getTextDocument().getUri()); 75 | openFiles.put(uri, params.getTextDocument().getText()); 76 | changedFiles.add(uri); 77 | } 78 | 79 | /** 80 | * When a file is changed, update the file contents in the cache 81 | * and mark it as changed. 82 | * 83 | * @param params 84 | */ 85 | public void didChange(DidChangeTextDocumentParams params) { 86 | var uri = URI.create(params.getTextDocument().getUri()); 87 | var oldText = openFiles.get(uri); 88 | var firstChange = params.getContentChanges().get(0); 89 | if( firstChange.getRange() == null ) { 90 | // update entire file contents 91 | openFiles.put(uri, firstChange.getText()); 92 | } 93 | else { 94 | // update file contents with incremental changes 95 | for( var change : params.getContentChanges() ) { 96 | var range = change.getRange(); 97 | var offsetStart = Positions.getOffset(oldText, range.getStart()); 98 | var offsetEnd = Positions.getOffset(oldText, range.getEnd()); 99 | oldText = new StringBuilder() 100 | .append(oldText.substring(0, offsetStart)) 101 | .append(change.getText()) 102 | .append(oldText.substring(offsetEnd)) 103 | .toString(); 104 | } 105 | openFiles.put(uri, oldText); 106 | } 107 | changedFiles.add(uri); 108 | } 109 | 110 | /** 111 | * When a file is closed, remove it from the cache and 112 | * mark it as changed. 113 | * 114 | * @param params 115 | */ 116 | public void didClose(DidCloseTextDocumentParams params) { 117 | var uri = URI.create(params.getTextDocument().getUri()); 118 | openFiles.remove(uri); 119 | changedFiles.add(uri); 120 | } 121 | 122 | /** 123 | * Get the contents of a file from the cache, or from the 124 | * filesystem if the file is not open. 125 | * 126 | * @param uri 127 | */ 128 | public String getContents(URI uri) { 129 | if( !openFiles.containsKey(uri) ) { 130 | BufferedReader reader = null; 131 | try { 132 | reader = Files.newBufferedReader(Paths.get(uri)); 133 | var builder = new StringBuilder(); 134 | int next = -1; 135 | while( (next = reader.read()) != -1 ) 136 | builder.append((char) next); 137 | return builder.toString(); 138 | } 139 | catch( IOException e ) { 140 | return null; 141 | } 142 | finally { 143 | if( reader != null ) { 144 | try { 145 | reader.close(); 146 | } 147 | catch( IOException e ) { 148 | } 149 | } 150 | } 151 | } 152 | return openFiles.get(uri); 153 | } 154 | 155 | public void setContents(URI uri, String contents) { 156 | openFiles.put(uri, contents); 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/spec/PluginSpecCache.java: -------------------------------------------------------------------------------- 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.spec; 17 | 18 | import java.io.IOException; 19 | import java.net.URI; 20 | import java.net.http.HttpClient; 21 | import java.net.http.HttpRequest; 22 | import java.util.Collections; 23 | import java.util.HashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | import groovy.json.JsonSlurper; 28 | import nextflow.lsp.util.Logger; 29 | import org.codehaus.groovy.ast.MethodNode; 30 | 31 | import static java.net.http.HttpResponse.BodyHandlers; 32 | 33 | /** 34 | * Cache plugin specs that are fetched from the plugin registry. 35 | * 36 | * @author Ben Sherman 37 | */ 38 | public class PluginSpecCache { 39 | 40 | private static Logger log = Logger.getInstance(); 41 | 42 | private URI registryUri; 43 | 44 | private Map cache = new HashMap<>(); 45 | 46 | private List currentVersions; 47 | 48 | public PluginSpecCache(String registryUrl) { 49 | this.registryUri = URI.create(registryUrl); 50 | } 51 | 52 | /** 53 | * Get the plugin spec for a given plugin release. 54 | * 55 | * If the version is not specified, the latest version is used instead. 56 | * 57 | * Results are cached to minimize registry API calls. 58 | * 59 | * @param name 60 | * @param version 61 | */ 62 | public PluginSpec get(String name, String version) { 63 | var ref = new PluginRef(name, version); 64 | if( !cache.containsKey(ref) ) 65 | updateCache(ref); 66 | return cache.get(ref); 67 | } 68 | 69 | private void updateCache(PluginRef ref) { 70 | try { 71 | // fetch plugin spec from registry 72 | var response = fetch(ref.name(), ref.version()); 73 | if( response == null ) 74 | return; 75 | 76 | // select plugin release (or latest if not specified) 77 | var release = pluginRelease(response); 78 | if( release == null ) 79 | return; 80 | 81 | // save plugin spec to cache 82 | cache.put(ref, pluginSpec(release)); 83 | } 84 | catch( IOException | InterruptedException e ) { 85 | e.printStackTrace(System.err); 86 | } 87 | } 88 | 89 | private Map fetch(String name, String version) throws IOException, InterruptedException { 90 | var path = version != null 91 | ? String.format("v1/plugins/%s/%s", name, version) 92 | : String.format("v1/plugins/%s", name); 93 | var uri = registryUri.resolve(path); 94 | 95 | log.debug("fetch plugin " + uri); 96 | 97 | var client = HttpClient.newBuilder().build(); 98 | var request = HttpRequest.newBuilder() 99 | .uri(uri) 100 | .GET() 101 | .header("Accept", "application/json") 102 | .build(); 103 | var httpResponse = client.send(request, BodyHandlers.ofString()); 104 | var response = new JsonSlurper().parseText(httpResponse.body()); 105 | return response instanceof Map m ? m : null; 106 | } 107 | 108 | private static Map pluginRelease(Map response) { 109 | if( response.containsKey("plugin") ) { 110 | var plugin = (Map) response.get("plugin"); 111 | var releases = (List) plugin.get("releases"); 112 | return releases.get(0); 113 | } 114 | if( response.containsKey("pluginRelease") ) { 115 | return (Map) response.get("pluginRelease"); 116 | } 117 | return null; 118 | } 119 | 120 | private static PluginSpec pluginSpec(Map release) { 121 | var definitions = pluginDefinitions(release); 122 | return new PluginSpec( 123 | ConfigSpecFactory.fromDefinitions(definitions), 124 | ScriptSpecFactory.fromDefinitions(definitions, "Factory"), 125 | ScriptSpecFactory.fromDefinitions(definitions, "Function"), 126 | ScriptSpecFactory.fromDefinitions(definitions, "Operator") 127 | ); 128 | } 129 | 130 | private static List pluginDefinitions(Map release) { 131 | var specJson = (String) release.get("spec"); 132 | if( specJson == null ) 133 | return Collections.emptyList(); 134 | var spec = (Map) new JsonSlurper().parseText(specJson); 135 | return (List) spec.get("definitions"); 136 | } 137 | 138 | /** 139 | * Set the plugin versions currently specified by the config. 140 | * 141 | * @param currentVersions 142 | */ 143 | public void setCurrentVersions(List currentVersions) { 144 | this.currentVersions = currentVersions; 145 | } 146 | 147 | /** 148 | * Get the currently loaded spec for a plugin. 149 | * 150 | * @param name 151 | */ 152 | public PluginSpec getCurrent(String name) { 153 | if( currentVersions == null ) 154 | return null; 155 | var ref = currentVersions.stream() 156 | .filter(r -> r.name().equals(name)) 157 | .findFirst().orElse(null); 158 | if( ref == null ) 159 | return null; 160 | return cache.get(ref); 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/spec/ConfigSpecFactory.java: -------------------------------------------------------------------------------- 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.spec; 17 | 18 | import java.io.IOException; 19 | import java.util.Collections; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.Set; 23 | 24 | import groovy.json.JsonSlurper; 25 | import groovy.lang.Closure; 26 | import nextflow.config.spec.SpecNode; 27 | import nextflow.script.types.Duration; 28 | import nextflow.script.types.MemoryUnit; 29 | import org.codehaus.groovy.runtime.IOGroovyMethods; 30 | 31 | /** 32 | * Load config scopes from core definitions and plugin specs. 33 | * 34 | * @author Ben Sherman 35 | */ 36 | public class ConfigSpecFactory { 37 | 38 | private static Map defaultScopes = null; 39 | 40 | /** 41 | * Load config scopes from core definitions. 42 | */ 43 | public static Map defaultScopes() { 44 | if( defaultScopes == null ) { 45 | var scope = SpecNode.ROOT; 46 | scope.children().putAll(fromCoreDefinitions()); 47 | defaultScopes = scope.children(); 48 | } 49 | return defaultScopes; 50 | } 51 | 52 | private static Map fromCoreDefinitions() { 53 | try { 54 | var classLoader = ConfigSpecFactory.class.getClassLoader(); 55 | var resource = classLoader.getResourceAsStream("spec/definitions.json"); 56 | var text = IOGroovyMethods.getText(resource); 57 | var definitions = (List) new JsonSlurper().parseText(text); 58 | return fromChildren(definitions); 59 | } 60 | catch( IOException e ) { 61 | System.err.println("Failed to read core definitions: " + e.toString()); 62 | return Collections.emptyMap(); 63 | } 64 | } 65 | 66 | /** 67 | * Load config scopes from a plugin spec. 68 | * 69 | * @param definitions 70 | */ 71 | public static Map fromDefinitions(List definitions) { 72 | var entries = definitions.stream() 73 | .filter(node -> "ConfigScope".equals(node.get("type"))) 74 | .map((node) -> { 75 | var spec = (Map) node.get("spec"); 76 | var name = (String) spec.get("name"); 77 | var scope = fromScope(spec); 78 | return Map.entry(name, scope); 79 | }) 80 | .toArray(Map.Entry[]::new); 81 | return Map.ofEntries(entries); 82 | } 83 | 84 | private static Map fromChildren(List children) { 85 | var entries = children.stream() 86 | .map((node) -> { 87 | var spec = (Map) node.get("spec"); 88 | var name = (String) spec.get("name"); 89 | return Map.entry(name, fromNode(node)); 90 | }) 91 | .toArray(Map.Entry[]::new); 92 | return Map.ofEntries(entries); 93 | } 94 | 95 | private static SpecNode fromNode(Map node) { 96 | var type = (String) node.get("type"); 97 | var spec = (Map) node.get("spec"); 98 | 99 | if( "ConfigOption".equals(type) ) 100 | return fromOption(spec); 101 | 102 | if( "ConfigPlaceholderScope".equals(type) ) 103 | return fromPlaceholder(spec); 104 | 105 | if( "ConfigScope".equals(type) ) 106 | return fromScope(spec); 107 | 108 | throw new IllegalStateException(); 109 | } 110 | 111 | private static SpecNode.Option fromOption(Map spec) { 112 | var description = (String) spec.get("description"); 113 | var type = fromType(spec.get("type")); 114 | return new SpecNode.Option(description, type); 115 | } 116 | 117 | private static SpecNode.Placeholder fromPlaceholder(Map spec) { 118 | var description = (String) spec.get("description"); 119 | var placeholderName = (String) spec.get("placeholderName"); 120 | var scope = fromScope((Map) spec.get("scope")); 121 | return new SpecNode.Placeholder(description, placeholderName, scope); 122 | } 123 | 124 | private static SpecNode.Scope fromScope(Map spec) { 125 | var description = (String) spec.get("description"); 126 | var children = fromChildren((List) spec.get("children")); 127 | return new SpecNode.Scope(description, children); 128 | } 129 | 130 | private static final Map STANDARD_TYPES = Map.ofEntries( 131 | Map.entry("Boolean", Boolean.class), 132 | Map.entry("boolean", Boolean.class), 133 | Map.entry("Closure", Closure.class), 134 | Map.entry("Duration", Duration.class), 135 | Map.entry("Float", Float.class), 136 | Map.entry("float", Float.class), 137 | Map.entry("Integer", Integer.class), 138 | Map.entry("int", Integer.class), 139 | Map.entry("List", List.class), 140 | Map.entry("MemoryUnit", MemoryUnit.class), 141 | Map.entry("Set", Set.class), 142 | Map.entry("String", String.class) 143 | ); 144 | 145 | private static Class fromType(Object type) { 146 | if( type instanceof String s ) { 147 | return STANDARD_TYPES.getOrDefault(s, Object.class); 148 | } 149 | if( type instanceof Map m ) { 150 | var name = (String) m.get("name"); 151 | // TODO: type arguments 152 | return STANDARD_TYPES.getOrDefault(name, Object.class); 153 | } 154 | throw new IllegalStateException(); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/nextflow/lsp/services/script/ScriptService.java: -------------------------------------------------------------------------------- 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.List; 19 | 20 | import com.google.gson.JsonPrimitive; 21 | import nextflow.lsp.ast.ASTNodeCache; 22 | import nextflow.lsp.services.CallHierarchyProvider; 23 | import nextflow.lsp.services.CodeLensProvider; 24 | import nextflow.lsp.services.CompletionProvider; 25 | import nextflow.lsp.services.DefinitionProvider; 26 | import nextflow.lsp.services.FormattingProvider; 27 | import nextflow.lsp.services.HoverProvider; 28 | import nextflow.lsp.services.LanguageServerConfiguration; 29 | import nextflow.lsp.services.LanguageService; 30 | import nextflow.lsp.services.LinkProvider; 31 | import nextflow.lsp.services.ReferenceProvider; 32 | import nextflow.lsp.services.RenameProvider; 33 | import nextflow.lsp.services.SemanticTokensProvider; 34 | import nextflow.lsp.services.SymbolProvider; 35 | import nextflow.script.formatter.FormattingOptions; 36 | import nextflow.lsp.spec.PluginSpecCache; 37 | 38 | /** 39 | * Implementation of language services for Nextflow scripts. 40 | * 41 | * @author Ben Sherman 42 | */ 43 | public class ScriptService extends LanguageService { 44 | 45 | private ScriptAstCache astCache; 46 | 47 | public ScriptService(String rootUri) { 48 | super(rootUri); 49 | astCache = new ScriptAstCache(rootUri); 50 | } 51 | 52 | @Override 53 | public boolean matchesFile(String uri) { 54 | return uri.endsWith(".nf"); 55 | } 56 | 57 | public void initialize(LanguageServerConfiguration configuration, PluginSpecCache pluginSpecCache) { 58 | synchronized (this) { 59 | astCache.initialize(configuration, pluginSpecCache); 60 | } 61 | super.initialize(configuration); 62 | } 63 | 64 | @Override 65 | protected ASTNodeCache getAstCache() { 66 | return astCache; 67 | } 68 | 69 | @Override 70 | protected CallHierarchyProvider getCallHierarchyProvider() { 71 | return new ScriptCallHierarchyProvider(astCache); 72 | } 73 | 74 | @Override 75 | protected CodeLensProvider getCodeLensProvider() { 76 | return new ScriptCodeLensProvider(astCache); 77 | } 78 | 79 | @Override 80 | protected CompletionProvider getCompletionProvider(int maxItems, boolean extended) { 81 | return new ScriptCompletionProvider(astCache, maxItems, extended); 82 | } 83 | 84 | @Override 85 | protected DefinitionProvider getDefinitionProvider() { 86 | return new ScriptDefinitionProvider(astCache); 87 | } 88 | 89 | @Override 90 | protected FormattingProvider getFormattingProvider() { 91 | return new ScriptFormattingProvider(astCache); 92 | } 93 | 94 | @Override 95 | protected HoverProvider getHoverProvider() { 96 | return new ScriptHoverProvider(astCache); 97 | } 98 | 99 | @Override 100 | protected LinkProvider getLinkProvider() { 101 | return new ScriptLinkProvider(astCache); 102 | } 103 | 104 | @Override 105 | protected ReferenceProvider getReferenceProvider() { 106 | return new ScriptReferenceProvider(astCache); 107 | } 108 | 109 | @Override 110 | protected RenameProvider getRenameProvider() { 111 | return new ScriptReferenceProvider(astCache); 112 | } 113 | 114 | @Override 115 | protected SemanticTokensProvider getSemanticTokensProvider() { 116 | return new ScriptSemanticTokensProvider(astCache); 117 | } 118 | 119 | @Override 120 | protected SymbolProvider getSymbolProvider() { 121 | return new ScriptSymbolProvider(astCache); 122 | } 123 | 124 | @Override 125 | public Object executeCommand(String command, List arguments, LanguageServerConfiguration configuration) { 126 | updateNow(); 127 | if( "nextflow.server.previewDag".equals(command) && arguments.size() == 2 ) { 128 | var uri = getJsonString(arguments.get(0)); 129 | var name = getJsonString(arguments.get(1)); 130 | var provider = new ScriptCodeLensProvider(astCache); 131 | return provider.previewDag(uri, name, configuration.dagDirection(), configuration.dagVerbose()); 132 | } 133 | if( "nextflow.server.previewWorkspace".equals(command) ) { 134 | var provider = new WorkspacePreviewProvider(astCache); 135 | return provider.preview(); 136 | } 137 | if( "nextflow.server.convertPipelineToTyped".equals(command) ) { 138 | var provider = new ScriptCodeLensProvider(astCache); 139 | var options = formattingOptions(configuration); 140 | return provider.convertPipelineToTyped(options); 141 | } 142 | if( "nextflow.server.convertScriptToTyped".equals(command) ) { 143 | var uri = getJsonString(arguments.get(0)); 144 | var provider = new ScriptCodeLensProvider(astCache); 145 | var options = formattingOptions(configuration); 146 | return provider.convertScriptToTyped(uri, options); 147 | } 148 | return null; 149 | } 150 | 151 | private String getJsonString(Object json) { 152 | return json instanceof JsonPrimitive jp ? jp.getAsString() : null; 153 | } 154 | 155 | private static FormattingOptions formattingOptions(LanguageServerConfiguration configuration) { 156 | return new FormattingOptions( 157 | 4, 158 | true, 159 | configuration.harshilAlignment(), 160 | configuration.maheshForm(), 161 | configuration.sortDeclarations() 162 | ); 163 | } 164 | 165 | } 166 | --------------------------------------------------------------------------------