Merge branch 'main' into improved_overview_tools

This commit is contained in:
Dominik Jain committed 2025-12-24 15:08:41 +01:00
commit a6c8c1ea8c
8 files changed
+89 -34

No files matched your search

+2 -1
View File
@@ -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.
<a href="https://plugins.jetbrains.com/plugin/28946-serena/"><img src="docs/_static/images/jetbrains-marketplace-button.png"></a>
+12 -1
View File
@@ -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 <mode-name>`; 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.
:::
+22 -8
View File
@@ -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
+1 -1
View File
@@ -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":
+5 -1
View File
@@ -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()
+2
View File
@@ -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,
+13 -19
View File
@@ -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)
@@ -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