From 20741931296f0a7cd71224a443242cfe41e7de9a Mon Sep 17 00:00:00 2001 From: flex-hyeonseok Date: Mon, 22 Dec 2025 15:47:07 +0900 Subject: [PATCH 1/6] :triangular_flag_on_post: add custom jvm option for kotlin language server --- .../kotlin_language_server.py | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/solidlsp/language_servers/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server.py index cd3624fd..c3f637fb 100644 --- a/src/solidlsp/language_servers/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server.py @@ -1,5 +1,17 @@ """ Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. + +You can configure the following options in ls_specific_settings (in serena_config.yml): + + ls_specific_settings: + kotlin: + jvm_options: '-Xmx4G' # JVM options for Kotlin Language Server (default: -Xmx4G) + +Example configuration for large projects: + + ls_specific_settings: + kotlin: + jvm_options: '-Xmx8G -XX:+UseG1GC' """ import dataclasses @@ -10,7 +22,7 @@ import stat from typing import cast from solidlsp.ls import SolidLanguageServer -from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import FileUtils, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @@ -18,6 +30,10 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +# Default JVM options for Kotlin Language Server +# -Xmx4G: Limit max heap to 4GB to prevent OOM on large projects +DEFAULT_KOTLIN_JVM_OPTIONS = "-Xmx4G" + @dataclasses.dataclass class KotlinRuntimeDependencyPaths: @@ -45,8 +61,21 @@ class KotlinLanguageServer(SolidLanguageServer): # Create command to execute the Kotlin Language Server script cmd = [self.runtime_dependency_paths.kotlin_executable_path, "--stdio"] - # Set environment variables including JAVA_HOME - proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} + # Get JVM options from settings or use default + jvm_options = DEFAULT_KOTLIN_JVM_OPTIONS + if solidlsp_settings.ls_specific_settings: + kotlin_settings = solidlsp_settings.get_ls_specific_settings(Language.KOTLIN) + custom_jvm_options = kotlin_settings.get("jvm_options", "") + if custom_jvm_options: + jvm_options = custom_jvm_options + log.info(f"Using custom JVM options for Kotlin Language Server: {jvm_options}") + + # Set environment variables including JAVA_HOME and JVM options + # JAVA_TOOL_OPTIONS is automatically picked up by any Java process + proc_env = { + "JAVA_HOME": self.runtime_dependency_paths.java_home_path, + "JAVA_TOOL_OPTIONS": jvm_options, + } super().__init__( config, repository_root_path, ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), "kotlin", solidlsp_settings From 65c2eaac13c32564c6f9734bd05a90881f51c354 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 22 Dec 2025 11:24:43 +0100 Subject: [PATCH 2/6] Fix: EditedFileContext (and therefore ReplaceContentTool) not using CodeEditor As a result, JetBrains IDE was not notified about changes (for reindexing) Resolves #865 --- src/serena/code_editor.py | 30 ++++++++++++++++++++++-------- src/serena/tools/file_tools.py | 2 +- src/serena/tools/tools_base.py | 32 +++++++++++++------------------- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/serena/code_editor.py b/src/serena/code_editor.py index 772294f0..4f5fe6bb 100644 --- a/src/serena/code_editor.py +++ b/src/serena/code_editor.py @@ -46,6 +46,14 @@ class CodeEditor(Generic[TSymbol], ABC): :return: the contents of the file. """ + @abstractmethod + def set_contents(self, contents: str) -> None: + """ + Fully resets the contents of the file. + + :param contents: the new contents + """ + @abstractmethod def delete_text_between_positions(self, start_pos: PositionInFile, end_pos: PositionInFile) -> None: pass @@ -62,7 +70,7 @@ class CodeEditor(Generic[TSymbol], ABC): raise NotImplementedError("This method must be overridden for each subclass") @contextmanager - def _edited_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]: + def edited_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]: """ Context manager for editing a file. """ @@ -99,7 +107,7 @@ class CodeEditor(Generic[TSymbol], ABC): start_pos = symbol.get_body_start_position_or_raise() end_pos = symbol.get_body_end_position_or_raise() - with self._edited_file_context(relative_file_path) as edited_file: + with self.edited_file_context(relative_file_path) as edited_file: # make sure the replacement adds no additional newlines (before or after) - all newlines # and whitespace before/after should remain the same, so we strip it entirely body = body.strip() @@ -154,7 +162,7 @@ class CodeEditor(Generic[TSymbol], ABC): # `line += 1`, is replaced body = body.rstrip("\r\n") + "\n" - with self._edited_file_context(relative_file_path) as edited_file: + with self.edited_file_context(relative_file_path) as edited_file: edited_file.insert_text_at_position(PositionInFile(line, col), body) def insert_before_symbol(self, name_path: str, relative_file_path: str, body: str) -> None: @@ -182,7 +190,7 @@ class CodeEditor(Generic[TSymbol], ABC): body += "\n" * num_trailing_newlines # apply edit - with self._edited_file_context(relative_file_path) as edited_file: + with self.edited_file_context(relative_file_path) as edited_file: edited_file.insert_text_at_position(PositionInFile(line=line, col=col), body) def insert_at_line(self, relative_path: str, line: int, content: str) -> None: @@ -193,7 +201,7 @@ class CodeEditor(Generic[TSymbol], ABC): :param line: the 0-based index of the line to insert content at :param content: the content to insert """ - with self._edited_file_context(relative_path) as edited_file: + with self.edited_file_context(relative_path) as edited_file: edited_file.insert_text_at_position(PositionInFile(line, 0), content) def delete_lines(self, relative_path: str, start_line: int, end_line: int) -> None: @@ -207,7 +215,7 @@ class CodeEditor(Generic[TSymbol], ABC): start_col = 0 end_line_for_delete = end_line + 1 end_col = 0 - with self._edited_file_context(relative_path) as edited_file: + with self.edited_file_context(relative_path) as edited_file: start_pos = PositionInFile(line=start_line, col=start_col) end_pos = PositionInFile(line=end_line_for_delete, col=end_col) edited_file.delete_text_between_positions(start_pos, end_pos) @@ -219,7 +227,7 @@ class CodeEditor(Generic[TSymbol], ABC): symbol = self._find_unique_symbol(name_path, relative_file_path) start_pos = symbol.get_body_start_position_or_raise() end_pos = symbol.get_body_end_position_or_raise() - with self._edited_file_context(relative_file_path) as edited_file: + with self.edited_file_context(relative_file_path) as edited_file: edited_file.delete_text_between_positions(start_pos, end_pos) @abstractmethod @@ -251,6 +259,9 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): def get_contents(self) -> str: return self._file_buffer.contents + def set_contents(self, contents: str) -> None: + self._file_buffer.contents = contents + def delete_text_between_positions(self, start_pos: PositionInFile, end_pos: PositionInFile) -> None: self._lang_server.delete_text_between_positions(self.relative_path, start_pos.to_lsp_position(), end_pos.to_lsp_position()) @@ -289,7 +300,7 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): self._text_edits = text_edits def apply(self) -> None: - with self._code_editor._edited_file_context(self._relative_path) as edited_file: + with self._code_editor.edited_file_context(self._relative_path) as edited_file: edited_file = cast(LanguageServerCodeEditor.EditedFile, edited_file) edited_file.apply_text_edits(self._text_edits) @@ -382,6 +393,9 @@ class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]): def get_contents(self) -> str: return self._content + def set_contents(self, contents: str) -> None: + self._content = contents + def delete_text_between_positions(self, start_pos: PositionInFile, end_pos: PositionInFile) -> None: self._content, _ = TextUtils.delete_text_between_positions( self._content, start_pos.line, start_pos.col, end_pos.line, end_pos.col diff --git a/src/serena/tools/file_tools.py b/src/serena/tools/file_tools.py index 09e7a0e3..d243cd31 100644 --- a/src/serena/tools/file_tools.py +++ b/src/serena/tools/file_tools.py @@ -256,7 +256,7 @@ class ReplaceContentTool(Tool, ToolMarkerCanEdit): This function can be used internally by other tools. """ self.project.validate_relative_path(relative_path, require_not_ignored=require_not_ignored) - with EditedFileContext(relative_path, self.agent) as context: + with EditedFileContext(relative_path, self.create_code_editor()) as context: original_content = context.get_original_content() if mode == "literal": diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index c1b244c3..879f8184 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -1,6 +1,5 @@ import inspect import json -import os from abc import ABC from collections.abc import Iterable from dataclasses import dataclass @@ -310,24 +309,23 @@ class EditedFileContext: When exiting the context without an exception, the updated content will be written back to the file. """ - def __init__(self, relative_path: str, agent: "SerenaAgent"): - self._project = agent.get_active_project() - assert self._project is not None - self._abs_path = os.path.join(self._project.project_root, relative_path) - if not os.path.isfile(self._abs_path): - raise FileNotFoundError(f"File {self._abs_path} does not exist.") - with open(self._abs_path, encoding=self._project.project_config.encoding) as f: - self._original_content = f.read() - self._updated_content: str | None = None + def __init__(self, relative_path: str, code_editor: "CodeEditor"): + self._relative_path = relative_path + self._code_editor = code_editor + self._edited_file: CodeEditor.EditedFile | None = None + self._edited_file_context: Any = None def __enter__(self) -> Self: + self._edited_file_context = self._code_editor.edited_file_context(self._relative_path) + self._edited_file = self._edited_file_context.__enter__() return self def get_original_content(self) -> str: """ :return: the original content of the file before any modifications. """ - return self._original_content + assert self._edited_file is not None + return self._edited_file.get_contents() def set_updated_content(self, content: str) -> None: """ @@ -336,16 +334,12 @@ class EditedFileContext: :param content: the updated content of the file """ - self._updated_content = content + assert self._edited_file is not None + self._edited_file.set_contents(content) def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None: - if self._updated_content is not None and exc_type is None: - assert self._project is not None - with open(self._abs_path, "w", encoding=self._project.project_config.encoding) as f: - f.write(self._updated_content) - log.info(f"Updated content written to {self._abs_path}") - # Language servers should automatically detect the change and update its state accordingly. - # If they do not, we may have to add a call to notify it. + assert self._edited_file_context is not None + self._edited_file_context.__exit__(exc_type, exc_value, traceback) @dataclass(kw_only=True) From 6b403f81f6805f0a443d4ca4247349fde00403fc Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 22 Dec 2025 19:31:05 +0100 Subject: [PATCH 3/6] Fix: Avoid passing relative_path='.' to plugin (not canonicalised) --- src/serena/tools/jetbrains_tools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index e103854b..e2b84e6e 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -48,6 +48,8 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): -1 means the default value from the config will be used. :return: JSON string: a list of symbols (with locations) matching the name. """ + if relative_path == ".": + relative_path = None with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.find_symbol( name_path=name_path_pattern, From e5e289d5efa2b9b0a32c06115c452f3b38ab2d57 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 23 Dec 2025 03:05:43 +0100 Subject: [PATCH 4/6] Add pointer on default modes (and retaining them) --- docs/02-usage/050_configuration.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 9ebd1186..ad384aaa 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -87,12 +87,23 @@ Examples of built-in modes include: Find the concrete definitions of these modes [here](https://github.com/oraios/serena/tree/main/src/serena/resources/config/modes). +:::{important} +By default, Serena activates the two modes `interactive` and `editing`. + +As soon as you start to specify modes, only the modes you explicitly specify will be active, however. +Therefore, if you want to keep the default modes, you must specify them as well. +For example, to add mode `no-memories` to the default behaviour, specify +```shell +--mode interactive --mode editing --mode no-memories +``` +::: + Modes can be set at startup (similar to contexts) but can also be _switched dynamically_ during a session. You can instruct the LLM to use the `switch_modes` tool to activate a different set of modes (e.g., "Switch to planning and one-shot modes"). When launching Serena, specify modes using `--mode `; multiple modes can be specified, e.g. `--mode planning --mode no-onboarding`. -:::{attention} +:::{note} **Mode Compatibility**: While you can combine modes, some may be semantically incompatible (e.g., `interactive` and `one-shot`). Serena currently does not prevent incompatible combinations; it is up to the user to choose sensible mode configurations. ::: From 4b437fe73808a68ab891ba882ea2d9c25b9b8b88 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 23 Dec 2025 13:23:41 +0100 Subject: [PATCH 5/6] Mention Rider IDE as unsupported [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 84e15b9e..48b17305 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,8 @@ AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, Go, Groovy (pa As an alternative to language servers, the [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) leverages the powerful code analysis capabilities of your JetBrains IDE. The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs, -including IntelliJ IDEA, PyCharm, Android Studio, AppCode, WebStorm, PhpStorm, RubyMine, GoLand, AppCode, CLion, Rider, and others. +including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, GoLand, CLion, and others. +Only Rider is not supported. From f593067c330b87310ad2df18c11ca381bb48d968 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 24 Dec 2025 15:03:14 +0100 Subject: [PATCH 6/6] Increase default timeout to 300s (as in the plugin) --- src/serena/tools/jetbrains_plugin_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/serena/tools/jetbrains_plugin_client.py b/src/serena/tools/jetbrains_plugin_client.py index cd31b7b1..f889e725 100644 --- a/src/serena/tools/jetbrains_plugin_client.py +++ b/src/serena/tools/jetbrains_plugin_client.py @@ -41,9 +41,13 @@ class JetBrainsPluginClient(ToStringMixin): """ BASE_PORT = 0x5EA2 + PLUGIN_REQUEST_TIMEOUT = 300 + """ + the timeout used for request handling within the plugin (a constant in the plugin) + """ last_port: int | None = None - def __init__(self, port: int, timeout: int = 30): + def __init__(self, port: int, timeout: int = PLUGIN_REQUEST_TIMEOUT): self.base_url = f"http://127.0.0.1:{port}" self.timeout = timeout self.session = requests.Session()