mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 10:19:02 +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
@@ -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