mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-15 00:18:04 +00:00
feat: Add configurable line_ending setting for file writes
Resolves #1139
This commit is contained in:
1 parent
66cbe31045
commit
d6a832b5d9
7 files changed
+73
-9
No files matched your search
@@ -21,11 +21,12 @@ TSymbol = TypeVar("TSymbol", bound=Symbol)
|
||||
|
||||
|
||||
class CodeEditor(Generic[TSymbol], ABC):
|
||||
def __init__(self, project_root: str, project_config: ProjectConfig | None = None) -> None:
|
||||
def __init__(self, project_root: str, project_config: ProjectConfig | None = None, newline: str | None = None) -> None:
|
||||
self.project_root = project_root
|
||||
|
||||
# set encoding based on project configuration, if available
|
||||
self.encoding = project_config.encoding if project_config is not None else DEFAULT_SOURCE_FILE_ENCODING
|
||||
self.newline = newline
|
||||
|
||||
class EditedFile(ABC):
|
||||
def __init__(self, relative_path: str) -> None:
|
||||
@@ -73,7 +74,7 @@ class CodeEditor(Generic[TSymbol], ABC):
|
||||
def _save_edited_file(self, edited_file: "CodeEditor.EditedFile") -> None:
|
||||
abs_path = os.path.join(self.project_root, edited_file.relative_path)
|
||||
new_contents = edited_file.get_contents()
|
||||
with open(abs_path, "w", encoding=self.encoding) as f:
|
||||
with open(abs_path, "w", encoding=self.encoding, newline=self.newline) as f:
|
||||
f.write(new_contents)
|
||||
|
||||
@abstractmethod
|
||||
@@ -235,8 +236,10 @@ class CodeEditor(Generic[TSymbol], ABC):
|
||||
|
||||
|
||||
class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]):
|
||||
def __init__(self, symbol_retriever: LanguageServerSymbolRetriever, project_config: ProjectConfig | None = None):
|
||||
super().__init__(project_root=symbol_retriever.get_root_path(), project_config=project_config)
|
||||
def __init__(
|
||||
self, symbol_retriever: LanguageServerSymbolRetriever, project_config: ProjectConfig | None = None, newline: str | None = None
|
||||
):
|
||||
super().__init__(project_root=symbol_retriever.get_root_path(), project_config=project_config, newline=newline)
|
||||
self._symbol_retriever = symbol_retriever
|
||||
|
||||
def _get_language_server(self, relative_path: str) -> SolidLanguageServer:
|
||||
@@ -372,7 +375,7 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]):
|
||||
class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]):
|
||||
def __init__(self, project: Project) -> None:
|
||||
self._project = project
|
||||
super().__init__(project_root=project.project_root, project_config=project.project_config)
|
||||
super().__init__(project_root=project.project_root, project_config=project.project_config, newline=project.line_ending.newline_str)
|
||||
|
||||
class EditedFile(CodeEditor.EditedFile):
|
||||
def __init__(self, relative_path: str, project: Project):
|
||||
|
||||
@@ -182,6 +182,35 @@ class LanguageBackend(Enum):
|
||||
return self == LanguageBackend.JETBRAINS
|
||||
|
||||
|
||||
class LineEnding(Enum):
|
||||
"""Line ending convention for file writes."""
|
||||
|
||||
LF = "lf"
|
||||
CRLF = "crlf"
|
||||
NATIVE = "native"
|
||||
|
||||
@property
|
||||
def newline_str(self) -> str | None:
|
||||
"""The newline parameter value for :func:`open` and :meth:`Path.write_text`.
|
||||
|
||||
Returns ``None`` for native mode (platform default).
|
||||
"""
|
||||
if self is LineEnding.LF:
|
||||
return "\n"
|
||||
elif self is LineEnding.CRLF:
|
||||
return "\r\n"
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, value: str) -> "LineEnding":
|
||||
"""Parse a string value into a :class:`LineEnding`."""
|
||||
try:
|
||||
return cls(value.lower())
|
||||
except ValueError as e:
|
||||
valid = [le.value for le in cls]
|
||||
raise ValueError(f"Invalid line_ending: {value!r}. Valid values are: {valid}") from e
|
||||
|
||||
|
||||
@dataclass
|
||||
class SharedConfig(ModeSelectionDefinition, ToolInclusionDefinition, ToStringMixin):
|
||||
"""Shared between SerenaConfig and ProjectConfig, the latter used to override values in the form
|
||||
@@ -191,6 +220,7 @@ class SharedConfig(ModeSelectionDefinition, ToolInclusionDefinition, ToStringMix
|
||||
|
||||
symbol_info_budget: float | None = None
|
||||
language_backend: LanguageBackend | None = None
|
||||
line_ending: LineEnding | None = None
|
||||
read_only_memory_patterns: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@@ -388,6 +418,9 @@ class ProjectConfig(SharedConfig):
|
||||
language_backend_value = data.get("language_backend")
|
||||
language_backend = LanguageBackend.from_str(language_backend_value) if language_backend_value else None
|
||||
|
||||
line_ending_value = data.get("line_ending")
|
||||
line_ending = LineEnding.from_str(line_ending_value) if line_ending_value else None
|
||||
|
||||
return cls(
|
||||
project_name=data["project_name"],
|
||||
languages=languages,
|
||||
@@ -400,6 +433,7 @@ class ProjectConfig(SharedConfig):
|
||||
ignore_all_files_in_gitignore=data["ignore_all_files_in_gitignore"],
|
||||
initial_prompt=data["initial_prompt"],
|
||||
encoding=data["encoding"],
|
||||
line_ending=line_ending,
|
||||
language_backend=language_backend,
|
||||
base_modes=data["base_modes"],
|
||||
default_modes=data["default_modes"],
|
||||
@@ -413,6 +447,7 @@ class ProjectConfig(SharedConfig):
|
||||
d = dataclasses.asdict(self)
|
||||
d["languages"] = [lang.value for lang in self.languages]
|
||||
d["language_backend"] = self.language_backend.value if self.language_backend is not None else None
|
||||
d["line_ending"] = self.line_ending.value if self.line_ending is not None else None
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
@@ -595,6 +630,7 @@ class SerenaConfig(SharedConfig):
|
||||
# settings with overridden defaults
|
||||
default_modes: Sequence[str] | None = ("interactive", "editing")
|
||||
symbol_info_budget: float = 10.0
|
||||
line_ending: LineEnding = LineEnding.NATIVE
|
||||
"""
|
||||
Time budget (seconds) for requests when tools request include_info (currently
|
||||
only supported for LSP-based tools).
|
||||
@@ -614,7 +650,7 @@ class SerenaConfig(SharedConfig):
|
||||
# *** static members ***
|
||||
|
||||
CONFIG_FILE = "serena_config.yml"
|
||||
CONFIG_FIELDS_WITH_TYPE_CONVERSION = {"projects", "language_backend"}
|
||||
CONFIG_FIELDS_WITH_TYPE_CONVERSION = {"projects", "language_backend", "line_ending"}
|
||||
|
||||
# *** methods ***
|
||||
@classmethod
|
||||
@@ -746,6 +782,13 @@ class SerenaConfig(SharedConfig):
|
||||
del loaded_commented_yaml["jetbrains"]
|
||||
instance.language_backend = language_backend
|
||||
|
||||
# determine line ending
|
||||
line_ending = get_dataclass_default(SerenaConfig, "line_ending")
|
||||
line_ending_value = loaded_commented_yaml.get("line_ending")
|
||||
if line_ending_value:
|
||||
line_ending = LineEnding.from_str(line_ending_value)
|
||||
instance.line_ending = line_ending
|
||||
|
||||
# migrate deprecated "gui_log_level" field if necessary
|
||||
if "gui_log_level" in loaded_commented_yaml:
|
||||
num_migrations += 1
|
||||
@@ -913,6 +956,9 @@ class SerenaConfig(SharedConfig):
|
||||
# convert language backend to string
|
||||
commented_yaml["language_backend"] = self.language_backend.value
|
||||
|
||||
# convert line ending to string
|
||||
commented_yaml["line_ending"] = self.line_ending.value
|
||||
|
||||
# transfer comments from the template file
|
||||
# NOTE: The template file now uses leading comments, but we previously used trailing comments,
|
||||
# so we apply a conversion, which detects the old style and transforms it.
|
||||
|
||||
@@ -249,6 +249,9 @@ class Project(ToStringMixin):
|
||||
read_only_memory_patterns = serena_config.read_only_memory_patterns + project_config.read_only_memory_patterns
|
||||
self.memories_manager = MemoriesManager(self._serena_data_folder, read_only_memory_patterns=read_only_memory_patterns)
|
||||
|
||||
# resolve line ending (project -> global)
|
||||
self.line_ending = project_config.line_ending or serena_config.line_ending
|
||||
|
||||
self.language_server_manager: LanguageServerManager | None = None
|
||||
self._is_newly_created = is_newly_created
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ languages: ["python"]
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# line ending convention for file writes: "lf", "crlf", or "native" (platform default)
|
||||
line_ending: "native"
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
@@ -126,4 +129,4 @@ symbol_info_budget:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
read_only_memory_patterns: []
|
||||
@@ -7,6 +7,11 @@
|
||||
# in your IDE).
|
||||
language_backend: LSP
|
||||
|
||||
# line ending convention for file writes across all projects.
|
||||
# Possible values: "lf" (Unix), "crlf" (Windows), "native" (platform default).
|
||||
# Can be overridden per project in project.yml.
|
||||
line_ending: native
|
||||
|
||||
# whether to open a graphical window with Serena's logs.
|
||||
# This is mainly supported on Windows and (partly) on Linux; not available on macOS.
|
||||
# If you prefer a browser-based tool, use the `web_dashboard` option instead.
|
||||
|
||||
@@ -73,7 +73,7 @@ class CreateTextFileTool(Tool, ToolMarkerCanEdit):
|
||||
), f"Cannot create file outside of the project directory, got {relative_path=}"
|
||||
|
||||
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
abs_path.write_text(content, encoding=self.project.project_config.encoding)
|
||||
abs_path.write_text(content, encoding=self.project.project_config.encoding, newline=self.project.line_ending.newline_str)
|
||||
answer = f"File created: {relative_path}."
|
||||
if will_overwrite_existing:
|
||||
answer += " Overwrote existing file."
|
||||
|
||||
@@ -63,7 +63,11 @@ class Component(ABC):
|
||||
|
||||
match self.agent.get_language_backend():
|
||||
case LanguageBackend.LSP:
|
||||
return LanguageServerCodeEditor(self.create_language_server_symbol_retriever(), project_config=self.project.project_config)
|
||||
return LanguageServerCodeEditor(
|
||||
self.create_language_server_symbol_retriever(),
|
||||
project_config=self.project.project_config,
|
||||
newline=self.project.line_ending.newline_str,
|
||||
)
|
||||
case LanguageBackend.JETBRAINS:
|
||||
return JetBrainsCodeEditor(project=self.project)
|
||||
case _:
|
||||
|
||||
Reference in new issue
Block a user