mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-20 00:15:41 +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'
|
if: runner.os != 'Windows'
|
||||||
uses: erlef/setup-beam@v1
|
uses: erlef/setup-beam@v1
|
||||||
with:
|
with:
|
||||||
elixir-version: "1.18.4"
|
elixir-version: "1.19.3"
|
||||||
otp-version: "26.1"
|
otp-version: "28"
|
||||||
# Erlang currently not tested in CI, random hangings on macos, always hangs on ubuntu
|
# Erlang currently not tested in CI, random hangings on macos, always hangs on ubuntu
|
||||||
# In local tests, erlang seems to work though
|
# In local tests, erlang seems to work though
|
||||||
# - name: Install Erlang Language Server
|
# - name: Install Erlang Language Server
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ With Serena, we provide direct, out-of-the-box support for:
|
|||||||
* **Clojure**
|
* **Clojure**
|
||||||
* **Dart**
|
* **Dart**
|
||||||
* **Elixir**
|
* **Elixir**
|
||||||
(requires installation of NextLS and Elixir; Windows not supported)
|
(requires Elixir installation; Expert language server is downloaded automatically)
|
||||||
* **Elm**
|
* **Elm**
|
||||||
(requires Elm compiler)
|
(requires Elm compiler)
|
||||||
* **Erlang**
|
* **Erlang**
|
||||||
|
|||||||
@@ -301,6 +301,7 @@ markers = [
|
|||||||
"fortran: language server running for Fortran",
|
"fortran: language server running for Fortran",
|
||||||
"haskell: Haskell language server tests",
|
"haskell: Haskell language server tests",
|
||||||
"yaml: language server running for YAML",
|
"yaml: language server running for YAML",
|
||||||
|
"slow: tests that require additional Expert instances and have long startup times (~60-90s each)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.codespell]
|
[tool.codespell]
|
||||||
|
|||||||
@@ -1,18 +1,6 @@
|
|||||||
# Elixir Language Server Integration
|
# 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.
|
This directory contains the integration for Elixir language support using [Expert](https://github.com/elixir-lang/expert), the official Elixir language server.
|
||||||
|
|
||||||
> **⚠️ 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.
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -22,9 +10,10 @@ Before using the Elixir language server integration, you need to have:
|
|||||||
- Install from: https://elixir-lang.org/install.html
|
- Install from: https://elixir-lang.org/install.html
|
||||||
- Verify with: `elixir --version`
|
- Verify with: `elixir --version`
|
||||||
|
|
||||||
2. **Next LS** installed and available in your PATH
|
2. **Expert** (optional - will be downloaded automatically if not found)
|
||||||
- Install from: https://github.com/elixir-tools/next-ls#installation
|
- Expert binaries are automatically downloaded from GitHub releases
|
||||||
- Verify with: `nextls --version`
|
- Manual installation: https://github.com/elixir-lang/expert#installation
|
||||||
|
- If installed manually, ensure `expert` is in your PATH
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -42,13 +31,20 @@ The Elixir integration provides:
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The integration uses the default Next LS configuration with:
|
The integration uses the default Expert configuration with:
|
||||||
|
|
||||||
- **MIX_ENV**: `dev`
|
- **MIX_ENV**: `dev`
|
||||||
- **MIX_TARGET**: `host`
|
- **MIX_TARGET**: `host`
|
||||||
- **Experimental completions**: Disabled by default
|
- **Experimental completions**: Disabled by default
|
||||||
- **Credo extension**: Enabled 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
|
## Usage
|
||||||
|
|
||||||
The Elixir language server is automatically selected when working with Elixir projects. It will be used for:
|
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
|
### 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
|
- Cross-file reference resolution
|
||||||
- Complete symbol information
|
- Complete symbol information
|
||||||
- Accurate go-to-definition
|
- Accurate go-to-definition
|
||||||
|
|
||||||
**For production use**: Ensure your project is compiled with `mix compile` before using the language server.
|
**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
|
## Testing
|
||||||
|
|
||||||
@@ -83,8 +79,9 @@ pytest test/solidlsp/elixir/ -m elixir
|
|||||||
## Implementation Details
|
## Implementation Details
|
||||||
|
|
||||||
- **Main class**: `ElixirTools` in `elixir_tools.py`
|
- **Main class**: `ElixirTools` in `elixir_tools.py`
|
||||||
- **Initialization parameters**: Defined in `initialize_params.json`
|
|
||||||
- **Language identifier**: `"elixir"`
|
- **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 import SolidLanguageServer
|
||||||
from solidlsp.ls_config import LanguageServerConfig
|
from solidlsp.ls_config import LanguageServerConfig
|
||||||
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
|
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.lsp_types import InitializeParams
|
||||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||||
from solidlsp.settings import SolidLSPSettings
|
from solidlsp.settings import SolidLSPSettings
|
||||||
@@ -23,12 +22,12 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class ElixirTools(SolidLanguageServer):
|
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
|
@override
|
||||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
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
|
@override
|
||||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||||
@@ -38,7 +37,8 @@ class ElixirTools(SolidLanguageServer):
|
|||||||
# - node_modules: if the project has JavaScript components
|
# - node_modules: if the project has JavaScript components
|
||||||
# - .elixir_ls: ElixirLS artifacts (in case both are present)
|
# - .elixir_ls: ElixirLS artifacts (in case both are present)
|
||||||
# - cover: coverage reports
|
# - 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
|
@override
|
||||||
def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool:
|
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)
|
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
|
@classmethod
|
||||||
def _get_elixir_version(cls) -> str | None:
|
def _get_elixir_version(cls) -> str | None:
|
||||||
"""Get the installed Elixir version or None if not found."""
|
"""Get the installed Elixir version or None if not found."""
|
||||||
@@ -99,8 +63,8 @@ class ElixirTools(SolidLanguageServer):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
|
def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> str:
|
||||||
"""
|
"""
|
||||||
Setup runtime dependencies for Next LS.
|
Setup runtime dependencies for Expert.
|
||||||
Downloads the Next LS binary for the current platform and returns the path to the executable.
|
Downloads the Expert binary for the current platform and returns the path to the executable.
|
||||||
"""
|
"""
|
||||||
# Check if Elixir is available first
|
# Check if Elixir is available first
|
||||||
elixir_version = cls._get_elixir_version()
|
elixir_version = cls._get_elixir_version()
|
||||||
@@ -111,113 +75,137 @@ class ElixirTools(SolidLanguageServer):
|
|||||||
|
|
||||||
log.info(f"Found Elixir: {elixir_version}")
|
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
|
expert_in_path = shutil.which("expert")
|
||||||
if platform_id.value.startswith("win"):
|
if expert_in_path:
|
||||||
raise RuntimeError(
|
log.info(f"Found Expert in PATH: {expert_in_path}")
|
||||||
"Windows is not supported by Next LS. The Next LS project does not provide Windows binaries. "
|
return expert_in_path
|
||||||
"Consider using Windows Subsystem for Linux (WSL) or a virtual machine with Linux/macOS."
|
|
||||||
)
|
platform_id = PlatformUtils.get_platform_id()
|
||||||
|
|
||||||
valid_platforms = [
|
valid_platforms = [
|
||||||
PlatformId.LINUX_x64,
|
PlatformId.LINUX_x64,
|
||||||
PlatformId.LINUX_arm64,
|
PlatformId.LINUX_arm64,
|
||||||
PlatformId.OSX_x64,
|
PlatformId.OSX_x64,
|
||||||
PlatformId.OSX_arm64,
|
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
|
# Define runtime dependencies inline
|
||||||
runtime_deps = {
|
runtime_deps = {
|
||||||
PlatformId.LINUX_x64: RuntimeDependency(
|
PlatformId.LINUX_x64: RuntimeDependency(
|
||||||
id="next_ls_linux_amd64",
|
id="expert_linux_amd64",
|
||||||
platform_id="linux-x64",
|
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",
|
archive_type="binary",
|
||||||
binary_name="next_ls_linux_amd64",
|
binary_name="expert_linux_amd64",
|
||||||
extract_path="next_ls",
|
extract_path="expert",
|
||||||
),
|
),
|
||||||
PlatformId.LINUX_arm64: RuntimeDependency(
|
PlatformId.LINUX_arm64: RuntimeDependency(
|
||||||
id="next_ls_linux_arm64",
|
id="expert_linux_arm64",
|
||||||
platform_id="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",
|
archive_type="binary",
|
||||||
binary_name="next_ls_linux_arm64",
|
binary_name="expert_linux_arm64",
|
||||||
extract_path="next_ls",
|
extract_path="expert",
|
||||||
),
|
),
|
||||||
PlatformId.OSX_x64: RuntimeDependency(
|
PlatformId.OSX_x64: RuntimeDependency(
|
||||||
id="next_ls_darwin_amd64",
|
id="expert_darwin_amd64",
|
||||||
platform_id="osx-x64",
|
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",
|
archive_type="binary",
|
||||||
binary_name="next_ls_darwin_amd64",
|
binary_name="expert_darwin_amd64",
|
||||||
extract_path="next_ls",
|
extract_path="expert",
|
||||||
),
|
),
|
||||||
PlatformId.OSX_arm64: RuntimeDependency(
|
PlatformId.OSX_arm64: RuntimeDependency(
|
||||||
id="next_ls_darwin_arm64",
|
id="expert_darwin_arm64",
|
||||||
platform_id="osx-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",
|
archive_type="binary",
|
||||||
binary_name="next_ls_darwin_arm64",
|
binary_name="expert_darwin_arm64",
|
||||||
extract_path="next_ls",
|
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]
|
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
|
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):
|
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
|
assert dependency.url is not None
|
||||||
FileUtils.download_file(dependency.url, binary_path)
|
FileUtils.download_file(dependency.url, binary_path)
|
||||||
|
|
||||||
# Make the binary executable on Unix-like systems
|
# 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
|
# Create a symlink with the expected name on Unix-like systems
|
||||||
if binary_path != executable_path:
|
if binary_path != executable_path and not platform_id.value.startswith("win"):
|
||||||
if os.path.exists(executable_path):
|
if os.path.exists(executable_path):
|
||||||
os.remove(executable_path)
|
os.remove(executable_path)
|
||||||
os.symlink(os.path.basename(binary_path), 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
|
return executable_path
|
||||||
|
|
||||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
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__(
|
super().__init__(
|
||||||
config,
|
config,
|
||||||
repository_root_path,
|
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",
|
"elixir",
|
||||||
solidlsp_settings,
|
solidlsp_settings,
|
||||||
)
|
)
|
||||||
self.server_ready = threading.Event()
|
self.server_ready = threading.Event()
|
||||||
self.request_id = 0
|
self.request_id = 0
|
||||||
|
|
||||||
# Set generous timeout for Next LS which can be slow to initialize and respond
|
# Set generous timeout for Expert which can be slow to initialize and respond
|
||||||
self.set_request_timeout(180.0) # 60 seconds for all environments
|
self.set_request_timeout(180.0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
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 = {
|
initialize_params = {
|
||||||
"processId": os.getpid(),
|
"processId": os.getpid(),
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"rootPath": repository_absolute_path,
|
"rootPath": abs_path,
|
||||||
"rootUri": root_uri,
|
"rootUri": root_uri,
|
||||||
"initializationOptions": {
|
"initializationOptions": {
|
||||||
"mix_env": "dev",
|
"mix_env": "dev",
|
||||||
@@ -275,89 +263,80 @@ class ElixirTools(SolidLanguageServer):
|
|||||||
return cast(InitializeParams, initialize_params)
|
return cast(InitializeParams, initialize_params)
|
||||||
|
|
||||||
def _start_server(self) -> None:
|
def _start_server(self) -> None:
|
||||||
"""Start Next LS server process"""
|
"""Start Expert server process"""
|
||||||
|
|
||||||
def register_capability_handler(params: Any) -> None:
|
def register_capability_handler(params: Any) -> None:
|
||||||
|
log.debug(f"LSP: client/registerCapability: {params}")
|
||||||
return
|
return
|
||||||
|
|
||||||
def window_log_message(msg: Any) -> None:
|
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", "")
|
message_text = msg.get("message", "")
|
||||||
log.info(f"LSP: window/logMessage: {message_text}")
|
|
||||||
|
|
||||||
# Check for the specific Next LS readiness signal
|
# Log at appropriate level based on message type
|
||||||
# Based on Next LS source: "Runtime for folder #{name} is ready..."
|
if message_type == 1:
|
||||||
if "Runtime for folder" in message_text and "is ready..." in message_text:
|
log.error(f"Expert: {message_text}")
|
||||||
log.info("Next LS runtime is ready based on official log message")
|
elif message_type == 2:
|
||||||
self.server_ready.set()
|
log.warning(f"Expert: {message_text}")
|
||||||
|
else:
|
||||||
def do_nothing(params: Any) -> None:
|
log.debug(f"Expert: {message_text}")
|
||||||
return
|
|
||||||
|
|
||||||
def check_server_ready(params: Any) -> None:
|
def check_server_ready(params: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Handle $/progress notifications from Next LS.
|
Handle $/progress notifications from Expert.
|
||||||
Keep as fallback for error detection, but primary readiness detection
|
Expert sends progress updates during compilation and indexing.
|
||||||
is now done via window/logMessage handler.
|
The server is considered ready when project build completes.
|
||||||
"""
|
"""
|
||||||
value = params.get("value", {})
|
value = params.get("value", {})
|
||||||
|
kind = value.get("kind", "")
|
||||||
|
title = value.get("title", "")
|
||||||
|
|
||||||
# Check for initialization completion progress (fallback signal)
|
if kind == "begin":
|
||||||
if value.get("kind") == "end":
|
# Track when building the project starts (not "Building engine")
|
||||||
message = value.get("message", "")
|
if title.startswith("Building ") and not title.startswith("Building engine"):
|
||||||
if "has initialized!" in message:
|
self._building_project = True
|
||||||
log.info("Next LS initialization progress completed")
|
elif kind == "end":
|
||||||
# Note: We don't set server_ready here - we wait for the log message
|
# 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:
|
def work_done_progress_create(params: Any) -> None:
|
||||||
"""
|
"""Handle window/workDoneProgress/create requests from Expert."""
|
||||||
Handle $/workDoneProgress notifications from Next LS.
|
return
|
||||||
Keep for completeness but primary readiness detection is via window/logMessage.
|
|
||||||
"""
|
def publish_diagnostics(params: Any) -> None:
|
||||||
value = params.get("value", {})
|
"""Handle textDocument/publishDiagnostics notifications."""
|
||||||
if value.get("kind") == "end":
|
return
|
||||||
log.info("Next LS work done progress completed")
|
|
||||||
# Note: We don't set server_ready here - we wait for the log message
|
|
||||||
|
|
||||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||||
self.server.on_notification("window/logMessage", window_log_message)
|
self.server.on_notification("window/logMessage", window_log_message)
|
||||||
self.server.on_notification("$/progress", check_server_ready)
|
self.server.on_notification("$/progress", check_server_ready)
|
||||||
self.server.on_request("window/workDoneProgress/create", do_nothing)
|
self.server.on_request("window/workDoneProgress/create", work_done_progress_create)
|
||||||
self.server.on_notification("$/workDoneProgress", work_done_progress)
|
self.server.on_notification("textDocument/publishDiagnostics", publish_diagnostics)
|
||||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
|
||||||
|
|
||||||
log.info("Starting Next LS server process")
|
log.debug("Starting Expert server process")
|
||||||
self.server.start()
|
self.server.start()
|
||||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
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)
|
init_response = self.server.send.initialize(initialize_params)
|
||||||
|
|
||||||
# Verify server capabilities - be more lenient with Next LS
|
# Verify basic server capabilities
|
||||||
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
|
|
||||||
assert "textDocumentSync" in init_response["capabilities"], f"Missing textDocumentSync in {init_response['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.server.notify.initialized({})
|
||||||
self.completions_available.set()
|
self.completions_available.set()
|
||||||
|
|
||||||
# Wait for Next LS to send the specific "Runtime for folder X is ready..." log message
|
# Expert needs time to compile the project and build indexes on first run.
|
||||||
# This is the authoritative signal that Next LS is truly ready for requests
|
# This can take 2-3+ minutes for mid-sized codebases.
|
||||||
ready_timeout = 180.0
|
# After the first run, subsequent startups are much faster.
|
||||||
log.info(f"Waiting up to {ready_timeout} seconds for Next LS runtime readiness...")
|
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):
|
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:
|
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.warning(f"Expert did not signal readiness within {ready_timeout}s. Proceeding with requests anyway.")
|
||||||
log.error(error_msg)
|
self.server_ready.set() # Mark as ready anyway to allow requests
|
||||||
raise RuntimeError(error_msg)
|
|
||||||
@@ -227,6 +227,9 @@ class SolidLanguageServer(ABC):
|
|||||||
if solidlsp_settings is None:
|
if solidlsp_settings is None:
|
||||||
solidlsp_settings = SolidLSPSettings()
|
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()
|
ls_class = config.code_language.get_ls_class()
|
||||||
# For now, we assume that all language server implementations have the same signature of the constructor
|
# 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).
|
# (which, unfortunately, differs from the signature of the base class).
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import platform
|
def _test_expert_available() -> str:
|
||||||
|
"""Test if Expert is available and return error reason if not."""
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
# Try to import and check Elixir availability
|
# Try to import and check Elixir availability
|
||||||
try:
|
try:
|
||||||
from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
|
from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
|
||||||
@@ -16,13 +9,13 @@ def _test_nextls_available() -> str:
|
|||||||
if not elixir_version:
|
if not elixir_version:
|
||||||
return "Elixir is not installed or not in PATH"
|
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:
|
except ImportError as e:
|
||||||
return f"Failed to import ElixirTools: {e}"
|
return f"Failed to import ElixirTools: {e}"
|
||||||
except Exception as 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()
|
EXPERT_UNAVAILABLE_REASON = _test_expert_available()
|
||||||
NEXTLS_UNAVAILABLE = bool(NEXTLS_UNAVAILABLE_REASON)
|
EXPERT_UNAVAILABLE = bool(EXPERT_UNAVAILABLE_REASON)
|
||||||
@@ -12,10 +12,10 @@ import pytest
|
|||||||
from solidlsp import SolidLanguageServer
|
from solidlsp import SolidLanguageServer
|
||||||
from solidlsp.ls_config import Language
|
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
|
# 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:
|
class TestElixirBasic:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -7,23 +8,38 @@ from solidlsp import SolidLanguageServer
|
|||||||
from solidlsp.ls_config import Language
|
from solidlsp.ls_config import Language
|
||||||
from test.conftest import start_ls_context
|
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
|
# 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]:
|
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"]
|
ignored_paths = ["scripts", "ignored_dir"]
|
||||||
with start_ls_context(language=Language.ELIXIR, ignored_paths=ignored_paths) as ls:
|
with start_ls_context(language=Language.ELIXIR, ignored_paths=ignored_paths) as ls:
|
||||||
yield 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):
|
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 = ls_with_ignored_dirs.request_full_symbol_tree()[0]
|
||||||
root_children = root["children"]
|
root_children = root["children"]
|
||||||
children_names = {child["name"] for child in 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}"
|
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):
|
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
|
# Location of User struct, which is referenced in scripts and ignored_dir
|
||||||
definition_file = "lib/models.ex"
|
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"
|
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)
|
@pytest.mark.parametrize("repo_path", [Language.ELIXIR], indirect=True)
|
||||||
def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
|
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
|
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:
|
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"
|
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)
|
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||||
def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer):
|
def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer):
|
||||||
"""Test that symbol tree excludes build and dependency directories."""
|
"""Test that symbol tree excludes build and dependency directories."""
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ from serena.project import Project
|
|||||||
from solidlsp import SolidLanguageServer
|
from solidlsp import SolidLanguageServer
|
||||||
from solidlsp.ls_config import Language
|
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
|
# 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:
|
class TestElixirIntegration:
|
||||||
|
|||||||
@@ -15,15 +15,18 @@ from solidlsp import SolidLanguageServer
|
|||||||
from solidlsp.ls_config import Language
|
from solidlsp.ls_config import Language
|
||||||
from solidlsp.ls_types import SymbolKind
|
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
|
# 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:
|
class TestElixirLanguageServerSymbols:
|
||||||
"""Test the Elixir language server's symbol-related functionality."""
|
"""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)
|
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||||
def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
|
def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
|
||||||
"""Test request_containing_symbol for a function."""
|
"""Test request_containing_symbol for a function."""
|
||||||
@@ -159,6 +162,9 @@ class TestElixirLanguageServerSymbols:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Tests for request_defining_symbol
|
# 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)
|
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||||
def test_request_defining_symbol_function_call(self, language_server: SolidLanguageServer) -> None:
|
def test_request_defining_symbol_function_call(self, language_server: SolidLanguageServer) -> None:
|
||||||
"""Test request_defining_symbol for a function call."""
|
"""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"]:
|
if "location" in defining_symbol and "uri" in defining_symbol["location"]:
|
||||||
assert "models.ex" in defining_symbol["location"]["uri"]
|
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)
|
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||||
def test_request_defining_symbol_struct_usage(self, language_server: SolidLanguageServer) -> None:
|
def test_request_defining_symbol_struct_usage(self, language_server: SolidLanguageServer) -> None:
|
||||||
"""Test request_defining_symbol for a struct usage."""
|
"""Test request_defining_symbol for a struct usage."""
|
||||||
@@ -205,10 +214,7 @@ class TestElixirLanguageServerSymbols:
|
|||||||
assert "User" in defining_symbol.get("name", "")
|
assert "User" in defining_symbol.get("name", "")
|
||||||
|
|
||||||
@pytest.mark.xfail(
|
@pytest.mark.xfail(
|
||||||
reason="Known intermittent bug in Next LS v0.23.3: Protocol.UndefinedError for :timeout atom. "
|
reason="Expert 0.1.0 bug: definition request crashes (FunctionClauseError in XPExpert.Protocol.Conversions.to_elixir/2)"
|
||||||
"Occurs in CI environments but may pass locally. "
|
|
||||||
"See https://github.com/elixir-tools/next-ls/issues/543",
|
|
||||||
strict=False,
|
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||||
def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
|
def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
|
||||||
@@ -271,7 +277,7 @@ class TestElixirLanguageServerSymbols:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if lib_dir:
|
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", [])]
|
file_names = [child["name"] for child in lib_dir.get("children", [])]
|
||||||
expected_modules = ["models", "services", "examples", "utils", "test_repo"]
|
expected_modules = ["models", "services", "examples", "utils", "test_repo"]
|
||||||
found_modules = [name for name in expected_modules if name in file_names]
|
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
|
# Should get an overview of the lib directory
|
||||||
assert lib_overview is not None
|
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 []
|
overview_keys = list(lib_overview.keys()) if hasattr(lib_overview, "keys") else []
|
||||||
lib_files = [key for key in overview_keys if key.startswith("lib/")]
|
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}"
|
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)
|
# @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
|
||||||
# def test_request_document_overview(self, language_server: SolidLanguageServer) -> None:
|
# def test_request_document_overview(self, language_server: SolidLanguageServer) -> None:
|
||||||
# """Test request_document_overview functionality."""
|
# """Test request_document_overview functionality."""
|
||||||
# # COMMENTED OUT: Next LS document overview doesn't contain expected terms
|
# # COMMENTED OUT: Expert document overview doesn't contain expected terms
|
||||||
# # Next LS return value: [('TestRepo.Models', 2, 0, 0)] - only module info, no detailed content
|
# # 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
|
# # 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")
|
# file_path = os.path.join("lib", "models.ex")
|
||||||
# doc_overview = language_server.request_document_overview(file_path)
|
# doc_overview = language_server.request_document_overview(file_path)
|
||||||
|
|||||||
Reference in new issue
Block a user