mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 20:15:56 +00:00
Merge remote-tracking branch 'origin/Main' into memory-cleanup
This commit is contained in:
commit
91ebba1c8c
20 files changed
+997
-218
No files matched your search
@@ -706,7 +706,11 @@ jobs:
|
||||
id: cache-language-servers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.serena/language_servers/static
|
||||
# JDTLS workspaces contain mutable Eclipse/Buildship runtime state.
|
||||
# Cache the installed servers, but create each JDTLS workspace locally.
|
||||
path: |
|
||||
~/.serena/language_servers/static
|
||||
!~/.serena/language_servers/static/EclipseJDTLS/workspaces/**
|
||||
key: language-servers-${{ runner.os }}-${{ matrix.batch }}-v1
|
||||
restore-keys: |
|
||||
language-servers-${{ runner.os }}-${{ matrix.batch }}-
|
||||
|
||||
+14
-1
@@ -3,7 +3,7 @@
|
||||
Status of the `main` branch. Changes prior to the next official version change will appear here.
|
||||
|
||||
* General:
|
||||
- Fix: the README, the Language Support docs page and the project template omitted several already-supported language servers
|
||||
- Fix: Race conditions in ProjectServer when used by multiple clients in parallel
|
||||
- Fix: a tool call exceeding the timeout blocked the task executor indefinitely; the executor now
|
||||
recovers without user-induced cancellation
|
||||
- Add Grok Build support (context `grok`, setup CLI, hooks)
|
||||
@@ -15,6 +15,8 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
failed `--project-from-cwd` auto-detection (#1773).
|
||||
- ProjectServer: Configure trusted hosts (local hosts only) when listening on localhost
|
||||
- SerenaDashboardTrayManager: Configure trusted hosts (local hosts only)
|
||||
- Enclose sub-prompts in XML-like tags to make scopes explicit
|
||||
- Allow initial project prompts and project-specific newly activated modes to use templating
|
||||
|
||||
* CLI:
|
||||
- Fix: `start-mcp-server` help text for `--project-from-cwd` falsely promised a fallback to the CWD, which was
|
||||
@@ -39,6 +41,10 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Allow language server priorities to be configured in `serena_config.yml` (for auto-detection during
|
||||
project creation)
|
||||
- Add `python_basedpyright` as an alternative Python language server
|
||||
- Java/JDTLS: stop downloading and loading the unused IntelliCode completion-ranking bundle; the retired
|
||||
`intellicode_version`, `intellicode_xmx` and `intellicode_xms` settings remain accepted but are ignored #1821
|
||||
- Kotlin: update the managed Kotlin LSP from `261.13587.0` to `262.9593.0`, including support for the
|
||||
new platform-specific archive layout and Windows ARM64 builds
|
||||
- Nix/nixd: support custom `ls_path` launchers and external JSON settings through `config_path` #1737
|
||||
- Fix: Nix/nixd diagnostics now use published diagnostics instead of the unsupported
|
||||
`textDocument/diagnostic` request, which terminated nixd #1802
|
||||
@@ -83,6 +89,13 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Language servers and their dependency providers now go through the `subprocess_run` helper instead of
|
||||
calling `subprocess.run` directly (e.g. for installation processes), so all such subprocesses get
|
||||
`stdin=DEVNULL` and can no longer interfere with the stdio MCP connection #1748
|
||||
- `scala`: Fix: Metals asks via `window/showMessageRequest` whether to import a workspace it has not
|
||||
seen before, and Serena had no handler, so the request failed with `MethodNotFound` and Metals gave
|
||||
up on the import ("Unexpected error initializing server"). No build server was ever connected and
|
||||
every cross-file query fell back to the presentation compiler, which sees one file at a time, unless
|
||||
the project happened to have been imported beforehand by another editor. The three prompts that lead
|
||||
to a build server are now answered; anything else is dismissed, including the choice between several
|
||||
build definitions in one workspace. `ls_specific_settings.scala.auto_import_build: false` opts out
|
||||
|
||||
* Dashboard:
|
||||
- Fix: Serena PyPI version check triggered by callback on main thread could delay agent startup #1774
|
||||
|
||||
@@ -147,7 +147,7 @@ Some languages require additional installations or setup steps, as noted.
|
||||
* **Rust**
|
||||
(requires [rustup](https://rustup.rs/) - uses rust-analyzer from your toolchain)
|
||||
* **Scala**
|
||||
(requires some [manual setup](../03-special-guides/scala_setup_guide_for_serena); uses Metals LSP)
|
||||
(uses Metals LSP, which imports the build on first use — see the [setup guide](../03-special-guides/scala_setup_guide_for_serena))
|
||||
* **SCSS / Sass / CSS**
|
||||
(experimental; requires Node.js + npm; uses [some-sass-language-server](https://github.com/wkillerud/some-sass) to handle
|
||||
`.scss`, `.sass`, and `.css`)
|
||||
|
||||
@@ -666,8 +666,7 @@ Supported settings:
|
||||
Java support has two installation modes:
|
||||
|
||||
1. **Default vscode-java VSIX mode** (no extra config required): Serena downloads the platform-specific
|
||||
vscode-java VSIX (~500 MB: JDTLS + bundled JRE 21 + Lombok + IntelliCode), Gradle distribution and
|
||||
IntelliCode VSIX from public hosts on first use.
|
||||
vscode-java VSIX (JDTLS + bundled JRE 21 + Lombok) and a Gradle distribution from public hosts on first use.
|
||||
2. **Upstream JDTLS mode** (offline-friendly): Activated by setting both `jdtls_path` and `lombok_path`.
|
||||
Uses an existing JDTLS installation (~100 MB) and the system JDK 21+. Nothing is downloaded.
|
||||
Recommended for restricted-network/corporate environments.
|
||||
@@ -677,9 +676,8 @@ Java support has two installation modes:
|
||||
- **Default vscode-java VSIX mode** — recommended for most users. No setup required;
|
||||
Serena downloads everything on first use.
|
||||
- **Upstream JDTLS mode** — recommended when:
|
||||
- you cannot reach `github.com`, `services.gradle.org` or `marketplace.visualstudio.com`
|
||||
from the host (corporate proxy, air-gapped network);
|
||||
- you want a smaller on-disk footprint (~100 MB vs ~500 MB);
|
||||
- you cannot reach `github.com` or `services.gradle.org` from the host (corporate proxy, air-gapped network);
|
||||
- you want a smaller on-disk footprint;
|
||||
- you already maintain a JDTLS installation (e.g. for `nvim-jdtls` or another editor);
|
||||
- your security policy prohibits per-project runtime downloads.
|
||||
|
||||
@@ -702,23 +700,19 @@ The following settings are supported for the Java language server:
|
||||
| `runtimes` | `[]` | Extra JRE/JDK entries registered with JDT-LS via `java.configuration.runtimes`. Use this when a project's source/target level exceeds the JDK JDT-LS itself runs on (currently JDK 21 in default vscode-java VSIX mode). Each entry is a mapping with required `name` (e.g. `JavaSE-25`, matching the `JavaSE-NN` container the build tool requests) and `path` (JDK/JRE home directory; must exist), plus optional `default`, `sources`, and `javadoc` (passed through to JDT-LS). Entries extend rather than replace the bundled `JavaSE-21` runtime; an entry that reuses the `JavaSE-21` name overrides the bundled one. Changing this setting invalidates the JDTLS workspace hash so a fresh import is performed. |
|
||||
| `gradle_version` | `8.14.2` | (vscode-java mode only) Override the Gradle distribution version Serena downloads by default. |
|
||||
| `vscode_java_version` | `1.54.0-923` | (vscode-java mode only) Override the bundled `vscode-java` runtime bundle version Serena downloads by default. |
|
||||
| `intellicode_version` | `1.2.30` | (vscode-java mode only) Override the IntelliCode VSIX version Serena downloads by default. |
|
||||
| `lombok_show_generated` | `true` | Show Lombok-generated methods (`getX/setX`, `builder()`, `equals/hashCode/toString`, `withX`, fluent accessors) in `find_symbol`, `get_symbols_overview` and the symbol-edit tools. Set to `false` to restore the previous JDTLS default and hide the synthetic methods (e.g. when `@Data` classes pollute the outline with too many getters/setters). Requires JDTLS commit `b2d8952` / `vscode-java >= 1.53.0`; the bundled default already meets this. |
|
||||
| `jdtls_xmx` | `3G` | Maximum heap size for the JDTLS server JVM. |
|
||||
| `jdtls_xms` | `100m` | Initial heap size for the JDTLS server JVM. |
|
||||
| `intellicode_xmx` | `1G` | (vscode-java mode only) Maximum heap size for the IntelliCode embedded JVM. |
|
||||
| `intellicode_xms` | `100m` | (vscode-java mode only) Initial heap size for the IntelliCode embedded JVM. |
|
||||
|
||||
Notes:
|
||||
- When overriding `vscode_java_version`, Serena still assumes that the downloaded runtime bundle keeps the same internal
|
||||
directory layout and file names as the bundled default version.
|
||||
- In upstream-jdtls mode, IntelliCode is not loaded (it's an ML completions ranker that is irrelevant to Serena's
|
||||
symbol-tools workflow), and Serena does not ship a Gradle distribution. Maven projects work via JDTLS's bundled m2e.
|
||||
- Serena does not download or load IntelliCode because its completion ranking is not used by Serena's tools. The retired
|
||||
`intellicode_version`, `intellicode_xmx` and `intellicode_xms` keys remain accepted and ignored so existing
|
||||
configurations continue to load; they can be removed.
|
||||
- In upstream-jdtls mode Serena does not ship a Gradle distribution. Maven projects work via JDTLS's bundled m2e.
|
||||
Gradle projects must have `./gradlew` in the project, or rely on a system-installed Gradle through Buildship's
|
||||
default discovery rules.
|
||||
- In upstream-jdtls mode the `gradle_version`, `vscode_java_version`, `intellicode_version`,
|
||||
`intellicode_xmx`, `intellicode_xms` settings are silently ignored — they only apply to the
|
||||
vscode-java VSIX mode.
|
||||
default discovery rules. The `gradle_version` and `vscode_java_version` settings are silently ignored in this mode.
|
||||
- Without `runtimes`, JDT-LS only knows about the bundled `JavaSE-21` JRE. Projects that request a newer
|
||||
container (e.g. `sourceCompatibility = JavaVersion.VERSION_25`) then fail to resolve JDK types such as
|
||||
`java.lang.Object`. Register the matching installed JDK via `runtimes` instead of symlinking over Serena's
|
||||
@@ -765,15 +759,20 @@ Supported settings:
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `ls_path` | managed download | Override the Kotlin Language Server executable path. |
|
||||
| `kotlin_lsp_version` | `261.13587.0` | Override the Kotlin Language Server version Serena downloads when `ls_path` is not set. |
|
||||
| `kotlin_lsp_version` | `262.9593.0` | Override the Kotlin Language Server version Serena downloads when `ls_path` is not set. |
|
||||
| `jvm_options` | `-Xmx2G` | Value assigned to `JAVA_TOOL_OPTIONS` for the Kotlin LS process. Set to `""` to disable JVM options entirely. |
|
||||
|
||||
The managed `262.9593.0` packages include a bundled JBR. For a custom `ls_path`, point directly to
|
||||
`bin/intellij-server` (`bin/intellij-server.exe` on Windows). Serena also retains the legacy download
|
||||
layout for custom Kotlin LSP versions older than `262.4739.0`. The pinned current and frozen initial
|
||||
releases are checksum-verified; arbitrary custom versions are downloaded without checksum verification.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
kotlin:
|
||||
kotlin_lsp_version: "261.13587.0"
|
||||
kotlin_lsp_version: "262.9593.0"
|
||||
jvm_options: "-Xmx4G -XX:+UseG1GC"
|
||||
```
|
||||
|
||||
|
||||
@@ -15,7 +15,16 @@ Install the following on your system and ensure they are available on `PATH`:
|
||||
- Serena uses `cs` if available; if only `coursier` exists, it will attempt to install `cs`. If neither is present, install Coursier first.
|
||||
|
||||
---
|
||||
## Quick Start (Recommended: VS Code + Metals auto‑import)
|
||||
## Quick Start
|
||||
|
||||
Start Serena in your project root. Metals asks whether to import a workspace it has not seen before, and Serena answers that prompt for it — the build is imported (for sbt, by running `sbt bloopInstall`), `.bloop/` and `.metals/` are created, and cross-file navigation works from there. The first run therefore takes as long as your build takes to load.
|
||||
|
||||
Set `auto_import_build: false` under `ls_specific_settings.scala` to decline instead; you then need to import the build yourself by one of the routes below, or cross-file queries will be served by the fallback presentation compiler and see only one file at a time.
|
||||
|
||||
Serena answers three of Metals' prompts — “Import build”, “Import changes”, “Connect”. Anything else Metals asks is dismissed and logged, including “Multiple build definitions found. Which would you like to use?”, so a workspace holding more than one kind of build (say both an sbt and a Maven definition) still needs importing by one of the routes below.
|
||||
|
||||
---
|
||||
## Importing the build yourself (VS Code)
|
||||
|
||||
1. Open your Scala project in VS Code.
|
||||
2. When prompted by Metals, accept “Import build”. Wait until the import and initial compile/indexing finish.
|
||||
@@ -25,7 +34,7 @@ Install the following on your system and ensure they are available on `PATH`:
|
||||
This flow ensures the `.bloop/` and (if applicable) `.metals/` directories are created and your build is known to the build server that Metals uses.
|
||||
|
||||
---
|
||||
## Manual Setup (No VS Code)
|
||||
## Importing the build yourself (No VS Code)
|
||||
|
||||
Follow these steps if you prefer a manual setup or you are not using VS Code:
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from sensai.util import logging
|
||||
|
||||
from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS
|
||||
from solidlsp.language_servers.kotlin_language_server import KotlinLanguageServer
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.configure()
|
||||
EclipseJDTLS.DependencyProvider.update_dep_hashes()
|
||||
KotlinLanguageServer.DependencyProvider.update_dep_hashes()
|
||||
+26
-17
@@ -956,14 +956,24 @@ class SerenaAgent:
|
||||
result[tool_class.get_name_from_cls()] = new_tool_class.get_name_from_cls()
|
||||
return result
|
||||
|
||||
def _format_prompt(self, prompt_template: str) -> str:
|
||||
def _format_prompt_tag(self, text: str, tag: str, tag_name_attr: str | None = None) -> str:
|
||||
open_tag = f"<{tag}" + (f' name="{tag_name_attr}"' if tag_name_attr is not None else "") + ">"
|
||||
close_tag = f"</{tag}>"
|
||||
return f"{open_tag}\n{text.strip()}\n{close_tag}"
|
||||
|
||||
def _format_prompt(self, prompt_template: str, tag: str | None = None, tag_name_attr: str | None = None) -> str:
|
||||
template = JinjaTemplate(prompt_template)
|
||||
return template.render(
|
||||
text = template.render(
|
||||
available_tools=self._exposed_tools.tool_names,
|
||||
available_markers=self._exposed_tools.tool_marker_names,
|
||||
tool_names=self._prompt_tool_names_mapping,
|
||||
)
|
||||
|
||||
if tag is not None:
|
||||
text = self._format_prompt_tag(text, tag=tag, tag_name_attr=tag_name_attr)
|
||||
|
||||
return text
|
||||
|
||||
def create_connection_prompt(self) -> str:
|
||||
"""
|
||||
Returns the bootstrap prompt to be sent at MCP connection time.
|
||||
@@ -997,8 +1007,8 @@ class SerenaAgent:
|
||||
self._project_prompt_status.mark_mode_prompts_as_provided(session_id)
|
||||
|
||||
system_prompt = self.prompt_factory.create_system_prompt(
|
||||
context_system_prompt=self._format_prompt(self._context.prompt),
|
||||
mode_system_prompts=[self._format_prompt(mode.prompt) for mode in relevant_modes],
|
||||
context_system_prompt=self._format_prompt(self._context.prompt, tag="context"),
|
||||
mode_system_prompts=[self._format_prompt(mode.prompt, tag="mode", tag_name_attr=mode.name) for mode in relevant_modes],
|
||||
available_tools=available_tools.tool_names,
|
||||
available_markers=available_markers,
|
||||
global_memories_list=global_memories_str,
|
||||
@@ -1007,11 +1017,11 @@ class SerenaAgent:
|
||||
|
||||
# provide the project activation message if it hasn't yet been provided
|
||||
if self._active_project is not None and not self._project_prompt_status.is_project_activation_message_already_provided(session_id):
|
||||
system_prompt += "\n\n" + self.get_project_activation_message(session_id)
|
||||
system_prompt += "\n\n" + self._format_prompt_tag(self.get_project_activation_message(session_id), tag="active-project")
|
||||
elif self._project_activation_error:
|
||||
system_prompt += f"\n\nNo project is active ({self._project_activation_error})."
|
||||
|
||||
return system_prompt
|
||||
return self._format_prompt_tag(system_prompt, tag="serena")
|
||||
|
||||
def get_project_activation_message(self, session_id: str) -> str:
|
||||
"""
|
||||
@@ -1031,13 +1041,13 @@ class SerenaAgent:
|
||||
|
||||
# provide basic project information (name, location, languages, encoding)
|
||||
if proj.is_newly_created:
|
||||
msg = f"Created and activated a new project with name '{proj.project_name}' at {proj.project_root}. "
|
||||
msg = f"Created and activated a new project with name '{proj.project_name}' at {proj.project_root}.\n"
|
||||
else:
|
||||
msg = f"The project with name '{proj.project_name}' at {proj.project_root} is activated."
|
||||
msg = f"The project with name '{proj.project_name}' at {proj.project_root} is activated.\n"
|
||||
if self._language_backend == LanguageBackend.LSP:
|
||||
languages_str = ", ".join([lang.value for lang in proj.project_config.language_servers])
|
||||
msg += f"\nProgramming languages: {languages_str}."
|
||||
msg += f"File encoding: {proj.project_config.encoding}."
|
||||
language_servers_str = ", ".join([ls.value for ls in proj.project_config.language_servers])
|
||||
msg += f"Active language servers: {language_servers_str}.\n"
|
||||
msg += f"File encoding: {proj.project_config.encoding}.\n"
|
||||
|
||||
# add list of memories (if memories are enabled)
|
||||
include_memories = self._active_tools.contains_tool_class(ReadMemoryTool)
|
||||
@@ -1045,23 +1055,22 @@ class SerenaAgent:
|
||||
project_memories = proj.memory_manager.list_project_memories()
|
||||
if project_memories:
|
||||
msg += (
|
||||
f"\n{json.dumps(project_memories.to_dict())}\n"
|
||||
+ "Use the `read_memory` tool to read these memories later if they are relevant to the task."
|
||||
f"{json.dumps(project_memories.to_dict())}\n"
|
||||
+ f"Use the `{ReadMemoryTool.get_name_from_cls()}` tool to read these memories later if they are relevant to the task.\n"
|
||||
)
|
||||
elif self._active_tools.contains_tool_class(OnboardingTool):
|
||||
msg += "Onboarding has not been performed yet, you should call Serena's `onboarding` tool now to set up project memories."
|
||||
msg += f"Onboarding has not been performed yet. Ask the user whether to perform onboarding via the `{OnboardingTool.get_name_from_cls()}` tool.\n"
|
||||
|
||||
# add prompts for modes that were dynamically activated by the project
|
||||
modes_with_prompts = self._project_prompt_status.get_modes_with_prompts_to_be_provided_for_project_activation(session_id)
|
||||
if modes_with_prompts:
|
||||
msg += "\nNewly applicable mode instructions:"
|
||||
for mode in modes_with_prompts:
|
||||
msg += f"\n{mode.prompt}"
|
||||
msg += self._format_prompt(mode.prompt, tag="mode", tag_name_attr=mode.name) + "\n"
|
||||
self._project_prompt_status.mark_mode_prompts_as_provided(session_id)
|
||||
|
||||
# add project-specific prompt
|
||||
if proj.project_config.initial_prompt:
|
||||
msg += f"\nProject-specific instructions:\n {proj.project_config.initial_prompt}"
|
||||
msg += "\n" + self._format_prompt(proj.project_config.initial_prompt, tag="project-instructions")
|
||||
|
||||
self._project_prompt_status.mark_project_activation_message_as_provided(session_id)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import requests as requests_lib
|
||||
@@ -54,13 +55,14 @@ class ProjectServer:
|
||||
if port is None:
|
||||
port = self.PORT
|
||||
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.gui_log_window = False
|
||||
serena_config.web_dashboard = False
|
||||
serena_config = SerenaConfig.from_config_file().with_headless_mode_overrides()
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
|
||||
self._agent = SerenaAgent(serena_config=serena_config)
|
||||
self._loaded_projects_by_root: dict[str, "Project"] = {}
|
||||
self._project_load_locks_by_root: dict[str, threading.Lock] = {}
|
||||
self._active_project_lock = threading.Lock()
|
||||
self._loaded_projects_lock = threading.Lock()
|
||||
self._port = port
|
||||
self._host = host
|
||||
|
||||
@@ -91,19 +93,41 @@ class ProjectServer:
|
||||
|
||||
key = str(registered_project.project_root)
|
||||
|
||||
if key in self._loaded_projects_by_root:
|
||||
return self._loaded_projects_by_root[key]
|
||||
# find or publish the per-project load lock while holding the shared dictionaries
|
||||
with self._loaded_projects_lock:
|
||||
project = self._loaded_projects_by_root.get(key)
|
||||
if project is not None:
|
||||
return project
|
||||
project_load_lock = self._project_load_locks_by_root.get(key)
|
||||
if project_load_lock is None:
|
||||
project_load_lock = threading.Lock()
|
||||
self._project_load_locks_by_root[key] = project_load_lock
|
||||
|
||||
with LogTime(f"Loading project '{project_root_or_name}'"):
|
||||
project = registered_project.get_project_instance(serena_config)
|
||||
project.create_language_server_manager()
|
||||
self._loaded_projects_by_root[key] = project
|
||||
return project
|
||||
# initialize only this project; another project's cached lookup or cold load can proceed
|
||||
with project_load_lock:
|
||||
with self._loaded_projects_lock:
|
||||
project = self._loaded_projects_by_root.get(key)
|
||||
if project is not None:
|
||||
return project
|
||||
|
||||
with LogTime(f"Loading project '{project_root_or_name}'"):
|
||||
project = registered_project.get_project_instance(serena_config)
|
||||
project.create_language_server_manager()
|
||||
|
||||
with self._loaded_projects_lock:
|
||||
self._loaded_projects_by_root[key] = project
|
||||
return project
|
||||
|
||||
def _query_project(self, req: QueryProjectRequest) -> str:
|
||||
"""Handle a /query_project request by invoking the agent on the specified project and tool."""
|
||||
"""Handle a /query_project request by invoking the agent on the specified project and tool.
|
||||
|
||||
The active project is process-wide state, whereas ``apply_ex`` runs the tool on the
|
||||
agent's task executor thread. Without the lock, a second request entering
|
||||
``active_project_context`` while the first request's tool is still executing would
|
||||
redirect that tool to the wrong project (and restore the wrong project afterwards).
|
||||
"""
|
||||
project = self._get_project(req.project_name)
|
||||
with self._agent.active_project_context(project):
|
||||
with self._active_project_lock, self._agent.active_project_context(project):
|
||||
tool = self._agent.get_tool_by_name(req.tool_name)
|
||||
if not tool.is_readonly():
|
||||
raise ValueError(f"Tool '{req.tool_name}' is not read-only and cannot be executed via the query_project route")
|
||||
|
||||
@@ -49,10 +49,8 @@ prompts:
|
||||
The context and modes of operation are described below. These determine how to interact with your user
|
||||
and which kinds of interactions are expected of you.
|
||||
|
||||
Context description:
|
||||
{{ context_system_prompt }}
|
||||
|
||||
Modes descriptions:
|
||||
{% for prompt in mode_system_prompts %}
|
||||
{{ prompt }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -35,11 +35,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
GRADLE_ALLOWED_HOSTS = ("services.gradle.org", "github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
|
||||
VSCODE_JAVA_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
|
||||
INTELLICODE_ALLOWED_HOSTS = (
|
||||
"visualstudioexptteam.gallery.vsassets.io",
|
||||
"marketplace.visualstudio.com",
|
||||
"download.visualstudio.microsoft.com",
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -77,10 +72,9 @@ class RuntimeDependencyPaths:
|
||||
"""
|
||||
Stores the paths to the runtime dependencies of EclipseJDTLS.
|
||||
|
||||
In the default mode (vscode-java VSIX), all paths are populated.
|
||||
In the upstream-jdtls mode (when ``jdtls_path`` and ``lombok_path`` are set),
|
||||
fields that have no upstream equivalent (gradle distribution, IntelliCode bundle)
|
||||
are set to None.
|
||||
In the default mode (vscode-java VSIX), all paths are populated. In the
|
||||
upstream-jdtls mode (when ``jdtls_path`` and ``lombok_path`` are set), the
|
||||
Gradle distribution path is ``None`` because Serena does not download it.
|
||||
"""
|
||||
|
||||
jre_path: str
|
||||
@@ -89,8 +83,6 @@ class RuntimeDependencyPaths:
|
||||
jdtls_readonly_config_path: str
|
||||
lombok_jar_path: str
|
||||
gradle_path: str | None = None
|
||||
intellicode_jar_path: str | None = None
|
||||
intellisense_members_path: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -113,8 +105,8 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
Two installation modes are supported:
|
||||
|
||||
1. **Default vscode-java VSIX mode** (no extra config required) — Serena downloads the platform-specific
|
||||
vscode-java VSIX bundle (~500 MB: JDTLS + bundled JRE 21 + Lombok + IntelliCode), Gradle distribution
|
||||
and IntelliCode VSIX from public hosts. Suitable when public network access is available.
|
||||
vscode-java VSIX bundle (JDTLS + bundled JRE 21 + Lombok) and Gradle distribution from public hosts.
|
||||
Suitable when public network access is available.
|
||||
|
||||
2. **Upstream JDTLS mode** (activated by setting both ``jdtls_path`` and ``lombok_path``) — uses an
|
||||
existing JDTLS installation and the system JDK. Nothing is downloaded. Suitable for restricted-network
|
||||
@@ -143,8 +135,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
gradle_java_home is unset, Gradle import (default: false)
|
||||
- jdtls_xmx: Maximum heap size for the JDTLS server JVM (default: "3G")
|
||||
- jdtls_xms: Initial heap size for the JDTLS server JVM (default: "100m")
|
||||
- intellicode_xmx: Maximum heap size for the IntelliCode embedded JVM (default: "1G")
|
||||
- intellicode_xms: Initial heap size for the IntelliCode embedded JVM (default: "100m")
|
||||
- lombok_show_generated: Show Lombok-generated methods (getX/setX/builder()/...) in document
|
||||
symbols by sending java.symbols.includeGeneratedCode=true to JDTLS (default: true).
|
||||
Set to false for @Data-heavy projects where the extra getters/setters are noise.
|
||||
@@ -155,7 +145,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
Pinned versions: "1.54.0-923" (default) and "1.42.0-561" (legacy / initial). Other versions
|
||||
are not supported in default VSIX mode (the resource paths inside the archive change between
|
||||
releases); use upstream-jdtls mode for arbitrary versions.
|
||||
- intellicode_version: Override the pinned IntelliCode VSIX version downloaded by Serena
|
||||
- runtimes: Additional JRE/JDK entries to register with JDT-LS's ``java.configuration.runtimes``,
|
||||
for projects whose source/target level exceeds the JDK JDT-LS itself runs on (currently
|
||||
JDK 21 in default vscode-java VSIX mode). Each entry is a mapping with:
|
||||
@@ -190,12 +179,9 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
use_system_java_home: true # set to true to use system JAVA_HOME for JDTLS and Gradle fallback
|
||||
jdtls_xmx: "3G" # maximum heap size for the JDTLS server JVM
|
||||
jdtls_xms: "100m" # initial heap size for the JDTLS server JVM
|
||||
intellicode_xmx: "1G" # maximum heap size for the IntelliCode embedded JVM
|
||||
intellicode_xms: "100m" # initial heap size for the IntelliCode embedded JVM
|
||||
lombok_show_generated: true # show Lombok-generated methods in document symbols (default true)
|
||||
gradle_version: "8.14.2"
|
||||
vscode_java_version: "1.54.0-923" # also accepts pinned legacy "1.42.0-561"
|
||||
intellicode_version: "1.2.30"
|
||||
runtimes: # register additional JDKs for projects targeting a newer Java version
|
||||
- name: "JavaSE-25"
|
||||
path: "/home/user/Java/jdk25"
|
||||
@@ -220,7 +206,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
|
||||
self._service_ready_event = threading.Event()
|
||||
self._project_ready_event = threading.Event()
|
||||
self._intellicode_enable_command_available = threading.Event()
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
ls_resources_dir = self.ls_resources_dir(self._solidlsp_settings)
|
||||
@@ -258,7 +243,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
|
||||
# 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",
|
||||
@@ -270,7 +254,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
# 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",
|
||||
@@ -320,19 +303,9 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
)
|
||||
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())
|
||||
deps = [cls._create_dep_gradle()] + list(cls._create_deps_vscode_java().values())
|
||||
with DownloadedDependencyHashDatabase.get_instance().update_context() as db:
|
||||
for dep in deps:
|
||||
db.update(dep)
|
||||
@@ -351,8 +324,8 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
``brew install jdtls`` or extracted ``jdt-language-server-*.tar.gz``) and the system JDK.
|
||||
Nothing is downloaded by Serena. Suitable for restricted-network/corporate environments.
|
||||
* **Default vscode-java VSIX mode** (activated otherwise): downloads the platform-specific
|
||||
vscode-java VSIX bundle (containing JDTLS, bundled JRE 21, Lombok, IntelliCode), Gradle
|
||||
distribution and IntelliCode VSIX from public hosts. Original behaviour, unchanged.
|
||||
vscode-java VSIX bundle (containing JDTLS, bundled JRE 21 and Lombok) and a Gradle
|
||||
distribution from public hosts.
|
||||
"""
|
||||
jdtls_path = custom_settings.get("jdtls_path")
|
||||
lombok_path = custom_settings.get("lombok_path")
|
||||
@@ -369,16 +342,12 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
platformId = PlatformUtils.get_platform_id()
|
||||
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 == cls.INITIAL_VSCODE_JAVA_VERSION else f"vscode-java-{vscode_java_version}"
|
||||
)
|
||||
intellicode_dirname = (
|
||||
"intellicode" if intellicode_version == cls.INITIAL_INTELLICODE_VERSION else f"intellicode-{intellicode_version}"
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -478,26 +447,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
assert os.path.exists(jdtls_launcher_jar_path)
|
||||
assert os.path.exists(jdtls_readonly_config_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, "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),
|
||||
os.path.exists(intellicode_jar_path),
|
||||
os.path.exists(intellisense_members_path),
|
||||
]
|
||||
):
|
||||
intellicode_dep.download_to(intellicode_directory_path)
|
||||
|
||||
assert os.path.exists(intellicode_directory_path)
|
||||
assert os.path.exists(intellicode_jar_path)
|
||||
assert os.path.exists(intellisense_members_path)
|
||||
|
||||
return RuntimeDependencyPaths(
|
||||
gradle_path=gradle_path,
|
||||
lombok_jar_path=lombok_jar_path,
|
||||
@@ -505,8 +454,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
jre_home_path=jre_home_path,
|
||||
jdtls_launcher_jar_path=jdtls_launcher_jar_path,
|
||||
jdtls_readonly_config_path=jdtls_readonly_config_path,
|
||||
intellicode_jar_path=intellicode_jar_path,
|
||||
intellisense_members_path=intellisense_members_path,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -520,7 +467,7 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
:param jdtls_path: absolute path to the JDTLS root (containing ``plugins/`` and ``config_<platform>/``)
|
||||
:param lombok_path: absolute path to the Lombok jar (mandatory; agent is always attached)
|
||||
:param custom_settings: language-server-specific settings from ls_specific_settings.java
|
||||
:return: populated RuntimeDependencyPaths with gradle/intellicode fields set to None
|
||||
:return: populated RuntimeDependencyPaths with ``gradle_path`` set to ``None``
|
||||
"""
|
||||
# validate jdtls_path structure (root + plugins dir)
|
||||
jdtls_root = Path(jdtls_path)
|
||||
@@ -569,8 +516,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
jdtls_readonly_config_path=str(config_dir),
|
||||
lombok_jar_path=lombok_path,
|
||||
gradle_path=None,
|
||||
intellicode_jar_path=None,
|
||||
intellisense_members_path=None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -1006,10 +951,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
gradle_user_home = None
|
||||
log.info(f"Gradle user home not found at default location ({default_gradle_home}), will use JDTLS defaults")
|
||||
|
||||
# IntelliCode JVM settings (used in vmargs for the embedded JVM)
|
||||
intellicode_xmx = self._custom_settings.get("intellicode_xmx", "1G")
|
||||
intellicode_xms = self._custom_settings.get("intellicode_xms", "100m")
|
||||
|
||||
# Lombok-generated symbols (getX/setX/builder()/equals/hashCode/toString/...): JDTLS filters
|
||||
# these out of documentSymbol results by default. Without them, find_symbol/get_symbols_overview
|
||||
# return only user-written sources, which breaks navigation around @Data/@Builder/@Getter/@Setter
|
||||
@@ -1194,19 +1135,10 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}},
|
||||
},
|
||||
"initializationOptions": {
|
||||
"bundles": ["intellicode-core.jar"],
|
||||
"bundles": [],
|
||||
"settings": {
|
||||
"java": {
|
||||
"home": None,
|
||||
"jdt": {
|
||||
"ls": {
|
||||
"java": {"home": None},
|
||||
"vmargs": f"-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx{intellicode_xmx} -Xms{intellicode_xms} -Xlog:disable",
|
||||
"lombokSupport": {"enabled": True},
|
||||
"protobufSupport": {"enabled": True},
|
||||
"androidSupport": {"enabled": True},
|
||||
}
|
||||
},
|
||||
"errors": {"incompleteClasspath": {"severity": "error"}},
|
||||
"configuration": {
|
||||
"checkProjectSettingsExclusions": False,
|
||||
@@ -1326,14 +1258,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
|
||||
initialize_params["initializationOptions"]["workspaceFolders"] = [repo_uri]
|
||||
|
||||
# IntelliCode bundle: only attached in default vscode-java VSIX mode.
|
||||
# In upstream-jdtls mode (jdtls_path set) we don't ship IntelliCode — agentic Serena workflows
|
||||
# don't use completion ranking, so the bundle would be inert dead weight.
|
||||
if self.runtime_dependency_paths.intellicode_jar_path is not None:
|
||||
initialize_params["initializationOptions"]["bundles"] = [self.runtime_dependency_paths.intellicode_jar_path]
|
||||
else:
|
||||
initialize_params["initializationOptions"]["bundles"] = []
|
||||
|
||||
# merge the bundled JRE with any additional runtimes configured via ls_specific_settings.java.runtimes
|
||||
# (e.g. so projects targeting a newer Java version than the bundled JRE resolve their JRE container)...
|
||||
default_runtime = {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True}
|
||||
@@ -1378,9 +1302,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"*",
|
||||
" ",
|
||||
]
|
||||
if registration["method"] == "workspace/executeCommand":
|
||||
if "java.intellicode.enable" in registration["registerOptions"]["commands"]:
|
||||
self._intellicode_enable_command_available.set()
|
||||
return
|
||||
|
||||
def lang_status_handler(params: dict) -> None:
|
||||
@@ -1424,23 +1345,6 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
|
||||
self.server.notify.workspace_did_change_configuration({"settings": initialize_params["initializationOptions"]["settings"]}) # type: ignore
|
||||
|
||||
# IntelliCode enablement is only relevant in the default vscode-java VSIX mode where the
|
||||
# IntelliCode bundle is shipped. In upstream-jdtls mode it's absent and the
|
||||
# 'java.intellicode.enable' command will never be registered, so we skip the wait/call.
|
||||
if self.runtime_dependency_paths.intellicode_jar_path is not None:
|
||||
self._intellicode_enable_command_available.wait()
|
||||
|
||||
java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path
|
||||
assert java_intellisense_members_path is not None
|
||||
assert os.path.exists(java_intellisense_members_path)
|
||||
intellicode_enable_result = self.server.send.execute_command(
|
||||
{
|
||||
"command": "java.intellicode.enable",
|
||||
"arguments": [True, java_intellisense_members_path],
|
||||
}
|
||||
)
|
||||
assert intellicode_enable_result
|
||||
|
||||
if not self._service_ready_event.is_set():
|
||||
log.info("Waiting for service to be ready ...")
|
||||
self._service_ready_event.wait()
|
||||
|
||||
@@ -5,8 +5,8 @@ You can configure the following options in ls_specific_settings (in serena_confi
|
||||
|
||||
ls_specific_settings:
|
||||
kotlin:
|
||||
ls_path: '/path/to/kotlin-lsp.sh' # Custom path to Kotlin Language Server executable
|
||||
kotlin_lsp_version: '261.13587.0' # Kotlin Language Server version (default: current bundled version)
|
||||
ls_path: '/path/to/bin/intellij-server' # Custom path to Kotlin Language Server executable
|
||||
kotlin_lsp_version: '262.9593.0' # Kotlin Language Server version (default: current bundled version)
|
||||
jvm_options: '-Xmx2G' # JVM options for Kotlin Language Server (default: -Xmx2G)
|
||||
|
||||
Example configuration for large projects:
|
||||
@@ -21,16 +21,18 @@ import os
|
||||
import pathlib
|
||||
import stat
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.dependency_provider import DownloadedDependency, DownloadedDependencyHashDatabase
|
||||
from solidlsp.ls import (
|
||||
LanguageServerDependencyProvider,
|
||||
LanguageServerDependencyProviderSinglePath,
|
||||
SolidLanguageServer,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -44,34 +46,21 @@ KOTLIN_LSP_ALLOWED_HOSTS = ("download-cdn.jetbrains.com",)
|
||||
# Version pinning convention (see eclipse_jdtls.py for the full spec):
|
||||
# INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it.
|
||||
# DEFAULT_* — bumped on upgrades; goes into a versioned subdir.
|
||||
# NOTE: After changing either pinned version, run scripts/update_downloaded_dependency_hashes.py.
|
||||
INITIAL_KOTLIN_LSP_VERSION = "261.13587.0"
|
||||
INITIAL_KOTLIN_LSP_SHA256_BY_SUFFIX = {
|
||||
"win-x64": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad",
|
||||
"linux-x64": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec",
|
||||
"linux-aarch64": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651",
|
||||
"mac-x64": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3",
|
||||
"mac-aarch64": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c",
|
||||
}
|
||||
DEFAULT_KOTLIN_LSP_VERSION = "261.13587.0"
|
||||
DEFAULT_KOTLIN_LSP_SHA256_BY_SUFFIX = {
|
||||
"win-x64": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad",
|
||||
"linux-x64": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec",
|
||||
"linux-aarch64": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651",
|
||||
"mac-x64": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3",
|
||||
"mac-aarch64": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c",
|
||||
}
|
||||
DEFAULT_KOTLIN_LSP_VERSION = "262.9593.0"
|
||||
|
||||
# Versions before this one use kotlin-lsp-{version}-{platform}.zip and a kotlin-lsp script.
|
||||
# Starting with 262.4739.0, JetBrains publishes kotlin-server archives with platform-specific
|
||||
# formats, a bundled JBR, and bin/intellij-server as the launcher.
|
||||
KOTLIN_SERVER_PACKAGING_MIN_VERSION = (262, 4739, 0)
|
||||
|
||||
def _kotlin_lsp_sha(version: str, kotlin_suffix: str) -> str | None:
|
||||
if version == INITIAL_KOTLIN_LSP_VERSION:
|
||||
return INITIAL_KOTLIN_LSP_SHA256_BY_SUFFIX.get(kotlin_suffix)
|
||||
if version == DEFAULT_KOTLIN_LSP_VERSION:
|
||||
return DEFAULT_KOTLIN_LSP_SHA256_BY_SUFFIX.get(kotlin_suffix)
|
||||
return None
|
||||
# The first modern archives remained under the legacy /kotlin-lsp CDN path.
|
||||
# Starting with 262.8190.0, JetBrains moved them to /language-server/kotlin-server.
|
||||
KOTLIN_SERVER_CDN_PATH_MIN_VERSION = (262, 8190, 0)
|
||||
|
||||
|
||||
# Platform-specific Kotlin LSP download suffixes
|
||||
PLATFORM_KOTLIN_SUFFIX = {
|
||||
# Platform-specific suffixes used by legacy Kotlin LSP ZIP archives.
|
||||
LEGACY_PLATFORM_KOTLIN_SUFFIX = {
|
||||
"win-x64": "win-x64",
|
||||
"linux-x64": "linux-x64",
|
||||
"linux-arm64": "linux-aarch64",
|
||||
@@ -79,6 +68,22 @@ PLATFORM_KOTLIN_SUFFIX = {
|
||||
"osx-arm64": "mac-aarch64",
|
||||
}
|
||||
|
||||
# Modern archive filename suffix, FileUtils archive type, and launcher path within the archive.
|
||||
KOTLIN_SERVER_ARTIFACT_BY_PLATFORM: dict[str, tuple[str, FileUtils.ArchiveType, tuple[str, ...]]] = {
|
||||
"win-x64": (".win.zip", "zip", ("bin", "intellij-server.exe")),
|
||||
"win-arm64": ("-aarch64.win.zip", "zip", ("bin", "intellij-server.exe")),
|
||||
"linux-x64": (".tar.gz", "gztar", ("kotlin-server-{version}", "bin", "intellij-server")),
|
||||
"linux-arm64": ("-aarch64.tar.gz", "gztar", ("kotlin-server-{version}", "bin", "intellij-server")),
|
||||
"osx-x64": (".sit", "zip", ("kotlin-server-{version}", "bin", "intellij-server")),
|
||||
"osx-arm64": ("-aarch64.sit", "zip", ("kotlin-server-{version}", "bin", "intellij-server")),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KotlinLSPArtifact:
|
||||
dependency: DownloadedDependency
|
||||
launcher_parts: tuple[str, ...]
|
||||
|
||||
|
||||
class KotlinLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -102,15 +107,78 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
# set again once all progress tokens have ended.
|
||||
self._indexing_complete = threading.Event()
|
||||
self._indexing_complete.set()
|
||||
self._intellij_server_ready = threading.Event()
|
||||
self._active_progress_tokens: set[str] = set()
|
||||
self._progress_lock = threading.Lock()
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir, str(self.cache_dir))
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
def __init__(self, custom_settings: SolidLSPSettings.CustomLSSettings, ls_resources_dir: str):
|
||||
def __init__(self, custom_settings: SolidLSPSettings.CustomLSSettings, ls_resources_dir: str, project_cache_dir: str):
|
||||
super().__init__(custom_settings, ls_resources_dir)
|
||||
self._project_cache_dir = project_cache_dir
|
||||
|
||||
@classmethod
|
||||
def _create_artifact(cls, version: str, platform_id: PlatformId) -> KotlinLSPArtifact:
|
||||
"""Build download and launcher metadata for one Kotlin LSP release.
|
||||
|
||||
JetBrains has three publishing layouts: legacy ZIPs before 262.4739.0,
|
||||
modern platform archives under ``/kotlin-lsp`` through 262.7569.0,
|
||||
and modern archives under ``/language-server/kotlin-server`` from
|
||||
262.8190.0 onward. Serena verifies the frozen initial and current default
|
||||
releases; arbitrary user-selected versions are unverified by design.
|
||||
"""
|
||||
try:
|
||||
version_parts = tuple(int(part) for part in version.split("."))
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Kotlin LSP version must contain only dot-separated integers: {version!r}") from exc
|
||||
|
||||
verified = version in {INITIAL_KOTLIN_LSP_VERSION, DEFAULT_KOTLIN_LSP_VERSION}
|
||||
if version_parts >= KOTLIN_SERVER_PACKAGING_MIN_VERSION:
|
||||
artifact_config = KOTLIN_SERVER_ARTIFACT_BY_PLATFORM.get(platform_id.value)
|
||||
if artifact_config is None:
|
||||
raise ValueError(f"Unsupported platform for Kotlin LSP {version}: {platform_id.value}")
|
||||
|
||||
asset_suffix, archive_type, launcher_parts = artifact_config
|
||||
asset_name = f"kotlin-server-{version}{asset_suffix}"
|
||||
cdn_path = "language-server/kotlin-server" if version_parts >= KOTLIN_SERVER_CDN_PATH_MIN_VERSION else "kotlin-lsp"
|
||||
return KotlinLSPArtifact(
|
||||
dependency=DownloadedDependency(
|
||||
url=f"https://download-cdn.jetbrains.com/{cdn_path}/{version}/{asset_name}",
|
||||
archive_type=archive_type,
|
||||
allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS,
|
||||
verified=verified,
|
||||
),
|
||||
launcher_parts=tuple(part.format(version=version) for part in launcher_parts),
|
||||
)
|
||||
|
||||
kotlin_suffix = LEGACY_PLATFORM_KOTLIN_SUFFIX.get(platform_id.value)
|
||||
if kotlin_suffix is None:
|
||||
raise ValueError(f"Unsupported platform for Kotlin LSP {version}: {platform_id.value}")
|
||||
|
||||
return KotlinLSPArtifact(
|
||||
dependency=DownloadedDependency(
|
||||
url=f"https://download-cdn.jetbrains.com/kotlin-lsp/{version}/kotlin-lsp-{version}-{kotlin_suffix}.zip",
|
||||
archive_type="zip",
|
||||
allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS,
|
||||
verified=verified,
|
||||
),
|
||||
launcher_parts=("kotlin-lsp.cmd",) if platform_id.is_windows() else ("kotlin-lsp.sh",),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def update_dep_hashes(cls) -> None:
|
||||
pinned_artifacts = [
|
||||
*(cls._create_artifact(INITIAL_KOTLIN_LSP_VERSION, PlatformId(platform)) for platform in LEGACY_PLATFORM_KOTLIN_SUFFIX),
|
||||
*(
|
||||
cls._create_artifact(DEFAULT_KOTLIN_LSP_VERSION, PlatformId(platform))
|
||||
for platform in KOTLIN_SERVER_ARTIFACT_BY_PLATFORM
|
||||
),
|
||||
]
|
||||
with DownloadedDependencyHashDatabase.get_instance().update_context() as database:
|
||||
for artifact in pinned_artifacts:
|
||||
database.update(artifact.dependency)
|
||||
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
"""
|
||||
@@ -118,16 +186,9 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
# Verify platform support
|
||||
assert platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-"), (
|
||||
"Only Windows, Linux and macOS platforms are supported for Kotlin in multilspy at the moment"
|
||||
)
|
||||
|
||||
kotlin_suffix = PLATFORM_KOTLIN_SUFFIX.get(platform_id.value)
|
||||
assert kotlin_suffix, f"Unsupported platform for Kotlin LSP: {platform_id.value}"
|
||||
|
||||
# Setup paths for dependencies; legacy unversioned dir reserved for INITIAL only
|
||||
kotlin_lsp_version = self._custom_settings.get("kotlin_lsp_version", DEFAULT_KOTLIN_LSP_VERSION)
|
||||
artifact = self._create_artifact(kotlin_lsp_version, platform_id)
|
||||
ls_dirname = (
|
||||
"kotlin_language_server"
|
||||
if kotlin_lsp_version == INITIAL_KOTLIN_LSP_VERSION
|
||||
@@ -137,22 +198,13 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
os.makedirs(static_dir, exist_ok=True)
|
||||
|
||||
# Setup Kotlin Language Server
|
||||
kotlin_script_name = "kotlin-lsp.cmd" if platform_id.value.startswith("win-") else "kotlin-lsp.sh"
|
||||
kotlin_script = os.path.join(static_dir, kotlin_script_name)
|
||||
kotlin_script = os.path.join(static_dir, *artifact.launcher_parts)
|
||||
|
||||
if not os.path.exists(kotlin_script):
|
||||
kotlin_url = f"https://download-cdn.jetbrains.com/kotlin-lsp/{kotlin_lsp_version}/kotlin-lsp-{kotlin_lsp_version}-{kotlin_suffix}.zip"
|
||||
expected_sha256 = _kotlin_lsp_sha(kotlin_lsp_version, kotlin_suffix)
|
||||
log.info("Downloading Kotlin Language Server...")
|
||||
FileUtils.download_and_extract_archive_verified(
|
||||
kotlin_url,
|
||||
static_dir,
|
||||
"zip",
|
||||
expected_sha256=expected_sha256,
|
||||
allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS,
|
||||
)
|
||||
artifact.dependency.download_to(static_dir)
|
||||
|
||||
if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"):
|
||||
if os.path.exists(kotlin_script) and not platform_id.is_windows():
|
||||
os.chmod(
|
||||
kotlin_script,
|
||||
stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH,
|
||||
@@ -165,10 +217,18 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
return kotlin_script
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
return [core_path, "--stdio"]
|
||||
command = [core_path, "--stdio"]
|
||||
# A custom ls_path is independent of Serena's managed version. Select
|
||||
# arguments from the actual launcher so existing kotlin-lsp.sh/.cmd
|
||||
# configurations do not inherit IntelliJ-server-only options.
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
intellij_launcher_name = "intellij-server.exe" if platform_id.is_windows() else "intellij-server"
|
||||
if os.path.basename(core_path).lower() == intellij_launcher_name:
|
||||
command.extend(["--system-path", os.path.join(self._project_cache_dir, "kotlin-lsp-system")])
|
||||
return command
|
||||
|
||||
def create_launch_command_env(self) -> dict[str, str]:
|
||||
"""Provides JAVA_HOME and JVM options for the Kotlin Language Server process."""
|
||||
"""Provides JVM options for the Kotlin Language Server process."""
|
||||
env: dict[str, str] = {}
|
||||
|
||||
# Get JVM options from settings or use default
|
||||
@@ -427,6 +487,7 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Starts the Kotlin Language Server
|
||||
"""
|
||||
self._intellij_server_ready.clear()
|
||||
|
||||
def execute_client_command_handler(params: dict) -> list:
|
||||
return []
|
||||
@@ -437,6 +498,11 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
def window_log_message(msg: dict) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
|
||||
def intellij_server_ready(_params: dict | None) -> None:
|
||||
"""Mark the modern IntelliJ-based server ready after its startup/import phase."""
|
||||
log.info("Kotlin IntelliJ language server reported that it is ready")
|
||||
self._intellij_server_ready.set()
|
||||
|
||||
def work_done_progress_create(params: dict) -> dict:
|
||||
"""Handle window/workDoneProgress/create: the server is about to report async progress.
|
||||
Clear the indexing-complete event so _start_server waits until all tokens finish.
|
||||
@@ -487,9 +553,12 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
self.server.on_request("workspace/codeLens/refresh", do_nothing)
|
||||
self.server.on_notification("language/status", do_nothing)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("window/showMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_request("workspace/diagnostic/refresh", do_nothing)
|
||||
self.server.on_request("window/workDoneProgress/create", work_done_progress_create)
|
||||
self.server.on_notification("$/progress", progress_handler)
|
||||
self.server.on_notification("intellij/ready-for-test", intellij_server_ready)
|
||||
self.server.on_notification("$/logTrace", do_nothing)
|
||||
self.server.on_notification("$/cancelRequest", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
@@ -503,6 +572,8 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
|
||||
capabilities = init_response["capabilities"]
|
||||
server_info = init_response.get("serverInfo", {})
|
||||
wait_for_intellij_ready = isinstance(server_info, dict) and server_info.get("name") == "IntelliJ Language Server by JetBrains"
|
||||
assert "textDocumentSync" in capabilities, "Server must support textDocumentSync"
|
||||
assert "hoverProvider" in capabilities, "Server must support hover"
|
||||
assert "completionProvider" in capabilities, "Server must support code completion"
|
||||
@@ -515,14 +586,17 @@ class KotlinLanguageServer(SolidLanguageServer):
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# Wait for any async indexing to complete.
|
||||
# Wait for workspace import and async indexing to complete.
|
||||
# - Older KLS (0.253.x): indexing is synchronous inside `initialize`, no $/progress is sent,
|
||||
# _indexing_complete stays SET -> wait() returns immediately.
|
||||
# - Newer KLS (261+): server sends window/workDoneProgress/create after initialized,
|
||||
# which clears the event; wait() blocks until all progress tokens end.
|
||||
# - IntelliJ-based KLS (262.4739+): server sends intellij/ready-for-test after
|
||||
# its workspace import and indexing phase completes.
|
||||
_INDEXING_TIMEOUT = 120.0
|
||||
log.info("Waiting for Kotlin LSP indexing to complete (if async)...")
|
||||
if self._indexing_complete.wait(timeout=_INDEXING_TIMEOUT):
|
||||
ready_event = self._intellij_server_ready if wait_for_intellij_ready else self._indexing_complete
|
||||
if ready_event.wait(timeout=_INDEXING_TIMEOUT):
|
||||
log.info("Kotlin LSP ready")
|
||||
else:
|
||||
log.warning("Kotlin LSP did not signal indexing completion within %.0fs; proceeding anyway", _INDEXING_TIMEOUT)
|
||||
|
||||
@@ -28,6 +28,22 @@ DEFAULT_METALS_VERSION = "1.6.4"
|
||||
DEFAULT_CLIENT_NAME = "Serena"
|
||||
DEFAULT_ON_STALE_LOCK = "auto-clean"
|
||||
DEFAULT_LOG_MULTI_INSTANCE_NOTICE = True
|
||||
DEFAULT_AUTO_IMPORT_BUILD = True
|
||||
|
||||
# The `window/showMessageRequest` actions Serena answers affirmatively: the ones standing between
|
||||
# an un-imported workspace and a build server. Everything else is dismissed, since a prompt we do
|
||||
# not recognise is one whose consequences we cannot judge — `Messages.OldBloopVersionRunning`
|
||||
# offers to kill a process, `Messages.NewScalaProject` to open a window. "Don't show again" is
|
||||
# never chosen: Metals persists that dismissal in the project's own state.
|
||||
# (scala/meta/internal/metals/Messages.scala, at `ImportBuild`, `ImportBuildChanges`,
|
||||
# `GenerateBspAndConnect`.)
|
||||
#
|
||||
# Not answered, deliberately: `Messages.ChooseBuildTool` ("Multiple build definitions found. Which
|
||||
# would you like to use?"), whose actions are the build tools' own executable names. It precedes
|
||||
# the import prompt in a workspace holding more than one kind of build, so such a workspace is
|
||||
# still not imported — but choosing a build tool for the user is a guess of a different order, and
|
||||
# Metals offers no way to say "whichever you would have picked".
|
||||
BUILD_IMPORT_PROMPT_ACTIONS = ("Import build", "Import changes", "Connect")
|
||||
|
||||
|
||||
class StaleLockMode(Enum):
|
||||
@@ -43,6 +59,28 @@ class StaleLockMode(Enum):
|
||||
"""Raise an error and refuse to start."""
|
||||
|
||||
|
||||
def choose_show_message_request_action(params: dict, auto_import_build: bool = DEFAULT_AUTO_IMPORT_BUILD) -> dict | None:
|
||||
"""
|
||||
Choose Serena's answer to a `window/showMessageRequest`, which Metals uses to ask whether to
|
||||
import the build.
|
||||
|
||||
:param params: the request's `ShowMessageRequestParams`
|
||||
:param auto_import_build: whether to answer the build-import prompts affirmatively
|
||||
:return: the action to select, or None to select none — which is what the LSP spec provides
|
||||
for, and what leaving the request unanswered fails to say
|
||||
"""
|
||||
message = params.get("message", "")
|
||||
actions = params.get("actions") or []
|
||||
if auto_import_build:
|
||||
for action in actions:
|
||||
if isinstance(action, dict) and action.get("title") in BUILD_IMPORT_PROMPT_ACTIONS:
|
||||
log.info(f"Metals asked: {message!r}; answering {action['title']!r}")
|
||||
return action
|
||||
offered = [action.get("title") for action in actions if isinstance(action, dict)]
|
||||
log.info(f"Metals asked: {message!r}; dismissing (offered: {offered})")
|
||||
return None
|
||||
|
||||
|
||||
def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object]:
|
||||
"""
|
||||
Extract Scala-specific settings with defaults applied.
|
||||
@@ -52,6 +90,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object
|
||||
- client_name: str
|
||||
- on_stale_lock: StaleLockMode
|
||||
- log_multi_instance_notice: bool
|
||||
- auto_import_build: bool
|
||||
"""
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
|
||||
@@ -60,6 +99,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object
|
||||
"client_name": DEFAULT_CLIENT_NAME,
|
||||
"on_stale_lock": StaleLockMode.AUTO_CLEAN,
|
||||
"log_multi_instance_notice": DEFAULT_LOG_MULTI_INSTANCE_NOTICE,
|
||||
"auto_import_build": DEFAULT_AUTO_IMPORT_BUILD,
|
||||
}
|
||||
|
||||
if not solidlsp_settings.ls_specific_settings:
|
||||
@@ -80,6 +120,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object
|
||||
"client_name": scala_settings.get("client_name", DEFAULT_CLIENT_NAME),
|
||||
"on_stale_lock": on_stale_lock,
|
||||
"log_multi_instance_notice": scala_settings.get("log_multi_instance_notice", DEFAULT_LOG_MULTI_INSTANCE_NOTICE),
|
||||
"auto_import_build": scala_settings.get("auto_import_build", DEFAULT_AUTO_IMPORT_BUILD),
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +141,14 @@ class ScalaLanguageServer(SolidLanguageServer):
|
||||
metals_version: '1.6.4'
|
||||
# Client identifier sent to Metals (default: DEFAULT_CLIENT_NAME)
|
||||
client_name: 'Serena'
|
||||
# Answer Metals' build-import prompts affirmatively, which lets it run the project's
|
||||
# build tool (e.g. sbt bloopInstall). Set false to leave the build un-imported.
|
||||
auto_import_build: true
|
||||
|
||||
Build import:
|
||||
Metals asks, via `window/showMessageRequest`, whether to import a workspace it has not
|
||||
seen before; until that is answered it has no build server and so no build target, and
|
||||
every cross-file query is served by the fallback presentation compiler.
|
||||
|
||||
Multi-instance support:
|
||||
Metals uses H2 AUTO_SERVER mode (enabled by default) to support multiple
|
||||
@@ -116,6 +165,8 @@ class ScalaLanguageServer(SolidLanguageServer):
|
||||
# Check for stale locks before setting up dependencies (fail-fast)
|
||||
self._check_metals_db_status(repository_root_path, solidlsp_settings)
|
||||
|
||||
self._auto_import_build: bool = _get_scala_settings(solidlsp_settings)["auto_import_build"] # type: ignore[assignment]
|
||||
|
||||
scala_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
|
||||
super().__init__(
|
||||
config,
|
||||
@@ -300,10 +351,15 @@ class ScalaLanguageServer(SolidLanguageServer):
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _answer_show_message_request(self, params: dict) -> dict | None:
|
||||
return choose_show_message_request_action(params, auto_import_build=self._auto_import_build)
|
||||
|
||||
def _start_server(self) -> None:
|
||||
"""
|
||||
Starts the Scala Language Server
|
||||
"""
|
||||
self.server.on_request("window/showMessageRequest", self._answer_show_message_request)
|
||||
|
||||
log.info("Starting Scala server process")
|
||||
self.server.start()
|
||||
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
"https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-win32-x64-1.54.0-923.vsix": "66f3914987edeccfee8a2558470e0fde4f8c4154232ff4baa5d73373ebc819d4",
|
||||
"https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-win-x64.zip": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad",
|
||||
"https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-linux-x64.zip": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec",
|
||||
"https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-linux-aarch64.zip": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651",
|
||||
"https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-mac-x64.zip": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3",
|
||||
"https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-mac-aarch64.zip": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c",
|
||||
"https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0.win.zip": "f2daaa476f26d99301b406f76de6d87c437d04dc72f06845154619d8f991c51f",
|
||||
"https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0-aarch64.win.zip": "73a552a6a420158622e5ad8d96b53da8aa8ced3f88a24fded01575927a2fd8e7",
|
||||
"https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0.tar.gz": "2d99d8e198fbe4aa8f4481e37799724ce94803b4ea12a60b416040e3fcd7cc5e",
|
||||
"https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0-aarch64.tar.gz": "2317831c6e5607d05b7ebc1da655330125ce0e3d66fbf24517dfce442debc14e",
|
||||
"https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0.sit": "17369fda97c85418ac24ab38a9df56b21522a3468dfe193832fe455c13920745",
|
||||
"https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0-aarch64.sit": "6ba6021a706b21e64cef33f7e2b79f187c0910320722bb2d3ed05ad1115ec43f"
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import threading
|
||||
from collections.abc import Iterator
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from concurrent.futures import TimeoutError as FutureTimeoutError
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from serena.project_server import ProjectServer, QueryProjectRequest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project_server() -> ProjectServer:
|
||||
server = ProjectServer.__new__(ProjectServer)
|
||||
server._agent = MagicMock()
|
||||
server._loaded_projects_by_root = {}
|
||||
server._project_load_locks_by_root = {}
|
||||
server._active_project_lock = threading.Lock()
|
||||
server._loaded_projects_lock = threading.Lock()
|
||||
return server
|
||||
|
||||
|
||||
def test_cached_project_lookup_is_not_blocked_by_unrelated_cold_load(project_server: ProjectServer) -> None:
|
||||
cached_root = Path("/cached")
|
||||
cold_root = Path("/cold")
|
||||
cached_project = MagicMock()
|
||||
cold_project = MagicMock()
|
||||
cold_load_started = threading.Event()
|
||||
allow_cold_load_to_finish = threading.Event()
|
||||
|
||||
cached_registration = MagicMock(project_root=cached_root)
|
||||
cold_registration = MagicMock(project_root=cold_root)
|
||||
|
||||
def block_cold_load() -> None:
|
||||
cold_load_started.set()
|
||||
assert allow_cold_load_to_finish.wait(timeout=5)
|
||||
|
||||
cold_registration.get_project_instance.return_value = cold_project
|
||||
cold_project.create_language_server_manager.side_effect = block_cold_load
|
||||
project_server._loaded_projects_by_root[str(cached_root)] = cached_project
|
||||
serena_config = cast(Any, project_server._agent.serena_config)
|
||||
serena_config.get_registered_project.side_effect = {
|
||||
"cached": cached_registration,
|
||||
"cold": cold_registration,
|
||||
}.get
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
cold_future = executor.submit(project_server._get_project, "cold")
|
||||
assert cold_load_started.wait(timeout=1)
|
||||
|
||||
cached_future = executor.submit(project_server._get_project, "cached")
|
||||
try:
|
||||
assert cached_future.result(timeout=1) is cached_project
|
||||
finally:
|
||||
allow_cold_load_to_finish.set()
|
||||
|
||||
assert cold_future.result(timeout=1) is cold_project
|
||||
|
||||
|
||||
def test_cold_loads_for_different_projects_can_run_concurrently(project_server: ProjectServer) -> None:
|
||||
first_root = Path("/first")
|
||||
second_root = Path("/second")
|
||||
first_project = MagicMock()
|
||||
second_project = MagicMock()
|
||||
first_load_started = threading.Event()
|
||||
allow_first_load_to_finish = threading.Event()
|
||||
|
||||
first_registration = MagicMock(project_root=first_root)
|
||||
second_registration = MagicMock(project_root=second_root)
|
||||
|
||||
def block_first_load() -> None:
|
||||
first_load_started.set()
|
||||
assert allow_first_load_to_finish.wait(timeout=5)
|
||||
|
||||
first_registration.get_project_instance.return_value = first_project
|
||||
first_project.create_language_server_manager.side_effect = block_first_load
|
||||
second_registration.get_project_instance.return_value = second_project
|
||||
serena_config = cast(Any, project_server._agent.serena_config)
|
||||
serena_config.get_registered_project.side_effect = {
|
||||
"first": first_registration,
|
||||
"second": second_registration,
|
||||
}.get
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
first_future = executor.submit(project_server._get_project, "first")
|
||||
assert first_load_started.wait(timeout=1)
|
||||
|
||||
second_future = executor.submit(project_server._get_project, "second")
|
||||
try:
|
||||
assert second_future.result(timeout=1) is second_project
|
||||
finally:
|
||||
allow_first_load_to_finish.set()
|
||||
|
||||
assert first_future.result(timeout=1) is first_project
|
||||
|
||||
|
||||
def test_concurrent_lookups_load_each_project_only_once(project_server: ProjectServer) -> None:
|
||||
project_root = Path("/project")
|
||||
project = MagicMock()
|
||||
load_started = threading.Event()
|
||||
second_lookup_started = threading.Event()
|
||||
allow_load_to_finish = threading.Event()
|
||||
registration = MagicMock(project_root=project_root)
|
||||
lookup_count = 0
|
||||
lookup_count_lock = threading.Lock()
|
||||
|
||||
def block_load() -> None:
|
||||
load_started.set()
|
||||
assert allow_load_to_finish.wait(timeout=5)
|
||||
|
||||
registration.get_project_instance.return_value = project
|
||||
project.create_language_server_manager.side_effect = block_load
|
||||
serena_config = cast(Any, project_server._agent.serena_config)
|
||||
|
||||
def get_registration(project_name: str) -> MagicMock:
|
||||
nonlocal lookup_count
|
||||
assert project_name == "project"
|
||||
with lookup_count_lock:
|
||||
lookup_count += 1
|
||||
if lookup_count == 2:
|
||||
second_lookup_started.set()
|
||||
return registration
|
||||
|
||||
serena_config.get_registered_project.side_effect = get_registration
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
first_future = executor.submit(project_server._get_project, "project")
|
||||
assert load_started.wait(timeout=1)
|
||||
second_future = executor.submit(project_server._get_project, "project")
|
||||
assert second_lookup_started.wait(timeout=1)
|
||||
allow_load_to_finish.set()
|
||||
|
||||
assert first_future.result(timeout=1) is project
|
||||
assert second_future.result(timeout=1) is project
|
||||
|
||||
registration.get_project_instance.assert_called_once_with(serena_config)
|
||||
project.create_language_server_manager.assert_called_once_with()
|
||||
|
||||
|
||||
def test_concurrent_queries_serialize_active_project_context(project_server: ProjectServer) -> None:
|
||||
first_query_started = threading.Event()
|
||||
second_lookup_finished = threading.Event()
|
||||
allow_first_query_to_finish = threading.Event()
|
||||
state_lock = threading.Lock()
|
||||
state: dict[str, str | None] = {"active_project": None}
|
||||
|
||||
def get_project(project_name: str) -> str:
|
||||
if project_name == "second":
|
||||
second_lookup_finished.set()
|
||||
return project_name
|
||||
|
||||
@contextmanager
|
||||
def active_project_context(project: str) -> Iterator[None]:
|
||||
with state_lock:
|
||||
assert state["active_project"] is None
|
||||
state["active_project"] = project
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
with state_lock:
|
||||
state["active_project"] = None
|
||||
|
||||
def apply_tool(hold: bool = False) -> str:
|
||||
if hold:
|
||||
first_query_started.set()
|
||||
assert allow_first_query_to_finish.wait(timeout=5)
|
||||
with state_lock:
|
||||
active_project = state["active_project"]
|
||||
assert active_project is not None
|
||||
return active_project
|
||||
|
||||
tool = MagicMock()
|
||||
tool.is_readonly.return_value = True
|
||||
tool.apply_ex.side_effect = apply_tool
|
||||
server = cast(Any, project_server)
|
||||
server._get_project = MagicMock(side_effect=get_project)
|
||||
agent = cast(Any, project_server._agent)
|
||||
agent.active_project_context.side_effect = active_project_context
|
||||
agent.get_tool_by_name.return_value = tool
|
||||
first_request = QueryProjectRequest(project_name="first", tool_name="read_file", tool_params_json='{"hold": true}')
|
||||
second_request = QueryProjectRequest(project_name="second", tool_name="read_file", tool_params_json="{}")
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
first_future = executor.submit(project_server._query_project, first_request)
|
||||
assert first_query_started.wait(timeout=1)
|
||||
second_future = executor.submit(project_server._query_project, second_request)
|
||||
assert second_lookup_finished.wait(timeout=1)
|
||||
|
||||
try:
|
||||
with pytest.raises(FutureTimeoutError):
|
||||
second_future.result(timeout=0.1)
|
||||
finally:
|
||||
allow_first_query_to_finish.set()
|
||||
|
||||
assert first_future.result(timeout=1) == "first"
|
||||
assert second_future.result(timeout=1) == "second"
|
||||
@@ -455,7 +455,7 @@ class TestSetupFromExistingInstall:
|
||||
(jdk / "bin" / "java").touch()
|
||||
return jdk
|
||||
|
||||
def test_happy_path_returns_runtime_paths_with_no_gradle_and_no_intellicode(
|
||||
def test_happy_path_returns_runtime_paths_with_no_gradle(
|
||||
self, tmp_path: Path, jdtls_root: Path, lombok_jar: Path, custom_settings: SolidLSPSettings.CustomLSSettings
|
||||
) -> None:
|
||||
jdk = self._fake_jdk(tmp_path)
|
||||
@@ -465,8 +465,6 @@ class TestSetupFromExistingInstall:
|
||||
result = EclipseJDTLS.DependencyProvider._setup_from_existing_install(str(jdtls_root), str(lombok_jar), custom_settings)
|
||||
|
||||
assert result.gradle_path is None
|
||||
assert result.intellicode_jar_path is None
|
||||
assert result.intellisense_members_path is None
|
||||
assert result.lombok_jar_path == str(lombok_jar)
|
||||
assert result.jre_home_path == str(jdk)
|
||||
assert Path(result.jdtls_launcher_jar_path).name.startswith("org.eclipse.equinox.launcher_")
|
||||
|
||||
@@ -10,7 +10,7 @@ from test.conftest import language_server_tests_enabled
|
||||
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
|
||||
|
||||
|
||||
# Kotlin LSP (IntelliJ-based, pre-alpha v261) crashes on JVM restart under CI resource constraints
|
||||
# Kotlin LSP (IntelliJ-based, pre-alpha v262) crashes on JVM restart under CI resource constraints
|
||||
# (2 CPUs, 7GB RAM). First start succeeds but subsequent starts fail with cancelled (-32800).
|
||||
# Tests pass reliably on developer machines. See PR #1061 for investigation details.
|
||||
# (The CI quarantine lives centrally in test/conftest.py::_determine_disabled_languages.)
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
"""Tests for Kotlin Language Server dependency resolution and installation."""
|
||||
|
||||
from contextlib import nullcontext
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.dependency_provider import DownloadedDependency, DownloadedDependencyHashDatabase
|
||||
from solidlsp.language_servers.kotlin_language_server import (
|
||||
DEFAULT_KOTLIN_LSP_VERSION,
|
||||
INITIAL_KOTLIN_LSP_VERSION,
|
||||
KOTLIN_LSP_ALLOWED_HOSTS,
|
||||
KotlinLanguageServer,
|
||||
)
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
def _make_provider(
|
||||
tmp_path: Path,
|
||||
custom_settings: dict[str, str] | None = None,
|
||||
) -> KotlinLanguageServer.DependencyProvider:
|
||||
return KotlinLanguageServer.DependencyProvider(
|
||||
custom_settings=SolidLSPSettings.CustomLSSettings(custom_settings or {}),
|
||||
ls_resources_dir=str(tmp_path),
|
||||
project_cache_dir=str(tmp_path / "project-cache"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.kotlin
|
||||
class TestKotlinDependencyProvider:
|
||||
@pytest.mark.parametrize(
|
||||
("platform_id", "asset_suffix", "archive_type", "launcher_parts", "sha256"),
|
||||
[
|
||||
(
|
||||
PlatformId.WIN_x64,
|
||||
".win.zip",
|
||||
"zip",
|
||||
("bin", "intellij-server.exe"),
|
||||
"f2daaa476f26d99301b406f76de6d87c437d04dc72f06845154619d8f991c51f",
|
||||
),
|
||||
(
|
||||
PlatformId.WIN_arm64,
|
||||
"-aarch64.win.zip",
|
||||
"zip",
|
||||
("bin", "intellij-server.exe"),
|
||||
"73a552a6a420158622e5ad8d96b53da8aa8ced3f88a24fded01575927a2fd8e7",
|
||||
),
|
||||
(
|
||||
PlatformId.LINUX_x64,
|
||||
".tar.gz",
|
||||
"gztar",
|
||||
(f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"),
|
||||
"2d99d8e198fbe4aa8f4481e37799724ce94803b4ea12a60b416040e3fcd7cc5e",
|
||||
),
|
||||
(
|
||||
PlatformId.LINUX_arm64,
|
||||
"-aarch64.tar.gz",
|
||||
"gztar",
|
||||
(f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"),
|
||||
"2317831c6e5607d05b7ebc1da655330125ce0e3d66fbf24517dfce442debc14e",
|
||||
),
|
||||
(
|
||||
PlatformId.OSX_x64,
|
||||
".sit",
|
||||
"zip",
|
||||
(f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"),
|
||||
"17369fda97c85418ac24ab38a9df56b21522a3468dfe193832fe455c13920745",
|
||||
),
|
||||
(
|
||||
PlatformId.OSX_arm64,
|
||||
"-aarch64.sit",
|
||||
"zip",
|
||||
(f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"),
|
||||
"6ba6021a706b21e64cef33f7e2b79f187c0910320722bb2d3ed05ad1115ec43f",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_default_artifacts_match_jetbrains_release_matrix(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
platform_id: PlatformId,
|
||||
asset_suffix: str,
|
||||
archive_type: str,
|
||||
launcher_parts: tuple[str, ...],
|
||||
sha256: str,
|
||||
) -> None:
|
||||
artifact = KotlinLanguageServer.DependencyProvider._create_artifact(DEFAULT_KOTLIN_LSP_VERSION, platform_id)
|
||||
url = (
|
||||
f"https://download-cdn.jetbrains.com/language-server/kotlin-server/{DEFAULT_KOTLIN_LSP_VERSION}/"
|
||||
f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}{asset_suffix}"
|
||||
)
|
||||
|
||||
with patch("solidlsp.dependency_provider.FileUtils.download_and_extract_archive_verified") as download:
|
||||
artifact.dependency.download_to(tmp_path)
|
||||
|
||||
download.assert_called_once_with(
|
||||
url,
|
||||
str(tmp_path),
|
||||
archive_type=archive_type,
|
||||
expected_sha256=sha256,
|
||||
allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS,
|
||||
)
|
||||
assert artifact.launcher_parts == launcher_parts
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("version", "platform_id", "url", "launcher_parts"),
|
||||
[
|
||||
(
|
||||
"262.2310.0",
|
||||
PlatformId.LINUX_x64,
|
||||
"https://download-cdn.jetbrains.com/kotlin-lsp/262.2310.0/kotlin-lsp-262.2310.0-linux-x64.zip",
|
||||
("kotlin-lsp.sh",),
|
||||
),
|
||||
(
|
||||
"262.4739.0",
|
||||
PlatformId.LINUX_x64,
|
||||
"https://download-cdn.jetbrains.com/kotlin-lsp/262.4739.0/kotlin-server-262.4739.0.tar.gz",
|
||||
("kotlin-server-262.4739.0", "bin", "intellij-server"),
|
||||
),
|
||||
(
|
||||
"262.8190.0",
|
||||
PlatformId.OSX_arm64,
|
||||
"https://download-cdn.jetbrains.com/language-server/kotlin-server/262.8190.0/kotlin-server-262.8190.0-aarch64.sit",
|
||||
("kotlin-server-262.8190.0", "bin", "intellij-server"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_version_boundaries_select_the_published_layout(
|
||||
self,
|
||||
version: str,
|
||||
platform_id: PlatformId,
|
||||
url: str,
|
||||
launcher_parts: tuple[str, ...],
|
||||
) -> None:
|
||||
artifact = KotlinLanguageServer.DependencyProvider._create_artifact(version, platform_id)
|
||||
|
||||
assert artifact.dependency.get_url() == url
|
||||
assert artifact.launcher_parts == launcher_parts
|
||||
|
||||
def test_custom_versions_skip_hash_lookup_by_design(self, tmp_path: Path) -> None:
|
||||
artifact = KotlinLanguageServer.DependencyProvider._create_artifact("262.8190.1", PlatformId.LINUX_x64)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"solidlsp.dependency_provider.DownloadedDependencyHashDatabase.get_instance",
|
||||
side_effect=AssertionError("custom versions must not consult the pinned hash database"),
|
||||
),
|
||||
patch("solidlsp.dependency_provider.FileUtils.download_and_extract_archive_verified") as download,
|
||||
):
|
||||
artifact.dependency.download_to(tmp_path)
|
||||
|
||||
download.assert_called_once_with(
|
||||
"https://download-cdn.jetbrains.com/language-server/kotlin-server/262.8190.1/kotlin-server-262.8190.1.tar.gz",
|
||||
str(tmp_path),
|
||||
archive_type="gztar",
|
||||
expected_sha256=None,
|
||||
allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS,
|
||||
)
|
||||
|
||||
def test_hash_updater_registers_frozen_and_default_artifacts(self) -> None:
|
||||
updated_asset_names: list[str] = []
|
||||
|
||||
class RecordingUpdater:
|
||||
def update(self, dependency: DownloadedDependency) -> None:
|
||||
updated_asset_names.append(dependency.get_url().rsplit("/", 1)[-1])
|
||||
|
||||
with patch.object(DownloadedDependencyHashDatabase, "get_instance") as get_hash_database:
|
||||
get_hash_database.return_value.update_context.return_value = nullcontext(RecordingUpdater())
|
||||
KotlinLanguageServer.DependencyProvider.update_dep_hashes()
|
||||
|
||||
assert set(updated_asset_names) == {
|
||||
"kotlin-lsp-261.13587.0-win-x64.zip",
|
||||
"kotlin-lsp-261.13587.0-linux-x64.zip",
|
||||
"kotlin-lsp-261.13587.0-linux-aarch64.zip",
|
||||
"kotlin-lsp-261.13587.0-mac-x64.zip",
|
||||
"kotlin-lsp-261.13587.0-mac-aarch64.zip",
|
||||
"kotlin-server-262.9593.0.win.zip",
|
||||
"kotlin-server-262.9593.0-aarch64.win.zip",
|
||||
"kotlin-server-262.9593.0.tar.gz",
|
||||
"kotlin-server-262.9593.0-aarch64.tar.gz",
|
||||
"kotlin-server-262.9593.0.sit",
|
||||
"kotlin-server-262.9593.0-aarch64.sit",
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("settings", "platform_id", "relative_launcher", "expected_directory"),
|
||||
[
|
||||
(
|
||||
{"kotlin_lsp_version": INITIAL_KOTLIN_LSP_VERSION},
|
||||
PlatformId.LINUX_x64,
|
||||
("kotlin-lsp.sh",),
|
||||
"kotlin_language_server",
|
||||
),
|
||||
(
|
||||
{},
|
||||
PlatformId.LINUX_arm64,
|
||||
(f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"),
|
||||
f"kotlin_language_server-{DEFAULT_KOTLIN_LSP_VERSION}",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_installs_pinned_versions_in_compatible_directories(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
settings: dict[str, str],
|
||||
platform_id: PlatformId,
|
||||
relative_launcher: tuple[str, ...],
|
||||
expected_directory: str,
|
||||
) -> None:
|
||||
provider = _make_provider(tmp_path, settings)
|
||||
|
||||
def fake_download(
|
||||
_url: str,
|
||||
target_path: str,
|
||||
archive_type: str,
|
||||
expected_sha256: str | None = None,
|
||||
allowed_hosts: tuple[str, ...] | list[str] | None = None,
|
||||
) -> None:
|
||||
del archive_type, expected_sha256, allowed_hosts
|
||||
launcher = Path(target_path).joinpath(*relative_launcher)
|
||||
launcher.parent.mkdir(parents=True, exist_ok=True)
|
||||
launcher.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
|
||||
with (
|
||||
patch(
|
||||
"solidlsp.language_servers.kotlin_language_server.PlatformUtils.get_platform_id",
|
||||
return_value=platform_id,
|
||||
),
|
||||
patch(
|
||||
"solidlsp.dependency_provider.FileUtils.download_and_extract_archive_verified",
|
||||
side_effect=fake_download,
|
||||
),
|
||||
):
|
||||
launcher_path = provider._get_or_install_core_dependency()
|
||||
|
||||
assert launcher_path == str((tmp_path / expected_directory).joinpath(*relative_launcher))
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("platform_id", "modern_launcher", "other_os_launcher"),
|
||||
[
|
||||
(PlatformId.LINUX_x64, "/path/to/intellij-server", "/path/to/intellij-server.exe"),
|
||||
(PlatformId.WIN_x64, "/path/to/intellij-server.exe", "/path/to/intellij-server"),
|
||||
],
|
||||
)
|
||||
def test_custom_launcher_arguments_use_the_active_os_name(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
platform_id: PlatformId,
|
||||
modern_launcher: str,
|
||||
other_os_launcher: str,
|
||||
) -> None:
|
||||
modern_provider = _make_provider(tmp_path, {"ls_path": modern_launcher})
|
||||
other_os_provider = _make_provider(tmp_path, {"ls_path": other_os_launcher})
|
||||
|
||||
with patch(
|
||||
"solidlsp.language_servers.kotlin_language_server.PlatformUtils.get_platform_id",
|
||||
return_value=platform_id,
|
||||
):
|
||||
assert modern_provider.create_launch_command() == [
|
||||
modern_launcher,
|
||||
"--stdio",
|
||||
"--system-path",
|
||||
str(tmp_path / "project-cache" / "kotlin-lsp-system"),
|
||||
]
|
||||
assert other_os_provider.create_launch_command() == [other_os_launcher, "--stdio"]
|
||||
|
||||
def test_invalid_version_is_rejected_before_download(self) -> None:
|
||||
with pytest.raises(ValueError, match="dot-separated integers"):
|
||||
KotlinLanguageServer.DependencyProvider._create_artifact("latest", PlatformId.OSX_arm64)
|
||||
|
||||
def test_unsupported_modern_platform_is_rejected(self) -> None:
|
||||
with pytest.raises(ValueError, match="Unsupported platform"):
|
||||
KotlinLanguageServer.DependencyProvider._create_artifact(DEFAULT_KOTLIN_LSP_VERSION, PlatformId.LINUX_x86)
|
||||
@@ -2,9 +2,11 @@ import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from test.conftest import language_server_tests_enabled
|
||||
from test.solidlsp.util.diagnostics import assert_file_diagnostics
|
||||
|
||||
|
||||
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.KOTLIN), reason="Kotlin tests are disabled")
|
||||
@pytest.mark.kotlin
|
||||
class TestKotlinDiagnostics:
|
||||
@pytest.mark.parametrize("language_server", [LanguageServerId.KOTLIN], indirect=True)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Regression tests for Kotlin LSP startup readiness handling."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.kotlin_language_server import KotlinLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
pytestmark = pytest.mark.kotlin
|
||||
|
||||
_REQUIRED_CAPABILITIES = {
|
||||
"textDocumentSync": 1,
|
||||
"hoverProvider": True,
|
||||
"completionProvider": {},
|
||||
"signatureHelpProvider": {},
|
||||
"definitionProvider": True,
|
||||
"referencesProvider": True,
|
||||
"documentSymbolProvider": True,
|
||||
"workspaceSymbolProvider": True,
|
||||
"semanticTokensProvider": {},
|
||||
}
|
||||
|
||||
|
||||
def _make_server(tmp_path: Path) -> tuple[KotlinLanguageServer, Mock, Mock]:
|
||||
settings = SolidLSPSettings(
|
||||
solidlsp_dir=str(tmp_path / "global"),
|
||||
project_data_path=str(tmp_path / "project"),
|
||||
ls_specific_settings={LanguageServerId.KOTLIN: {}},
|
||||
)
|
||||
server_interface = Mock()
|
||||
with patch.object(KotlinLanguageServer, "_create_language_server_interface", return_value=server_interface):
|
||||
server = KotlinLanguageServer(
|
||||
LanguageServerConfig(ls_id=LanguageServerId.KOTLIN),
|
||||
str(tmp_path),
|
||||
settings,
|
||||
)
|
||||
|
||||
indexing_complete = Mock()
|
||||
indexing_complete.wait.return_value = True
|
||||
intellij_server_ready = Mock()
|
||||
intellij_server_ready.wait.return_value = True
|
||||
server_state = cast(Any, server)
|
||||
server_state._indexing_complete = indexing_complete
|
||||
server_state._intellij_server_ready = intellij_server_ready
|
||||
return server, indexing_complete, intellij_server_ready
|
||||
|
||||
|
||||
def _configure_initialize(server: KotlinLanguageServer, server_name: str | None) -> dict[str, Any]:
|
||||
response: dict[str, Any] = {"capabilities": _REQUIRED_CAPABILITIES}
|
||||
if server_name is not None:
|
||||
response["serverInfo"] = {"name": server_name}
|
||||
language_server_interface = cast(Any, server.server)
|
||||
language_server_interface.send.initialize.return_value = response
|
||||
return response
|
||||
|
||||
|
||||
def test_modern_server_registers_and_waits_for_explicit_ready_notification(tmp_path: Path) -> None:
|
||||
server, indexing_complete, intellij_server_ready = _make_server(tmp_path)
|
||||
_configure_initialize(server, "IntelliJ Language Server by JetBrains")
|
||||
events: list[str] = []
|
||||
notification_handlers: dict[str, Any] = {}
|
||||
language_server_interface = cast(Any, server.server)
|
||||
|
||||
def on_notification(method: str, handler: Any) -> None:
|
||||
events.append(f"notification:{method}")
|
||||
notification_handlers[method] = handler
|
||||
|
||||
language_server_interface.on_notification.side_effect = on_notification
|
||||
language_server_interface.start.side_effect = lambda: events.append("start")
|
||||
language_server_interface.send.initialize.side_effect = lambda _params: (
|
||||
events.append("initialize")
|
||||
or {"capabilities": _REQUIRED_CAPABILITIES, "serverInfo": {"name": "IntelliJ Language Server by JetBrains"}}
|
||||
)
|
||||
|
||||
server._start_server()
|
||||
|
||||
assert events.index("notification:intellij/ready-for-test") < events.index("start") < events.index("initialize")
|
||||
intellij_server_ready.clear.assert_called_once_with()
|
||||
intellij_server_ready.wait.assert_called_once_with(timeout=120.0)
|
||||
indexing_complete.wait.assert_not_called()
|
||||
|
||||
notification_handlers["intellij/ready-for-test"](None)
|
||||
intellij_server_ready.set.assert_called_once_with()
|
||||
|
||||
|
||||
def test_legacy_server_keeps_progress_event_wait(tmp_path: Path) -> None:
|
||||
server, indexing_complete, intellij_server_ready = _make_server(tmp_path)
|
||||
_configure_initialize(server, None)
|
||||
|
||||
server._start_server()
|
||||
|
||||
indexing_complete.wait.assert_called_once_with(timeout=120.0)
|
||||
intellij_server_ready.wait.assert_not_called()
|
||||
|
||||
|
||||
def test_modern_ready_timeout_warns_and_continues(tmp_path: Path, caplog: Any) -> None:
|
||||
server, _indexing_complete, intellij_server_ready = _make_server(tmp_path)
|
||||
_configure_initialize(server, "IntelliJ Language Server by JetBrains")
|
||||
intellij_server_ready.wait.return_value = False
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
server._start_server()
|
||||
|
||||
assert "Kotlin LSP did not signal indexing completion within 120s; proceeding anyway" in caplog.text
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Unit tests for Serena's answer to Metals' `window/showMessageRequest` prompts.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.scala_language_server import (
|
||||
_get_scala_settings,
|
||||
choose_show_message_request_action,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
# Metals' own prompts, with the message and action titles as its `Messages` object builds them
|
||||
# (scala/meta/internal/metals/Messages.scala) and `type` as lsp4j serialises `MessageType.Info`.
|
||||
IMPORT_BUILD = {
|
||||
"message": "New sbt workspace detected, would you like to import the build?",
|
||||
"type": 3,
|
||||
"actions": [{"title": "Import build"}, {"title": "Not now"}, {"title": "Don't show again"}],
|
||||
}
|
||||
IMPORT_CHANGES = {
|
||||
"message": "sbt build needs to be re-imported",
|
||||
"type": 3,
|
||||
"actions": [{"title": "Import changes"}, {"title": "Not now"}, {"title": "Don't show again"}],
|
||||
}
|
||||
GENERATE_BSP_AND_CONNECT = {
|
||||
"message": "New sbt workspace detected, would you like connect to the Bloop build server?",
|
||||
"type": 3,
|
||||
"actions": [{"title": "Connect"}, {"title": "Not now"}, {"title": "Don't show again"}],
|
||||
}
|
||||
OLD_BLOOP_VERSION_RUNNING = {
|
||||
"message": "Deprecated Bloop server is still running and is taking up resources, do you want to kill the process?",
|
||||
"type": 3,
|
||||
"actions": [{"title": "Yes"}, {"title": "Not now"}],
|
||||
}
|
||||
CHOOSE_BUILD_TOOL = {
|
||||
"message": "Multiple build definitions found. Which would you like to use?",
|
||||
"type": 3,
|
||||
"actions": [{"title": "sbt"}, {"title": "mill"}],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.scala
|
||||
class TestChooseShowMessageRequestAction:
|
||||
@pytest.mark.parametrize("params", [IMPORT_BUILD, IMPORT_CHANGES, GENERATE_BSP_AND_CONNECT])
|
||||
def test_build_import_prompts_are_answered_affirmatively(self, params: dict) -> None:
|
||||
chosen = choose_show_message_request_action(params)
|
||||
assert chosen is not None
|
||||
assert chosen["title"] in ("Import build", "Import changes", "Connect")
|
||||
|
||||
@pytest.mark.parametrize("params", [IMPORT_BUILD, IMPORT_CHANGES, GENERATE_BSP_AND_CONNECT])
|
||||
def test_auto_import_build_can_be_turned_off(self, params: dict) -> None:
|
||||
assert choose_show_message_request_action(params, auto_import_build=False) is None
|
||||
|
||||
def test_dont_show_again_is_never_chosen(self) -> None:
|
||||
"""Metals persists that dismissal in the project's own state."""
|
||||
params = {"message": "…", "actions": [{"title": "Don't show again"}, {"title": "Not now"}]}
|
||||
assert choose_show_message_request_action(params) is None
|
||||
|
||||
def test_an_unrecognised_prompt_is_dismissed(self) -> None:
|
||||
"""Answering "Yes" here would kill a process on the user's machine."""
|
||||
assert choose_show_message_request_action(OLD_BLOOP_VERSION_RUNNING) is None
|
||||
|
||||
def test_a_prompt_with_no_actions_is_dismissed(self) -> None:
|
||||
assert choose_show_message_request_action({"message": "just so you know"}) is None
|
||||
assert choose_show_message_request_action({"message": "just so you know", "actions": None}) is None
|
||||
|
||||
def test_malformed_actions_do_not_raise(self) -> None:
|
||||
params = {"message": "…", "actions": ["Import build", None, {"title": "Import build"}]}
|
||||
assert choose_show_message_request_action(params) == {"title": "Import build"}
|
||||
|
||||
def test_choosing_between_build_tools_is_left_alone(self) -> None:
|
||||
"""Naming the build tool for the user is a guess of a different order; see the comment
|
||||
on BUILD_IMPORT_PROMPT_ACTIONS. A workspace with several build definitions is therefore
|
||||
still not imported.
|
||||
"""
|
||||
assert choose_show_message_request_action(CHOOSE_BUILD_TOOL) is None
|
||||
|
||||
|
||||
@pytest.mark.scala
|
||||
class TestAutoImportBuildSetting:
|
||||
"""`auto_import_build` has to reach the handler from the project configuration."""
|
||||
|
||||
@staticmethod
|
||||
def setting(**scala_settings) -> object:
|
||||
settings = SolidLSPSettings(ls_specific_settings={LanguageServerId.SCALA: scala_settings})
|
||||
return _get_scala_settings(settings)["auto_import_build"]
|
||||
|
||||
def test_defaults_to_true(self) -> None:
|
||||
assert self.setting() is True
|
||||
assert _get_scala_settings(SolidLSPSettings())["auto_import_build"] is True
|
||||
|
||||
def test_can_be_disabled(self) -> None:
|
||||
assert self.setting(auto_import_build=False) is False
|
||||
Reference in new issue
Block a user