mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 18:19:14 +00:00
Merge remote-tracking branch 'origin/main' into release-1.7
Conflicts: CHANGELOG.md
This commit is contained in:
commit
d3441d3e23
69 files changed
+3201
-1259
No files matched your search
+67
-25
@@ -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
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -323,6 +323,7 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook):
|
||||
".lua",
|
||||
".m",
|
||||
".matlab",
|
||||
".nf",
|
||||
".php",
|
||||
".proto",
|
||||
".ps1",
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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({})
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
Reference in new issue
Block a user