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.
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.
:::
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/jetbrains_plugin_client.py b/src/serena/tools/jetbrains_plugin_client.py
index 7a3c6f38..1695e4db 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()
diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py
index a14a9402..6407de7c 100644
--- a/src/serena/tools/jetbrains_tools.py
+++ b/src/serena/tools/jetbrains_tools.py
@@ -51,6 +51,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,
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)
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