Merge remote-tracking branch 'origin/main' into ls-notify-on-reopen-changed

Conflicts:
	CHANGELOG.md
This commit is contained in:
Dominik Jain committed 2026-08-03 22:21:48 +02:00
commit 0720dd92e0
16 files changed
+553 -236

No files matched your search

+4 -3
View File
@@ -81,7 +81,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.11"]
# 5 batches, each installs ONLY the toolchains its languages need (steps gated by matrix.batch).
# Node/Python/uv stay in every batch (cheap, needed for the venv + auto-installed LSes).
# Python/uv stay in every batch; Node is limited to other-langs and catch-all.
# catch-all = web + python + the core framework.
batch: [jvm, native, other-langs, niche, catch-all]
# --- OS gating: which batch runs on which OS -----------------------------------------
@@ -106,9 +106,10 @@ jobs:
with:
go-version: ">=1.17.0"
- name: Set up Node.js
uses: actions/setup-node@v4
if: matrix.batch == 'other-langs' || matrix.batch == 'catch-all'
uses: actions/setup-node@v7
with:
node-version: '20.x'
node-version: '24.x'
- name: Ensure cached directory exist before calling cache-related actions
shell: bash
run: |
@@ -66,6 +66,50 @@ To implement a new language server using the DependencyProvider pattern:
You should look at at least one existing implementation of each base class to understand how they work.
#### Downloading Runtime Dependencies
Use `DownloadedDependency` (in `solidlsp.dependency_provider`), which bundles the URL,
archive type, allowed hosts and checksum verification behind a single `download_to()` call:
```python
dep = DownloadedDependency(
url=f"https://example.org/foo-{version}-{platform}.zip",
archive_type="zip", # optional FileUtils.ArchiveType for extraction
allowed_hosts=FOO_ALLOWED_HOSTS, # optional list of allowed hosts
)
dep.download_to(target_dir)
```
Checksums for downloads live in a URL-keyed database, `src/solidlsp/resources/downloaded_dependency_hashes.json`,
managed by `DownloadedDependencyHashDatabase`.
Consequences for your implementation:
* Build each dependency in a factory classmethod (`_create_dep_*`) that takes an
optional version and falls back to the pinned `DEFAULT_*` constant.
* Add an `update_dep_hashes()` classmethod that constructs every dependency
and updates the hashes:
```python
@classmethod
def update_dep_hashes(cls) -> None:
deps = [cls._create_dep_foo(), cls_._create_dep_bar(), ...]
with DownloadedDependencyHashDatabase.get_instance().update_context() as db:
for dep in deps:
db.update(dep)
```
Hook a call to this method into `scripts/update_downloaded_dependency_hashes.py`, run that script,
and commit the resulting JSON changes.
* After bumping any pinned version, re-run the script. Add a NOTE comment next to
the version constants saying so; a stale database means unverified downloads
locally and a CI failure.
* Pass `verified=False` only for dependencies whose hash cannot be pinned by design
(e.g. a user-supplied version override).
Reference implementation: `EclipseJDTLS.DependencyProvider`
Note that several older language servers still define hashes locally in constants and
call `FileUtils.download_and_extract_archive_verified`directly. Do not apply this legacy approach.
### 1.2 LSP Initialization
Override `_create_base_initialize_params` to provide server-specific initialization
+5
View File
@@ -38,6 +38,11 @@ Status of the `main` branch. Changes prior to the next official version change w
- Fix: F#'s `module <Name>` declarations reported a `selectionRange` pointing at the `module`
keyword instead of at `<Name>`, so looking up hover/references from a module symbol's position
returned the keyword's own docs instead of the module's #925
- Fix: Erlang functions could not be addressed by any tool taking an exact name path, because
Erlang LS identifies them as `name/arity` and `/` separates name path components. The arity is
now separated by `#` instead (e.g. `create_user#4`), so the reported name path round-trips and
`find_referencing_symbols`/`replace_symbol_body`/`insert_after_symbol` work on Erlang
functions #1797
- Fix: `LSPFileBuffer`: a stale content hash could be returned if files are kept open
and file contents were not read before trying to retrieve the hash value
- Fix: Change semantics of file opening (`open_file`) in the language server from "open file (if not already open)"
+2 -1
View File
@@ -68,7 +68,8 @@ Some languages require additional installations or setup steps, as noted.
* **Elm**
(requires Elm compiler)
* **Erlang**
(requires installation of beam and [erlang_ls](https://github.com/erlang-ls/erlang_ls); experimental, might be slow or hang)
(requires installation of beam and [erlang_ls](https://github.com/erlang-ls/erlang_ls); experimental, might be slow or hang;
note that functions are addressed as `name#arity`, e.g. `create_user#4`, because `/` is reserved as the name path separator)
* **F#**
(requires [.NET v8.0+](https://dotnet.microsoft.com/en-us/download/dotnet); uses FsAutoComplete/Ionide, which is auto-installed; for Homebrew .NET on macOS, set DOTNET_ROOT in your environment)
* **Fortran**
@@ -0,0 +1,7 @@
from sensai.util import logging
from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS
if __name__ == "__main__":
logging.configure()
EclipseJDTLS.DependencyProvider.update_dep_hashes()
+130 -2
View File
@@ -1,10 +1,16 @@
import json
import logging
import os
import shutil
import tempfile
import threading
from abc import ABC, abstractmethod
from collections.abc import Sequence
from collections.abc import Iterator, Sequence
from contextlib import contextmanager
from os import PathLike
from solidlsp.settings import SolidLSPSettings
from solidlsp.ls_utils import FileUtils, is_running_in_ci
from solidlsp.settings import SOLIDLSP_RESOURCES_DIR, SolidLSPSettings
log = logging.getLogger(__name__)
@@ -239,3 +245,125 @@ class LanguageServerDependencyProviderUvx(LanguageServerDependencyProviderBaseCo
def _create_launch_command_from_base_command(self, base_command: list[str]) -> list[str]:
return base_command + list(self._extra_args)
class DownloadedDependencyHashDatabase:
RESOURCE_FILE_NAME = "downloaded_dependency_hashes.json"
_instance: "DownloadedDependencyHashDatabase | None" = None
_instance_lock: threading.Lock = threading.Lock()
def __init__(self, _singleton: bool):
self._json_file = os.path.join(SOLIDLSP_RESOURCES_DIR, self.RESOURCE_FILE_NAME)
self._hashes = {}
if os.path.exists(self._json_file):
try:
with open(self._json_file, encoding="utf-8") as f:
self._hashes = json.load(f)
except:
log.warning("Error loading %s", self._json_file, exc_info=True)
@classmethod
def get_instance(cls) -> "DownloadedDependencyHashDatabase":
if cls._instance is None:
with cls._instance_lock:
if cls._instance is None:
cls._instance = DownloadedDependencyHashDatabase(True)
return cls._instance
def get_sha256(self, dep: "DownloadedDependency") -> str | None:
return self._hashes.get(dep.get_url())
class Updater:
def __init__(self, db: "DownloadedDependencyHashDatabase"):
self._db = db
def update(self, dep: "DownloadedDependency", force: bool = False) -> None:
"""
:param dep: the dependency to update the hash for
:param force: whether to force an update even if the hash already exists
"""
self._db._update(dep, force)
@contextmanager
def update_context(self) -> Iterator[Updater]:
try:
yield self.Updater(self)
finally:
self._save()
def _update(self, dep: "DownloadedDependency", force: bool = False) -> None:
if not force and dep.get_url() in self._hashes:
log.info("SHA256 for %s already exists, skipping update", dep.get_url())
return
try:
with tempfile.TemporaryDirectory() as temp_dir:
path = os.path.join(temp_dir, "downloaded_dependency")
log.info("Downloading %s for hash calculation", dep.get_url())
FileUtils.download_file(dep.get_url(), path)
sha = FileUtils.calculate_sha256(path)
log.info("SHA256 for %s: %s", dep.get_url(), sha)
self._hashes[dep.get_url()] = sha
except:
log.warning("Error updating hash for %s", dep.get_url(), exc_info=True)
def _save(self) -> None:
try:
with open(self._json_file, "w", encoding="utf-8") as f:
json.dump(self._hashes, f, indent=2)
except:
log.warning("Error saving %s", self._json_file, exc_info=True)
class DownloadedDependency:
"""
Represents a dependency that can be downloaded from a URL, optionally verified against a known SHA256 hash,
and optionally extracted if it is an archive.
"""
def __init__(
self,
url: str,
archive_type: FileUtils.ArchiveType | None = None,
allowed_hosts: Sequence[str] | None = None,
verified: bool = True,
):
"""
:param url: the URL of the file to be downloaded
:param archive_type: optional type of archive for the case where `url` points to an archive.
Use `None` or "binary" for a binary file (no extraction).
:param allowed_hosts: optional list of allowed hosts for the download URL
:param verified: whether to verify the downloaded file against a known SHA256 hash (if available).
If enabled and the hash for the URL is not known, will raise an exception in CI and issue a warning
in non-CI environments.
"""
self._url = url
self._allowed_hosts = allowed_hosts
self._archive_type = archive_type if archive_type is not None else "binary"
self._verified = verified
def get_url(self) -> str:
return self._url
def download_to(self, target_path: str | PathLike) -> None:
"""
Downloads the dependency to the specified target path.
If defined at construction, will apply hash verification and archive extraction.
:param target_path: the path to which the dependency should be downloaded (or extracted if applicable)
"""
if not isinstance(target_path, str):
target_path = str(target_path)
if self._verified:
sha256 = DownloadedDependencyHashDatabase.get_instance().get_sha256(self)
if sha256 is None:
if is_running_in_ci():
raise RuntimeError(
f"No SHA256 hash found for {self._url}. "
"Please update the hash database by running 'scripts/update_downloaded_dependency_hashes.py'."
)
log.warning("No SHA256 hash found for %s. The downloaded file will not be verified.", self._url)
else:
sha256 = None
FileUtils.download_and_extract_archive_verified(
self._url, target_path, archive_type=self._archive_type, expected_sha256=sha256, allowed_hosts=self._allowed_hosts
)
+1 -1
View File
@@ -24,7 +24,7 @@ class RuntimeDependency:
url: str | None = None
sha256: str | None = None
allowed_hosts: tuple[str, ...] | list[str] | None = None
archive_type: str | None = None
archive_type: FileUtils.ArchiveType | None = None
binary_name: str | None = None
command: str | list[str] | None = None
package_name: str | None = None
+185 -218
View File
@@ -13,18 +13,20 @@ import re
import shutil
import subprocess
import threading
from dataclasses import dataclass
from pathlib import Path, PurePath
from time import sleep
from typing import Any, cast
from typing import Any
from overrides import override
from solidlsp import ls_types
from solidlsp.dependency_provider import DownloadedDependency, DownloadedDependencyHashDatabase
from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_types import UnifiedSymbolInformation
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.ls_utils import PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, SymbolInformation
from solidlsp.settings import SolidLSPSettings
from solidlsp.util.subprocess_util import subprocess_run
@@ -32,7 +34,6 @@ from solidlsp.util.subprocess_util import subprocess_run
log = logging.getLogger(__name__)
GRADLE_ALLOWED_HOSTS = ("services.gradle.org", "github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
GRADLE_SHA256 = "7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999"
VSCODE_JAVA_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
INTELLICODE_ALLOWED_HOSTS = (
"visualstudioexptteam.gallery.vsassets.io",
@@ -41,30 +42,6 @@ INTELLICODE_ALLOWED_HOSTS = (
)
# Version pinning convention (read this before bumping anything below):
#
# INITIAL_* — the very first version we shipped runtime-dependency support for, paired with
# its SHA. NEVER edited. The legacy unversioned install directory (e.g. "vscode-java/")
# is reserved exclusively for this version, so users who installed under the original
# layout keep their existing cache forever.
#
# DEFAULT_* — the version (and matching SHA) used when the user does not override via
# custom_settings. THIS is what we edit to upgrade: copy the new version + new SHA over
# the existing DEFAULT_* literals, leave INITIAL_* alone.
#
# Resolution at install time:
# resolved_version = custom_settings.get("..._version", DEFAULT_*_VERSION)
# - resolved == INITIAL_* -> legacy unversioned dir, SHA = INITIAL_*_SHA256
# - resolved == DEFAULT_* -> versioned dir "{name}-{resolved}", SHA = DEFAULT_*_SHA256
# - any other version -> versioned dir "{name}-{resolved}", SHA verification skipped
#
# Consequences of this scheme:
# - Bumping DEFAULT_* makes new installs land in a new versioned subdir => no silent reuse
# of stale binaries (the bug this scheme exists to fix).
# - Previously-downloaded versions are not lost: a user can request any past version via
# custom_settings and the matching cached subdir is picked up without re-downloading.
# - INITIAL_* and DEFAULT_* hold identical literals when first introduced; they diverge on
# the first DEFAULT_* bump and stay independent thereafter.
@dataclasses.dataclass(frozen=True)
class VsixResourcePaths:
"""
@@ -81,40 +58,6 @@ class VsixResourcePaths:
equinox_launcher_basename: str # e.g. "org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar"
INITIAL_VSCODE_JAVA_VERSION = "1.42.0-561"
INITIAL_VSCODE_JAVA_SHA256_BY_PLATFORM = {
"osx-arm64": "bc00c2699d4b8d478eb9a1621db9d6d3a12ea0dcc247a9cd8040e8ac19c03933",
"osx-x64": "03ae1db1a22c15561a620f1b722d6797d35d4faaa7c4666dbe6ca2715089852f",
"linux-arm64": "e15bc9b2a665d3453203402621b5441062aa41b0ec2d140661f439326fd248c1",
"linux-x64": "7660b7b527be6fda46a917966b34d828e7416d5cc84287b29b88e7b99c1737f9",
"win-x64": "ef195b45bd260976ad2e84618f4044b5d7248deed41d647573f0ee22c4233df3",
}
INITIAL_VSCODE_JAVA_PATHS = VsixResourcePaths(
jre_version="21.0.7",
lombok_jar_basename="lombok-1.18.36.jar",
equinox_launcher_basename="org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
)
# Bumped from 1.42.0-561 to surface Lombok-generated methods (#1432); brings JDTLS commit b2d8952
# (java.symbols.includeGeneratedCode), JRE 21.0.10, Lombok 1.18.39 and Equinox launcher 1.7.100.
DEFAULT_VSCODE_JAVA_VERSION = "1.54.0-923"
DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM = {
"osx-arm64": "c54c45cb0d2579d8e0a4ddeb24d4a9dd0b460d07d9366adea2b38a1da22a463c",
"osx-x64": "dfc98abc4e54165a78372e280242a039671729b1b03420608df3b10c6b629fb6",
"linux-arm64": "e2bb22c427d90da8dbb1afff72ff1e2dce38d50b76deb02d7bc313a330a1330c",
"linux-x64": "9d4b15da54e25a0192f9bac073f086c015397d3676623b68dbf83a5dbaf5132b",
"win-x64": "66f3914987edeccfee8a2558470e0fde4f8c4154232ff4baa5d73373ebc819d4",
}
DEFAULT_VSCODE_JAVA_PATHS = VsixResourcePaths(
jre_version="21.0.10",
lombok_jar_basename="lombok-1.18.39-4050.jar",
equinox_launcher_basename="org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar",
)
INITIAL_INTELLICODE_VERSION = "1.2.30"
INITIAL_INTELLICODE_SHA256 = "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251"
DEFAULT_INTELLICODE_VERSION = "1.2.30"
DEFAULT_INTELLICODE_SHA256 = "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251"
# Mapping from Serena's platform identifiers to upstream JDTLS config_<platform> directory names
JDTLS_CONFIG_DIR_BY_PLATFORM = {
"osx-arm64": "config_mac_arm",
@@ -150,6 +93,19 @@ class RuntimeDependencyPaths:
intellisense_members_path: str | None = None
@dataclass
class VSCodeJavaConfig:
"""
Represents a (platform-specific) configuration for VSCode Java
"""
jre_home_path: str
jre_path: str
lombok_jar_path: str
jdtls_launcher_jar_path: str
jdtls_readonly_config_path: str
class EclipseJDTLS(SolidLanguageServer):
r"""
The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class
@@ -271,6 +227,56 @@ class EclipseJDTLS(SolidLanguageServer):
return self.DependencyProvider(self._custom_settings, ls_resources_dir, self._solidlsp_settings, self.repository_root_path)
class DependencyProvider(LanguageServerDependencyProvider):
"""
Dependency provider for Eclipse JDTLS.
Version pinning convention (read this before bumping anything below):
INITIAL_* — the very first version we shipped runtime-dependency support for, paired with
its SHA. NEVER edited. The legacy unversioned install directory (e.g. "vscode-java/")
is reserved exclusively for this version, so users who installed under the original
layout keep their existing cache forever.
DEFAULT_* — the version (and matching SHA) used when the user does not override via
custom_settings. THIS is what we edit to upgrade: copy the new version + new SHA over
the existing DEFAULT_* literals, leave INITIAL_* alone.
Resolution at install time:
resolved_version = custom_settings.get("..._version", DEFAULT_*_VERSION)
- resolved == INITIAL_* -> legacy unversioned dir, SHA = INITIAL_*_SHA256
- resolved == DEFAULT_* -> versioned dir "{name}-{resolved}", SHA = DEFAULT_*_SHA256
- any other version -> versioned dir "{name}-{resolved}", SHA verification skipped
Consequences of this scheme:
- Bumping DEFAULT_* makes new installs land in a new versioned subdir => no silent reuse
of stale binaries (the bug this scheme exists to fix).
- Previously-downloaded versions are not lost: a user can request any past version via
custom_settings and the matching cached subdir is picked up without re-downloading.
- INITIAL_* and DEFAULT_* hold identical literals when first introduced; they diverge on
the first DEFAULT_* bump and stay independent thereafter.
"""
# versions used initially (which use default paths without version suffix)
INITIAL_VSCODE_JAVA_VERSION = "1.42.0-561"
INITIAL_INTELLICODE_VERSION = "1.2.30"
INITIAL_VSCODE_JAVA_PATHS = VsixResourcePaths(
jre_version="21.0.7",
lombok_jar_basename="lombok-1.18.36.jar",
equinox_launcher_basename="org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar",
)
# currently used default versions
# NOTE: After bumping any of these, also update the corresponding hashes by running
# scripts/update_downloaded_dependency_hashes.py and commit the resulting changes.
DEFAULT_GRADLE_VERSION = "8.14.2"
DEFAULT_VSCODE_JAVA_VERSION = "1.54.0-923"
DEFAULT_INTELLICODE_VERSION = "1.2.30"
DEFAULT_VSCODE_JAVA_PATHS = VsixResourcePaths(
jre_version="21.0.10",
lombok_jar_basename="lombok-1.18.39-4050.jar",
equinox_launcher_basename="org.eclipse.equinox.launcher_1.7.100.v20251111-0406.jar",
)
def __init__(
self,
custom_settings: SolidLSPSettings.CustomLSSettings,
@@ -283,9 +289,57 @@ class EclipseJDTLS(SolidLanguageServer):
self._repository_root_path = repository_root_path
self.runtime_dependency_paths = self._setup_runtime_dependencies(ls_resources_dir, custom_settings)
@staticmethod
@classmethod
def _create_dep_gradle(cls, gradle_version: str | None = None) -> DownloadedDependency:
gradle_version = gradle_version or cls.DEFAULT_GRADLE_VERSION
return DownloadedDependency(
url=f"https://services.gradle.org/distributions/gradle-{gradle_version}-bin.zip",
archive_type="zip",
allowed_hosts=GRADLE_ALLOWED_HOSTS,
verified=gradle_version == cls.DEFAULT_GRADLE_VERSION,
)
@classmethod
def _create_deps_vscode_java(cls, vscode_java_version: str | None = None) -> dict[str, DownloadedDependency]:
vscode_java_version = vscode_java_version or cls.DEFAULT_VSCODE_JAVA_VERSION
vscode_java_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}"
urls = {
"osx-arm64": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-arm64-{vscode_java_version}.vsix",
"osx-x64": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-x64-{vscode_java_version}.vsix",
"linux-arm64": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-arm64-{vscode_java_version}.vsix",
"linux-x64": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-x64-{vscode_java_version}.vsix",
"win-x64": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-win32-x64-{vscode_java_version}.vsix",
}
deps = {}
for platform_key, url in urls.items():
deps[platform_key] = DownloadedDependency(
url=url,
archive_type="zip",
allowed_hosts=VSCODE_JAVA_ALLOWED_HOSTS,
verified=vscode_java_version == cls.DEFAULT_VSCODE_JAVA_VERSION,
)
return deps
@classmethod
def _create_dep_intellicode(cls, intellicode_version: str | None = None) -> DownloadedDependency:
intellicode_version = intellicode_version or cls.DEFAULT_INTELLICODE_VERSION
return DownloadedDependency(
url=f"https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/{intellicode_version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage",
archive_type="zip",
allowed_hosts=INTELLICODE_ALLOWED_HOSTS,
verified=intellicode_version == cls.DEFAULT_INTELLICODE_VERSION,
)
@classmethod
def update_dep_hashes(cls) -> None:
deps = [cls._create_dep_gradle(), cls._create_dep_intellicode()] + list(cls._create_deps_vscode_java().values())
with DownloadedDependencyHashDatabase.get_instance().update_context() as db:
for dep in deps:
db.update(dep)
@classmethod
def _setup_runtime_dependencies(
ls_resources_dir: str, custom_settings: SolidLSPSettings.CustomLSSettings
cls, ls_resources_dir: str, custom_settings: SolidLSPSettings.CustomLSSettings
) -> RuntimeDependencyPaths:
"""
Setup runtime dependencies for EclipseJDTLS and return the paths.
@@ -313,145 +367,72 @@ class EclipseJDTLS(SolidLanguageServer):
return EclipseJDTLS.DependencyProvider._setup_from_existing_install(str(jdtls_path), str(lombok_path), custom_settings)
platformId = PlatformUtils.get_platform_id()
gradle_version = custom_settings.get("gradle_version", "8.14.2")
vscode_java_version = custom_settings.get("vscode_java_version", DEFAULT_VSCODE_JAVA_VERSION)
vscode_java_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}"
intellicode_version = custom_settings.get("intellicode_version", DEFAULT_INTELLICODE_VERSION)
default_gradle_version = gradle_version == "8.14.2"
gradle_version = custom_settings.get("gradle_version", cls.DEFAULT_GRADLE_VERSION)
vscode_java_version = custom_settings.get("vscode_java_version", cls.DEFAULT_VSCODE_JAVA_VERSION)
intellicode_version = custom_settings.get("intellicode_version", cls.DEFAULT_INTELLICODE_VERSION)
# install-dir name per the version-pinning convention (see module-level block):
# INITIAL -> legacy unversioned dir; everything else -> "{name}-{resolved}" subdir
vscode_java_dirname = (
"vscode-java" if vscode_java_version == INITIAL_VSCODE_JAVA_VERSION else f"vscode-java-{vscode_java_version}"
"vscode-java" if vscode_java_version == cls.INITIAL_VSCODE_JAVA_VERSION else f"vscode-java-{vscode_java_version}"
)
intellicode_dirname = (
"intellicode" if intellicode_version == INITIAL_INTELLICODE_VERSION else f"intellicode-{intellicode_version}"
"intellicode" if intellicode_version == cls.INITIAL_INTELLICODE_VERSION else f"intellicode-{intellicode_version}"
)
# SHA is only known for our two pinned versions (INITIAL and current DEFAULT);
# for any other user-supplied version we skip verification (returns None)
def vscode_java_sha(platform_key: str) -> str | None:
if vscode_java_version == INITIAL_VSCODE_JAVA_VERSION:
return INITIAL_VSCODE_JAVA_SHA256_BY_PLATFORM[platform_key]
if vscode_java_version == DEFAULT_VSCODE_JAVA_VERSION:
return DEFAULT_VSCODE_JAVA_SHA256_BY_PLATFORM[platform_key]
return None
def intellicode_sha() -> str | None:
if intellicode_version == INITIAL_INTELLICODE_VERSION:
return INITIAL_INTELLICODE_SHA256
if intellicode_version == DEFAULT_INTELLICODE_VERSION:
return DEFAULT_INTELLICODE_SHA256
return None
# Resolve internal VSIX paths (JRE / Lombok / launcher filenames). For pinned versions
# these are known; for any other user-supplied version we bail out — guessing would
# silently produce broken paths at JDTLS launch time, which is a worse UX than failing
# fast here with a pointer to upstream-JDTLS mode (which doesn't need pinned paths).
if vscode_java_version == INITIAL_VSCODE_JAVA_VERSION:
vsix_paths = INITIAL_VSCODE_JAVA_PATHS
elif vscode_java_version == DEFAULT_VSCODE_JAVA_VERSION:
vsix_paths = DEFAULT_VSCODE_JAVA_PATHS
if vscode_java_version == cls.INITIAL_VSCODE_JAVA_VERSION:
vsix_paths = cls.INITIAL_VSCODE_JAVA_PATHS
elif vscode_java_version == cls.DEFAULT_VSCODE_JAVA_VERSION:
vsix_paths = cls.DEFAULT_VSCODE_JAVA_PATHS
else:
raise SolidLSPException(
f"Resource paths inside the vscode-java {vscode_java_version} VSIX are not pinned in serena "
f"(known: {INITIAL_VSCODE_JAVA_VERSION}, {DEFAULT_VSCODE_JAVA_VERSION}). "
f"Either remove the 'vscode_java_version' override (defaults to {DEFAULT_VSCODE_JAVA_VERSION}), "
f"(known: {cls.INITIAL_VSCODE_JAVA_VERSION}, {cls.DEFAULT_VSCODE_JAVA_VERSION}). "
f"Either remove the 'vscode_java_version' override (defaults to {cls.DEFAULT_VSCODE_JAVA_VERSION}), "
f"or use upstream JDTLS mode by setting both 'jdtls_path' and 'lombok_path' in "
f"ls_specific_settings.java (no pinning required)."
)
runtime_dependencies: dict[str, dict[str, dict[str, object]]] = {
"gradle": {
"platform-agnostic": {
"url": f"https://services.gradle.org/distributions/gradle-{gradle_version}-bin.zip",
"archiveType": "zip",
"relative_extraction_path": ".",
"sha256": GRADLE_SHA256 if default_gradle_version else None,
"allowed_hosts": GRADLE_ALLOWED_HOSTS,
}
},
"vscode-java": {
"darwin-arm64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("osx-arm64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
},
"osx-arm64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("osx-arm64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": f"extension/jre/{vsix_paths.jre_version}-macosx-aarch64",
"jre_path": f"extension/jre/{vsix_paths.jre_version}-macosx-aarch64/bin/java",
"lombok_jar_path": f"extension/lombok/{vsix_paths.lombok_jar_basename}",
"jdtls_launcher_jar_path": f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
"jdtls_readonly_config_path": "extension/server/config_mac_arm",
},
"osx-x64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-darwin-x64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("osx-x64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": f"extension/jre/{vsix_paths.jre_version}-macosx-x86_64",
"jre_path": f"extension/jre/{vsix_paths.jre_version}-macosx-x86_64/bin/java",
"lombok_jar_path": f"extension/lombok/{vsix_paths.lombok_jar_basename}",
"jdtls_launcher_jar_path": f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
"jdtls_readonly_config_path": "extension/server/config_mac",
},
"linux-arm64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-arm64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("linux-arm64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": f"extension/jre/{vsix_paths.jre_version}-linux-aarch64",
"jre_path": f"extension/jre/{vsix_paths.jre_version}-linux-aarch64/bin/java",
"lombok_jar_path": f"extension/lombok/{vsix_paths.lombok_jar_basename}",
"jdtls_launcher_jar_path": f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
"jdtls_readonly_config_path": "extension/server/config_linux_arm",
},
"linux-x64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-linux-x64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("linux-x64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": f"extension/jre/{vsix_paths.jre_version}-linux-x86_64",
"jre_path": f"extension/jre/{vsix_paths.jre_version}-linux-x86_64/bin/java",
"lombok_jar_path": f"extension/lombok/{vsix_paths.lombok_jar_basename}",
"jdtls_launcher_jar_path": f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
"jdtls_readonly_config_path": "extension/server/config_linux",
},
"win-x64": {
"url": f"https://github.com/redhat-developer/vscode-java/releases/download/{vscode_java_tag}/java-win32-x64-{vscode_java_version}.vsix",
"archiveType": "zip",
"relative_extraction_path": vscode_java_dirname,
"sha256": vscode_java_sha("win-x64"),
"allowed_hosts": VSCODE_JAVA_ALLOWED_HOSTS,
"jre_home_path": f"extension/jre/{vsix_paths.jre_version}-win32-x86_64",
"jre_path": f"extension/jre/{vsix_paths.jre_version}-win32-x86_64/bin/java.exe",
"lombok_jar_path": f"extension/lombok/{vsix_paths.lombok_jar_basename}",
"jdtls_launcher_jar_path": f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
"jdtls_readonly_config_path": "extension/server/config_win",
},
},
"intellicode": {
"platform-agnostic": {
"url": f"https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/{intellicode_version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage",
"alternate_url": f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/{intellicode_version}/vspackage",
"archiveType": "zip",
"relative_extraction_path": intellicode_dirname,
"sha256": intellicode_sha(),
"allowed_hosts": INTELLICODE_ALLOWED_HOSTS,
"intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar",
"intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members",
}
},
vscode_java_configs: dict[str, VSCodeJavaConfig] = {
"osx-arm64": VSCodeJavaConfig(
jre_home_path=f"extension/jre/{vsix_paths.jre_version}-macosx-aarch64",
jre_path=f"extension/jre/{vsix_paths.jre_version}-macosx-aarch64/bin/java",
lombok_jar_path=f"extension/lombok/{vsix_paths.lombok_jar_basename}",
jdtls_launcher_jar_path=f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
jdtls_readonly_config_path="extension/server/config_mac_arm",
),
"osx-x64": VSCodeJavaConfig(
jre_home_path=f"extension/jre/{vsix_paths.jre_version}-macosx-x86_64",
jre_path=f"extension/jre/{vsix_paths.jre_version}-macosx-x86_64/bin/java",
lombok_jar_path=f"extension/lombok/{vsix_paths.lombok_jar_basename}",
jdtls_launcher_jar_path=f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
jdtls_readonly_config_path="extension/server/config_mac",
),
"linux-arm64": VSCodeJavaConfig(
jre_home_path=f"extension/jre/{vsix_paths.jre_version}-linux-aarch64",
jre_path=f"extension/jre/{vsix_paths.jre_version}-linux-aarch64/bin/java",
lombok_jar_path=f"extension/lombok/{vsix_paths.lombok_jar_basename}",
jdtls_launcher_jar_path=f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
jdtls_readonly_config_path="extension/server/config_linux_arm",
),
"linux-x64": VSCodeJavaConfig(
jre_home_path=f"extension/jre/{vsix_paths.jre_version}-linux-x86_64",
jre_path=f"extension/jre/{vsix_paths.jre_version}-linux-x86_64/bin/java",
lombok_jar_path=f"extension/lombok/{vsix_paths.lombok_jar_basename}",
jdtls_launcher_jar_path=f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
jdtls_readonly_config_path="extension/server/config_linux",
),
"win-x64": VSCodeJavaConfig(
jre_home_path=f"extension/jre/{vsix_paths.jre_version}-win32-x86_64",
jre_path=f"extension/jre/{vsix_paths.jre_version}-win32-x86_64/bin/java.exe",
lombok_jar_path=f"extension/lombok/{vsix_paths.lombok_jar_basename}",
jdtls_launcher_jar_path=f"extension/server/plugins/{vsix_paths.equinox_launcher_basename}",
jdtls_readonly_config_path="extension/server/config_win",
),
}
gradle_path = str(
@@ -462,25 +443,20 @@ class EclipseJDTLS(SolidLanguageServer):
)
if not os.path.exists(gradle_path):
gradle_dependency = runtime_dependencies["gradle"]["platform-agnostic"]
FileUtils.download_and_extract_archive_verified(
cast(str, gradle_dependency["url"]),
str(PurePath(gradle_path).parent),
cast(str, gradle_dependency["archiveType"]),
expected_sha256=cast(str | None, gradle_dependency["sha256"]),
allowed_hosts=cast(tuple[str, ...], gradle_dependency["allowed_hosts"]),
)
gradle_dependency = cls._create_dep_gradle(gradle_version)
gradle_dependency.download_to(PurePath(gradle_path).parent)
assert os.path.exists(gradle_path)
dependency = runtime_dependencies["vscode-java"][platformId.value]
vscode_java_path = str(PurePath(ls_resources_dir, cast(str, dependency["relative_extraction_path"])))
vscode_java_dep = cls._create_deps_vscode_java(vscode_java_version)[platformId.value]
vscode_java_cfg = vscode_java_configs[platformId.value]
vscode_java_path = str(PurePath(ls_resources_dir, vscode_java_dirname))
os.makedirs(vscode_java_path, exist_ok=True)
jre_home_path = str(PurePath(vscode_java_path, cast(str, dependency["jre_home_path"])))
jre_path = str(PurePath(vscode_java_path, cast(str, dependency["jre_path"])))
lombok_jar_path = str(PurePath(vscode_java_path, cast(str, dependency["lombok_jar_path"])))
jdtls_launcher_jar_path = str(PurePath(vscode_java_path, cast(str, dependency["jdtls_launcher_jar_path"])))
jdtls_readonly_config_path = str(PurePath(vscode_java_path, cast(str, dependency["jdtls_readonly_config_path"])))
jre_home_path = str(PurePath(vscode_java_path, vscode_java_cfg.jre_home_path))
jre_path = str(PurePath(vscode_java_path, vscode_java_cfg.jre_path))
lombok_jar_path = str(PurePath(vscode_java_path, vscode_java_cfg.lombok_jar_path))
jdtls_launcher_jar_path = str(PurePath(vscode_java_path, vscode_java_cfg.jdtls_launcher_jar_path))
jdtls_readonly_config_path = str(PurePath(vscode_java_path, vscode_java_cfg.jdtls_readonly_config_path))
if not all(
[
os.path.exists(vscode_java_path),
@@ -491,13 +467,7 @@ class EclipseJDTLS(SolidLanguageServer):
os.path.exists(jdtls_readonly_config_path),
]
):
FileUtils.download_and_extract_archive_verified(
cast(str, dependency["url"]),
vscode_java_path,
cast(str, dependency["archiveType"]),
expected_sha256=cast(str | None, dependency["sha256"]),
allowed_hosts=cast(tuple[str, ...], dependency["allowed_hosts"]),
)
vscode_java_dep.download_to(vscode_java_path)
os.chmod(jre_path, 0o755)
@@ -508,11 +478,13 @@ class EclipseJDTLS(SolidLanguageServer):
assert os.path.exists(jdtls_launcher_jar_path)
assert os.path.exists(jdtls_readonly_config_path)
dependency = runtime_dependencies["intellicode"]["platform-agnostic"]
intellicode_directory_path = str(PurePath(ls_resources_dir, cast(str, dependency["relative_extraction_path"])))
intellicode_dep = cls._create_dep_intellicode(intellicode_version)
intellicode_directory_path = str(PurePath(ls_resources_dir, intellicode_dirname))
os.makedirs(intellicode_directory_path, exist_ok=True)
intellicode_jar_path = str(PurePath(intellicode_directory_path, cast(str, dependency["intellicode_jar_path"])))
intellisense_members_path = str(PurePath(intellicode_directory_path, cast(str, dependency["intellisense_members_path"])))
intellicode_jar_path = str(
PurePath(intellicode_directory_path, "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar")
)
intellisense_members_path = str(PurePath(intellicode_directory_path, "extension/dist/bundledModels/java_intellisense-members"))
if not all(
[
os.path.exists(intellicode_directory_path),
@@ -520,13 +492,7 @@ class EclipseJDTLS(SolidLanguageServer):
os.path.exists(intellisense_members_path),
]
):
FileUtils.download_and_extract_archive_verified(
cast(str, dependency["url"]),
intellicode_directory_path,
cast(str, dependency["archiveType"]),
expected_sha256=cast(str | None, dependency["sha256"]),
allowed_hosts=cast(tuple[str, ...], dependency["allowed_hosts"]),
)
intellicode_dep.download_to(intellicode_directory_path)
assert os.path.exists(intellicode_directory_path)
assert os.path.exists(intellicode_jar_path)
@@ -764,8 +730,9 @@ class EclipseJDTLS(SolidLanguageServer):
major = int(version_match.group(1))
return real_home, major
@staticmethod
@classmethod
def _compute_workspace_hash(
cls,
repository_root_path: str,
jdtls_launcher_jar_path: str,
custom_settings: SolidLSPSettings.CustomLSSettings,
@@ -802,7 +769,7 @@ class EclipseJDTLS(SolidLanguageServer):
is_legacy_initial = (
not custom_settings.get("jdtls_path")
and custom_settings.get("vscode_java_version", DEFAULT_VSCODE_JAVA_VERSION) == INITIAL_VSCODE_JAVA_VERSION
and custom_settings.get("vscode_java_version", cls.DEFAULT_VSCODE_JAVA_VERSION) == cls.INITIAL_VSCODE_JAVA_VERSION
and not workspace_settings
)
if is_legacy_initial:
@@ -6,17 +6,27 @@ import shutil
import subprocess
import threading
import time
from collections.abc import Hashable
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import is_running_in_ci
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from solidlsp.util.subprocess_util import subprocess_run
log = logging.getLogger(__name__)
ARITY_SEPARATOR = "#"
"""
The character that replaces the `/` in the `name/arity` identifiers reported by Erlang LS.
`#` was chosen because it cannot occur in an unquoted Erlang atom, so it can never collide with a
real function, type or macro name (unlike `@`, which is a legal atom character).
"""
class ErlangLanguageServer(SolidLanguageServer):
"""Language server for Erlang using Erlang LS."""
@@ -47,6 +57,26 @@ class ErlangLanguageServer(SolidLanguageServer):
# Set generous timeout for Erlang LS initialization
self.set_request_timeout(120.0)
@override
def _document_symbols_cache_fingerprint(self) -> Hashable:
normalize_symbol_name_version = 1
return normalize_symbol_name_version
@override
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
"""
Replaces the `/` in Erlang's `name/arity` identifiers, which would otherwise be interpreted
as Serena's name path separator.
Erlang LS names functions, types and parameterised macros `name/arity` (e.g. `create_user/2`).
Since `/` separates name path components, such a name is parsed as "symbol `2` nested inside
`create_user`" and can never be matched, not even by the very name path that Serena itself
reports for the symbol. The arity is not simply dropped because it is part of a function's
identity in Erlang: `create_user/2` and `create_user/3` are different functions which may
both be defined in the same module.
"""
return symbol["name"].replace("/", ARITY_SEPARATOR)
def _check_erlang_installation(self) -> bool:
"""Check if Erlang/OTP is available."""
try:
@@ -160,7 +190,7 @@ class ErlangLanguageServer(SolidLanguageServer):
self.server.notify.initialized({})
# Wait for Erlang LS to be ready - adjust timeout based on environment
is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true"
is_ci = is_running_in_ci()
is_macos = os.uname().sysname == "Darwin" if hasattr(os, "uname") else False
# macOS in CI can be particularly slow for language server startup
@@ -6,6 +6,7 @@ import dataclasses
import logging
import os
import shlex
from typing import cast
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
@@ -184,7 +185,7 @@ class GroovyLanguageServer(SolidLanguageServer):
java_home_relative_path = java_dependency["java_home_path"]
java_relative_path = java_dependency["java_path"]
java_download_url = java_dependency["url"]
java_archive_type = java_dependency["archiveType"]
java_archive_type = cast(FileUtils.ArchiveType, java_dependency["archiveType"])
assert java_home_relative_path is not None
assert java_relative_path is not None
assert java_download_url is not None
+6
View File
@@ -1909,6 +1909,12 @@ class SolidLanguageServer(ABC):
NOTE: When changing the override of this method after the initial LS implementation,
be sure to also override `_document_symbols_cache_fingerprint` in order to ensure that
the caches are invalidated appropriately.
NOTE: The returned name must not contain '/', which separates the components of a name path
(see :class:`serena.symbol.NamePathMatcher`). A symbol whose name contains it cannot be
addressed by any tool taking an exact name path, not even via the name path that is
reported for the symbol itself. Language servers that identify symbols in such a way
(e.g. Erlang LS, which reports functions as `name/arity`) must substitute another
character here.
:param symbol: the symbol
:param relative_file_path: the relative path of the file the symbol is located in
+17 -5
View File
@@ -13,6 +13,7 @@ import tarfile
import tempfile
import uuid
import zipfile
from collections.abc import Sequence
from enum import Enum
from pathlib import Path, PurePath
from typing import Literal, cast
@@ -28,6 +29,15 @@ from solidlsp.util.subprocess_util import subprocess_run
log = logging.getLogger(__name__)
def is_running_in_ci() -> bool:
"""
Determines whether the current process is running in a Continuous Integration (CI) environment.
:return: True if running in CI, False otherwise
"""
return os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true"
class TextStepper:
r"""
A utility class for stepping through a text string line by line, keeping track of the current line and column numbers.
@@ -383,6 +393,8 @@ class FileUtils:
Utility functions for file operations.
"""
ArchiveType = Literal["tar", "gztar", "bztar", "xztar", "zip", "zip.gz", "gz", "binary"]
@staticmethod
def read_file(file_path: str, encoding: str) -> str:
"""
@@ -429,7 +441,7 @@ class FileUtils:
url: str,
target_path: str,
expected_sha256: str | None = None,
allowed_hosts: tuple[str, ...] | list[str] | None = None,
allowed_hosts: Sequence[str] | None = None,
) -> None:
"""
Downloads a file from ``url`` to ``target_path`` with optional integrity and host validation.
@@ -468,7 +480,7 @@ class FileUtils:
Path.unlink(Path(temp_file_path))
@staticmethod
def download_and_extract_archive(url: str, target_path: str, archive_type: str) -> None:
def download_and_extract_archive(url: str, target_path: str, archive_type: ArchiveType) -> None:
"""
Downloads the archive from the given URL having format {archive_type} and extracts it to the given {target_path}
"""
@@ -478,9 +490,9 @@ class FileUtils:
def download_and_extract_archive_verified(
url: str,
target_path: str,
archive_type: str,
archive_type: ArchiveType,
expected_sha256: str | None = None,
allowed_hosts: tuple[str, ...] | list[str] | None = None,
allowed_hosts: Sequence[str] | None = None,
) -> None:
"""
Downloads an archive from ``url`` and extracts it safely into ``target_path``.
@@ -560,7 +572,7 @@ class FileUtils:
raise SolidLSPException(f"Checksum verification failed for '{file_path}': expected {expected_sha256}, got {actual_sha256}")
@staticmethod
def _validate_download_host(url: str, allowed_hosts: tuple[str, ...] | list[str] | None) -> None:
def _validate_download_host(url: str, allowed_hosts: Sequence[str] | None) -> None:
"""
Validates that a download URL resolves to one of the configured hosts.
"""
@@ -0,0 +1,9 @@
{
"https://services.gradle.org/distributions/gradle-8.14.2-bin.zip": "7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999",
"https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage": "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251",
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-darwin-arm64-1.54.0-923.vsix": "c54c45cb0d2579d8e0a4ddeb24d4a9dd0b460d07d9366adea2b38a1da22a463c",
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-darwin-x64-1.54.0-923.vsix": "dfc98abc4e54165a78372e280242a039671729b1b03420608df3b10c6b629fb6",
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-arm64-1.54.0-923.vsix": "e2bb22c427d90da8dbb1afff72ff1e2dce38d50b76deb02d7bc313a330a1330c",
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-x64-1.54.0-923.vsix": "9d4b15da54e25a0192f9bac073f086c015397d3676623b68dbf83a5dbaf5132b",
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-win32-x64-1.54.0-923.vsix": "66f3914987edeccfee8a2558470e0fde4f8c4154232ff4baa5d73373ebc819d4"
}
+3
View File
@@ -16,6 +16,9 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
SOLIDLSP_RESOURCES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources")
@dataclass
class SolidLSPSettings:
"""
@@ -0,0 +1,105 @@
"""Normalization of Erlang's ``name/arity`` symbol identifiers.
Reproduces https://github.com/oraios/serena/issues/1797: Erlang LS names functions, types and
parameterised macros ``name/arity`` (e.g. ``create_user/4``), but ``/`` separates the components of
a Serena name path. Such a name was therefore parsed as "symbol ``4`` nested inside ``create_user``"
and could never be matched -- not even by the very name path Serena itself reported for the symbol.
In practice that made ``find_referencing_symbols``, ``replace_symbol_body`` and
``insert_after_symbol`` unusable on Erlang functions, while read-only browsing kept working, so the
language looked supported until one tried to do anything with a function.
``ErlangLanguageServer._normalize_symbol_name`` now substitutes
:data:`~solidlsp.language_servers.erlang_language_server.ARITY_SEPARATOR` for the ``/``, which makes
the reported name path round-trip back into the symbol tools.
"""
import os
import pytest
from serena.project import Project
from serena.symbol import LanguageServerSymbolRetriever
from solidlsp.language_servers.erlang_language_server import ARITY_SEPARATOR
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation
from test.conftest import language_server_tests_enabled
pytestmark = [
pytest.mark.erlang,
pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.ERLANG), reason="Erlang tests are disabled"),
]
MODELS_ERL = os.path.join("src", "models.erl")
SERVICES_ERL = os.path.join("src", "services.erl")
RECORDS_HRL = os.path.join("include", "records.hrl")
# `models:create_user/4`, spelled the way Serena addresses it
CREATE_USER_4 = f"create_user{ARITY_SEPARATOR}4"
# file and directory symbols are named after path components, so `/` is legitimate for them
CONTAINER_KINDS = (SymbolKind.File, SymbolKind.Package)
class TestErlangSymbolNames:
@pytest.mark.parametrize("project_with_ls", [LanguageServerId.ERLANG], indirect=True)
def test_no_symbol_name_contains_the_name_path_separator(self, project_with_ls: Project) -> None:
"""No Erlang symbol may carry a `/` in its name, since that is the name path separator."""
offenders: list[str] = []
def visit(symbol: UnifiedSymbolInformation) -> None:
if symbol["kind"] not in CONTAINER_KINDS and "/" in symbol["name"]:
offenders.append(symbol["name"])
for child in symbol.get("children", []):
visit(child)
for ls in project_with_ls.get_language_server_manager_or_raise().iter_language_servers():
for root in ls.request_full_symbol_tree():
visit(root)
assert not offenders, f"Symbol names containing the name path separator: {sorted(set(offenders))}"
@pytest.mark.parametrize("project_with_ls", [LanguageServerId.ERLANG], indirect=True)
def test_function_name_path_round_trips(self, project_with_ls: Project) -> None:
"""The name path reported for a function must find that same function again."""
retriever = LanguageServerSymbolRetriever(project_with_ls)
symbol = retriever.find_unique(CREATE_USER_4, within_relative_path=MODELS_ERL)
assert symbol.symbol_kind == SymbolKind.Function
assert symbol.get_name_path() == CREATE_USER_4
# feeding the reported name path back in is what used to yield no match at all
assert [s.get_name_path() for s in retriever.find(symbol.get_name_path())] == [CREATE_USER_4]
@pytest.mark.parametrize("project_with_ls", [LanguageServerId.ERLANG], indirect=True)
def test_arity_remains_part_of_the_name(self, project_with_ls: Project) -> None:
"""The arity is kept rather than stripped: it is part of a function's identity in Erlang."""
retriever = LanguageServerSymbolRetriever(project_with_ls)
# models:create_order/3 and services:create_order/2 are different functions
models_create_order = retriever.find_unique(f"create_order{ARITY_SEPARATOR}3")
services_create_order = retriever.find_unique(f"create_order{ARITY_SEPARATOR}2")
assert models_create_order.relative_path is not None
assert models_create_order.relative_path.replace("\\", "/") == "src/models.erl"
assert services_create_order.relative_path is not None
assert services_create_order.relative_path.replace("\\", "/") == "src/services.erl"
@pytest.mark.parametrize("project_with_ls", [LanguageServerId.ERLANG], indirect=True)
def test_find_referencing_symbols_locates_a_function(self, project_with_ls: Project) -> None:
"""The headline symptom of #1797: this used to raise `No symbol matching ...`."""
retriever = LanguageServerSymbolRetriever(project_with_ls)
references = retriever.find_referencing_symbols(CREATE_USER_4, MODELS_ERL)
# create_user/4 is called from models.erl itself and from at least one other module
referencing_paths = {r.symbol.relative_path.replace("\\", "/") for r in references if r.symbol.relative_path is not None}
assert referencing_paths, f"Expected references to {CREATE_USER_4}, got none"
assert referencing_paths - {"src/models.erl"}, f"Expected cross-file references, got only {referencing_paths}"
@pytest.mark.parametrize("project_with_ls", [LanguageServerId.ERLANG], indirect=True)
def test_names_without_an_arity_are_untouched(self, project_with_ls: Project) -> None:
"""Records and macros have no arity, so normalization must leave their names alone."""
retriever = LanguageServerSymbolRetriever(project_with_ls)
user_record = retriever.find_unique("user", within_relative_path=RECORDS_HRL)
assert user_record.name == "user"
@@ -545,9 +545,7 @@ class TestComputeWorkspaceHash:
UPSTREAM_LAUNCHER = "/opt/homebrew/Cellar/jdtls/1.50.0/libexec/plugins/org.eclipse.equinox.launcher_1.7.0.jar"
def _initial_settings(self) -> "SolidLSPSettings.CustomLSSettings":
from solidlsp.language_servers.eclipse_jdtls import INITIAL_VSCODE_JAVA_VERSION
return SolidLSPSettings.CustomLSSettings({"vscode_java_version": INITIAL_VSCODE_JAVA_VERSION})
return SolidLSPSettings.CustomLSSettings({"vscode_java_version": EclipseJDTLS.DependencyProvider.INITIAL_VSCODE_JAVA_VERSION})
def test_initial_default_mode_matches_pre_upstream_format(self) -> None:
"""Legacy carve-out: INITIAL default-mode hash MUST equal md5(repository_root_path)."""