mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-03 07:13:33 +00:00
Merge branch 'main' into v1.1-develop
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
812b6e066a
18 files changed
+815
-3
No files matched your search
@@ -0,0 +1,4 @@
|
||||
# Force LF line endings for Haxe files across platforms.
|
||||
# The Haxe language server cannot properly parse/index files with CRLF endings, causing it to fail on Windows without this.
|
||||
*.hxml text eol=lf
|
||||
*.hx text eol=lf
|
||||
@@ -386,6 +386,12 @@ jobs:
|
||||
- name: Install ansible-core and ansible-lint (for Ansible language server tests)
|
||||
shell: bash
|
||||
run: uv run pip install ansible-core ansible-lint
|
||||
- name: Install Haxe
|
||||
# The Haxe LS binary (server.js) is auto-downloaded and runs on Node.js,
|
||||
# but it delegates to the Haxe compiler for code analysis at runtime.
|
||||
uses: krdlab/setup-haxe@v2
|
||||
with:
|
||||
haxe-version: 4.3.7
|
||||
- name: Install Elm
|
||||
shell: bash
|
||||
run: npm install -g elm@0.19.1-6
|
||||
|
||||
@@ -10,6 +10,7 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Fix: git commit id in Serena version strings was incorrect
|
||||
|
||||
* Language Servers:
|
||||
- Add support for Haxe via vshaxe/haxe-language-server. Requires Haxe compiler 3.4.0+ and Node.js. Auto-discovered from the vshaxe VSCode extension or configurable via `ls_path` in `ls_specific_settings`.
|
||||
- Add Crystal language support (uses [Crystalline](https://github.com/elbywan/crystalline) language server)
|
||||
- Fix: Reactivation of the same project restarted language servers #1280
|
||||
|
||||
|
||||
@@ -89,7 +89,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
|
||||
AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
|
||||
AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
|
||||
|
||||
### The Serena JetBrains Plugin
|
||||
|
||||
|
||||
@@ -64,7 +64,10 @@ Some languages require additional installations or setup steps, as noted.
|
||||
(requires local groovy-language-server.jar setup via `GROOVY_LS_JAR_PATH` or configuration)
|
||||
* **Haskell**
|
||||
(automatically locates HLS via ghcup, stack, or system PATH; supports Stack and Cabal projects)
|
||||
* **HLSL / GLSL / WGSL**
|
||||
* **Haxe**
|
||||
(requires Haxe compiler 3.4.0+ and Node.js; uses the [vshaxe language server](https://github.com/vshaxe/haxe-language-server);
|
||||
automatically downloaded from Open VSX, or discovered from the vshaxe VSCode extension)
|
||||
* **HLSL / GLSL / WGSL**
|
||||
(uses [shader-language-server](https://github.com/antaalt/shader-sense) (language `hlsl`); automatically downloaded;
|
||||
on macOS, requires Rust toolchain for building from source;
|
||||
note: reference search is not supported by this language server)
|
||||
|
||||
@@ -475,6 +475,33 @@ Supported settings:
|
||||
| `version` | `1.3.0` | Override the bundled version Serena downloads, or builds from source on macOS, when `ls_path` is not set. |
|
||||
|
||||
|
||||
#### Haxe
|
||||
|
||||
Serena uses the [vshaxe/haxe-language-server](https://github.com/vshaxe/haxe-language-server) for Haxe support.
|
||||
Requires Haxe compiler (3.4.0+) and Node.js.
|
||||
|
||||
The server is discovered in order: user-configured `ls_path`, system PATH, vshaxe VSCode extension, auto-download from Open VSX.
|
||||
|
||||
Supported settings:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `ls_path` | auto-discovered | Path to the Haxe language server binary (e.g., `/path/to/server.js`). |
|
||||
| `version` | `2.34.2` | Override the vshaxe extension version downloaded from Open VSX. SHA256 verification is only performed for the default version. |
|
||||
| `buildFile` | auto-discovered `.hxml` | Relative path to the `.hxml` build file used for compilation (e.g., `build/debug.hxml`). If not set, Serena searches the project for `.hxml` files (max depth 5, skipping dependency directories). |
|
||||
| `haxePath` | `haxe` from PATH | Path to the Haxe compiler executable. The LS delegates to this for code analysis. Useful when multiple Haxe versions are installed or when `haxe` is not on the PATH. |
|
||||
| `renameSourceFolders` | not set (LS default) | List of source directories for scoping rename operations (e.g., `["src", "lib"]`). If not set, the Haxe LS uses its own defaults. |
|
||||
|
||||
Example (typically in `project.yml`, since these are project-specific):
|
||||
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
haxe:
|
||||
buildFile: "build/debug.hxml"
|
||||
haxePath: "/usr/local/bin/haxe"
|
||||
renameSourceFolders: ["src", "lib"]
|
||||
```
|
||||
|
||||
#### Java (`eclipse.jdt.ls`)
|
||||
|
||||
The following settings are supported for the Java language server:
|
||||
|
||||
@@ -331,6 +331,7 @@ markers = [
|
||||
"julia: Julia language server tests",
|
||||
"fortran: language server running for Fortran",
|
||||
"haskell: Haskell language server tests",
|
||||
"haxe: Haxe language server tests",
|
||||
"yaml: language server running for YAML",
|
||||
"powershell: language server running for PowerShell",
|
||||
"pascal: language server running for Pascal (Free Pascal/Lazarus)",
|
||||
|
||||
@@ -6,7 +6,8 @@ project_name: "project_name"
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# haxe java julia kotlin lua
|
||||
# markdown
|
||||
# matlab nix pascal perl php
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
"""Haxe language server integration using vshaxe/haxe-language-server."""
|
||||
|
||||
import glob
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import (
|
||||
LanguageServerDependencyProvider,
|
||||
LanguageServerDependencyProviderSinglePath,
|
||||
LSPFileBuffer,
|
||||
SolidLanguageServer,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.ls_types import Hover
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticSeverity, InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HaxeLanguageServer(SolidLanguageServer):
|
||||
"""Haxe language server integration using vshaxe/haxe-language-server.
|
||||
|
||||
Requires Haxe compiler (3.4.0+) and Node.js.
|
||||
"""
|
||||
|
||||
_COMPILATION_TIMEOUT = 60.0
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""Creates a HaxeLanguageServer instance. Use LanguageServer.create() instead."""
|
||||
super().__init__(
|
||||
config,
|
||||
repository_root_path,
|
||||
None,
|
||||
"haxe",
|
||||
solidlsp_settings,
|
||||
)
|
||||
|
||||
self._server_ready = threading.Event()
|
||||
self._server_ready.set()
|
||||
self._active_progress_tokens: set[str] = set()
|
||||
self._progress_lock = threading.Lock()
|
||||
|
||||
@override
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
# Downloaded from Open VSX (not the VS Code Marketplace) because Open VSX
|
||||
# provides stable versioned URLs and SHA256 checksums for integrity verification.
|
||||
_DEFAULT_VSHAXE_VERSION = "2.34.2"
|
||||
_DEFAULT_VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2"
|
||||
|
||||
@override
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
"""Find the Haxe Language Server binary."""
|
||||
# 1. Check for haxe-language-server in PATH
|
||||
system_haxe_ls = shutil.which("haxe-language-server")
|
||||
if system_haxe_ls:
|
||||
log.info(f"Found system-installed haxe-language-server at {system_haxe_ls}")
|
||||
return system_haxe_ls
|
||||
|
||||
# 2. Check VSCode extension locations
|
||||
vscode_server_path = self._find_vscode_extension_server()
|
||||
if vscode_server_path:
|
||||
log.info(f"Found Haxe Language Server in VSCode extension at {vscode_server_path}")
|
||||
return vscode_server_path
|
||||
|
||||
# 3. Check resource dir / download from Open VSX
|
||||
haxe_ls_dir = os.path.join(self._ls_resources_dir, "haxe-lsp")
|
||||
server_js_path = os.path.join(haxe_ls_dir, "bin", "server.js")
|
||||
if os.path.exists(server_js_path):
|
||||
log.info(f"Found Haxe Language Server at {server_js_path}")
|
||||
return server_js_path
|
||||
|
||||
if shutil.which("node") is None:
|
||||
raise FileNotFoundError(
|
||||
"Haxe Language Server not found and Node.js is not installed (required to run it).\n"
|
||||
"Install options:\n"
|
||||
" 1. Install Node.js and re-run (auto-download will proceed)\n"
|
||||
" 2. Install the vshaxe VSCode extension: code --install-extension nadako.vshaxe\n"
|
||||
" 3. Set ls_path in serena_config.yml under ls_specific_settings.haxe"
|
||||
)
|
||||
|
||||
version = self._custom_settings.get("version", self._DEFAULT_VSHAXE_VERSION)
|
||||
downloaded_path = self._download_from_open_vsx(haxe_ls_dir, version)
|
||||
if downloaded_path:
|
||||
return downloaded_path
|
||||
|
||||
raise FileNotFoundError(
|
||||
"Haxe Language Server not found. Install options:\n"
|
||||
" 1. Install the vshaxe VSCode extension: code --install-extension nadako.vshaxe\n"
|
||||
" 2. Set ls_path in serena_config.yml under ls_specific_settings.haxe"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _find_vscode_extension_server() -> str | None:
|
||||
"""Search for the Haxe language server in VSCode extension directories."""
|
||||
search_paths = [
|
||||
os.path.expanduser("~/.vscode/extensions/nadako.vshaxe-*/bin/server.js"),
|
||||
os.path.expanduser("~/.vscode-server/extensions/nadako.vshaxe-*/bin/server.js"),
|
||||
os.path.expanduser("~/.vscode-insiders/extensions/nadako.vshaxe-*/bin/server.js"),
|
||||
]
|
||||
for pattern in search_paths:
|
||||
matches = sorted(glob.glob(pattern), reverse=True) # newest version first
|
||||
for match in matches:
|
||||
if os.path.isfile(match):
|
||||
return match
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _download_from_open_vsx(cls, target_dir: str, version: str) -> str | None:
|
||||
"""Download a vshaxe VSIX from Open VSX and extract server.js.
|
||||
Verifies the download against a hardcoded SHA256 checksum when using the default version.
|
||||
"""
|
||||
try:
|
||||
download_url = f"https://open-vsx.org/api/nadako/vshaxe/{version}/file/nadako.vshaxe-{version}.vsix"
|
||||
log.info("Downloading Haxe Language Server v%s from Open VSX...", version)
|
||||
vsix_path = os.path.join(tempfile.gettempdir(), "vshaxe.vsix")
|
||||
urllib.request.urlretrieve(download_url, vsix_path)
|
||||
|
||||
# Verify SHA256 checksum only for the default (pinned) version
|
||||
if version == cls._DEFAULT_VSHAXE_VERSION:
|
||||
sha256 = hashlib.sha256()
|
||||
with open(vsix_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
sha256.update(chunk)
|
||||
if sha256.hexdigest().lower() != cls._DEFAULT_VSHAXE_SHA256:
|
||||
os.remove(vsix_path)
|
||||
raise RuntimeError(
|
||||
f"SHA256 checksum mismatch for vshaxe VSIX. Expected {cls._DEFAULT_VSHAXE_SHA256}, "
|
||||
f"got {sha256.hexdigest()}. The file may be corrupted or tampered with."
|
||||
)
|
||||
log.info("SHA256 checksum verified")
|
||||
else:
|
||||
log.info("Using custom version %s — skipping SHA256 verification", version)
|
||||
|
||||
# VSIX files are ZIP archives — extract bin/ contents
|
||||
bin_dir = os.path.join(target_dir, "bin")
|
||||
os.makedirs(bin_dir, exist_ok=True)
|
||||
with zipfile.ZipFile(vsix_path, "r") as zf:
|
||||
for entry in zf.namelist():
|
||||
if "/bin/" in entry:
|
||||
filename = entry.split("/bin/", 1)[-1]
|
||||
if filename and ".." not in filename:
|
||||
dest_path = os.path.join(bin_dir, filename)
|
||||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||
with zf.open(entry) as src, open(dest_path, "wb") as dst:
|
||||
dst.write(src.read())
|
||||
|
||||
os.remove(vsix_path)
|
||||
|
||||
server_js_path = os.path.join(bin_dir, "server.js")
|
||||
if os.path.exists(server_js_path):
|
||||
log.info(f"Haxe Language Server v{version} installed to {server_js_path}")
|
||||
return server_js_path
|
||||
|
||||
log.error("Downloaded VSIX but server.js not found after extraction")
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
log.exception("Failed to download Haxe Language Server from Open VSX")
|
||||
return None
|
||||
|
||||
@override
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
if core_path.endswith(".js"):
|
||||
return ["node", core_path]
|
||||
return [core_path, "--stdio"]
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in [
|
||||
"node_modules",
|
||||
"export",
|
||||
"dump",
|
||||
]
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""Return initialize params for the Haxe Language Server.
|
||||
|
||||
displayArguments are resolved from user-configured buildFile or auto-discovered .hxml files.
|
||||
"""
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
|
||||
# 1. Check for user-configured .hxml path
|
||||
configured_build_file = self._custom_settings.get("buildFile")
|
||||
if configured_build_file:
|
||||
log.info(f"Using user-configured Haxe build file: {configured_build_file}")
|
||||
display_arguments = [configured_build_file]
|
||||
else:
|
||||
# 2. Auto-discover .hxml files recursively
|
||||
display_arguments = self._discover_hxml_file(repository_absolute_path)
|
||||
|
||||
init_options: dict = {"displayArguments": display_arguments}
|
||||
rename_source_folders = self._custom_settings.get("renameSourceFolders")
|
||||
if rename_source_folders:
|
||||
init_options["renameSourceFolders"] = rename_source_folders
|
||||
|
||||
haxe_path = self._custom_settings.get("haxePath")
|
||||
if haxe_path:
|
||||
init_options["haxePath"] = haxe_path
|
||||
|
||||
initialize_params = {
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True},
|
||||
"completion": {"completionItem": {"snippetSupport": True}},
|
||||
"definition": {},
|
||||
"references": {},
|
||||
"documentSymbol": {
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
"hover": {"contentFormat": ["markdown", "plaintext"]},
|
||||
"codeAction": {},
|
||||
"rename": {},
|
||||
"signatureHelp": {},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"didChangeConfiguration": {},
|
||||
"symbol": {},
|
||||
},
|
||||
},
|
||||
"initializationOptions": init_options,
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": os.path.basename(repository_absolute_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
return initialize_params # type: ignore[return-value]
|
||||
|
||||
@staticmethod
|
||||
def _discover_hxml_file(repository_absolute_path: str) -> list[str]:
|
||||
"""Return the first .hxml file found, filtering out dependency directories.
|
||||
|
||||
For more control, set ``ls_specific_settings.haxe.buildFile`` in ``serena_config.yml``.
|
||||
"""
|
||||
max_depth = 5
|
||||
skip_dirs = {"node_modules", "haxe_libraries", ".haxelib", "export", "dump", "bin", ".git", "build"}
|
||||
|
||||
for root, dirs, files in os.walk(repository_absolute_path):
|
||||
# Skip dependency/build output directories
|
||||
dirs[:] = [d for d in dirs if d not in skip_dirs]
|
||||
depth = len(pathlib.Path(root).relative_to(repository_absolute_path).parts)
|
||||
if depth > max_depth:
|
||||
dirs.clear()
|
||||
continue
|
||||
for f in files:
|
||||
if f.endswith(".hxml") and "haxe_libraries" not in root:
|
||||
hxml_path = os.path.relpath(os.path.join(root, f), repository_absolute_path)
|
||||
log.info(
|
||||
"Auto-discovered Haxe build file: %s. To use a different file, set "
|
||||
"ls_specific_settings.haxe.buildFile in serena_config.yml.",
|
||||
hxml_path,
|
||||
)
|
||||
return [hxml_path]
|
||||
|
||||
log.info("No .hxml file found in project")
|
||||
return []
|
||||
|
||||
@override
|
||||
def _start_server(self) -> None:
|
||||
"""Start the Haxe Language Server and wait for initial compilation.
|
||||
|
||||
Uses textDocument/publishDiagnostics as the primary compilation-complete signal
|
||||
and $/progress tokens as a secondary signal.
|
||||
"""
|
||||
|
||||
def diagnostics_handler(params: dict) -> None:
|
||||
"""Signal compilation complete when diagnostics arrive.
|
||||
Defers if progress tokens are still active to avoid a race condition.
|
||||
"""
|
||||
uri = params.get("uri", "unknown")
|
||||
diags = params.get("diagnostics", [])
|
||||
errors = [d for d in diags if d.get("severity") == DiagnosticSeverity.Error]
|
||||
if errors:
|
||||
log.warning("Haxe LSP diagnostics for %s: %d errors: %s", uri, len(errors), errors)
|
||||
else:
|
||||
log.info("Haxe LSP diagnostics for %s: clean (%d total)", uri, len(diags))
|
||||
|
||||
with self._progress_lock:
|
||||
if not self._active_progress_tokens:
|
||||
log.info("Haxe LSP: no active progress tokens — signalling compilation complete")
|
||||
self._server_ready.set()
|
||||
else:
|
||||
log.info(
|
||||
"Haxe LSP: diagnostics received but %d progress tokens still active — deferring",
|
||||
len(self._active_progress_tokens),
|
||||
)
|
||||
|
||||
def window_log_message(msg: dict) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
|
||||
def register_capability_handler(params: dict) -> None:
|
||||
# Haxe LS sends this but we don't need dynamic capability registration.
|
||||
return
|
||||
|
||||
def work_done_progress_create(params: dict) -> dict:
|
||||
"""Handle window/workDoneProgress/create — clear compilation event until tokens finish."""
|
||||
token = str(params.get("token", ""))
|
||||
log.debug(f"Haxe LSP workDoneProgress/create: token={token!r}")
|
||||
with self._progress_lock:
|
||||
self._active_progress_tokens.add(token)
|
||||
self._server_ready.clear()
|
||||
return {}
|
||||
|
||||
def progress_handler(params: dict) -> None:
|
||||
"""Track $/progress begin/end to detect when all async compilation work finishes."""
|
||||
token = str(params.get("token", ""))
|
||||
value = params.get("value", {})
|
||||
kind = value.get("kind")
|
||||
if kind == "begin":
|
||||
title = value.get("title", "")
|
||||
log.info(f"Haxe LSP progress [{token}]: started - {title}")
|
||||
with self._progress_lock:
|
||||
self._active_progress_tokens.add(token)
|
||||
self._server_ready.clear()
|
||||
elif kind == "report":
|
||||
pct = value.get("percentage")
|
||||
msg = value.get("message", "")
|
||||
pct_str = f" ({pct}%)" if pct is not None else ""
|
||||
log.debug(f"Haxe LSP progress [{token}]: {msg}{pct_str}")
|
||||
elif kind == "end":
|
||||
msg = value.get("message", "")
|
||||
log.info(f"Haxe LSP progress [{token}]: ended - {msg}")
|
||||
with self._progress_lock:
|
||||
self._active_progress_tokens.discard(token)
|
||||
if not self._active_progress_tokens:
|
||||
self._server_ready.set()
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
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", diagnostics_handler)
|
||||
|
||||
log.info("Starting Haxe server process")
|
||||
self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
|
||||
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
|
||||
self.server.send.initialize(initialize_params)
|
||||
|
||||
self._server_ready.clear()
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# LS doesn't properly initialise without a workspace_did_change_configuration notification here.
|
||||
config_settings: dict = {}
|
||||
haxe_path = self._custom_settings.get("haxePath")
|
||||
if haxe_path:
|
||||
config_settings["haxePath"] = haxe_path
|
||||
self.server.notify.workspace_did_change_configuration({"settings": config_settings})
|
||||
|
||||
log.info("Waiting for Haxe LSP compilation to complete...")
|
||||
if self._server_ready.wait(timeout=self._COMPILATION_TIMEOUT):
|
||||
log.info("Haxe server compilation completed, server ready")
|
||||
else:
|
||||
log.warning(
|
||||
"Haxe LSP did not signal compilation completion within %.0fs — responses may be incomplete",
|
||||
self._COMPILATION_TIMEOUT,
|
||||
)
|
||||
|
||||
@override
|
||||
def request_hover(self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None) -> Hover | None:
|
||||
"""Request hover info, returning None instead of raising on failure.
|
||||
|
||||
The Haxe language server does not provide hover for all symbol types (e.g. class
|
||||
declarations), and may raise errors instead of returning None in those cases.
|
||||
"""
|
||||
try:
|
||||
return super().request_hover(relative_file_path, line, column, file_buffer=file_buffer)
|
||||
except SolidLSPException:
|
||||
log.warning("Hover request failed for %s:%d:%d", relative_file_path, line, column, exc_info=True)
|
||||
return None
|
||||
|
||||
@override
|
||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
||||
return 5
|
||||
@@ -69,6 +69,11 @@ class Language(str, Enum):
|
||||
JULIA = "julia"
|
||||
FORTRAN = "fortran"
|
||||
HASKELL = "haskell"
|
||||
HAXE = "haxe"
|
||||
"""Haxe language server using vshaxe/haxe-language-server.
|
||||
Requires Haxe compiler (3.4.0+) and Node.js.
|
||||
Discovered from system PATH or vshaxe VSCode extension, otherwise downloaded from Open VSX.
|
||||
"""
|
||||
LEAN4 = "lean4"
|
||||
GROOVY = "groovy"
|
||||
VUE = "vue"
|
||||
@@ -273,6 +278,8 @@ class Language(str, Enum):
|
||||
)
|
||||
case self.HASKELL:
|
||||
return FilenameMatcher("*.hs", "*.lhs")
|
||||
case self.HAXE:
|
||||
return FilenameMatcher("*.hx")
|
||||
case self.LEAN4:
|
||||
return FilenameMatcher("*.lean")
|
||||
case self.VUE:
|
||||
@@ -493,6 +500,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.haskell_language_server import HaskellLanguageServer
|
||||
|
||||
return HaskellLanguageServer
|
||||
case self.HAXE:
|
||||
from solidlsp.language_servers.haxe_language_server import HaxeLanguageServer
|
||||
|
||||
return HaxeLanguageServer
|
||||
case self.LEAN4:
|
||||
from solidlsp.language_servers.lean4_language_server import Lean4LanguageServer
|
||||
|
||||
|
||||
@@ -255,6 +255,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = {
|
||||
Language.CSHARP: [pytest.mark.csharp],
|
||||
Language.FSHARP: [pytest.mark.fsharp],
|
||||
Language.GO: [pytest.mark.go],
|
||||
Language.HAXE: [pytest.mark.haxe],
|
||||
Language.JAVA: [pytest.mark.java],
|
||||
Language.KOTLIN: [pytest.mark.kotlin, pytest.mark.skipif(is_ci, reason="Kotlin LSP JVM crashes on restart in CI")],
|
||||
Language.LEAN4: [pytest.mark.lean4, pytest.mark.skipif(_sh.which("lean") is None, reason="Lean is not installed")],
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export/
|
||||
dump/
|
||||
bin/
|
||||
@@ -0,0 +1,3 @@
|
||||
-cp src
|
||||
-main Main
|
||||
--no-output
|
||||
@@ -0,0 +1,40 @@
|
||||
import utils.Helper;
|
||||
|
||||
/**
|
||||
* Main class for testing Haxe language server functionality.
|
||||
*
|
||||
* This class tests:
|
||||
* - Symbol discovery (class, methods, fields)
|
||||
* - Within-file references
|
||||
* - Cross-file references to utils.Helper
|
||||
*/
|
||||
class Main {
|
||||
var message:String;
|
||||
var count:Int;
|
||||
|
||||
public function new() {
|
||||
message = greet("World");
|
||||
count = Helper.addNumbers(5, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Greet someone by name.
|
||||
*/
|
||||
public function greet(name:String):String {
|
||||
return "Hello, " + name + "!";
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and return the formatted result.
|
||||
*/
|
||||
public function calculateResult():String {
|
||||
var sum = Helper.addNumbers(count, 20);
|
||||
var formatted = Helper.formatMessage(message);
|
||||
return formatted + " (sum: " + Std.string(sum) + ")";
|
||||
}
|
||||
|
||||
public static function main() {
|
||||
var app = new Main();
|
||||
Sys.println(app.calculateResult());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package utils;
|
||||
|
||||
/**
|
||||
* Utility functions for the Haxe test application.
|
||||
*
|
||||
* This class provides helper functions used by the Main class.
|
||||
*/
|
||||
class Helper {
|
||||
/**
|
||||
* Format a message by adding brackets around it.
|
||||
*/
|
||||
public static function formatMessage(msg:String):String {
|
||||
return "[ " + msg + " ]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Add two numbers together.
|
||||
*/
|
||||
public static function addNumbers(x:Int, y:Int):Int {
|
||||
return x + y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiply two numbers.
|
||||
*/
|
||||
public static function multiplyNumbers(x:Int, y:Int):Int {
|
||||
return x * y;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ def serena_config():
|
||||
Language.FSHARP,
|
||||
Language.POWERSHELL,
|
||||
Language.CPP_CCLS,
|
||||
Language.HAXE,
|
||||
Language.LEAN4,
|
||||
]:
|
||||
repo_path = get_repo_path(language)
|
||||
@@ -196,6 +197,7 @@ class TestSerenaAgent:
|
||||
pytest.param(Language.CSHARP, "Calculator", "Class", "Program.cs", marks=pytest.mark.csharp),
|
||||
pytest.param(Language.POWERSHELL, "Greet-User", "Function", "main.ps1", marks=pytest.mark.powershell),
|
||||
pytest.param(Language.CPP_CCLS, "add", "Function", "b.cpp", marks=pytest.mark.cpp),
|
||||
pytest.param(Language.HAXE, "Main", "Class", "Main.hx", marks=pytest.mark.haxe),
|
||||
pytest.param(Language.LEAN4, "add", "Method", "Helper.lean", marks=pytest.mark.lean4),
|
||||
],
|
||||
indirect=["serena_agent"],
|
||||
@@ -297,6 +299,13 @@ class TestSerenaAgent:
|
||||
pytest.param(Language.CSHARP, "Calculator", "Program.cs", "Program.cs", marks=pytest.mark.csharp),
|
||||
pytest.param(Language.POWERSHELL, "Greet-User", "main.ps1", "main.ps1", marks=pytest.mark.powershell),
|
||||
pytest.param(Language.CPP_CCLS, "add", "b.cpp", "a.cpp", marks=pytest.mark.cpp),
|
||||
pytest.param(
|
||||
Language.HAXE,
|
||||
"addNumbers",
|
||||
os.path.join("src", "utils", "Helper.hx"),
|
||||
os.path.join("src", "Main.hx"),
|
||||
marks=pytest.mark.haxe,
|
||||
),
|
||||
pytest.param(Language.LEAN4, "add", "Helper.lean", "Main.lean", marks=pytest.mark.lean4),
|
||||
],
|
||||
indirect=["serena_agent"],
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,277 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
|
||||
|
||||
|
||||
@pytest.mark.haxe
|
||||
class TestHaxeLanguageServer:
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_ls_is_running(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that the Haxe language server starts successfully."""
|
||||
assert language_server.is_running()
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "greet"), "greet method not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "calculateResult"), "calculateResult method not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "Helper"), "Helper class not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "addNumbers"), "addNumbers method not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "formatMessage"), "formatMessage method not found in symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that symbol names do not contain unexpected formatting characters."""
|
||||
all_symbols = request_all_symbols(language_server)
|
||||
malformed_symbols = []
|
||||
for s in all_symbols:
|
||||
if has_malformed_name(s):
|
||||
malformed_symbols.append(s)
|
||||
if malformed_symbols:
|
||||
pytest.fail(
|
||||
f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
|
||||
file_path = os.path.join("src", "Main.hx")
|
||||
all_symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
greet_symbol = next((s for s in all_symbols if s.get("name") == "greet"), None)
|
||||
assert greet_symbol is not None, "Could not find 'greet' symbol in Main.hx"
|
||||
sel_start = greet_symbol["selectionRange"]["start"]
|
||||
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
|
||||
|
||||
assert refs, f"Expected non-empty references for greet but got {refs=}"
|
||||
|
||||
# Convert references to a comparable format
|
||||
actual_locations = [
|
||||
{
|
||||
"uri_suffix": os.path.basename(ref.get("relativePath", ref.get("uri", ""))),
|
||||
"line": ref["range"]["start"]["line"],
|
||||
}
|
||||
for ref in refs
|
||||
]
|
||||
|
||||
# greet is called on line 15 (0-indexed) in Main.hx: `message = greet("World");`
|
||||
call_site = {"uri_suffix": "Main.hx", "line": 15}
|
||||
assert call_site in actual_locations, f"Expected reference to greet at line 15 in Main.hx, got {actual_locations}"
|
||||
|
||||
# All references should be within Main.hx (greet is not used in other files)
|
||||
assert all(loc["uri_suffix"] == "Main.hx" for loc in actual_locations), (
|
||||
f"Expected all greet references in Main.hx, got {actual_locations}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
|
||||
# Test addNumbers which is defined in Helper.hx and used in Main.hx
|
||||
helper_path = os.path.join("src", "utils", "Helper.hx")
|
||||
all_symbols, _ = language_server.request_document_symbols(helper_path).get_all_symbols_and_roots()
|
||||
add_numbers_symbol = next((s for s in all_symbols if s.get("name") == "addNumbers"), None)
|
||||
assert add_numbers_symbol is not None, "Could not find 'addNumbers' symbol in Helper.hx"
|
||||
|
||||
sel_start = add_numbers_symbol["selectionRange"]["start"]
|
||||
refs = language_server.request_references(helper_path, sel_start["line"], sel_start["character"])
|
||||
|
||||
assert refs, f"Expected non-empty references for addNumbers but got {refs=}"
|
||||
|
||||
# Convert references to a comparable format
|
||||
actual_locations = [
|
||||
{
|
||||
"uri_suffix": os.path.basename(ref.get("relativePath", ref.get("uri", ""))),
|
||||
"line": ref["range"]["start"]["line"],
|
||||
}
|
||||
for ref in refs
|
||||
]
|
||||
|
||||
# addNumbers is called on line 16 in Main.hx: `count = Helper.addNumbers(5, 10);`
|
||||
call_site_1 = {"uri_suffix": "Main.hx", "line": 16}
|
||||
assert call_site_1 in actual_locations, f"Expected reference to addNumbers at line 16 in Main.hx, got {actual_locations}"
|
||||
|
||||
# addNumbers is called on line 30 in Main.hx: `var sum = Helper.addNumbers(count, 20);`
|
||||
call_site_2 = {"uri_suffix": "Main.hx", "line": 30}
|
||||
assert call_site_2 in actual_locations, f"Expected reference to addNumbers at line 30 in Main.hx, got {actual_locations}"
|
||||
|
||||
# Verify cross-file: at least one reference is in Main.hx (different file from definition)
|
||||
main_refs = [loc for loc in actual_locations if loc["uri_suffix"] == "Main.hx"]
|
||||
assert len(main_refs) >= 2, f"Expected at least 2 references in Main.hx (lines 16 and 30), got {main_refs}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_document_symbols_structure(self, language_server: SolidLanguageServer) -> None:
|
||||
file_path = os.path.join("src", "Main.hx")
|
||||
result = language_server.request_document_symbols(file_path)
|
||||
all_symbols, roots = result.get_all_symbols_and_roots()
|
||||
|
||||
# Main class should be a root symbol
|
||||
main_symbol = None
|
||||
for sym in roots:
|
||||
if sym.get("name") == "Main":
|
||||
main_symbol = sym
|
||||
break
|
||||
assert main_symbol is not None, "Main class not found as root symbol"
|
||||
assert main_symbol.get("kind") in (SymbolKind.Class, SymbolKind.Struct), f"Expected Main to be Class, got {main_symbol.get('kind')}"
|
||||
|
||||
# Check that methods and fields exist and are children of Main
|
||||
child_names = {s.get("name") for s in all_symbols if s.get("name") != "Main"}
|
||||
assert "greet" in child_names, "greet method not found in symbols"
|
||||
assert "calculateResult" in child_names, "calculateResult method not found in symbols"
|
||||
assert "message" in child_names, "message field not found in symbols"
|
||||
assert "count" in child_names, "count field not found in symbols"
|
||||
|
||||
# Verify symbol kinds for specific symbols
|
||||
for sym in all_symbols:
|
||||
if sym.get("name") == "greet":
|
||||
assert sym.get("kind") in (
|
||||
SymbolKind.Method,
|
||||
SymbolKind.Function,
|
||||
), f"Expected greet to be Method/Function, got {sym.get('kind')}"
|
||||
if sym.get("name") == "message":
|
||||
assert sym.get("kind") in (
|
||||
SymbolKind.Field,
|
||||
SymbolKind.Variable,
|
||||
SymbolKind.Property,
|
||||
), f"Expected message to be Field/Variable, got {sym.get('kind')}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_workspace_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
result = language_server.request_workspace_symbol("Helper")
|
||||
assert result is not None, "Workspace symbol search returned None"
|
||||
assert len(result) > 0, "Workspace symbol search returned no results"
|
||||
assert any("Helper" in str(s.get("name", "")) for s in result), f"Expected at least one result containing 'Helper', got {result}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_go_to_definition_within_file(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Go to definition of greet from its call site in Main.hx -- should resolve within the same file."""
|
||||
main_path = os.path.join("src", "Main.hx")
|
||||
# Line 16 (0-indexed: 15): `message = greet("World");`
|
||||
# `greet` starts at character 12 on that line.
|
||||
definitions = language_server.request_definition(main_path, 15, 12)
|
||||
assert definitions, "Expected to find definition for greet"
|
||||
assert any("Main.hx" in d.get("uri", d.get("relativePath", "")) for d in definitions), (
|
||||
f"Expected definition in Main.hx, got {definitions}"
|
||||
)
|
||||
# greet is defined on line 23 (0-indexed: 22) in Main.hx
|
||||
definition_lines = [d["range"]["start"]["line"] for d in definitions if "Main.hx" in d.get("uri", d.get("relativePath", ""))]
|
||||
assert 22 in definition_lines, f"Expected definition of greet at line 22 in Main.hx, got lines {definition_lines}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_go_to_definition(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Go to definition of addNumbers from a call site in Main.hx -- should resolve to Helper.hx."""
|
||||
main_path = os.path.join("src", "Main.hx")
|
||||
# Line 17 (0-indexed: 16): `\t\tcount = Helper.addNumbers(5, 10);`
|
||||
# `addNumbers` starts at character 17 (0-indexed) on that line.
|
||||
definitions = language_server.request_definition(main_path, 16, 17)
|
||||
assert definitions, "Expected to find definition for addNumbers"
|
||||
assert any("Helper.hx" in d.get("uri", d.get("relativePath", "")) for d in definitions), (
|
||||
f"Expected definition in Helper.hx, got {definitions}"
|
||||
)
|
||||
# addNumbers is defined on line 18 (0-indexed) in Helper.hx
|
||||
definition_lines = [d["range"]["start"]["line"] for d in definitions if "Helper.hx" in d.get("uri", d.get("relativePath", ""))]
|
||||
assert 18 in definition_lines, f"Expected definition of addNumbers at line 18 in Helper.hx, got lines {definition_lines}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_hover(self, language_server: SolidLanguageServer) -> None:
|
||||
file_path = os.path.join("src", "Main.hx")
|
||||
result = language_server.request_document_symbols(file_path)
|
||||
all_symbols, _ = result.get_all_symbols_and_roots()
|
||||
greet_symbol = next((s for s in all_symbols if s.get("name") == "greet"), None)
|
||||
assert greet_symbol is not None, "Could not find 'greet' symbol"
|
||||
|
||||
sel_start = greet_symbol["selectionRange"]["start"]
|
||||
hover = language_server.request_hover(file_path, sel_start["line"], sel_start["character"])
|
||||
assert hover is not None, "Hover returned None for greet method"
|
||||
hover_str = str(hover)
|
||||
assert "String" in hover_str or "greet" in hover_str, f"Expected hover to contain type info, got {hover_str}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_hover_on_class_declaration(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Hovering on a class name should return hover info."""
|
||||
file_path = os.path.join("src", "Main.hx")
|
||||
result = language_server.request_document_symbols(file_path)
|
||||
all_symbols, _ = result.get_all_symbols_and_roots()
|
||||
|
||||
main_symbol = next((s for s in all_symbols if s.get("name") == "Main"), None)
|
||||
assert main_symbol is not None
|
||||
|
||||
sel_start = main_symbol["selectionRange"]["start"]
|
||||
hover = language_server.request_hover(file_path, sel_start["line"], sel_start["character"])
|
||||
assert hover is not None, "Hover on class declaration returned None"
|
||||
hover_str = str(hover)
|
||||
assert "Main" in hover_str, f"Expected hover to contain 'Main', got {hover_str}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_rename_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
file_path = os.path.join("src", "Main.hx")
|
||||
result = language_server.request_document_symbols(file_path)
|
||||
all_symbols, _ = result.get_all_symbols_and_roots()
|
||||
greet_symbol = next((s for s in all_symbols if s.get("name") == "greet"), None)
|
||||
assert greet_symbol is not None, "Could not find 'greet' symbol"
|
||||
|
||||
sel_start = greet_symbol["selectionRange"]["start"]
|
||||
edits = language_server.request_rename_symbol_edit(file_path, sel_start["line"], sel_start["character"], "sayHello")
|
||||
assert edits is not None, "Rename returned None"
|
||||
# Verify edits contain changes (WorkspaceEdit has 'changes' or 'documentChanges')
|
||||
edits_str = str(edits)
|
||||
assert "Main.hx" in edits_str, f"Expected rename edits for Main.hx, got {edits}"
|
||||
assert "sayHello" in edits_str, f"Expected new name 'sayHello' in rename edits, got {edits}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_completions(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Request completions after Helper. in calculateResult — should return Helper's static methods."""
|
||||
file_path = os.path.join("src", "Main.hx")
|
||||
# Line 31 (0-indexed: 30): `var sum = Helper.addNumbers(count, 20);`
|
||||
# Trigger completions after `Helper.` — character 19 is right after the dot.
|
||||
completions = language_server.request_completions(file_path, 30, 19)
|
||||
assert completions, "Expected non-empty completions after Helper."
|
||||
completion_texts = [c.get("completionText", c.get("label", "")) for c in completions]
|
||||
assert "addNumbers" in completion_texts, f"Expected 'addNumbers' in completions after Helper., got {completion_texts[:10]}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_document_overview(self, language_server: SolidLanguageServer) -> None:
|
||||
overview = language_server.request_document_overview(os.path.join("src", "Main.hx"))
|
||||
assert overview, "Document overview returned empty list"
|
||||
symbol_names = [s.get("name", "") for s in overview]
|
||||
assert any("Main" in name for name in symbol_names), f"Expected 'Main' in overview, got {symbol_names}"
|
||||
main_entry = next((s for s in overview if s.get("name") == "Main"), None)
|
||||
assert main_entry is not None, f"Expected 'Main' entry in overview, got {symbol_names}"
|
||||
assert main_entry.get("kind") in (SymbolKind.Class, SymbolKind.Struct), (
|
||||
f"Expected Main to be Class/Struct in overview, got kind {main_entry.get('kind')}"
|
||||
)
|
||||
for s in overview:
|
||||
assert s.get("name"), "Symbol missing 'name' field"
|
||||
assert s.get("kind"), "Symbol missing 'kind' field"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_rapid_successive_requests(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Verify that rapid successive requests don't return empty results.
|
||||
|
||||
The Haxe LS triggers a recompilation on didOpen. Without suppression,
|
||||
subsequent requests during recompilation may return empty results.
|
||||
"""
|
||||
main_path = os.path.join("src", "Main.hx")
|
||||
helper_path = os.path.join("src", "utils", "Helper.hx")
|
||||
|
||||
# First request: hover on greet in Main.hx (line 22 is the greet method definition)
|
||||
hover1 = language_server.request_hover(main_path, 22, 20)
|
||||
|
||||
# Second request: references in a different file (triggers another didOpen)
|
||||
all_symbols, _ = language_server.request_document_symbols(helper_path).get_all_symbols_and_roots()
|
||||
add_numbers = next((s for s in all_symbols if s.get("name") == "addNumbers"), None)
|
||||
assert add_numbers is not None
|
||||
sel_start = add_numbers["selectionRange"]["start"]
|
||||
refs = language_server.request_references(helper_path, sel_start["line"], sel_start["character"])
|
||||
|
||||
# Third request: back to Main.hx hover (would be affected by recompilation)
|
||||
hover2 = language_server.request_hover(main_path, 22, 20)
|
||||
|
||||
# All three should return non-empty results
|
||||
assert hover1 is not None, "First hover returned None"
|
||||
assert refs, "References returned empty after switching files"
|
||||
assert hover2 is not None, "Second hover returned None after file switching"
|
||||
Reference in new issue
Block a user