mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-11 03:13:51 +00:00
Add Nextflow language server support (#1815)
The Nextflow language server schedules a debounced (1s) AST update on every didOpen/didChange, and LanguageService.references is the only request that does not await it -- documentSymbol, codeLens, documentLink and semanticTokensFull all do. A references request issued inside that window races the recompile of the file it asks about and can return an empty list, which is what the macOS CI runner hit. Send a documentSymbol request for the same file first, which blocks server-side until the pending update has been applied. Also compare reference paths using the platform separator in the tests, since relativePath is OS-native and the hardcoded forward slashes failed on Windows.
This commit is contained in:
1 parent
27d7d0e0ec
commit
76b94f3274
17 files changed
+529
-9
No files matched your search
@@ -66,7 +66,7 @@ jobs:
|
||||
# test can fall through to "no batch". To add/rebalance: edit a MARKERS_* var, and (for a new
|
||||
# group) add a case branch below + the batch name to the matrix list.
|
||||
env:
|
||||
MARKERS_JVM: "java or csharp or fsharp or kotlin or scala or groovy or clojure or bsl"
|
||||
MARKERS_JVM: "java or csharp or fsharp or kotlin or scala or groovy or clojure or bsl or nextflow"
|
||||
MARKERS_NATIVE: "cpp or rust or go or zig or pascal or swift" # swift is macOS-only (skips elsewhere via the guard)
|
||||
# other-langs: grab-bag + scripting languages + haskell + qml. Haskell (GHC/HLS) and qml (qmlls) are
|
||||
# installed only on Linux -- too slow/expensive to build on the macOS runner (Haskell), or not worth
|
||||
|
||||
@@ -40,6 +40,9 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
* Language Servers:
|
||||
- Allow language server priorities to be configured in `serena_config.yml` (for auto-detection during
|
||||
project creation)
|
||||
- **Add support for Nextflow** (language server `nextflow`), using the official
|
||||
[Nextflow language server](https://github.com/nextflow-io/language-server); the JAR is downloaded
|
||||
automatically, a Java 17+ runtime is required
|
||||
- Add `python_basedpyright` as an alternative Python language server
|
||||
- Java/JDTLS: stop downloading and loading the unused IntelliCode completion-ranking bundle; the retired
|
||||
`intellicode_version`, `intellicode_xmx` and `intellicode_xms` settings remain accepted but are ignored #1821
|
||||
|
||||
@@ -110,7 +110,7 @@ Serena incorporates a powerful abstraction layer for the integration of language
|
||||
The underlying language servers are typically open-source projects or at least freely available for use.
|
||||
|
||||
When using Serena's language server backend, we provide **support for over 40 programming languages**, including
|
||||
Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Deno, Elixir, Elm, Erlang, Fortran, F#, GDScript, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, LaTeX, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Pascal, Perl, PHP, PowerShell, Python, QML, R, Rego, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, SystemVerilog, Terraform, TOML, TypeScript, Vue, WGSL, YAML, and Zig.
|
||||
Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Deno, Elixir, Elm, Erlang, Fortran, F#, GDScript, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, LaTeX, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nextflow, Nix, OCaml, Pascal, Perl, PHP, PowerShell, Python, QML, R, Rego, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, SystemVerilog, Terraform, TOML, TypeScript, Vue, WGSL, YAML, and Zig.
|
||||
|
||||
### The Serena JetBrains Plugin
|
||||
|
||||
|
||||
@@ -115,6 +115,11 @@ Some languages require additional installations or setup steps, as noted.
|
||||
* **mSL** (mIRC Scripting Language)
|
||||
(auto-installed; no external dependencies required — uses a custom pygls-based LSP server shipped with Serena;
|
||||
supports document symbols, workspace symbols, references, and go-to-definition for aliases, events, menus, dialogs, and CTCP handlers in `.mrc` files)
|
||||
* **Nextflow**
|
||||
(uses the official [Nextflow language server](https://github.com/nextflow-io/language-server), which is automatically
|
||||
downloaded; requires a Java 17+ runtime, discovered via `ls_specific_settings.nextflow.java_home`, `JAVA_HOME` or `java` on PATH;
|
||||
covers `.nf` scripts — Nextflow `.config` files are not treated as source files, since the language server reports no symbols for them;
|
||||
processes, workflows and functions are reported under their declared name, e.g. `GREET` for `process GREET`)
|
||||
* **Nix**
|
||||
(requires nixd installation)
|
||||
* **OCaml**
|
||||
|
||||
@@ -345,6 +345,7 @@ markers = [
|
||||
"zig: language server running for Zig",
|
||||
"lua: language server running for Lua",
|
||||
"luau: language server running for Luau",
|
||||
"nextflow: language server running for Nextflow",
|
||||
"nix: language server running for Nix",
|
||||
"dart: language server running for Dart",
|
||||
"erlang: language server running for Erlang",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from sensai.util import logging
|
||||
|
||||
from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS
|
||||
from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer
|
||||
from solidlsp.language_servers.kotlin_language_server import KotlinLanguageServer
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.configure()
|
||||
EclipseJDTLS.DependencyProvider.update_dep_hashes()
|
||||
NextflowLanguageServer.DependencyProvider.update_dep_hashes()
|
||||
KotlinLanguageServer.DependencyProvider.update_dep_hashes()
|
||||
@@ -323,6 +323,7 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook):
|
||||
".lua",
|
||||
".m",
|
||||
".matlab",
|
||||
".nf",
|
||||
".php",
|
||||
".proto",
|
||||
".ps1",
|
||||
|
||||
@@ -9,13 +9,14 @@ project_name: "project_name"
|
||||
# gdscript go groovy haskell haxe
|
||||
# hlsl html java json julia
|
||||
# kotlin latex lean4 lua luau
|
||||
# markdown matlab msl nix ocaml
|
||||
# pascal perl php php_phpactor php_phpantom
|
||||
# powershell python python_basedpyright python_jedi python_pyrefly
|
||||
# python_ty qml r rego ruby
|
||||
# ruby_solargraph rust scala scss solidity
|
||||
# svelte swift systemverilog terraform toml
|
||||
# typescript typescript_vts vue yaml zig
|
||||
# markdown matlab msl nextflow nix
|
||||
# ocaml pascal perl php php_phpactor
|
||||
# php_phpantom powershell python python_basedpyright python_jedi
|
||||
# python_pyrefly python_ty qml r rego
|
||||
# ruby ruby_solargraph rust scala scss
|
||||
# solidity svelte swift systemverilog terraform
|
||||
# toml typescript typescript_vts vue yaml
|
||||
# zig
|
||||
# (This list may be outdated; generated with scripts/print_language_list.py;
|
||||
# For the current list, see values of the LanguageServerId enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
Provides Nextflow specific instantiation of the LanguageServer class, using the official
|
||||
Nextflow language server (https://github.com/nextflow-io/language-server), which is
|
||||
distributed as a fat JAR and requires a Java runtime.
|
||||
|
||||
You can configure the following options in ls_specific_settings (in serena_config.yml):
|
||||
|
||||
ls_specific_settings:
|
||||
nextflow:
|
||||
ls_path: '/path/to/language-server-all.jar' # use an existing JAR instead of downloading one
|
||||
nextflow_ls_version: '26.04.3' # JAR version to download (default: bundled version)
|
||||
java_home: '/path/to/jdk' # JDK to run the JAR with (default: JAVA_HOME or PATH)
|
||||
jvm_options: ['-Xmx2G'] # JVM options for the language server process
|
||||
exclude_patterns: ['work', '.nextflow'] # workspace paths the language server shall ignore
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
from collections.abc import Hashable
|
||||
from pathlib import Path
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.dependency_provider import DownloadedDependency, DownloadedDependencyHashDatabase
|
||||
from solidlsp.ls import (
|
||||
LanguageServerDependencyProvider,
|
||||
LanguageServerDependencyProviderSinglePath,
|
||||
RawDocumentSymbol,
|
||||
SolidLanguageServer,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.lsp_protocol_handler import lsp_types
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
NEXTFLOW_LS_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
|
||||
|
||||
# NOTE: after bumping this, re-run scripts/update_downloaded_dependency_hashes.py and commit the
|
||||
# updated src/solidlsp/resources/downloaded_dependency_hashes.json.
|
||||
DEFAULT_NEXTFLOW_LS_VERSION = "26.04.3"
|
||||
|
||||
NEXTFLOW_LS_JAR_NAME = "language-server-all.jar"
|
||||
|
||||
#: the Nextflow language server is built for Java 17+
|
||||
MIN_JDK_VERSION = 17
|
||||
|
||||
#: paths that hold Nextflow's own outputs rather than pipeline sources; scanning them is pure overhead
|
||||
DEFAULT_EXCLUDE_PATTERNS = ["work", ".nextflow"]
|
||||
|
||||
#: the server scans the workspace only when a configuration change asks it to, and it compares the new
|
||||
#: configuration against its own defaults (see LanguageServerConfiguration.defaults() upstream, where the
|
||||
#: error reporting mode is "warnings"), so the settings we push must differ in at least one of the keys
|
||||
#: that trigger a scan
|
||||
_SCAN_TRIGGERING_ERROR_REPORTING_MODE = "errors"
|
||||
|
||||
#: upper bound for the initial workspace scan, which compiles every .nf file in the project
|
||||
_WORKSPACE_SCAN_TIMEOUT = 180.0
|
||||
|
||||
#: declaration keywords the server prepends to symbol names (see ScriptSymbolProvider.getSymbolName upstream)
|
||||
_SYMBOL_NAME_PREFIXES = frozenset(("process", "workflow", "function", "record", "enum"))
|
||||
|
||||
|
||||
class NextflowLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Nextflow specific instantiation of the LanguageServer class.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""
|
||||
Creates a Nextflow Language Server instance. This class is not meant to be instantiated directly.
|
||||
Use LanguageServer.create() instead.
|
||||
"""
|
||||
super().__init__(
|
||||
config,
|
||||
repository_root_path,
|
||||
None,
|
||||
"nextflow",
|
||||
solidlsp_settings,
|
||||
)
|
||||
|
||||
# The workspace scan is asynchronous and reported via $/progress under the "initialize" token;
|
||||
# requests answered before it ends see an empty AST cache.
|
||||
self._scan_complete = threading.Event()
|
||||
self._active_progress_tokens: set[str] = set()
|
||||
self._progress_lock = threading.Lock()
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
@classmethod
|
||||
def _create_dep_language_server(cls, version: str | None = None) -> DownloadedDependency:
|
||||
version = version or DEFAULT_NEXTFLOW_LS_VERSION
|
||||
return DownloadedDependency(
|
||||
url=f"https://github.com/nextflow-io/language-server/releases/download/v{version}/{NEXTFLOW_LS_JAR_NAME}",
|
||||
allowed_hosts=NEXTFLOW_LS_ALLOWED_HOSTS,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def update_dep_hashes(cls) -> None:
|
||||
deps = [cls._create_dep_language_server()]
|
||||
with DownloadedDependencyHashDatabase.get_instance().update_context() as db:
|
||||
for dep in deps:
|
||||
db.update(dep)
|
||||
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
version = self._custom_settings.get("nextflow_ls_version", DEFAULT_NEXTFLOW_LS_VERSION)
|
||||
install_dir = os.path.join(self._ls_resources_dir, f"nextflow_language_server-{version}")
|
||||
jar_path = os.path.join(install_dir, NEXTFLOW_LS_JAR_NAME)
|
||||
|
||||
if not os.path.exists(jar_path):
|
||||
os.makedirs(install_dir, exist_ok=True)
|
||||
log.info("Downloading Nextflow Language Server %s ...", version)
|
||||
self._create_dep_language_server(version).download_to(jar_path)
|
||||
|
||||
if not os.path.exists(jar_path):
|
||||
raise FileNotFoundError(f"Nextflow Language Server JAR not found at {jar_path}")
|
||||
return jar_path
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
java_exe = self._resolve_java()
|
||||
jvm_options = self._custom_settings.get("jvm_options", [])
|
||||
if not isinstance(jvm_options, list):
|
||||
log.warning("The 'jvm_options' setting should be a list of strings. Ignoring the provided value: %s", jvm_options)
|
||||
jvm_options = []
|
||||
return [java_exe, *jvm_options, "-jar", core_path]
|
||||
|
||||
def _resolve_java(self) -> str:
|
||||
"""
|
||||
Locates a ``java`` executable (``java_home`` setting -> ``JAVA_HOME`` -> PATH) and verifies
|
||||
that it is new enough to run the language server.
|
||||
|
||||
:return: the path to the java executable
|
||||
"""
|
||||
java_exe_name = "java.exe" if os.name == "nt" else "java"
|
||||
|
||||
java_exe: str | None = None
|
||||
if explicit_home := self._custom_settings.get("java_home"):
|
||||
candidate = str(Path(explicit_home) / "bin" / java_exe_name)
|
||||
if not os.path.exists(candidate):
|
||||
raise SolidLSPException(
|
||||
f"java_home='{explicit_home}' is invalid: '{candidate}' does not exist. "
|
||||
f"Set ls_specific_settings.nextflow.java_home to a JDK home that contains bin/{java_exe_name}."
|
||||
)
|
||||
java_exe = candidate
|
||||
elif env_home := os.environ.get("JAVA_HOME"):
|
||||
candidate = str(Path(env_home) / "bin" / java_exe_name)
|
||||
if os.path.exists(candidate):
|
||||
java_exe = candidate
|
||||
else:
|
||||
log.warning("JAVA_HOME='%s' invalid (no '%s'), falling back to PATH.", env_home, candidate)
|
||||
if java_exe is None:
|
||||
java_exe = shutil.which("java")
|
||||
if java_exe is None:
|
||||
raise SolidLSPException(
|
||||
"Could not locate a Java installation for the Nextflow language server. "
|
||||
"Set ls_specific_settings.nextflow.java_home, set the JAVA_HOME environment variable, "
|
||||
f"or ensure 'java' is on PATH. Required: JDK {MIN_JDK_VERSION}+."
|
||||
)
|
||||
|
||||
# reuse JDTLS's JVM interrogation, which reports the real java.home even for stub launchers
|
||||
from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS
|
||||
|
||||
java_home, major_version = EclipseJDTLS.DependencyProvider._inspect_java(java_exe)
|
||||
if major_version < MIN_JDK_VERSION:
|
||||
raise SolidLSPException(
|
||||
f"The Nextflow language server requires JDK {MIN_JDK_VERSION}+ but '{java_exe}' is JDK {major_version} "
|
||||
f"(java.home={java_home}). Install a newer JDK and update ls_specific_settings.nextflow.java_home "
|
||||
"or JAVA_HOME."
|
||||
)
|
||||
log.info("Using JDK %d at %s for the Nextflow language server", major_version, java_exe)
|
||||
return java_exe
|
||||
|
||||
def _create_base_initialize_params(self) -> dict:
|
||||
"""
|
||||
Returns the initialize params for the Nextflow Language Server.
|
||||
"""
|
||||
return {
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"dynamicRegistration": True, "didSave": True},
|
||||
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": False}},
|
||||
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
||||
"definition": {"dynamicRegistration": True, "linkSupport": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
},
|
||||
"documentLink": {"dynamicRegistration": True},
|
||||
"callHierarchy": {"dynamicRegistration": True},
|
||||
"rename": {"dynamicRegistration": True, "prepareSupport": True},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"symbol": {"dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}},
|
||||
},
|
||||
"window": {"workDoneProgress": True},
|
||||
},
|
||||
}
|
||||
|
||||
def _create_configuration_settings(self) -> dict:
|
||||
"""
|
||||
Returns the settings pushed via workspace/didChangeConfiguration, which is what makes the server
|
||||
scan the workspace (see ``_start_server``).
|
||||
"""
|
||||
exclude_patterns = self._custom_settings.get("exclude_patterns", DEFAULT_EXCLUDE_PATTERNS)
|
||||
if not isinstance(exclude_patterns, list):
|
||||
log.warning("The 'exclude_patterns' setting should be a list of strings. Ignoring the provided value: %s", exclude_patterns)
|
||||
exclude_patterns = DEFAULT_EXCLUDE_PATTERNS
|
||||
return {
|
||||
"nextflow": {
|
||||
"debug": False,
|
||||
# deliberately not the server's default of "warnings": the value must differ from the
|
||||
# defaults for the configuration change to trigger the workspace scan
|
||||
"errorReportingMode": _SCAN_TRIGGERING_ERROR_REPORTING_MODE,
|
||||
"files": {"exclude": exclude_patterns},
|
||||
}
|
||||
}
|
||||
|
||||
def _start_server(self) -> None:
|
||||
"""
|
||||
Starts the Nextflow Language Server.
|
||||
"""
|
||||
|
||||
def do_nothing(params: dict) -> None:
|
||||
return
|
||||
|
||||
def window_log_message(msg: dict) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
|
||||
def work_done_progress_create(params: dict) -> dict:
|
||||
token = str(params.get("token", ""))
|
||||
with self._progress_lock:
|
||||
self._active_progress_tokens.add(token)
|
||||
self._scan_complete.clear()
|
||||
return {}
|
||||
|
||||
def progress_handler(params: dict) -> None:
|
||||
token = str(params.get("token", ""))
|
||||
value = params.get("value", {})
|
||||
kind = value.get("kind")
|
||||
if kind == "begin":
|
||||
with self._progress_lock:
|
||||
self._active_progress_tokens.add(token)
|
||||
self._scan_complete.clear()
|
||||
log.info("Nextflow LS progress [%s]: started - %s", token, value.get("title", ""))
|
||||
elif kind == "report":
|
||||
log.debug("Nextflow LS progress [%s]: %s", token, value.get("message", ""))
|
||||
elif kind == "end":
|
||||
with self._progress_lock:
|
||||
self._active_progress_tokens.discard(token)
|
||||
if not self._active_progress_tokens:
|
||||
self._scan_complete.set()
|
||||
log.info("Nextflow LS progress [%s]: ended", token)
|
||||
|
||||
self.server.on_request("client/registerCapability", do_nothing)
|
||||
self.server.on_request("window/workDoneProgress/create", work_done_progress_create)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", progress_handler)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
|
||||
log.info("Starting Nextflow server process")
|
||||
self.server.start()
|
||||
|
||||
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
|
||||
init_response = self.server.send.initialize(self._create_initialize_params())
|
||||
|
||||
capabilities = init_response["capabilities"]
|
||||
assert "textDocumentSync" in capabilities, "Server must support textDocumentSync"
|
||||
assert "hoverProvider" in capabilities, "Server must support hover"
|
||||
assert "completionProvider" in capabilities, "Server must support code completion"
|
||||
assert "definitionProvider" in capabilities, "Server must support go to definition"
|
||||
assert "referencesProvider" in capabilities, "Server must support find references"
|
||||
assert "documentSymbolProvider" in capabilities, "Server must support document symbols"
|
||||
assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols"
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# The server does not index anything on `initialized`; it (re-)scans the workspace only when a
|
||||
# configuration change arrives whose error reporting mode, exclude patterns or plugin registry
|
||||
# URL differ from what it currently holds. Without this notification, every symbol request
|
||||
# answers with an empty result.
|
||||
self._scan_complete.clear()
|
||||
self.server.notify.workspace_did_change_configuration({"settings": self._create_configuration_settings()})
|
||||
|
||||
log.info("Waiting for the Nextflow workspace scan to complete...")
|
||||
if self._scan_complete.wait(timeout=_WORKSPACE_SCAN_TIMEOUT):
|
||||
log.info("Nextflow LS ready")
|
||||
else:
|
||||
log.warning("Nextflow LS did not signal scan completion within %.0fs; proceeding anyway", _WORKSPACE_SCAN_TIMEOUT)
|
||||
|
||||
@override
|
||||
def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
|
||||
"""
|
||||
Flushes the server's pending recompile before asking for references.
|
||||
|
||||
Every ``didOpen``/``didChange`` schedules a debounced (1s) update of the file's AST, and
|
||||
``LanguageService.references`` is the one request that does not await it (``documentSymbol``,
|
||||
``codeLens``, ``documentLink`` and ``semanticTokensFull`` all do). A references request issued
|
||||
within the debounce window therefore races the recompile of the very file it asks about and can
|
||||
come back empty. Sending a ``documentSymbol`` request for the same file first is a cheap way to
|
||||
block until the update has been applied.
|
||||
"""
|
||||
self.server.send.document_symbol({"textDocument": {"uri": self._resolve_file_uri(relative_file_path)}})
|
||||
return super()._send_references_request(relative_file_path, line, column)
|
||||
|
||||
@override
|
||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
||||
"""Small safety buffer; the actual synchronisation happens in _send_references_request."""
|
||||
return 1.0
|
||||
|
||||
@override
|
||||
def _document_symbols_cache_fingerprint(self) -> Hashable:
|
||||
normalize_symbol_name_version = 1
|
||||
return normalize_symbol_name_version
|
||||
|
||||
@override
|
||||
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
|
||||
"""
|
||||
Strips the declaration keyword the language server prefixes to every symbol name
|
||||
("process GREET", "workflow SAY_HELLO", "function foo"), so that symbols can be addressed
|
||||
by the name they are written with in the source. The implicit entry workflow keeps its
|
||||
"<entry>" placeholder name, which is what the server calls it.
|
||||
"""
|
||||
name = symbol["name"]
|
||||
prefix, sep, rest = name.partition(" ")
|
||||
if sep and prefix in _SYMBOL_NAME_PREFIXES:
|
||||
return rest
|
||||
return name
|
||||
@@ -185,6 +185,12 @@ class LanguageServerId(str, Enum):
|
||||
Supports .qml files. Requires Qt 6 installation providing qmlls on PATH.
|
||||
See https://doc.qt.io/qt-6/qtqml-tool-qmlls.html
|
||||
"""
|
||||
NEXTFLOW = "nextflow"
|
||||
"""Nextflow language server (https://github.com/nextflow-io/language-server), the one that backs the
|
||||
official VS Code extension. Supports .nf scripts (Nextflow .config files are parsed by the server, but
|
||||
it reports no symbols for them, so they are not treated as source files here).
|
||||
Automatically downloads the language server JAR; requires Java 17+ (JAVA_HOME or 'java' on PATH).
|
||||
"""
|
||||
# Experimental or deprecated Language Servers
|
||||
TYPESCRIPT_VTS = "typescript_vts"
|
||||
"""Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls"""
|
||||
@@ -581,6 +587,10 @@ class LanguageServerId(str, Enum):
|
||||
return FilenameMatcher(".gd", ".gdscript")
|
||||
case self.QML:
|
||||
return FilenameMatcher(".qml")
|
||||
case self.NEXTFLOW:
|
||||
# only scripts: the language server does have a service for .config files, but it provides
|
||||
# no symbols for them, so treating them as source files would only pollute the symbol index
|
||||
return FilenameMatcher(".nf")
|
||||
case self.HTML:
|
||||
return FilenameMatcher(".html", ".htm")
|
||||
case self.SCSS:
|
||||
@@ -876,6 +886,10 @@ class LanguageServerId(str, Enum):
|
||||
from solidlsp.language_servers.qml_language_server import QmlLanguageServer
|
||||
|
||||
return QmlLanguageServer
|
||||
case self.NEXTFLOW:
|
||||
from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer
|
||||
|
||||
return NextflowLanguageServer
|
||||
case self.HTML:
|
||||
from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-arm64-1.54.0-923.vsix": "e2bb22c427d90da8dbb1afff72ff1e2dce38d50b76deb02d7bc313a330a1330c",
|
||||
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-x64-1.54.0-923.vsix": "9d4b15da54e25a0192f9bac073f086c015397d3676623b68dbf83a5dbaf5132b",
|
||||
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-win32-x64-1.54.0-923.vsix": "66f3914987edeccfee8a2558470e0fde4f8c4154232ff4baa5d73373ebc819d4",
|
||||
"https://github.com/nextflow-io/language-server/releases/download/v26.04.3/language-server-all.jar": "20cfa34f6e202d6b8babd8d786202ce00e0d39b70ccec3290e2ab3fbd02bc016",
|
||||
"https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-win-x64.zip": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad",
|
||||
"https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-linux-x64.zip": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec",
|
||||
"https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-linux-aarch64.zip": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
work/
|
||||
.nextflow/
|
||||
.nextflow.log*
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env nextflow
|
||||
|
||||
include { GREET; SHOUT } from './modules/greet/main.nf'
|
||||
include { buildGreeting } from './modules/util/main.nf'
|
||||
|
||||
params.names = ['Ada', 'Grace']
|
||||
|
||||
workflow SAY_HELLO {
|
||||
take:
|
||||
names
|
||||
|
||||
main:
|
||||
greetings = GREET(names)
|
||||
shouted = SHOUT(greetings)
|
||||
|
||||
emit:
|
||||
shouted
|
||||
}
|
||||
|
||||
workflow {
|
||||
log.info buildGreeting('World')
|
||||
SAY_HELLO(Channel.fromList(params.names))
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
process GREET {
|
||||
tag "$name"
|
||||
|
||||
input:
|
||||
val name
|
||||
|
||||
output:
|
||||
path "greeting.txt"
|
||||
|
||||
script:
|
||||
"""
|
||||
echo "Hello, ${name}!" > greeting.txt
|
||||
"""
|
||||
}
|
||||
|
||||
process SHOUT {
|
||||
input:
|
||||
path greeting
|
||||
|
||||
output:
|
||||
path "shout.txt"
|
||||
|
||||
script:
|
||||
"""
|
||||
tr '[:lower:]' '[:upper:]' < ${greeting} > shout.txt
|
||||
"""
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
def normalizeName(String name) {
|
||||
return name.trim().toLowerCase()
|
||||
}
|
||||
|
||||
def buildGreeting(String name) {
|
||||
return "Hello, " + normalizeName(name)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
manifest {
|
||||
name = 'serena-nextflow-test-repo'
|
||||
description = 'Minimal Nextflow pipeline used by the solidlsp Nextflow language server tests'
|
||||
}
|
||||
|
||||
process {
|
||||
cpus = 1
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("java") is None, reason="Nextflow language server requires a Java runtime")
|
||||
|
||||
|
||||
@pytest.mark.nextflow
|
||||
class TestNextflowLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [LanguageServerId.NEXTFLOW], indirect=True)
|
||||
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""Language server starts and attaches to the test repository."""
|
||||
assert language_server.is_running()
|
||||
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
|
||||
|
||||
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
|
||||
def test_document_symbols_workflows(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Workflows declared in a script are reported, with the ``workflow`` keyword stripped."""
|
||||
all_symbols, _ = language_server.request_document_symbols("main.nf").get_all_symbols_and_roots()
|
||||
names = [s["name"] for s in all_symbols]
|
||||
assert "SAY_HELLO" in names, f"SAY_HELLO not found in main.nf symbols. Found: {names}"
|
||||
# the implicit entry workflow has no name of its own; the language server calls it "<entry>"
|
||||
assert "<entry>" in names, f"entry workflow not found in main.nf symbols. Found: {names}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
|
||||
def test_document_symbols_processes_and_functions(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Processes and functions are reported without their declaration keyword."""
|
||||
process_symbols, _ = language_server.request_document_symbols("modules/greet/main.nf").get_all_symbols_and_roots()
|
||||
process_names = [s["name"] for s in process_symbols]
|
||||
assert "GREET" in process_names, f"GREET not found in modules/greet/main.nf symbols. Found: {process_names}"
|
||||
assert "SHOUT" in process_names, f"SHOUT not found in modules/greet/main.nf symbols. Found: {process_names}"
|
||||
|
||||
function_symbols, _ = language_server.request_document_symbols("modules/util/main.nf").get_all_symbols_and_roots()
|
||||
function_names = [s["name"] for s in function_symbols]
|
||||
assert "normalizeName" in function_names, f"normalizeName not found in modules/util/main.nf symbols. Found: {function_names}"
|
||||
assert "buildGreeting" in function_names, f"buildGreeting not found in modules/util/main.nf symbols. Found: {function_names}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
|
||||
def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None:
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
for name in ("SAY_HELLO", "GREET", "SHOUT", "normalizeName", "buildGreeting"):
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, name), f"{name} not found in symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
|
||||
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""``normalizeName`` is defined and called within modules/util/main.nf only."""
|
||||
util_path = os.path.join("modules", "util", "main.nf")
|
||||
symbol = self._get_symbol(language_server, util_path, "normalizeName")
|
||||
start = symbol["selectionRange"]["start"]
|
||||
|
||||
refs = language_server.request_references(util_path, start["line"], start["character"])
|
||||
ref_lines = self._reference_lines(refs)
|
||||
# modules/util/main.nf, 0-indexed line 5: return "Hello, " + normalizeName(name)
|
||||
assert (util_path, 5) in ref_lines, f"call to normalizeName inside buildGreeting not found. Found: {ref_lines}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
|
||||
def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
|
||||
"""The GREET process is defined in a module and used by the workflow in main.nf."""
|
||||
module_path = os.path.join("modules", "greet", "main.nf")
|
||||
symbol = self._get_symbol(language_server, module_path, "GREET")
|
||||
start = symbol["selectionRange"]["start"]
|
||||
|
||||
refs = language_server.request_references(module_path, start["line"], start["character"])
|
||||
ref_lines = self._reference_lines(refs)
|
||||
# main.nf, 0-indexed line 2: include { GREET; SHOUT } from './modules/greet/main.nf'
|
||||
assert ("main.nf", 2) in ref_lines, f"include of GREET in main.nf not found. Found: {ref_lines}"
|
||||
# main.nf, 0-indexed line 12: greetings = GREET(names)
|
||||
assert ("main.nf", 12) in ref_lines, f"invocation of GREET in main.nf not found. Found: {ref_lines}"
|
||||
|
||||
@staticmethod
|
||||
def _reference_lines(references: list[dict]) -> set[tuple[str, int]]:
|
||||
"""(relative path, 0-indexed start line) per reference; paths use the platform separator."""
|
||||
return {(ref["relativePath"], ref["range"]["start"]["line"]) for ref in references}
|
||||
|
||||
@staticmethod
|
||||
def _get_symbol(language_server: SolidLanguageServer, relative_path: str, name: str) -> dict:
|
||||
all_symbols, _ = language_server.request_document_symbols(relative_path).get_all_symbols_and_roots()
|
||||
matches = [s for s in all_symbols if s["name"] == name]
|
||||
assert matches, f"{name} not found in {relative_path}. Found: {[s['name'] for s in all_symbols]}"
|
||||
return matches[0]
|
||||
Reference in new issue
Block a user