Merge remote-tracking branch 'origin/main' into release-1.7

Conflicts:
  CHANGELOG.md
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-08-09 13:53:19 +02:00
commit d3441d3e23
69 files changed
+3201 -1259

No files matched your search

+67 -25
View File
@@ -956,14 +956,43 @@ 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:
@staticmethod
def _format_prompt_tag(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 _render_prompt(self, prompt_template: str, tag: str | None = None, tag_name_attr: str | None = None) -> str:
"""
Renders the given prompt template, providing the necessary variables and functions
:param prompt_template: the template text (jinja2) to render
:param tag: if not None, wraps the rendered prompt text in a tag
:param tag_name_attr: for the case where tag is not None, specifies the value of the "name" attribute of the tag
:return: the rendered prompt
"""
def embed_memory(memory_name: str) -> str:
try:
memory_manager = self._get_memory_manager()
return self._format_prompt_tag(memory_manager.load_memory(memory_name), tag="memory", tag_name_attr=memory_name)
except Exception as e:
log.error("Tried to embed memory '%s' but failed to load it: %s", memory_name, e)
return ""
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,
embed_memory=embed_memory,
)
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.
@@ -972,6 +1001,21 @@ class SerenaAgent:
"""
return self.prompt_factory.create_connection_prompt()
def _create_global_memory_manager(self) -> MemoryManager:
"""
:return: a memory manager for global memories only (no project memories)
"""
return MemoryManager(serena_data_folder=None, read_only_memory_patterns=self.serena_config.read_only_memory_patterns)
def _get_memory_manager(self) -> MemoryManager:
"""
:return: the memory manager for the active project (if any) or a global memory manager if no project is active
"""
if self._active_project is not None:
return self._active_project.memory_manager
else:
return self._create_global_memory_manager()
def create_system_prompt(self, session_id: str = "global") -> str:
"""
Returns the 'Serena Instructions Manual', i.e. Serena's system prompt.
@@ -981,9 +1025,7 @@ class SerenaAgent:
"""
available_tools = self._active_tools
available_markers = available_tools.tool_marker_names
global_memories = MemoryManager(
serena_data_folder=None, read_only_memory_patterns=self.serena_config.read_only_memory_patterns
).list_global_memories()
global_memories = self._create_global_memory_manager().list_global_memories()
global_memories_str = dict_string(global_memories.to_dict()) if len(global_memories) > 0 else ""
log.info("Generating system prompt with available_tools=(see active tools), available_markers=%s", available_markers)
@@ -997,8 +1039,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._render_prompt(self._context.prompt, tag="context"),
mode_system_prompts=[self._render_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 +1049,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 +1073,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,27 +1087,26 @@ 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._render_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._render_prompt(proj.project_config.initial_prompt, tag="project-instructions")
self._project_prompt_status.mark_project_activation_message_as_provided(session_id)
return self._format_prompt(msg)
return msg
def _update_active_modes(self, log_message: bool = True) -> None:
"""
@@ -1431,10 +1472,7 @@ class SerenaAgent:
Shutdown handler of the agent, freeing resources and stopping background tasks.
"""
log.info("SerenaAgent is shutting down ...")
if self._active_project is not None:
log.info(f"Shutting down active project '{self._active_project.project_name}' ...")
self._active_project.shutdown(timeout=timeout)
self._active_project = None
# apply shutdown depending on allocated resources, handling quick ones first (dashboard manager & GUI viewer)
if self._gui_log_viewer:
log.info("Stopping the GUI log window ...")
self._gui_log_viewer.stop()
@@ -1442,6 +1480,10 @@ class SerenaAgent:
if self._dashboard_manager:
self._dashboard_manager.shutdown()
self._dashboard_manager = None
if self._active_project is not None:
log.info(f"Shutting down active project '{self._active_project.project_name}' ...")
self._active_project.shutdown(timeout=timeout)
self._active_project = None
def shutdown(self) -> None:
"""
+2 -5
View File
@@ -489,11 +489,8 @@ class TopLevelCommands(AutoRegisteringGroup):
log.info("Starting Serena project server")
log.info("Storing logs in %s", log_path)
server = ProjectServer()
run_kwargs: dict[str, Any] = {"host": host}
if port is not None:
run_kwargs["port"] = port
server.run(**run_kwargs)
server = ProjectServer(host=host, port=port)
server.run()
@staticmethod
@click.command(
+6 -2
View File
@@ -207,7 +207,7 @@ class SerenaDashboardAPI:
self._tool_names = tool_names
self._agent = agent
self._host = host
self._app = Flask(__name__)
self._app = Flask(self.__class__.__name__)
if trusted_hosts:
self._app.config["TRUSTED_HOSTS"] = trusted_hosts
self._tool_usage_stats = tool_usage_stats
@@ -966,6 +966,8 @@ class SerenaDashboardTrayManager:
HOST = "127.0.0.1"
"""listen address (local only)"""
TRUSTED_HOSTS = ["localhost", "127.0.0.1"]
ALIVE_CHECK_INTERVAL_SECONDS = 3
"""interval in seconds between alive checks of registered instances"""
@@ -984,7 +986,8 @@ class SerenaDashboardTrayManager:
self._lock = threading.Lock()
self._tray_icon: Optional["pystray.Icon"] = None
self._alive_check_use_pid = alive_check_use_pid
self._app = Flask(__name__)
self._app = Flask(self.__class__.__name__)
self._app.config["TRUSTED_HOSTS"] = self.TRUSTED_HOSTS
self._setup_routes()
self._use_pywebview = use_pywebview
@@ -1152,6 +1155,7 @@ class SerenaDashboardTrayManager:
for port in dead_ports:
self._instances.pop(port, None)
log.info("Removed unreachable instance on port %d", port)
self._update_menu()
# terminate if no instances remain
with self._lock:
+1
View File
@@ -323,6 +323,7 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook):
".lua",
".m",
".matlab",
".nf",
".php",
".proto",
".ps1",
+55 -22
View File
@@ -1,5 +1,6 @@
import json
import logging
import threading
from typing import TYPE_CHECKING
import requests as requests_lib
@@ -44,19 +45,33 @@ class ProjectServer:
PORT = SerenaPorts.PROJECT_SERVER_PORT
def __init__(self) -> None:
def __init__(self, host: str = "127.0.0.1", port: int | None = None) -> None:
"""
:param host: the host address to listen on.
:param port: the port to listen on; if None, use default
"""
from serena.agent import SerenaAgent
serena_config = SerenaConfig.from_config_file()
serena_config.gui_log_window = False
serena_config.web_dashboard = False
if port is None:
port = self.PORT
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
# create the Flask application
# create the Flask application, limiting trusted hosts for the case where the server is running on localhost
self._app = Flask(__name__)
local_hosts = ["localhost", "127.0.0.1"]
if self._host in local_hosts:
self._app.config["TRUSTED_HOSTS"] = local_hosts
self._setup_routes()
def _setup_routes(self) -> None:
@@ -78,31 +93,50 @@ 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")
params = json.loads(req.tool_params_json)
return tool.apply_ex(**params)
def run(self, host: str = "127.0.0.1", port: int = PORT) -> int:
"""Run the server on the given host and port.
:param host: the host address to listen on.
:param port: the port to listen on.
:return: the port number the server is running on.
def run(self) -> None:
"""
Run the server on the given host and port.
"""
from flask import cli
@@ -111,8 +145,7 @@ class ProjectServer:
# replacement, even one with an identical signature), so the monkeypatch is suppressed here
cli.show_server_banner = lambda *args, **kwargs: None # ty: ignore[invalid-assignment]
self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True)
return port
self._app.run(host=self._host, port=self._port, debug=False, use_reloader=False, threaded=True)
class ProjectServerClient:
@@ -1,11 +1,28 @@
# See Serena's documentation for more details on concept of contexts.
description: Description of the context, not used in the code.
prompt: Prompt that will form part of the system prompt/initial instructions for agents started in this context.
# See Serena's documentation for more details on concept of contexts:
# https://oraios.github.io/serena/02-usage/050_configuration.html#contexts
description: Description of the context (meta-information only)
# define a prompt which will be included in the system prompt when agents are started in this context.
# See: https://oraios.github.io/serena/02-usage/050_configuration.html#prompt-templates
prompt: |
This prompt will form part of the system prompt/initial instructions for agents started in this context.
Add as much content as needed.
Leave it undefined if no prompt is needed, i.e.
prompt: null
# tools to be excluded in this context.
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
excluded_tools: []
# several tools are excluded by default and have to be explicitly included by the user
# included tools that would otherwise be excluded (particularly optional tools that are disabled by default).
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
fixed_tools: []
# mapping of tool names to an override of their descriptions (the default description is the docstring of the Tool's apply method).
# Sometimes, tool descriptions are too long (e.g., for ChatGPT), or users may want to override them for another reason.
tool_description_overrides: {}
@@ -1,8 +1,25 @@
# See Serena's documentation for more details on concept of modes.
# See Serena's documentation for more details on concept of modes:
# https://oraios.github.io/serena/02-usage/050_configuration.html#modes
description: Description of the mode (meta-information only)
# define a prompt which will be included in the system prompt when this mode is activated
# (or will be provided in the project activation message if the mode is activated dynamically).
# See: https://oraios.github.io/serena/02-usage/050_configuration.html#prompt-templates
prompt: |
Provide a prompt that will form part of the instructions sent to the model when this mode is activated.
# tools that are to be excluded by this mode
This prompt will form part of the system prompt/initial instructions for agents started in this context.
Add as much content as needed.
Leave it undefined if no prompt is needed, i.e.
prompt: null
# tools to be excluded in this context.
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
excluded_tools: []
# several tools are excluded by default and have to be explicitly included by the user
included_optional_tools: []
# included tools that would otherwise be excluded (particularly optional tools that are disabled by default).
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
fixed_tools: []
@@ -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 %}
+14 -12
View File
@@ -4,18 +4,19 @@ project_name: "project_name"
# list of language servers to start when using the LSP backend; choose from:
# ada al angular ansible bash
# bsl clojure cpp cpp_ccls crystal
# csharp csharp_omnisharp cue dart elixir
# elm erlang fortran fsharp gdscript
# go groovy haskell haxe hlsl
# html java json julia kotlin
# latex lean4 lua luau markdown
# matlab msl nix ocaml pascal
# perl php php_phpactor php_phpantom powershell
# python python_basedpyright python_jedi python_pyrefly python_ty
# qml r rego ruby ruby_solargraph
# rust scala scss solidity svelte
# swift systemverilog terraform toml typescript
# typescript_vts vue yaml zig
# csharp csharp_omnisharp cue dart deno
# elixir elm erlang fortran fsharp
# gdscript go groovy haskell haxe
# hlsl html java json julia
# kotlin latex lean4 lua luau
# markdown matlab msl nextflow nix
# ocaml pascal perl php php_phpactor
# php_phpantom powershell python python_basedpyright python_jedi
# python_pyrefly python_ty qml r rego
# ruby ruby_solargraph rust scala scss
# solidity svelte swift systemverilog terraform
# toml typescript typescript_vts vue yaml
# zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# For the current list, see values of the LanguageServerId enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
@@ -25,6 +26,7 @@ project_name: "project_name"
# - For JavaScript, use typescript
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
@@ -0,0 +1,152 @@
"""
Provides Deno-specific instantiation of the LanguageServer class, using the
language server built into the Deno CLI (``deno lsp``).
"""
import logging
import shutil
from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
class DenoLanguageServer(SolidLanguageServer):
"""
Deno instantiation of the LanguageServer class, backed by ``deno lsp``.
Serves TypeScript/JavaScript in Deno projects. Unlike the plain
typescript-language-server, ``deno lsp`` understands Deno-specific module
resolution (``npm:`` / ``jsr:`` / ``https:`` imports) and the ``Deno.*``
global namespace.
This server overlaps the TypeScript server on file extensions and is therefore
marked experimental: it is not auto-detected and must be selected explicitly via
``languages: [deno]`` in ``project.yml``.
"""
@classmethod
def supports_implementation_request(cls) -> bool:
return True
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
super().__init__(
config,
repository_root_path,
None,
"typescript",
solidlsp_settings,
)
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
# node_modules appears in Deno projects using npm compatibility; vendor/ holds
# vendored remote dependencies. Neither should be indexed as project sources.
return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "dist", "build"]
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
"""Return the path to the ``deno`` executable (the language server ships with it)."""
deno_path = shutil.which("deno")
if deno_path is None:
raise FileNotFoundError(
"The 'deno' executable was not found on PATH. Install Deno "
"(https://docs.deno.com/runtime/getting_started/installation/) — it bundles "
"the language server used here — and ensure 'deno' is on your PATH."
)
return deno_path
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "lsp"]
def _get_language_id_for_file(self, relative_file_path: str) -> str:
# deno lsp relies on the correct languageId; .tsx/.jsx in particular must not
# be sent as plain "typescript" or symbol ranges get truncated at JSX expressions.
if relative_file_path.endswith(".tsx"):
return "typescriptreact"
if relative_file_path.endswith(".jsx"):
return "javascriptreact"
if relative_file_path.endswith((".js", ".mjs", ".cjs")):
return "javascript"
return "typescript"
def _create_base_initialize_params(self) -> dict:
return {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
"rename": {"dynamicRegistration": True, "prepareSupport": True},
"publishDiagnostics": {"relatedInformation": True},
},
"workspace": {
"workspaceFolders": True,
"configuration": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"symbol": {"dynamicRegistration": True},
},
},
# deno lsp reads its settings from initializationOptions; enabling the server
# and the linter mirrors the defaults of the official VS Code Deno extension.
"initializationOptions": {
"enable": True,
"lint": True,
"unstable": False,
},
}
def _start_server(self) -> None:
"""Start the ``deno lsp`` process and drive the LSP initialize handshake."""
def register_capability_handler(params: dict) -> None:
return
def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(params: dict) -> None:
return
def configuration_handler(params: dict) -> list:
# deno lsp requests workspace/configuration during startup; return an empty
# settings object per requested item so it proceeds with its defaults.
return [{} for _ in params.get("items", [])]
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_request("workspace/configuration", configuration_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
# Deno-specific notifications emitted after config discovery; no action needed.
self.server.on_notification("deno/didRefreshDenoConfigurationTree", do_nothing)
self.server.on_notification("deno/didChangeDenoConfiguration", do_nothing)
log.info("Starting deno lsp server process")
self.server.start()
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to deno lsp and awaiting response")
init_response = self.server.send.initialize(initialize_params)
assert "textDocumentSync" in init_response["capabilities"]
assert "definitionProvider" in init_response["capabilities"]
assert "documentSymbolProvider" in init_response["capabilities"]
assert "referencesProvider" in init_response["capabilities"]
self.server.notify.initialized({})
+10 -106
View File
@@ -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()
@@ -0,0 +1,230 @@
"""
Provides Gleam specific instantiation of the LanguageServer class.
The Gleam language server is bundled with the Gleam compiler and is started via
``gleam lsp``. No separate language-server package is required beyond the Gleam
compiler itself (https://gleam.run).
"""
import logging
import shutil
import subprocess
import threading
from overrides import override
from solidlsp.ls import (
LanguageServerDependencyProvider,
LanguageServerDependencyProviderSinglePath,
SolidLanguageServer,
)
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
# Gleam LSP emits $/progress (begin/end) while resolving/downloading project
# dependencies on first start. Once dependencies are resolved, symbol, reference,
# hover and diagnostics queries are served synchronously — no further
# compile-wait or per-request retry is needed (this was verified against
# gleam 1.17 on Windows: cross-file references return immediately once the
# dependency-download progress phase ends).
_INITIAL_PROGRESS_BEGIN_TIMEOUT = 10 # seconds to wait for the first $/progress begin
_INITIAL_PROGRESS_IDLE_TIMEOUT = 180 # seconds to wait for all progress phases to end
class GleamLanguageServer(SolidLanguageServer):
"""
Provides Gleam specific instantiation of the LanguageServer class.
Uses the language server bundled with the Gleam compiler (``gleam lsp``).
Requires the ``gleam`` binary to be installed and available on PATH; see
https://gleam.run/getting-started/installing/ for installation instructions.
The Gleam compiler is a self-contained Rust binary — neither Erlang/OTP nor
a separate LSP package is required for symbol, reference, hover, definition,
or diagnostics queries. (Full project compilation to BEAM bytecode does
require Erlang, but the LSP's analysis path does not.)
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
super().__init__(config, repository_root_path, None, "gleam", solidlsp_settings)
self._progress_lock = threading.Lock()
self._active_progress_tokens: set[str | int] = set()
self._first_progress_seen = threading.Event()
self._idle = threading.Event()
self._idle.set() # optimistic: assume ready until a $/progress begin arrives
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
"""
Discover the Gleam compiler executable on PATH.
:return: path to the ``gleam`` binary
:raises FileNotFoundError: if ``gleam`` is neither on PATH nor provided via ``ls_path``
"""
gleam_binary = shutil.which("gleam")
if gleam_binary is None:
raise FileNotFoundError(
"Gleam is not installed or not in PATH.\n"
"Please install the Gleam compiler from https://gleam.run/getting-started/installing/\n"
"and ensure the 'gleam' binary is available on your PATH.\n"
"The Gleam language server is bundled with the compiler and is started via `gleam lsp`."
)
return gleam_binary
def _create_launch_command(self, core_path: str) -> list[str]:
# `gleam lsp` runs the language server over stdio.
return [core_path, "lsp"]
@override
def is_ignored_dirname(self, dirname: str) -> bool:
# Gleam writes compiled output and downloaded dependencies under build/.
return super().is_ignored_dirname(dirname) or dirname == "build"
@override
def _supports_pull_diagnostics(self) -> bool:
# `gleam lsp` does not implement `textDocument/diagnostic` (LSP 3.17 pull diagnostics) and
# advertises no `diagnosticProvider` capability. Sending a pull request would hang (the server
# neither responds nor errors), so opt out and rely on `textDocument/publishDiagnostics` (push),
# which the base class stores via `_observe_server_notification`.
return False
def _create_base_initialize_params(self) -> dict:
"""
Return the language-specific initialize params for the Gleam language server.
``processId``, ``rootPath``, ``rootUri`` and ``workspaceFolders`` are populated by the
default ``InitializeParamsBuilder`` and must not be set here.
"""
return {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"definition": {"dynamicRegistration": True, "linkSupport": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"completion": {
"dynamicRegistration": True,
"completionItem": {
"snippetSupport": True,
"documentationFormat": ["markdown", "plaintext"],
},
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"],
},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"configuration": True,
},
},
}
def _download_dependencies(self) -> None:
"""Run ``gleam deps download`` so the stdlib is available before the LSP starts.
Gleam LSP also fetches missing dependencies on start and reports progress via
``$/progress``; pre-fetching keeps the readiness wait short and avoids first-request
races on slow CI runners.
"""
gleam_path = shutil.which("gleam")
if gleam_path is None:
return # _get_or_install_core_dependency will raise a clear error when the server starts
try:
result = subprocess.run(
[gleam_path, "deps", "download"],
cwd=self.repository_root_path,
capture_output=True,
text=True,
timeout=120,
check=False,
)
if result.returncode != 0:
log.warning("gleam deps download failed (exit %s): %s", result.returncode, result.stderr[:200])
else:
log.info("gleam deps download completed")
except Exception as e:
log.warning("Failed to run gleam deps download: %s", e)
def _start_server(self) -> None:
"""Start the Gleam language server process."""
def register_capability_handler(_params: dict) -> None:
return
def window_log_message(msg: dict) -> None:
log.info("LSP: window/logMessage: %s", msg)
def on_progress(params: dict) -> None:
token = params.get("token")
value = params.get("value", {})
if not isinstance(value, dict) or token is None:
return
kind = value.get("kind")
with self._progress_lock:
if kind == "begin":
self._active_progress_tokens.add(token)
self._first_progress_seen.set()
self._idle.clear()
log.info(
"Gleam LSP: progress begin (token=%s), active=%d",
token,
len(self._active_progress_tokens),
)
elif kind == "end":
self._active_progress_tokens.discard(token)
if not self._active_progress_tokens:
self._idle.set()
log.info("Gleam LSP: all progress phases finished")
def do_nothing(_params: dict) -> None:
return
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", on_progress)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
# Pre-fetch dependencies so the LSP's own dependency-download phase is short.
self._download_dependencies()
log.info("Starting Gleam language server (gleam lsp) process")
self.server.start()
initialize_params = self._create_initialize_params()
log.info("Sending initialize request to Gleam LSP and awaiting response")
init_response = self.server.send.initialize(initialize_params)
capabilities = init_response.get("capabilities", {}) if isinstance(init_response, dict) else {}
log.info("Gleam LSP capabilities: %s", list(capabilities.keys()))
self.server.notify.initialized({})
# Wait for Gleam's initial dependency-download progress to finish.
# Gleam emits $/progress begin/end for dependency resolution on first start; once all
# phases end, queries are served synchronously. If no progress is reported within the
# short begin-timeout, dependencies were already resolved and the server is ready.
if not self._first_progress_seen.wait(timeout=_INITIAL_PROGRESS_BEGIN_TIMEOUT):
log.info(
"Gleam LSP: no $/progress within %ss, assuming ready (dependencies already resolved)",
_INITIAL_PROGRESS_BEGIN_TIMEOUT,
)
elif not self._idle.wait(timeout=_INITIAL_PROGRESS_IDLE_TIMEOUT):
log.warning(
"Gleam LSP: timed out waiting for initial progress after %ss, proceeding anyway",
_INITIAL_PROGRESS_IDLE_TIMEOUT,
)
log.info("Gleam language server ready")
@@ -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)
@@ -0,0 +1,337 @@
"""
Provides Nextflow specific instantiation of the LanguageServer class, using the official
Nextflow language server (https://github.com/nextflow-io/language-server), which is
distributed as a fat JAR and requires a Java runtime.
You can configure the following options in ls_specific_settings (in serena_config.yml):
ls_specific_settings:
nextflow:
ls_path: '/path/to/language-server-all.jar' # use an existing JAR instead of downloading one
nextflow_ls_version: '26.04.3' # JAR version to download (default: bundled version)
java_home: '/path/to/jdk' # JDK to run the JAR with (default: JAVA_HOME or PATH)
jvm_options: ['-Xmx2G'] # JVM options for the language server process
exclude_patterns: ['work', '.nextflow'] # workspace paths the language server shall ignore
"""
import logging
import os
import shutil
import threading
from collections.abc import Hashable
from pathlib import Path
from overrides import override
from solidlsp.dependency_provider import DownloadedDependency, DownloadedDependencyHashDatabase
from solidlsp.ls import (
LanguageServerDependencyProvider,
LanguageServerDependencyProviderSinglePath,
RawDocumentSymbol,
SolidLanguageServer,
)
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.lsp_protocol_handler import lsp_types
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
NEXTFLOW_LS_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
# NOTE: after bumping this, re-run scripts/update_downloaded_dependency_hashes.py and commit the
# updated src/solidlsp/resources/downloaded_dependency_hashes.json.
DEFAULT_NEXTFLOW_LS_VERSION = "26.04.3"
NEXTFLOW_LS_JAR_NAME = "language-server-all.jar"
#: the Nextflow language server is built for Java 17+
MIN_JDK_VERSION = 17
#: paths that hold Nextflow's own outputs rather than pipeline sources; scanning them is pure overhead
DEFAULT_EXCLUDE_PATTERNS = ["work", ".nextflow"]
#: the server scans the workspace only when a configuration change asks it to, and it compares the new
#: configuration against its own defaults (see LanguageServerConfiguration.defaults() upstream, where the
#: error reporting mode is "warnings"), so the settings we push must differ in at least one of the keys
#: that trigger a scan
_SCAN_TRIGGERING_ERROR_REPORTING_MODE = "errors"
#: upper bound for the initial workspace scan, which compiles every .nf file in the project
_WORKSPACE_SCAN_TIMEOUT = 180.0
#: declaration keywords the server prepends to symbol names (see ScriptSymbolProvider.getSymbolName upstream)
_SYMBOL_NAME_PREFIXES = frozenset(("process", "workflow", "function", "record", "enum"))
class NextflowLanguageServer(SolidLanguageServer):
"""
Provides Nextflow specific instantiation of the LanguageServer class.
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a Nextflow Language Server instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
super().__init__(
config,
repository_root_path,
None,
"nextflow",
solidlsp_settings,
)
# The workspace scan is asynchronous and reported via $/progress under the "initialize" token;
# requests answered before it ends see an empty AST cache.
self._scan_complete = 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)
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
@classmethod
def _create_dep_language_server(cls, version: str | None = None) -> DownloadedDependency:
version = version or DEFAULT_NEXTFLOW_LS_VERSION
return DownloadedDependency(
url=f"https://github.com/nextflow-io/language-server/releases/download/v{version}/{NEXTFLOW_LS_JAR_NAME}",
allowed_hosts=NEXTFLOW_LS_ALLOWED_HOSTS,
)
@classmethod
def update_dep_hashes(cls) -> None:
deps = [cls._create_dep_language_server()]
with DownloadedDependencyHashDatabase.get_instance().update_context() as db:
for dep in deps:
db.update(dep)
def _get_or_install_core_dependency(self) -> str:
version = self._custom_settings.get("nextflow_ls_version", DEFAULT_NEXTFLOW_LS_VERSION)
install_dir = os.path.join(self._ls_resources_dir, f"nextflow_language_server-{version}")
jar_path = os.path.join(install_dir, NEXTFLOW_LS_JAR_NAME)
if not os.path.exists(jar_path):
os.makedirs(install_dir, exist_ok=True)
log.info("Downloading Nextflow Language Server %s ...", version)
self._create_dep_language_server(version).download_to(jar_path)
if not os.path.exists(jar_path):
raise FileNotFoundError(f"Nextflow Language Server JAR not found at {jar_path}")
return jar_path
def _create_launch_command(self, core_path: str) -> list[str]:
java_exe = self._resolve_java()
jvm_options = self._custom_settings.get("jvm_options", [])
if not isinstance(jvm_options, list):
log.warning("The 'jvm_options' setting should be a list of strings. Ignoring the provided value: %s", jvm_options)
jvm_options = []
return [java_exe, *jvm_options, "-jar", core_path]
def _resolve_java(self) -> str:
"""
Locates a ``java`` executable (``java_home`` setting -> ``JAVA_HOME`` -> PATH) and verifies
that it is new enough to run the language server.
:return: the path to the java executable
"""
java_exe_name = "java.exe" if os.name == "nt" else "java"
java_exe: str | None = None
if explicit_home := self._custom_settings.get("java_home"):
candidate = str(Path(explicit_home) / "bin" / java_exe_name)
if not os.path.exists(candidate):
raise SolidLSPException(
f"java_home='{explicit_home}' is invalid: '{candidate}' does not exist. "
f"Set ls_specific_settings.nextflow.java_home to a JDK home that contains bin/{java_exe_name}."
)
java_exe = candidate
elif env_home := os.environ.get("JAVA_HOME"):
candidate = str(Path(env_home) / "bin" / java_exe_name)
if os.path.exists(candidate):
java_exe = candidate
else:
log.warning("JAVA_HOME='%s' invalid (no '%s'), falling back to PATH.", env_home, candidate)
if java_exe is None:
java_exe = shutil.which("java")
if java_exe is None:
raise SolidLSPException(
"Could not locate a Java installation for the Nextflow language server. "
"Set ls_specific_settings.nextflow.java_home, set the JAVA_HOME environment variable, "
f"or ensure 'java' is on PATH. Required: JDK {MIN_JDK_VERSION}+."
)
# reuse JDTLS's JVM interrogation, which reports the real java.home even for stub launchers
from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS
java_home, major_version = EclipseJDTLS.DependencyProvider._inspect_java(java_exe)
if major_version < MIN_JDK_VERSION:
raise SolidLSPException(
f"The Nextflow language server requires JDK {MIN_JDK_VERSION}+ but '{java_exe}' is JDK {major_version} "
f"(java.home={java_home}). Install a newer JDK and update ls_specific_settings.nextflow.java_home "
"or JAVA_HOME."
)
log.info("Using JDK %d at %s for the Nextflow language server", major_version, java_exe)
return java_exe
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the Nextflow Language Server.
"""
return {
"capabilities": {
"textDocument": {
"synchronization": {"dynamicRegistration": True, "didSave": True},
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": False}},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"definition": {"dynamicRegistration": True, "linkSupport": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
"hierarchicalDocumentSymbolSupport": True,
},
"documentLink": {"dynamicRegistration": True},
"callHierarchy": {"dynamicRegistration": True},
"rename": {"dynamicRegistration": True, "prepareSupport": True},
"publishDiagnostics": {"relatedInformation": True},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"symbol": {"dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}},
},
"window": {"workDoneProgress": True},
},
}
def _create_configuration_settings(self) -> dict:
"""
Returns the settings pushed via workspace/didChangeConfiguration, which is what makes the server
scan the workspace (see ``_start_server``).
"""
exclude_patterns = self._custom_settings.get("exclude_patterns", DEFAULT_EXCLUDE_PATTERNS)
if not isinstance(exclude_patterns, list):
log.warning("The 'exclude_patterns' setting should be a list of strings. Ignoring the provided value: %s", exclude_patterns)
exclude_patterns = DEFAULT_EXCLUDE_PATTERNS
return {
"nextflow": {
"debug": False,
# deliberately not the server's default of "warnings": the value must differ from the
# defaults for the configuration change to trigger the workspace scan
"errorReportingMode": _SCAN_TRIGGERING_ERROR_REPORTING_MODE,
"files": {"exclude": exclude_patterns},
}
}
def _start_server(self) -> None:
"""
Starts the Nextflow Language Server.
"""
def do_nothing(params: dict) -> None:
return
def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")
def work_done_progress_create(params: dict) -> dict:
token = str(params.get("token", ""))
with self._progress_lock:
self._active_progress_tokens.add(token)
self._scan_complete.clear()
return {}
def progress_handler(params: dict) -> None:
token = str(params.get("token", ""))
value = params.get("value", {})
kind = value.get("kind")
if kind == "begin":
with self._progress_lock:
self._active_progress_tokens.add(token)
self._scan_complete.clear()
log.info("Nextflow LS progress [%s]: started - %s", token, value.get("title", ""))
elif kind == "report":
log.debug("Nextflow LS progress [%s]: %s", token, value.get("message", ""))
elif kind == "end":
with self._progress_lock:
self._active_progress_tokens.discard(token)
if not self._active_progress_tokens:
self._scan_complete.set()
log.info("Nextflow LS progress [%s]: ended", token)
self.server.on_request("client/registerCapability", do_nothing)
self.server.on_request("window/workDoneProgress/create", work_done_progress_create)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", progress_handler)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
log.info("Starting Nextflow server process")
self.server.start()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(self._create_initialize_params())
capabilities = init_response["capabilities"]
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"
assert "definitionProvider" in capabilities, "Server must support go to definition"
assert "referencesProvider" in capabilities, "Server must support find references"
assert "documentSymbolProvider" in capabilities, "Server must support document symbols"
assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols"
self.server.notify.initialized({})
# The server does not index anything on `initialized`; it (re-)scans the workspace only when a
# configuration change arrives whose error reporting mode, exclude patterns or plugin registry
# URL differ from what it currently holds. Without this notification, every symbol request
# answers with an empty result.
self._scan_complete.clear()
self.server.notify.workspace_did_change_configuration({"settings": self._create_configuration_settings()})
log.info("Waiting for the Nextflow workspace scan to complete...")
if self._scan_complete.wait(timeout=_WORKSPACE_SCAN_TIMEOUT):
log.info("Nextflow LS ready")
else:
log.warning("Nextflow LS did not signal scan completion within %.0fs; proceeding anyway", _WORKSPACE_SCAN_TIMEOUT)
@override
def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
"""
Flushes the server's pending recompile before asking for references.
Every ``didOpen``/``didChange`` schedules a debounced (1s) update of the file's AST, and
``LanguageService.references`` is the one request that does not await it (``documentSymbol``,
``codeLens``, ``documentLink`` and ``semanticTokensFull`` all do). A references request issued
within the debounce window therefore races the recompile of the very file it asks about and can
come back empty. Sending a ``documentSymbol`` request for the same file first is a cheap way to
block until the update has been applied.
"""
self.server.send.document_symbol({"textDocument": {"uri": self._resolve_file_uri(relative_file_path)}})
return super()._send_references_request(relative_file_path, line, column)
@override
def _get_wait_time_for_cross_file_referencing(self) -> float:
"""Small safety buffer; the actual synchronisation happens in _send_references_request."""
return 1.0
@override
def _document_symbols_cache_fingerprint(self) -> Hashable:
normalize_symbol_name_version = 1
return normalize_symbol_name_version
@override
def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str:
"""
Strips the declaration keyword the language server prefixes to every symbol name
("process GREET", "workflow SAY_HELLO", "function foo"), so that symbols can be addressed
by the name they are written with in the source. The implicit entry workflow keeps its
"<entry>" placeholder name, which is what the server calls it.
"""
name = symbol["name"]
prefix, sep, rest = name.partition(" ")
if sep and prefix in _SYMBOL_NAME_PREFIXES:
return rest
return name
@@ -10,6 +10,7 @@ from enum import Enum
from overrides import override
from solidlsp.initialize_params import DefaultInitializeParamsBuilder, InitializeParamsBuilder
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PlatformUtils
@@ -28,6 +29,56 @@ 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
DEFAULT_PROJECT_ROOT_SCAN_DEPTH = 3
# 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")
# Files whose presence marks a directory as the root of a build Metals can import, following the
# per-build-tool probes in Metals' `BuildTools` (scala/meta/internal/builds/BuildTools.scala) and
# `BazelBuildTool.workspaceSupportsBsp`. Deliberately partial: the probes that need to read a file's
# contents (scala-cli's BSP scope) are left out, since missing a build root only returns us to the
# previous behaviour, whereas a false positive would hide the real builds beneath it.
BUILD_ROOT_MARKER_FILES = (
"MODULE.bazel",
"WORKSPACE",
"build.gradle",
"build.gradle.kts",
"build.mill",
"build.mill.scala",
"build.mill.yaml",
"build.sbt",
"build.sc",
"deder.pkl",
"mill",
"mill.bat",
"pom.xml",
"project.scala",
"settings.gradle",
"settings.gradle.kts",
)
# Directories which, when they hold a JSON file, mark an already-configured Metals project
# (`BuildTools.hasJsonFile`). An empty one is a leftover, not a build.
BUILD_ROOT_MARKER_JSON_DIRS = (".bloop", ".bsp")
# Directories not worth descending into when scanning for build roots: build output, dependencies,
# and the directories belonging to a build we would have recognised at their parent. They are still
# probed themselves — only the descent below them is skipped.
BUILD_ROOT_SCAN_SKIP_DIRS = frozenset({"node_modules", "out", "project", "src", "target", "venv"})
class StaleLockMode(Enum):
@@ -43,6 +94,150 @@ 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 _contains_json_file(path: str) -> bool:
try:
return any(entry.name.endswith(".json") for entry in os.scandir(path))
except OSError:
return False
def _is_build_root(path: str) -> bool:
"""
Whether `path` is the root of a build that Metals can import.
"""
if any(os.path.isfile(os.path.join(path, name)) for name in BUILD_ROOT_MARKER_FILES):
return True
if any(_contains_json_file(os.path.join(path, name)) for name in BUILD_ROOT_MARKER_JSON_DIRS):
return True
# sbt allows the build to be defined entirely under project/, with no build.sbt
build_properties = os.path.join(path, "project", "build.properties")
if os.path.isfile(build_properties):
try:
with open(build_properties, encoding="utf-8", errors="replace") as f:
return any(line.lstrip().startswith("sbt.version") for line in f)
except OSError:
return False
return False
def find_build_roots(repository_root_path: str, max_depth: int = DEFAULT_PROJECT_ROOT_SCAN_DEPTH) -> list[str]:
"""
Find the roots of the builds contained in the given repository.
Metals serves one build per workspace folder, so a repository holding several builds
(or a single build below its root) must name those directories rather than the repository root.
:param repository_root_path: the repository root
:param max_depth: how many directory levels below the repository root to search;
the search does not descend into a directory that is itself a build root
:return: the absolute paths of the build roots found, or `[repository_root_path]` if there are none
(which leaves Metals' own behaviour unchanged)
"""
if _is_build_root(repository_root_path):
return [repository_root_path]
roots: list[str] = []
visited: set[str] = set()
def scan(directory: str, depth: int) -> None:
# symlinks are followed, as Metals' own search does, so guard against cycles
real_path = os.path.realpath(directory)
if depth > max_depth or real_path in visited:
return
visited.add(real_path)
try:
entries = sorted(os.scandir(directory), key=lambda e: e.name)
except OSError:
return
for entry in entries:
if entry.name.startswith(".") or not entry.is_dir():
continue
if _is_build_root(entry.path):
roots.append(entry.path)
elif entry.name not in BUILD_ROOT_SCAN_SKIP_DIRS:
scan(entry.path, depth + 1)
scan(repository_root_path, 1)
return roots or [repository_root_path]
class ScalaInitializeParamsBuilder(DefaultInitializeParamsBuilder):
"""
Sends the repository's build roots as the workspace folders, so that Metals creates one
service per build (see `MetalsLanguageServer.initialize`), in place of `ls_workspace_folders`,
which is about what SolidLSP indexes and is shared across a project's language servers.
`ls_additional_workspace_folders` is still honoured: those folders can lie outside the
repository and so could never be detected, which is the whole point of the setting.
"""
def __init__(self, ls: SolidLanguageServer, build_roots: list[str]):
super().__init__(ls, set_workspace_folders=False)
self._build_roots = build_roots
@override
def _apply_updates(self) -> None:
super()._apply_updates()
folders = list(self._build_roots)
for path in self._ls.config.get_absolute_additional_workspace_folders(self._ls.repository_root_path):
if path not in folders:
folders.append(path)
log.info("Workspace folders sent to Metals: %s", folders)
self._set("workspaceFolders", [self._create_workspace_folder_entry(path) for path in folders])
def _parse_project_roots(value: object) -> list[str] | None:
"""
Validate the `project_roots` setting, returning None (i.e. detect them) if it is unusable.
"""
if value is None:
return None
if isinstance(value, str) or not isinstance(value, list) or not all(isinstance(item, str) for item in value):
log.warning(f"Invalid project_roots value {value!r}, expected a list of paths; detecting the build roots instead")
return None
roots: list[str] = [item for item in value if isinstance(item, str)]
if not roots:
log.warning("Empty project_roots; detecting the build roots instead")
return None
return roots
def _parse_project_root_scan_depth(value: object) -> int:
"""
Validate the `project_root_scan_depth` setting, falling back to the default if it is unusable.
"""
if value is None:
return DEFAULT_PROJECT_ROOT_SCAN_DEPTH
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
log.warning(
f"Invalid project_root_scan_depth value {value!r}, expected a positive integer; using {DEFAULT_PROJECT_ROOT_SCAN_DEPTH}"
)
return DEFAULT_PROJECT_ROOT_SCAN_DEPTH
return value
def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object]:
"""
Extract Scala-specific settings with defaults applied.
@@ -52,6 +247,9 @@ 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
- project_roots: list[str] | None
- project_root_scan_depth: int
"""
from solidlsp.ls_config import LanguageServerId
@@ -60,6 +258,9 @@ 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,
"project_roots": None,
"project_root_scan_depth": DEFAULT_PROJECT_ROOT_SCAN_DEPTH,
}
if not solidlsp_settings.ls_specific_settings:
@@ -80,6 +281,9 @@ 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),
"project_roots": _parse_project_roots(scala_settings.get("project_roots")),
"project_root_scan_depth": _parse_project_root_scan_depth(scala_settings.get("project_root_scan_depth")),
}
@@ -100,6 +304,24 @@ 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 roots to serve, relative to the repository root; when unset, they are
# auto-detected (see find_build_roots)
project_roots: ['backend', 'tooling/plugin']
# How many levels below the repository root auto-detection searches
project_root_scan_depth: 3
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.
Monorepo support:
Metals serves one build per workspace folder, so the build roots — not the repository
root — are what it must be given. They are detected automatically; `project_roots`
overrides the detection where it guesses wrong.
Multi-instance support:
Metals uses H2 AUTO_SERVER mode (enabled by default) to support multiple
@@ -113,8 +335,14 @@ class ScalaLanguageServer(SolidLanguageServer):
Creates a ScalaLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
self._build_roots = self._resolve_build_roots(repository_root_path, solidlsp_settings)
log.info(f"Metals will be given these build roots as workspace folders: {self._build_roots}")
# Check for stale locks before setting up dependencies (fail-fast)
self._check_metals_db_status(repository_root_path, solidlsp_settings)
for build_root in self._build_roots:
self._check_metals_db_status(build_root, 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__(
@@ -125,9 +353,37 @@ class ScalaLanguageServer(SolidLanguageServer):
solidlsp_settings,
)
def _check_metals_db_status(self, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
@staticmethod
def _resolve_build_roots(repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> list[str]:
"""
Check the Metals H2 database status and handle stale locks.
Determine the build roots to serve, from the `project_roots` setting if given and by
detection otherwise.
"""
settings = _get_scala_settings(solidlsp_settings)
configured_roots: list[str] | None = settings["project_roots"] # type: ignore[assignment]
if configured_roots is None:
scan_depth: int = settings["project_root_scan_depth"] # type: ignore[assignment]
return find_build_roots(repository_root_path, scan_depth)
roots = []
for root in configured_roots:
abs_root = os.path.abspath(os.path.join(repository_root_path, root))
if os.path.isdir(abs_root):
roots.append(abs_root)
else:
log.warning(f"Configured Scala project root does not exist, skipping: {abs_root}")
if not roots:
log.error("No configured Scala project root exists; detecting the build roots instead")
return find_build_roots(repository_root_path, settings["project_root_scan_depth"]) # type: ignore[arg-type]
return roots
@override
def _create_initialize_params_builder(self) -> InitializeParamsBuilder:
return ScalaInitializeParamsBuilder(self, self._build_roots)
def _check_metals_db_status(self, build_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
"""
Check the Metals H2 database status of one build root and handle stale locks.
This method is called before setting up runtime dependencies to fail-fast
if there's a stale lock that the user has configured to fail on.
@@ -141,7 +397,7 @@ class ScalaLanguageServer(SolidLanguageServer):
cleanup_stale_lock,
)
project_path = Path(repository_root_path)
project_path = Path(build_root_path)
status, lock_info = check_metals_db_status(project_path)
# Get settings using the shared helper function
@@ -300,10 +556,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()
+46
View File
@@ -185,6 +185,17 @@ class LanguageServerId(str, Enum):
Supports .qml files. Requires Qt 6 installation providing qmlls on PATH.
See https://doc.qt.io/qt-6/qtqml-tool-qmlls.html
"""
GLEAM = "gleam"
"""Gleam language server bundled with the Gleam compiler (`gleam lsp`).
Supports .gleam files. Requires the `gleam` binary on PATH.
See https://gleam.run/getting-started/installing/ for installation.
"""
NEXTFLOW = "nextflow"
"""Nextflow language server (https://github.com/nextflow-io/language-server), the one that backs the
official VS Code extension. Supports .nf scripts (Nextflow .config files are parsed by the server, but
it reports no symbols for them, so they are not treated as source files here).
Automatically downloads the language server JAR; requires Java 17+ (JAVA_HOME or 'java' on PATH).
"""
# Experimental or deprecated Language Servers
TYPESCRIPT_VTS = "typescript_vts"
"""Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls"""
@@ -286,6 +297,14 @@ class LanguageServerId(str, Enum):
project.yml — Angular LS supersedes both for Angular projects.
Must be explicitly specified in project.yml.
"""
DENO = "deno"
"""Deno's built-in language server (``deno lsp``) for Deno TypeScript/JavaScript projects.
Understands Deno module resolution (``npm:`` / ``jsr:`` / ``https:`` imports) and the
``Deno.*`` globals, which the plain typescript-language-server does not. Overlaps the
TypeScript server on file extensions (.ts/.tsx/.js/.jsx/.mts/.cts/.mjs/.cjs), so it is
experimental and must be explicitly specified via ``languages: [deno]`` in project.yml;
do not also enable typescript for the same files. Requires the ``deno`` CLI on PATH.
"""
@classmethod
def iter_all(cls, include_experimental: bool = True, include_non_programming_languages: bool = True) -> Iterable[Self]:
@@ -321,6 +340,7 @@ class LanguageServerId(str, Enum):
self.HTML,
self.SCSS,
self.ANGULAR,
self.DENO,
}
def is_programming_language(self) -> bool:
@@ -572,6 +592,12 @@ class LanguageServerId(str, Enum):
return FilenameMatcher(".gd", ".gdscript")
case self.QML:
return FilenameMatcher(".qml")
case self.GLEAM:
return FilenameMatcher(".gleam")
case self.NEXTFLOW:
# only scripts: the language server does have a service for .config files, but it provides
# no symbols for them, so treating them as source files would only pollute the symbol index
return FilenameMatcher(".nf")
case self.HTML:
return FilenameMatcher(".html", ".htm")
case self.SCSS:
@@ -588,6 +614,14 @@ class LanguageServerId(str, Enum):
for postfix in ["x", ""]:
path_patterns.append(f".{prefix}ts{postfix}")
return FilenameMatcher(*path_patterns)
case self.DENO:
# Deno serves the same TS/JS family as the TypeScript server.
path_patterns = []
for prefix in ["c", "m", ""]:
for postfix in ["x", ""]:
for base_pattern in ["ts", "js"]:
path_patterns.append(f".{prefix}{base_pattern}{postfix}")
return FilenameMatcher(*path_patterns)
case _:
raise ValueError(f"Unhandled language: {self}")
@@ -859,6 +893,14 @@ class LanguageServerId(str, Enum):
from solidlsp.language_servers.qml_language_server import QmlLanguageServer
return QmlLanguageServer
case self.GLEAM:
from solidlsp.language_servers.gleam_language_server import GleamLanguageServer
return GleamLanguageServer
case self.NEXTFLOW:
from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer
return NextflowLanguageServer
case self.HTML:
from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer
@@ -871,6 +913,10 @@ class LanguageServerId(str, Enum):
from solidlsp.language_servers.angular_language_server import AngularLanguageServer
return AngularLanguageServer
case self.DENO:
from solidlsp.language_servers.deno_language_server import DenoLanguageServer
return DenoLanguageServer
case _:
raise ValueError(f"Unhandled language: {self}")
@@ -1,9 +1,20 @@
{
"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://github.com/nextflow-io/language-server/releases/download/v26.04.3/language-server-all.jar": "20cfa34f6e202d6b8babd8d786202ce00e0d39b70ccec3290e2ab3fbd02bc016",
"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"
}