mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 22:14:54 +00:00
Merge pull request #866 from oraios/fix-edited-file-context-use-editor
Fix: EditedFileContext (and therefore ReplaceContentTool) not using CodeEditor
This commit is contained in:
3 files changed
+36
-28
No files matched your search
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user