mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 18:16:43 +00:00
Merge branch 'main' into improved_overview_tools
This commit is contained in:
commit
a6c8c1ea8c
8 files changed
+89
-34
No files matched your search
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user