mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-11 03:13:51 +00:00
Merge remote-tracking branch 'origin/main' into ls-notify-on-reopen-changed
Conflicts: CHANGELOG.md
This commit is contained in:
commit
0720dd92e0
16 files changed
+553
-236
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)."""
|
||||
|
||||
Reference in new issue
Block a user