mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 08:15:59 +00:00
Scala: answer Metals' build-import prompt (#1769)
* scala: answer Metals' build-import prompt
Metals asks, via window/showMessageRequest, whether to import a workspace it
has not seen before. Serena registered no handler, so the request came back
`method 'window/showMessageRequest' not handled on client` and Metals logged
"Unexpected error initializing server" and gave up. No build server, no build
target, and every cross-file query answered by the fallback presentation
compiler — which cannot see past the file it is given.
A client cannot decline the question instead: Metals' `disableShowMessageRequest`
is server-side configuration, and its no-op fallback answers "Not now", which
imports nothing either. Answering is the only route to a build.
So answer the three prompts that lead to a build server, and dismiss anything
else with `null` — a prompt we do not recognise is one whose consequences we
cannot judge, and two of Metals' others offer to kill a process and to open a
window. "Don't show again" is never chosen; Metals persists that in the
project's own state.
Since answering yes lets Metals run the project's build tool,
`ls_specific_settings.scala.auto_import_build: false` declines instead.
* scala: name the build-tool choice as a gap, and test the setting
`Messages.ChooseBuildTool` ("Multiple build definitions found. Which would
you like to use?") offers the build tools' own executable names, and precedes
the import prompt wherever a workspace holds more than one kind of build. It
is dismissed like anything else unrecognised, so such a workspace is still
not imported — a deliberate choice, since picking one is a guess of a
different order, but one the comment and the setup guide should admit to
rather than claim every prompt on the path is answered.
Also: cover the route from `ls_specific_settings` to `auto_import_build`,
which nothing exercised, and correct the `ImportBuildChanges` message in the
fixtures, which had the notification variant's trailing full stop rather than
the request's own text.
This commit is contained in:
1 parent
217517ecdf
commit
600d3b2fad
5 files changed
+169
-3
No files matched your search
@@ -87,6 +87,13 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Language servers and their dependency providers now go through the `subprocess_run` helper instead of
|
||||
calling `subprocess.run` directly (e.g. for installation processes), so all such subprocesses get
|
||||
`stdin=DEVNULL` and can no longer interfere with the stdio MCP connection #1748
|
||||
- `scala`: Fix: Metals asks via `window/showMessageRequest` whether to import a workspace it has not
|
||||
seen before, and Serena had no handler, so the request failed with `MethodNotFound` and Metals gave
|
||||
up on the import ("Unexpected error initializing server"). No build server was ever connected and
|
||||
every cross-file query fell back to the presentation compiler, which sees one file at a time, unless
|
||||
the project happened to have been imported beforehand by another editor. The three prompts that lead
|
||||
to a build server are now answered; anything else is dismissed, including the choice between several
|
||||
build definitions in one workspace. `ls_specific_settings.scala.auto_import_build: false` opts out
|
||||
|
||||
* Dashboard:
|
||||
- Fix: Serena PyPI version check triggered by callback on main thread could delay agent startup #1774
|
||||
|
||||
@@ -147,7 +147,7 @@ Some languages require additional installations or setup steps, as noted.
|
||||
* **Rust**
|
||||
(requires [rustup](https://rustup.rs/) - uses rust-analyzer from your toolchain)
|
||||
* **Scala**
|
||||
(requires some [manual setup](../03-special-guides/scala_setup_guide_for_serena); uses Metals LSP)
|
||||
(uses Metals LSP, which imports the build on first use — see the [setup guide](../03-special-guides/scala_setup_guide_for_serena))
|
||||
* **SCSS / Sass / CSS**
|
||||
(experimental; requires Node.js + npm; uses [some-sass-language-server](https://github.com/wkillerud/some-sass) to handle
|
||||
`.scss`, `.sass`, and `.css`)
|
||||
|
||||
@@ -15,7 +15,16 @@ Install the following on your system and ensure they are available on `PATH`:
|
||||
- Serena uses `cs` if available; if only `coursier` exists, it will attempt to install `cs`. If neither is present, install Coursier first.
|
||||
|
||||
---
|
||||
## Quick Start (Recommended: VS Code + Metals auto‑import)
|
||||
## Quick Start
|
||||
|
||||
Start Serena in your project root. Metals asks whether to import a workspace it has not seen before, and Serena answers that prompt for it — the build is imported (for sbt, by running `sbt bloopInstall`), `.bloop/` and `.metals/` are created, and cross-file navigation works from there. The first run therefore takes as long as your build takes to load.
|
||||
|
||||
Set `auto_import_build: false` under `ls_specific_settings.scala` to decline instead; you then need to import the build yourself by one of the routes below, or cross-file queries will be served by the fallback presentation compiler and see only one file at a time.
|
||||
|
||||
Serena answers three of Metals' prompts — “Import build”, “Import changes”, “Connect”. Anything else Metals asks is dismissed and logged, including “Multiple build definitions found. Which would you like to use?”, so a workspace holding more than one kind of build (say both an sbt and a Maven definition) still needs importing by one of the routes below.
|
||||
|
||||
---
|
||||
## Importing the build yourself (VS Code)
|
||||
|
||||
1. Open your Scala project in VS Code.
|
||||
2. When prompted by Metals, accept “Import build”. Wait until the import and initial compile/indexing finish.
|
||||
@@ -25,7 +34,7 @@ Install the following on your system and ensure they are available on `PATH`:
|
||||
This flow ensures the `.bloop/` and (if applicable) `.metals/` directories are created and your build is known to the build server that Metals uses.
|
||||
|
||||
---
|
||||
## Manual Setup (No VS Code)
|
||||
## Importing the build yourself (No VS Code)
|
||||
|
||||
Follow these steps if you prefer a manual setup or you are not using VS Code:
|
||||
|
||||
|
||||
@@ -28,6 +28,22 @@ DEFAULT_METALS_VERSION = "1.6.4"
|
||||
DEFAULT_CLIENT_NAME = "Serena"
|
||||
DEFAULT_ON_STALE_LOCK = "auto-clean"
|
||||
DEFAULT_LOG_MULTI_INSTANCE_NOTICE = True
|
||||
DEFAULT_AUTO_IMPORT_BUILD = True
|
||||
|
||||
# The `window/showMessageRequest` actions Serena answers affirmatively: the ones standing between
|
||||
# an un-imported workspace and a build server. Everything else is dismissed, since a prompt we do
|
||||
# not recognise is one whose consequences we cannot judge — `Messages.OldBloopVersionRunning`
|
||||
# offers to kill a process, `Messages.NewScalaProject` to open a window. "Don't show again" is
|
||||
# never chosen: Metals persists that dismissal in the project's own state.
|
||||
# (scala/meta/internal/metals/Messages.scala, at `ImportBuild`, `ImportBuildChanges`,
|
||||
# `GenerateBspAndConnect`.)
|
||||
#
|
||||
# Not answered, deliberately: `Messages.ChooseBuildTool` ("Multiple build definitions found. Which
|
||||
# would you like to use?"), whose actions are the build tools' own executable names. It precedes
|
||||
# the import prompt in a workspace holding more than one kind of build, so such a workspace is
|
||||
# still not imported — but choosing a build tool for the user is a guess of a different order, and
|
||||
# Metals offers no way to say "whichever you would have picked".
|
||||
BUILD_IMPORT_PROMPT_ACTIONS = ("Import build", "Import changes", "Connect")
|
||||
|
||||
|
||||
class StaleLockMode(Enum):
|
||||
@@ -43,6 +59,28 @@ class StaleLockMode(Enum):
|
||||
"""Raise an error and refuse to start."""
|
||||
|
||||
|
||||
def choose_show_message_request_action(params: dict, auto_import_build: bool = DEFAULT_AUTO_IMPORT_BUILD) -> dict | None:
|
||||
"""
|
||||
Choose Serena's answer to a `window/showMessageRequest`, which Metals uses to ask whether to
|
||||
import the build.
|
||||
|
||||
:param params: the request's `ShowMessageRequestParams`
|
||||
:param auto_import_build: whether to answer the build-import prompts affirmatively
|
||||
:return: the action to select, or None to select none — which is what the LSP spec provides
|
||||
for, and what leaving the request unanswered fails to say
|
||||
"""
|
||||
message = params.get("message", "")
|
||||
actions = params.get("actions") or []
|
||||
if auto_import_build:
|
||||
for action in actions:
|
||||
if isinstance(action, dict) and action.get("title") in BUILD_IMPORT_PROMPT_ACTIONS:
|
||||
log.info(f"Metals asked: {message!r}; answering {action['title']!r}")
|
||||
return action
|
||||
offered = [action.get("title") for action in actions if isinstance(action, dict)]
|
||||
log.info(f"Metals asked: {message!r}; dismissing (offered: {offered})")
|
||||
return None
|
||||
|
||||
|
||||
def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object]:
|
||||
"""
|
||||
Extract Scala-specific settings with defaults applied.
|
||||
@@ -52,6 +90,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object
|
||||
- client_name: str
|
||||
- on_stale_lock: StaleLockMode
|
||||
- log_multi_instance_notice: bool
|
||||
- auto_import_build: bool
|
||||
"""
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
|
||||
@@ -60,6 +99,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object
|
||||
"client_name": DEFAULT_CLIENT_NAME,
|
||||
"on_stale_lock": StaleLockMode.AUTO_CLEAN,
|
||||
"log_multi_instance_notice": DEFAULT_LOG_MULTI_INSTANCE_NOTICE,
|
||||
"auto_import_build": DEFAULT_AUTO_IMPORT_BUILD,
|
||||
}
|
||||
|
||||
if not solidlsp_settings.ls_specific_settings:
|
||||
@@ -80,6 +120,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object
|
||||
"client_name": scala_settings.get("client_name", DEFAULT_CLIENT_NAME),
|
||||
"on_stale_lock": on_stale_lock,
|
||||
"log_multi_instance_notice": scala_settings.get("log_multi_instance_notice", DEFAULT_LOG_MULTI_INSTANCE_NOTICE),
|
||||
"auto_import_build": scala_settings.get("auto_import_build", DEFAULT_AUTO_IMPORT_BUILD),
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +141,14 @@ class ScalaLanguageServer(SolidLanguageServer):
|
||||
metals_version: '1.6.4'
|
||||
# Client identifier sent to Metals (default: DEFAULT_CLIENT_NAME)
|
||||
client_name: 'Serena'
|
||||
# Answer Metals' build-import prompts affirmatively, which lets it run the project's
|
||||
# build tool (e.g. sbt bloopInstall). Set false to leave the build un-imported.
|
||||
auto_import_build: true
|
||||
|
||||
Build import:
|
||||
Metals asks, via `window/showMessageRequest`, whether to import a workspace it has not
|
||||
seen before; until that is answered it has no build server and so no build target, and
|
||||
every cross-file query is served by the fallback presentation compiler.
|
||||
|
||||
Multi-instance support:
|
||||
Metals uses H2 AUTO_SERVER mode (enabled by default) to support multiple
|
||||
@@ -116,6 +165,8 @@ class ScalaLanguageServer(SolidLanguageServer):
|
||||
# Check for stale locks before setting up dependencies (fail-fast)
|
||||
self._check_metals_db_status(repository_root_path, solidlsp_settings)
|
||||
|
||||
self._auto_import_build: bool = _get_scala_settings(solidlsp_settings)["auto_import_build"] # type: ignore[assignment]
|
||||
|
||||
scala_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings)
|
||||
super().__init__(
|
||||
config,
|
||||
@@ -300,10 +351,15 @@ class ScalaLanguageServer(SolidLanguageServer):
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _answer_show_message_request(self, params: dict) -> dict | None:
|
||||
return choose_show_message_request_action(params, auto_import_build=self._auto_import_build)
|
||||
|
||||
def _start_server(self) -> None:
|
||||
"""
|
||||
Starts the Scala Language Server
|
||||
"""
|
||||
self.server.on_request("window/showMessageRequest", self._answer_show_message_request)
|
||||
|
||||
log.info("Starting Scala server process")
|
||||
self.server.start()
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Unit tests for Serena's answer to Metals' `window/showMessageRequest` prompts.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.scala_language_server import (
|
||||
_get_scala_settings,
|
||||
choose_show_message_request_action,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
# Metals' own prompts, with the message and action titles as its `Messages` object builds them
|
||||
# (scala/meta/internal/metals/Messages.scala) and `type` as lsp4j serialises `MessageType.Info`.
|
||||
IMPORT_BUILD = {
|
||||
"message": "New sbt workspace detected, would you like to import the build?",
|
||||
"type": 3,
|
||||
"actions": [{"title": "Import build"}, {"title": "Not now"}, {"title": "Don't show again"}],
|
||||
}
|
||||
IMPORT_CHANGES = {
|
||||
"message": "sbt build needs to be re-imported",
|
||||
"type": 3,
|
||||
"actions": [{"title": "Import changes"}, {"title": "Not now"}, {"title": "Don't show again"}],
|
||||
}
|
||||
GENERATE_BSP_AND_CONNECT = {
|
||||
"message": "New sbt workspace detected, would you like connect to the Bloop build server?",
|
||||
"type": 3,
|
||||
"actions": [{"title": "Connect"}, {"title": "Not now"}, {"title": "Don't show again"}],
|
||||
}
|
||||
OLD_BLOOP_VERSION_RUNNING = {
|
||||
"message": "Deprecated Bloop server is still running and is taking up resources, do you want to kill the process?",
|
||||
"type": 3,
|
||||
"actions": [{"title": "Yes"}, {"title": "Not now"}],
|
||||
}
|
||||
CHOOSE_BUILD_TOOL = {
|
||||
"message": "Multiple build definitions found. Which would you like to use?",
|
||||
"type": 3,
|
||||
"actions": [{"title": "sbt"}, {"title": "mill"}],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.scala
|
||||
class TestChooseShowMessageRequestAction:
|
||||
@pytest.mark.parametrize("params", [IMPORT_BUILD, IMPORT_CHANGES, GENERATE_BSP_AND_CONNECT])
|
||||
def test_build_import_prompts_are_answered_affirmatively(self, params: dict) -> None:
|
||||
chosen = choose_show_message_request_action(params)
|
||||
assert chosen is not None
|
||||
assert chosen["title"] in ("Import build", "Import changes", "Connect")
|
||||
|
||||
@pytest.mark.parametrize("params", [IMPORT_BUILD, IMPORT_CHANGES, GENERATE_BSP_AND_CONNECT])
|
||||
def test_auto_import_build_can_be_turned_off(self, params: dict) -> None:
|
||||
assert choose_show_message_request_action(params, auto_import_build=False) is None
|
||||
|
||||
def test_dont_show_again_is_never_chosen(self) -> None:
|
||||
"""Metals persists that dismissal in the project's own state."""
|
||||
params = {"message": "…", "actions": [{"title": "Don't show again"}, {"title": "Not now"}]}
|
||||
assert choose_show_message_request_action(params) is None
|
||||
|
||||
def test_an_unrecognised_prompt_is_dismissed(self) -> None:
|
||||
"""Answering "Yes" here would kill a process on the user's machine."""
|
||||
assert choose_show_message_request_action(OLD_BLOOP_VERSION_RUNNING) is None
|
||||
|
||||
def test_a_prompt_with_no_actions_is_dismissed(self) -> None:
|
||||
assert choose_show_message_request_action({"message": "just so you know"}) is None
|
||||
assert choose_show_message_request_action({"message": "just so you know", "actions": None}) is None
|
||||
|
||||
def test_malformed_actions_do_not_raise(self) -> None:
|
||||
params = {"message": "…", "actions": ["Import build", None, {"title": "Import build"}]}
|
||||
assert choose_show_message_request_action(params) == {"title": "Import build"}
|
||||
|
||||
def test_choosing_between_build_tools_is_left_alone(self) -> None:
|
||||
"""Naming the build tool for the user is a guess of a different order; see the comment
|
||||
on BUILD_IMPORT_PROMPT_ACTIONS. A workspace with several build definitions is therefore
|
||||
still not imported.
|
||||
"""
|
||||
assert choose_show_message_request_action(CHOOSE_BUILD_TOOL) is None
|
||||
|
||||
|
||||
@pytest.mark.scala
|
||||
class TestAutoImportBuildSetting:
|
||||
"""`auto_import_build` has to reach the handler from the project configuration."""
|
||||
|
||||
@staticmethod
|
||||
def setting(**scala_settings) -> object:
|
||||
settings = SolidLSPSettings(ls_specific_settings={LanguageServerId.SCALA: scala_settings})
|
||||
return _get_scala_settings(settings)["auto_import_build"]
|
||||
|
||||
def test_defaults_to_true(self) -> None:
|
||||
assert self.setting() is True
|
||||
assert _get_scala_settings(SolidLSPSettings())["auto_import_build"] is True
|
||||
|
||||
def test_can_be_disabled(self) -> None:
|
||||
assert self.setting(auto_import_build=False) is False
|
||||
Reference in new issue
Block a user