diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b15b206..75c9309c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ Status of the `main` branch. Changes prior to the next official version change w processed by the Svelte LS instead of the TypeScript LS. - Improve quoting of arguments in shell executions +* JetBrains: + - Add configuration option `jetbrains_launch_command`, allowing Serena to spawn IDE instances automatically + upon project activation + * Dashboard: - Make list of trusted hosts configurable, fixing host validation introduced in v1.5.2 allowing only default local hostnames, effectively preventing remote connections diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index 4eb08688..73638445 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -84,7 +84,7 @@ language_backend: JetBrains Note that the file might not exist yet if you never executed Serena before. -**Per-Server Instance Configuration**. +**Per-Instance Configuration**. The configuration setting in the global config file can be overridden on a per-instance basis by providing the arguments `--language-backend JetBrains` when launching the Serena MCP server. @@ -121,8 +121,12 @@ You will also notice that your client will use the JetBrains-specific tools like Having installed the plugin in your IDE and having configured Serena to use the JetBrains backend, the general workflow is simple: + 1. Open the project you want to work on in your JetBrains IDE. - Note that the project must be appropriately set up in your IDE, i.e. symbol lookups for all relevant programming languages and frameworks should work in the IDE. + Note that the project must be appropriately set up in your IDE, i.e. symbol lookups for all relevant programming languages and frameworks should work in the IDE. + + You can optionally make Serena open an IDE instance for your project root folder automatically upon project activation, allowing you to skip this step for a project that was previously set up correctly. + To enable this, configure `jetbrains_launch_command` in [Serena's global configuration file](global-config) appropriately. 2. Activate the project's root folder as a project in Serena (see [Project Creation](project-creation-indexing) and [Project Activation](project-activation)). 3. Start using Serena's tools as usual. diff --git a/src/serena/agent.py b/src/serena/agent.py index a1db940a..bef1b201 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -7,6 +7,7 @@ import multiprocessing import os import platform import signal +import subprocess import threading from collections import defaultdict from collections.abc import Callable, Iterator, Sequence @@ -40,6 +41,7 @@ from serena.config.serena_config import ( ToolInclusionDefinition, ) from serena.dashboard import SerenaDashboardAPI, SerenaDashboardTrayManager, SerenaDashboardViewer, open_url_in_browser +from serena.jetbrains import jetbrains_plugin_client from serena.ls_manager import LanguageServerManager from serena.memories.memory_manager import MemoryManager from serena.project import Project @@ -60,6 +62,7 @@ from serena.util.gui import system_has_usable_display from serena.util.inspection import iter_subclasses from serena.util.logging import MemoryLogHandler from solidlsp.ls_config import Language +from solidlsp.util import subprocess_util if TYPE_CHECKING: from serena.gui_log_viewer import GuiLogViewer @@ -1165,6 +1168,25 @@ class SerenaAgent: if self.get_language_backend().is_lsp(): self.issue_task(init_language_server_manager) + def init_jetbrains_ide() -> None: + assert self.serena_config.jetbrains_launch_command is not None + try: + client = jetbrains_plugin_client.JetBrainsPluginClient.from_project(project, log_warning=False) + log.info("Found Serena JetBrains Plugin server: %s", client) + except jetbrains_plugin_client.ServerNotFoundError: + log.info("Serena JetBrains Plugin server not found for project %s", project.project_name) + if self.serena_config.jetbrains_launch_command: + cmd = subprocess_util.convert_shell_cmd([self.serena_config.jetbrains_launch_command, project.project_root]) + log.info("Launching IDE with command: %s", cmd) + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + stdout, stderr = p.communicate() + if p.returncode != 0: + log.error(f"Failed to launch JetBrains IDE: {stderr.decode('utf-8')}") + + # for JetBrains mode, search for plugin server and spawn IDE if not found and enabled + if self.get_language_backend().is_jetbrains(): + self.issue_task(init_jetbrains_ide) + if self._project_activation_callback is not None: self._project_activation_callback() diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 7207a9b1..03a18e72 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -722,6 +722,10 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): web_dashboard_listen_address: str = "127.0.0.1" web_dashboard_trusted_hosts: list[str] = field(default_factory=lambda: ["127.0.0.1", "localhost"]) jetbrains_plugin_server_address: str = "127.0.0.1" + jetbrains_launch_command: str | None = None + """ + JetBrains IDE launch command, which can be used to auto-start an IDE instance on demand. + """ tool_timeout: float = DEFAULT_TOOL_TIMEOUT token_count_estimator: str = RegisteredTokenCountEstimator.CHAR_COUNT.name diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index c7b440ce..3cbd915f 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -105,7 +105,12 @@ class JetBrainsPluginClientManager: futures.append(future) return futures - def find_client(self, project_root: Path) -> "JetBrainsPluginClient": + def find_client(self, project_root: Path, log_warning: bool = True) -> "JetBrainsPluginClient": + """ + :param project_root: the project root path to find a plugin instance for + :param log_warning: whether to log a warning if no matching plugin instance is found + :return: the instance + """ plugin_paths_found = [] for future in self._submit_scan(): client = future.result() @@ -114,12 +119,13 @@ class JetBrainsPluginClientManager: elif client.project_root is not None: plugin_paths_found.append(client.project_root) - log.warning( - "Searched for Serena JetBrains plugin service for project at %s but found no matching service. " - "Found plugin instances for the following project paths: %s", - project_root, - plugin_paths_found, - ) + if log_warning: + log.warning( + "Searched for Serena JetBrains plugin service for project at %s but found no matching service. " + "Found plugin instances for the following project paths: %s", + project_root, + plugin_paths_found, + ) raise ServerNotFoundError( f"Found no Serena service in a JetBrains IDE instance for the project at {project_root}. " "STOP. Do not attempt any other tools or workarounds. Ask the user to open this folder as a project in a JetBrains IDE " @@ -219,7 +225,7 @@ class JetBrainsPluginClient(ToStringMixin): return ["_port", "project_root", "_plugin_version"] @classmethod - def from_project(cls, project: Project) -> Self: + def from_project(cls, project: Project, log_warning: bool = True) -> Self: resolved_path = Path(project.project_root).resolve() if cls._last_port is not None: @@ -227,7 +233,7 @@ class JetBrainsPluginClient(ToStringMixin): if client.matches(resolved_path): return client - client = JetBrainsPluginClientManager().find_client(resolved_path) + client = JetBrainsPluginClientManager().find_client(resolved_path, log_warning=log_warning) cls._last_port = client._port return client diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index 36f22bf3..fb5c1d40 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -80,6 +80,13 @@ web_dashboard_trusted_hosts: # address where JetBrains plugin servers are running (only relevant when using the JetBrains language backend) jetbrains_plugin_server_address: 127.0.0.1 +# command used to launch a JetBrains IDE on demand (only relevant when using the JetBrains language backend) +# If this is non-empty and, at project activation, the Serena JetBrains Plugin server instance corresponding +# to the project is not found, use this command to spawn a new instance. +# Specifically, if this is , then ` ` is launched. +# Provide the full path to the executable/script (e.g., "/usr/bin/idea" or "C:/Users/bob/AppData/Local/JetBrains/Toolbox/scripts/idea.cmd"). +jetbrains_launch_command: + # the minimum log level for the GUI log window and the dashboard (10 = debug, 20 = info, 30 = warning, 40 = error) log_level: 20