mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-04 16:13:54 +00:00
updated: elixir language server, replaced with expert (#783)
This commit is contained in:
1 parent
fb01cc38f7
commit
2ec96089f6
11 files changed
+210
-200
No files matched your search
@@ -90,8 +90,8 @@ jobs:
|
||||
if: runner.os != 'Windows'
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
elixir-version: "1.18.4"
|
||||
otp-version: "26.1"
|
||||
elixir-version: "1.19.3"
|
||||
otp-version: "28"
|
||||
# Erlang currently not tested in CI, random hangings on macos, always hangs on ubuntu
|
||||
# In local tests, erlang seems to work though
|
||||
# - name: Install Erlang Language Server
|
||||
|
||||
@@ -20,7 +20,7 @@ With Serena, we provide direct, out-of-the-box support for:
|
||||
* **Clojure**
|
||||
* **Dart**
|
||||
* **Elixir**
|
||||
(requires installation of NextLS and Elixir; Windows not supported)
|
||||
(requires Elixir installation; Expert language server is downloaded automatically)
|
||||
* **Elm**
|
||||
(requires Elm compiler)
|
||||
* **Erlang**
|
||||
|
||||
@@ -301,6 +301,7 @@ markers = [
|
||||
"fortran: language server running for Fortran",
|
||||
"haskell: Haskell language server tests",
|
||||
"yaml: language server running for YAML",
|
||||
"slow: tests that require additional Expert instances and have long startup times (~60-90s each)",
|
||||
]
|
||||
|
||||
[tool.codespell]
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
# Elixir Language Server Integration
|
||||
|
||||
This directory contains the integration for Elixir language support using [Next LS](https://github.com/elixir-tools/next-ls) from the elixir-tools project.
|
||||
|
||||
> **⚠️ Windows Not Supported**: Next LS does not provide Windows binaries, so Elixir language server integration is only available on Linux and macOS.
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Next LS v0.23.3 Timeout Enumeration Bug
|
||||
There is a known intermittent bug in Next LS v0.23.3 where `textDocument/definition` requests can fail with:
|
||||
```
|
||||
Protocol.UndefinedError: protocol Enumerable not implemented for :timeout of type Atom
|
||||
```
|
||||
|
||||
This bug is tracked in [Next LS Issue #543](https://github.com/elixir-tools/next-ls/issues/543) and primarily occurs in CI environments. The affected test (`test_request_defining_symbol_none`) is marked as expected to fail until this upstream bug is resolved.
|
||||
This directory contains the integration for Elixir language support using [Expert](https://github.com/elixir-lang/expert), the official Elixir language server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -22,9 +10,10 @@ Before using the Elixir language server integration, you need to have:
|
||||
- Install from: https://elixir-lang.org/install.html
|
||||
- Verify with: `elixir --version`
|
||||
|
||||
2. **Next LS** installed and available in your PATH
|
||||
- Install from: https://github.com/elixir-tools/next-ls#installation
|
||||
- Verify with: `nextls --version`
|
||||
2. **Expert** (optional - will be downloaded automatically if not found)
|
||||
- Expert binaries are automatically downloaded from GitHub releases
|
||||
- Manual installation: https://github.com/elixir-lang/expert#installation
|
||||
- If installed manually, ensure `expert` is in your PATH
|
||||
|
||||
## Features
|
||||
|
||||
@@ -42,13 +31,20 @@ The Elixir integration provides:
|
||||
|
||||
## Configuration
|
||||
|
||||
The integration uses the default Next LS configuration with:
|
||||
The integration uses the default Expert configuration with:
|
||||
|
||||
- **MIX_ENV**: `dev`
|
||||
- **MIX_TARGET**: `host`
|
||||
- **Experimental completions**: Disabled by default
|
||||
- **Credo extension**: Enabled by default
|
||||
|
||||
### Version Management (asdf)
|
||||
|
||||
Expert automatically respects project-specific Elixir versions when using asdf:
|
||||
- If a `.tool-versions` file exists in the project root, Expert will use the specified Elixir version
|
||||
- Expert is launched from the project directory, allowing it to pick up project configuration
|
||||
- No additional configuration needed - just ensure asdf is installed and the project has a `.tool-versions` file
|
||||
|
||||
## Usage
|
||||
|
||||
The Elixir language server is automatically selected when working with Elixir projects. It will be used for:
|
||||
@@ -63,14 +59,14 @@ The Elixir language server is automatically selected when working with Elixir pr
|
||||
|
||||
### Important: Project Compilation
|
||||
|
||||
Next LS requires your Elixir project to be **compiled** for optimal performance, especially for:
|
||||
Expert requires your Elixir project to be **compiled** for optimal performance, especially for:
|
||||
- Cross-file reference resolution
|
||||
- Complete symbol information
|
||||
- Accurate go-to-definition
|
||||
|
||||
**For production use**: Ensure your project is compiled with `mix compile` before using the language server.
|
||||
|
||||
**For testing**: The test suite automatically compiles the test repositories before running tests to ensure optimal Next LS performance.
|
||||
**For testing**: The test suite automatically compiles the test repositories before running tests to ensure optimal Expert performance.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -83,8 +79,9 @@ pytest test/solidlsp/elixir/ -m elixir
|
||||
## Implementation Details
|
||||
|
||||
- **Main class**: `ElixirTools` in `elixir_tools.py`
|
||||
- **Initialization parameters**: Defined in `initialize_params.json`
|
||||
- **Language identifier**: `"elixir"`
|
||||
- **Command**: `nextls --stdio`
|
||||
- **Command**: `expert --stdio`
|
||||
- **Supported platforms**: Linux (x64, arm64), macOS (x64, arm64), Windows (x64, arm64)
|
||||
- **Binary distribution**: Downloaded from [GitHub releases](https://github.com/elixir-lang/expert/releases)
|
||||
|
||||
The implementation follows the same patterns as other language servers in this project, inheriting from `SolidLanguageServer` and providing Elixir-specific configuration and behavior.
|
||||
The implementation follows the same patterns as other language servers in this project, inheriting from `SolidLanguageServer` and providing Elixir-specific configuration and behavior.
|
||||
@@ -11,7 +11,6 @@ from overrides import override
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler import lsp_types
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
@@ -23,12 +22,12 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class ElixirTools(SolidLanguageServer):
|
||||
"""
|
||||
Provides Elixir specific instantiation of the LanguageServer class using Next LS from elixir-tools.
|
||||
Provides Elixir specific instantiation of the LanguageServer class using Expert, the official Elixir language server.
|
||||
"""
|
||||
|
||||
@override
|
||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
||||
return 10.0 # Elixir projects need a lot of time to compile and index before cross-file references work
|
||||
return 10.0 # Elixir projects need time to compile and index before cross-file references work
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
@@ -38,7 +37,8 @@ class ElixirTools(SolidLanguageServer):
|
||||
# - node_modules: if the project has JavaScript components
|
||||
# - .elixir_ls: ElixirLS artifacts (in case both are present)
|
||||
# - cover: coverage reports
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["_build", "deps", "node_modules", ".elixir_ls", "cover"]
|
||||
# - .expert: Expert artifacts
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["_build", "deps", "node_modules", ".elixir_ls", ".expert", "cover"]
|
||||
|
||||
@override
|
||||
def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool:
|
||||
@@ -49,42 +49,6 @@ class ElixirTools(SolidLanguageServer):
|
||||
|
||||
return super().is_ignored_path(relative_path, ignore_unsupported_files)
|
||||
|
||||
@staticmethod
|
||||
def _is_next_ls_internal_file(abs_path: str) -> bool:
|
||||
"""Check if an absolute path is a Next LS internal file that should be ignored."""
|
||||
return any(
|
||||
pattern in abs_path
|
||||
for pattern in [
|
||||
".burrito", # Next LS runtime directory
|
||||
"next_ls_erts-", # Next LS Erlang runtime
|
||||
"_next_ls_private_", # Next LS private files
|
||||
"/priv/monkey/", # Next LS monkey patching directory
|
||||
]
|
||||
)
|
||||
|
||||
@override
|
||||
def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
|
||||
"""Override to filter out Next LS internal files from references."""
|
||||
from solidlsp.ls_utils import PathUtils
|
||||
|
||||
# Get the raw response from the parent implementation
|
||||
raw_response = super()._send_references_request(relative_file_path, line, column)
|
||||
|
||||
if raw_response is None:
|
||||
return None
|
||||
|
||||
# Filter out Next LS internal files
|
||||
filtered_response = []
|
||||
for item in raw_response:
|
||||
if isinstance(item, dict) and "uri" in item:
|
||||
abs_path = PathUtils.uri_to_path(item["uri"])
|
||||
if self._is_next_ls_internal_file(abs_path):
|
||||
log.debug(f"Filtering out Next LS internal file: {abs_path}")
|
||||
continue
|
||||
filtered_response.append(item)
|
||||
|
||||
return filtered_response
|
||||
|
||||
@classmethod
|
||||
def _get_elixir_version(cls) -> str | None:
|
||||
"""Get the installed Elixir version or None if not found."""
|
||||
@@ -99,8 +63,8 @@ class ElixirTools(SolidLanguageServer):
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for Next LS.
|
||||
Downloads the Next LS binary for the current platform and returns the path to the executable.
|
||||
Setup runtime dependencies for Expert.
|
||||
Downloads the Expert binary for the current platform and returns the path to the executable.
|
||||
"""
|
||||
# Check if Elixir is available first
|
||||
elixir_version = cls._get_elixir_version()
|
||||
@@ -111,113 +75,137 @@ class ElixirTools(SolidLanguageServer):
|
||||
|
||||
log.info(f"Found Elixir: {elixir_version}")
|
||||
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
# First, check if expert is already in PATH (user may have installed it manually)
|
||||
import shutil
|
||||
|
||||
# Check for Windows and provide a helpful error message
|
||||
if platform_id.value.startswith("win"):
|
||||
raise RuntimeError(
|
||||
"Windows is not supported by Next LS. The Next LS project does not provide Windows binaries. "
|
||||
"Consider using Windows Subsystem for Linux (WSL) or a virtual machine with Linux/macOS."
|
||||
)
|
||||
expert_in_path = shutil.which("expert")
|
||||
if expert_in_path:
|
||||
log.info(f"Found Expert in PATH: {expert_in_path}")
|
||||
return expert_in_path
|
||||
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
valid_platforms = [
|
||||
PlatformId.LINUX_x64,
|
||||
PlatformId.LINUX_arm64,
|
||||
PlatformId.OSX_x64,
|
||||
PlatformId.OSX_arm64,
|
||||
PlatformId.WIN_x64,
|
||||
PlatformId.WIN_arm64,
|
||||
]
|
||||
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Next LS at the moment"
|
||||
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for Expert at the moment"
|
||||
|
||||
next_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "next-ls")
|
||||
expert_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "expert")
|
||||
|
||||
NEXTLS_VERSION = "v0.23.4"
|
||||
EXPERT_VERSION = "nightly"
|
||||
|
||||
# Define runtime dependencies inline
|
||||
runtime_deps = {
|
||||
PlatformId.LINUX_x64: RuntimeDependency(
|
||||
id="next_ls_linux_amd64",
|
||||
id="expert_linux_amd64",
|
||||
platform_id="linux-x64",
|
||||
url=f"https://github.com/elixir-tools/next-ls/releases/download/{NEXTLS_VERSION}/next_ls_linux_amd64",
|
||||
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_linux_amd64",
|
||||
archive_type="binary",
|
||||
binary_name="next_ls_linux_amd64",
|
||||
extract_path="next_ls",
|
||||
binary_name="expert_linux_amd64",
|
||||
extract_path="expert",
|
||||
),
|
||||
PlatformId.LINUX_arm64: RuntimeDependency(
|
||||
id="next_ls_linux_arm64",
|
||||
id="expert_linux_arm64",
|
||||
platform_id="linux-arm64",
|
||||
url=f"https://github.com/elixir-tools/next-ls/releases/download/{NEXTLS_VERSION}/next_ls_linux_arm64",
|
||||
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_linux_arm64",
|
||||
archive_type="binary",
|
||||
binary_name="next_ls_linux_arm64",
|
||||
extract_path="next_ls",
|
||||
binary_name="expert_linux_arm64",
|
||||
extract_path="expert",
|
||||
),
|
||||
PlatformId.OSX_x64: RuntimeDependency(
|
||||
id="next_ls_darwin_amd64",
|
||||
id="expert_darwin_amd64",
|
||||
platform_id="osx-x64",
|
||||
url=f"https://github.com/elixir-tools/next-ls/releases/download/{NEXTLS_VERSION}/next_ls_darwin_amd64",
|
||||
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_darwin_amd64",
|
||||
archive_type="binary",
|
||||
binary_name="next_ls_darwin_amd64",
|
||||
extract_path="next_ls",
|
||||
binary_name="expert_darwin_amd64",
|
||||
extract_path="expert",
|
||||
),
|
||||
PlatformId.OSX_arm64: RuntimeDependency(
|
||||
id="next_ls_darwin_arm64",
|
||||
id="expert_darwin_arm64",
|
||||
platform_id="osx-arm64",
|
||||
url=f"https://github.com/elixir-tools/next-ls/releases/download/{NEXTLS_VERSION}/next_ls_darwin_arm64",
|
||||
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_darwin_arm64",
|
||||
archive_type="binary",
|
||||
binary_name="next_ls_darwin_arm64",
|
||||
extract_path="next_ls",
|
||||
binary_name="expert_darwin_arm64",
|
||||
extract_path="expert",
|
||||
),
|
||||
PlatformId.WIN_x64: RuntimeDependency(
|
||||
id="expert_windows_amd64",
|
||||
platform_id="win-x64",
|
||||
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_windows_amd64.exe",
|
||||
archive_type="binary",
|
||||
binary_name="expert_windows_amd64.exe",
|
||||
extract_path="expert.exe",
|
||||
),
|
||||
PlatformId.WIN_arm64: RuntimeDependency(
|
||||
id="expert_windows_arm64",
|
||||
platform_id="win-arm64",
|
||||
url=f"https://github.com/elixir-lang/expert/releases/download/{EXPERT_VERSION}/expert_windows_arm64.exe",
|
||||
archive_type="binary",
|
||||
binary_name="expert_windows_arm64.exe",
|
||||
extract_path="expert.exe",
|
||||
),
|
||||
}
|
||||
|
||||
dependency = runtime_deps[platform_id]
|
||||
executable_path = os.path.join(next_ls_dir, "nextls")
|
||||
# On Windows, use .exe extension
|
||||
executable_name = "expert.exe" if platform_id.value.startswith("win") else "expert"
|
||||
executable_path = os.path.join(expert_dir, executable_name)
|
||||
assert dependency.binary_name is not None
|
||||
binary_path = os.path.join(next_ls_dir, dependency.binary_name)
|
||||
binary_path = os.path.join(expert_dir, dependency.binary_name)
|
||||
|
||||
if not os.path.exists(executable_path):
|
||||
log.info(f"Downloading Next LS binary from {dependency.url}")
|
||||
log.info(f"Downloading Expert binary from {dependency.url}")
|
||||
assert dependency.url is not None
|
||||
FileUtils.download_file(dependency.url, binary_path)
|
||||
|
||||
# Make the binary executable on Unix-like systems
|
||||
os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||
if not platform_id.value.startswith("win"):
|
||||
os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||
|
||||
# Create a symlink with the expected name
|
||||
if binary_path != executable_path:
|
||||
# Create a symlink with the expected name on Unix-like systems
|
||||
if binary_path != executable_path and not platform_id.value.startswith("win"):
|
||||
if os.path.exists(executable_path):
|
||||
os.remove(executable_path)
|
||||
os.symlink(os.path.basename(binary_path), executable_path)
|
||||
|
||||
assert os.path.exists(executable_path), f"Next LS executable not found at {executable_path}"
|
||||
assert os.path.exists(executable_path), f"Expert executable not found at {executable_path}"
|
||||
|
||||
log.info(f"Next LS binary ready at: {executable_path}")
|
||||
log.info(f"Expert binary ready at: {executable_path}")
|
||||
return executable_path
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
nextls_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
|
||||
expert_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=f'"{nextls_executable_path}" --stdio', cwd=repository_root_path),
|
||||
ProcessLaunchInfo(cmd=f"{expert_executable_path} --stdio", cwd=repository_root_path),
|
||||
"elixir",
|
||||
solidlsp_settings,
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.request_id = 0
|
||||
|
||||
# Set generous timeout for Next LS which can be slow to initialize and respond
|
||||
self.set_request_timeout(180.0) # 60 seconds for all environments
|
||||
# Set generous timeout for Expert which can be slow to initialize and respond
|
||||
self.set_request_timeout(180.0)
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Next LS Language Server.
|
||||
Returns the initialize params for the Expert Language Server.
|
||||
"""
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
# Ensure the path is absolute
|
||||
abs_path = os.path.abspath(repository_absolute_path)
|
||||
root_uri = pathlib.Path(abs_path).as_uri()
|
||||
initialize_params = {
|
||||
"processId": os.getpid(),
|
||||
"locale": "en",
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootPath": abs_path,
|
||||
"rootUri": root_uri,
|
||||
"initializationOptions": {
|
||||
"mix_env": "dev",
|
||||
@@ -275,89 +263,80 @@ class ElixirTools(SolidLanguageServer):
|
||||
return cast(InitializeParams, initialize_params)
|
||||
|
||||
def _start_server(self) -> None:
|
||||
"""Start Next LS server process"""
|
||||
"""Start Expert server process"""
|
||||
|
||||
def register_capability_handler(params: Any) -> None:
|
||||
log.debug(f"LSP: client/registerCapability: {params}")
|
||||
return
|
||||
|
||||
def window_log_message(msg: Any) -> None:
|
||||
"""Handle window/logMessage notifications from Next LS"""
|
||||
"""Handle window/logMessage notifications from Expert"""
|
||||
message_type = msg.get("type", 4) # 1=Error, 2=Warning, 3=Info, 4=Log
|
||||
message_text = msg.get("message", "")
|
||||
log.info(f"LSP: window/logMessage: {message_text}")
|
||||
|
||||
# Check for the specific Next LS readiness signal
|
||||
# Based on Next LS source: "Runtime for folder #{name} is ready..."
|
||||
if "Runtime for folder" in message_text and "is ready..." in message_text:
|
||||
log.info("Next LS runtime is ready based on official log message")
|
||||
self.server_ready.set()
|
||||
|
||||
def do_nothing(params: Any) -> None:
|
||||
return
|
||||
# Log at appropriate level based on message type
|
||||
if message_type == 1:
|
||||
log.error(f"Expert: {message_text}")
|
||||
elif message_type == 2:
|
||||
log.warning(f"Expert: {message_text}")
|
||||
else:
|
||||
log.debug(f"Expert: {message_text}")
|
||||
|
||||
def check_server_ready(params: Any) -> None:
|
||||
"""
|
||||
Handle $/progress notifications from Next LS.
|
||||
Keep as fallback for error detection, but primary readiness detection
|
||||
is now done via window/logMessage handler.
|
||||
Handle $/progress notifications from Expert.
|
||||
Expert sends progress updates during compilation and indexing.
|
||||
The server is considered ready when project build completes.
|
||||
"""
|
||||
value = params.get("value", {})
|
||||
kind = value.get("kind", "")
|
||||
title = value.get("title", "")
|
||||
|
||||
# Check for initialization completion progress (fallback signal)
|
||||
if value.get("kind") == "end":
|
||||
message = value.get("message", "")
|
||||
if "has initialized!" in message:
|
||||
log.info("Next LS initialization progress completed")
|
||||
# Note: We don't set server_ready here - we wait for the log message
|
||||
if kind == "begin":
|
||||
# Track when building the project starts (not "Building engine")
|
||||
if title.startswith("Building ") and not title.startswith("Building engine"):
|
||||
self._building_project = True
|
||||
elif kind == "end":
|
||||
# Project build completion is the main readiness signal
|
||||
if getattr(self, "_building_project", False):
|
||||
log.debug("Expert project build completed - server is ready")
|
||||
self._building_project = False
|
||||
self.server_ready.set()
|
||||
|
||||
def work_done_progress(params: Any) -> None:
|
||||
"""
|
||||
Handle $/workDoneProgress notifications from Next LS.
|
||||
Keep for completeness but primary readiness detection is via window/logMessage.
|
||||
"""
|
||||
value = params.get("value", {})
|
||||
if value.get("kind") == "end":
|
||||
log.info("Next LS work done progress completed")
|
||||
# Note: We don't set server_ready here - we wait for the log message
|
||||
def work_done_progress_create(params: Any) -> None:
|
||||
"""Handle window/workDoneProgress/create requests from Expert."""
|
||||
return
|
||||
|
||||
def publish_diagnostics(params: Any) -> None:
|
||||
"""Handle textDocument/publishDiagnostics notifications."""
|
||||
return
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", check_server_ready)
|
||||
self.server.on_request("window/workDoneProgress/create", do_nothing)
|
||||
self.server.on_notification("$/workDoneProgress", work_done_progress)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_request("window/workDoneProgress/create", work_done_progress_create)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", publish_diagnostics)
|
||||
|
||||
log.info("Starting Next LS server process")
|
||||
log.debug("Starting Expert 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")
|
||||
log.debug("Sending initialize request to Expert")
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
|
||||
# Verify server capabilities - be more lenient with Next LS
|
||||
log.info(f"Next LS capabilities: {list(init_response['capabilities'].keys())}")
|
||||
|
||||
# Next LS may not provide all capabilities immediately, so we check for basic ones
|
||||
# Verify basic server capabilities
|
||||
assert "textDocumentSync" in init_response["capabilities"], f"Missing textDocumentSync in {init_response['capabilities']}"
|
||||
|
||||
# Some capabilities might be optional or provided later. This is expected, so we log as info
|
||||
if "completionProvider" not in init_response["capabilities"]:
|
||||
log.info("completionProvider not available in initial capabilities")
|
||||
|
||||
if "definitionProvider" not in init_response["capabilities"]:
|
||||
log.info("definitionProvider not available in initial capabilities")
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
|
||||
# Wait for Next LS to send the specific "Runtime for folder X is ready..." log message
|
||||
# This is the authoritative signal that Next LS is truly ready for requests
|
||||
ready_timeout = 180.0
|
||||
log.info(f"Waiting up to {ready_timeout} seconds for Next LS runtime readiness...")
|
||||
|
||||
# Expert needs time to compile the project and build indexes on first run.
|
||||
# This can take 2-3+ minutes for mid-sized codebases.
|
||||
# After the first run, subsequent startups are much faster.
|
||||
ready_timeout = 300.0 # 5 minutes
|
||||
log.debug(f"Waiting up to {ready_timeout}s for Expert to compile and index...")
|
||||
if self.server_ready.wait(timeout=ready_timeout):
|
||||
log.info("Next LS is ready and available for requests")
|
||||
|
||||
log.debug("Expert is ready for requests")
|
||||
else:
|
||||
error_msg = f"Next LS failed to initialize within {ready_timeout} seconds. This may indicate a problem with the Elixir installation, project compilation, or Next LS itself."
|
||||
log.error(error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
log.warning(f"Expert did not signal readiness within {ready_timeout}s. Proceeding with requests anyway.")
|
||||
self.server_ready.set() # Mark as ready anyway to allow requests
|
||||
@@ -227,6 +227,9 @@ class SolidLanguageServer(ABC):
|
||||
if solidlsp_settings is None:
|
||||
solidlsp_settings = SolidLSPSettings()
|
||||
|
||||
# Ensure repository_root_path is absolute to avoid issues with file URIs
|
||||
repository_root_path = os.path.abspath(repository_root_path)
|
||||
|
||||
ls_class = config.code_language.get_ls_class()
|
||||
# For now, we assume that all language server implementations have the same signature of the constructor
|
||||
# (which, unfortunately, differs from the signature of the base class).
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import platform
|
||||
|
||||
|
||||
def _test_nextls_available() -> str:
|
||||
"""Test if Next LS is available and return error reason if not."""
|
||||
# Check if we're on Windows (Next LS doesn't support Windows)
|
||||
if platform.system() == "Windows":
|
||||
return "Next LS does not support Windows"
|
||||
|
||||
def _test_expert_available() -> str:
|
||||
"""Test if Expert is available and return error reason if not."""
|
||||
# Try to import and check Elixir availability
|
||||
try:
|
||||
from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
|
||||
@@ -16,13 +9,13 @@ def _test_nextls_available() -> str:
|
||||
if not elixir_version:
|
||||
return "Elixir is not installed or not in PATH"
|
||||
|
||||
return "" # No error, Next LS should be available
|
||||
return "" # No error, Expert should be available
|
||||
|
||||
except ImportError as e:
|
||||
return f"Failed to import ElixirTools: {e}"
|
||||
except Exception as e:
|
||||
return f"Error checking Next LS availability: {e}"
|
||||
return f"Error checking Expert availability: {e}"
|
||||
|
||||
|
||||
NEXTLS_UNAVAILABLE_REASON = _test_nextls_available()
|
||||
NEXTLS_UNAVAILABLE = bool(NEXTLS_UNAVAILABLE_REASON)
|
||||
EXPERT_UNAVAILABLE_REASON = _test_expert_available()
|
||||
EXPERT_UNAVAILABLE = bool(EXPERT_UNAVAILABLE_REASON)
|
||||
@@ -12,10 +12,10 @@ import pytest
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON
|
||||
from . import EXPERT_UNAVAILABLE, EXPERT_UNAVAILABLE_REASON
|
||||
|
||||
# These marks will be applied to all tests in this module
|
||||
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")]
|
||||
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(EXPERT_UNAVAILABLE, reason=f"Next LS not available: {EXPERT_UNAVAILABLE_REASON}")]
|
||||
|
||||
|
||||
class TestElixirBasic:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
@@ -7,23 +8,38 @@ from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from test.conftest import start_ls_context
|
||||
|
||||
from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON
|
||||
from . import EXPERT_UNAVAILABLE, EXPERT_UNAVAILABLE_REASON
|
||||
|
||||
# These marks will be applied to all tests in this module
|
||||
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")]
|
||||
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(EXPERT_UNAVAILABLE, reason=f"Expert not available: {EXPERT_UNAVAILABLE_REASON}")]
|
||||
|
||||
# Skip slow tests in CI - they require multiple Expert instances which is too slow
|
||||
IN_CI = bool(os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS"))
|
||||
SKIP_SLOW_IN_CI = pytest.mark.skipif(
|
||||
IN_CI,
|
||||
reason="Slow tests skipped in CI - require multiple Expert instances (~60-90s each)",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]:
|
||||
"""Fixture to set up an LS for the elixir test repo with the 'scripts' directory ignored."""
|
||||
"""Fixture to set up an LS for the elixir test repo with the 'scripts' directory ignored.
|
||||
|
||||
Uses session scope to avoid restarting Expert for each test.
|
||||
"""
|
||||
ignored_paths = ["scripts", "ignored_dir"]
|
||||
with start_ls_context(language=Language.ELIXIR, ignored_paths=ignored_paths) as ls:
|
||||
yield ls
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ELIXIR], indirect=True)
|
||||
@pytest.mark.slow
|
||||
@SKIP_SLOW_IN_CI
|
||||
def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
|
||||
"""Tests that request_full_symbol_tree ignores the configured directory."""
|
||||
"""Tests that request_full_symbol_tree ignores the configured directory.
|
||||
|
||||
Note: This test uses a separate Expert instance with custom ignored paths,
|
||||
which adds ~60-90s startup time.
|
||||
"""
|
||||
root = ls_with_ignored_dirs.request_full_symbol_tree()[0]
|
||||
root_children = root["children"]
|
||||
children_names = {child["name"] for child in root_children}
|
||||
@@ -35,9 +51,14 @@ def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
|
||||
assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ELIXIR], indirect=True)
|
||||
@pytest.mark.slow
|
||||
@SKIP_SLOW_IN_CI
|
||||
def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
|
||||
"""Tests that find_references ignores the configured directory."""
|
||||
"""Tests that find_references ignores the configured directory.
|
||||
|
||||
Note: This test uses a separate Expert instance with custom ignored paths,
|
||||
which adds ~60-90s startup time.
|
||||
"""
|
||||
# Location of User struct, which is referenced in scripts and ignored_dir
|
||||
definition_file = "lib/models.ex"
|
||||
|
||||
@@ -60,9 +81,15 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
|
||||
assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored"
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@SKIP_SLOW_IN_CI
|
||||
@pytest.mark.parametrize("repo_path", [Language.ELIXIR], indirect=True)
|
||||
def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
|
||||
"""Tests that refs and symbols with glob patterns are ignored."""
|
||||
"""Tests that refs and symbols with glob patterns are ignored.
|
||||
|
||||
Note: This test uses a separate Expert instance with custom ignored paths,
|
||||
which adds ~60-90s startup time.
|
||||
"""
|
||||
ignored_paths = ["*cripts", "ignored_*"] # codespell:ignore cripts
|
||||
with start_ls_context(language=Language.ELIXIR, repo_path=str(repo_path), ignored_paths=ignored_paths) as ls:
|
||||
|
||||
@@ -114,6 +141,10 @@ def test_default_ignored_directories(language_server: SolidLanguageServer):
|
||||
assert not language_server.is_ignored_dirname("priv"), "priv should not be ignored"
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="Expert 0.1.0 bug: document_symbols may return nil for some files (flaky)",
|
||||
raises=Exception,
|
||||
)
|
||||
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||
def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer):
|
||||
"""Test that symbol tree excludes build and dependency directories."""
|
||||
|
||||
@@ -14,10 +14,10 @@ from serena.project import Project
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON
|
||||
from . import EXPERT_UNAVAILABLE, EXPERT_UNAVAILABLE_REASON
|
||||
|
||||
# These marks will be applied to all tests in this module
|
||||
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")]
|
||||
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(EXPERT_UNAVAILABLE, reason=f"Next LS not available: {EXPERT_UNAVAILABLE_REASON}")]
|
||||
|
||||
|
||||
class TestElixirIntegration:
|
||||
|
||||
@@ -15,15 +15,18 @@ from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
|
||||
from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON
|
||||
from . import EXPERT_UNAVAILABLE, EXPERT_UNAVAILABLE_REASON
|
||||
|
||||
# These marks will be applied to all tests in this module
|
||||
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")]
|
||||
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(EXPERT_UNAVAILABLE, reason=f"Next LS not available: {EXPERT_UNAVAILABLE_REASON}")]
|
||||
|
||||
|
||||
class TestElixirLanguageServerSymbols:
|
||||
"""Test the Elixir language server's symbol-related functionality."""
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="Expert 0.1.0 bug: document_symbols returns nil for some files (FunctionClauseError in XPExpert.EngineApi.document_symbols/2)"
|
||||
)
|
||||
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||
def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_containing_symbol for a function."""
|
||||
@@ -159,6 +162,9 @@ class TestElixirLanguageServerSymbols:
|
||||
pass
|
||||
|
||||
# Tests for request_defining_symbol
|
||||
@pytest.mark.xfail(
|
||||
reason="Expert 0.1.0 bug: definition request crashes (FunctionClauseError in XPExpert.Protocol.Conversions.to_elixir/2)"
|
||||
)
|
||||
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||
def test_request_defining_symbol_function_call(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_defining_symbol for a function call."""
|
||||
@@ -183,6 +189,9 @@ class TestElixirLanguageServerSymbols:
|
||||
if "location" in defining_symbol and "uri" in defining_symbol["location"]:
|
||||
assert "models.ex" in defining_symbol["location"]["uri"]
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="Expert 0.1.0 bug: definition request crashes (FunctionClauseError in XPExpert.Protocol.Conversions.to_elixir/2)"
|
||||
)
|
||||
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||
def test_request_defining_symbol_struct_usage(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_defining_symbol for a struct usage."""
|
||||
@@ -205,10 +214,7 @@ class TestElixirLanguageServerSymbols:
|
||||
assert "User" in defining_symbol.get("name", "")
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="Known intermittent bug in Next LS v0.23.3: Protocol.UndefinedError for :timeout atom. "
|
||||
"Occurs in CI environments but may pass locally. "
|
||||
"See https://github.com/elixir-tools/next-ls/issues/543",
|
||||
strict=False,
|
||||
reason="Expert 0.1.0 bug: definition request crashes (FunctionClauseError in XPExpert.Protocol.Conversions.to_elixir/2)"
|
||||
)
|
||||
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||
def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
|
||||
@@ -271,7 +277,7 @@ class TestElixirLanguageServerSymbols:
|
||||
break
|
||||
|
||||
if lib_dir:
|
||||
# Next LS returns module names instead of file names (e.g., 'services' instead of 'services.ex')
|
||||
# Expert returns module names instead of file names (e.g., 'services' instead of 'services.ex')
|
||||
file_names = [child["name"] for child in lib_dir.get("children", [])]
|
||||
expected_modules = ["models", "services", "examples", "utils", "test_repo"]
|
||||
found_modules = [name for name in expected_modules if name in file_names]
|
||||
@@ -284,7 +290,7 @@ class TestElixirLanguageServerSymbols:
|
||||
|
||||
# Should get an overview of the lib directory
|
||||
assert lib_overview is not None
|
||||
# Next LS returns keys like 'lib/services.ex' instead of just 'lib'
|
||||
# Expert returns keys like 'lib/services.ex' instead of just 'lib'
|
||||
overview_keys = list(lib_overview.keys()) if hasattr(lib_overview, "keys") else []
|
||||
lib_files = [key for key in overview_keys if key.startswith("lib/")]
|
||||
assert len(lib_files) > 0, f"Expected to find lib/ files in overview keys: {overview_keys}"
|
||||
@@ -298,10 +304,10 @@ class TestElixirLanguageServerSymbols:
|
||||
# @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||
# def test_request_document_overview(self, language_server: SolidLanguageServer) -> None:
|
||||
# """Test request_document_overview functionality."""
|
||||
# # COMMENTED OUT: Next LS document overview doesn't contain expected terms
|
||||
# # Next LS return value: [('TestRepo.Models', 2, 0, 0)] - only module info, no detailed content
|
||||
# # COMMENTED OUT: Expert document overview doesn't contain expected terms
|
||||
# # Expert return value: [('TestRepo.Models', 2, 0, 0)] - only module info, no detailed content
|
||||
# # Expected terms like 'user', 'item', 'order', 'struct', 'defmodule' are not present
|
||||
# # This appears to be a limitation of Next LS document overview functionality
|
||||
# # This appears to be a limitation of Expert document overview functionality
|
||||
# #
|
||||
# file_path = os.path.join("lib", "models.ex")
|
||||
# doc_overview = language_server.request_document_overview(file_path)
|
||||
|
||||
Reference in new issue
Block a user