Skip to content

Commit e03ee51

Browse files
authored
Merge pull request oraios#225 from oraios/editing-improvements
Editing improvements
2 parents 188f0d0 + bd25df4 commit e03ee51

File tree

24 files changed

+1113
-862
lines changed

24 files changed

+1113
-862
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Status of the `main` branch. Changes prior to the next official version change w
1313
Fixes:
1414
* Fix `ExecuteShellCommandTool` and `GetCurrentConfigTool` hanging on Windows
1515
* Fix project activation by name via `--project` not working (was broken in previous release)
16+
* Improve handling of indentation and newlines in symbolic editing tools
17+
* Fix that `insert_after_symbol` was failing for insertions at the end of a file that did not end with a newline
1618

1719
# 2025-06-20
1820

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ google = [
7373
Homepage = "https://github.com/oraios/serena"
7474

7575
[tool.hatch.build.targets.wheel]
76-
packages = ["src/serena", "src/multilspy", "src/interprompt", "src/solidlsp"]
76+
packages = ["src/serena", "src/interprompt", "src/solidlsp"]
7777

7878
[tool.black]
7979
line-length = 140
@@ -82,7 +82,7 @@ target-version = [
8282
]
8383
exclude = '''
8484
/(
85-
src/multilspy
85+
src/solidlsp/language_servers/.*/static,
8686
)/
8787
'''
8888

@@ -138,7 +138,7 @@ type-check = [
138138
target-version = "py311"
139139
line-length = 140
140140
exclude = [
141-
"src/multilspy",
141+
"src/solidlsp/language_servers/**/static",
142142
]
143143

144144
[tool.ruff.format]

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
addopts = --snapshot-patch-pycharm-diff

src/serena/agent.py

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,27 +1693,16 @@ def apply(
16931693
r"""
16941694
Replaces the body of the symbol with the given `name_path`.
16951695
1696-
Important:
1697-
You don't need to provide an adjusted indentation,
1698-
as the tool will automatically add the indentation of the original symbol to each line. For example,
1699-
for replacing a method in python, you can just write (using the standard python indentation):
1700-
body="def my_method_replacement(self, ...):\n first_line\n second_line...". So each line after the first line only has
1701-
an indentation of 4 (the indentation relative to the first character),
1702-
since the additional indentation will be added by the tool. Same for more deeply nested
1703-
cases. You always only need to write the relative indentation to the first character of the first line, and that
1704-
in turn should not have any indentation.
1705-
ALWAYS REMEMBER TO USE THE CORRECT INDENTATION IN THE BODY!
1706-
17071696
:param name_path: for finding the symbol to replace, same logic as in the `find_symbol` tool.
17081697
:param relative_path: the relative path to the file containing the symbol
1709-
:param body: the new symbol body.
1710-
1698+
:param body: the new symbol body. Important: Begin directly with the symbol definition and provide no
1699+
leading indentation for the first line (but do indent the rest of the body according to the context).
17111700
"""
17121701
self.symbol_manager.replace_body(
17131702
name_path,
17141703
relative_file_path=relative_path,
17151704
body=body,
1716-
use_same_indentation=True,
1705+
use_same_indentation=False,
17171706
)
17181707
return SUCCESS_RESULT
17191708

@@ -1733,17 +1722,12 @@ def apply(
17331722
Inserts the given body/content after the end of the definition of the given symbol (via the symbol's location).
17341723
A typical use case is to insert a new class, function, method, field or variable assignment.
17351724
1736-
:param name_path: for finding the symbol to insert after, same logic as in the `find_symbol` tool.
1725+
:param name_path: name path of the symbol after which to insert content (definitions in the `find_symbol` tool apply)
17371726
:param relative_path: the relative path to the file containing the symbol
1738-
:param body: the body/content to be inserted. Important: the inserted code will automatically have the
1739-
same indentation as the symbol's body, so you do not need to provide any additional indentation.
1727+
:param body: the body/content to be inserted. The inserted code shall begin with the next line after
1728+
the symbol.
17401729
"""
1741-
self.symbol_manager.insert_after_symbol(
1742-
name_path,
1743-
relative_file_path=relative_path,
1744-
body=body,
1745-
use_same_indentation=True,
1746-
)
1730+
self.symbol_manager.insert_after_symbol(name_path, relative_file_path=relative_path, body=body, use_same_indentation=False)
17471731
return SUCCESS_RESULT
17481732

17491733

@@ -1763,17 +1747,11 @@ def apply(
17631747
A typical use case is to insert a new class, function, method, field or variable assignment.
17641748
It also can be used to insert a new import statement before the first symbol in the file.
17651749
1766-
:param name_path: for finding the symbol to insert before, same logic as in the `find_symbol` tool.
1750+
:param name_path: name path of the symbol before which to insert content (definitions in the `find_symbol` tool apply)
17671751
:param relative_path: the relative path to the file containing the symbol
1768-
:param body: the body/content to be inserted. Important: the inserted code will automatically have the
1769-
same indentation as the symbol's body, so you do not need to provide any additional indentation.
1752+
:param body: the body/content to be inserted before the line in which the referenced symbol is defined
17701753
"""
1771-
self.symbol_manager.insert_before_symbol(
1772-
name_path,
1773-
relative_file_path=relative_path,
1774-
body=body,
1775-
use_same_indentation=True,
1776-
)
1754+
self.symbol_manager.insert_before_symbol(name_path, relative_file_path=relative_path, body=body, use_same_indentation=False)
17771755
return SUCCESS_RESULT
17781756

17791757

src/serena/resources/config/modes/editing.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ prompt: |
2323
use `find_symbol` with the name path `Foo/__init__` and `include_body=True`. If you don't know yet which methods in `Foo` you need to read or edit,
2424
you can use `find_symbol` with the name path `Foo`, `include_body=False` and `depth=1` to get all (top-level) methods of `Foo` before proceeding
2525
to read the desired methods with `include_body=True`.
26-
Note that you never need to add additional indentation, as all symbol editing tools will automatically add the indentation of the symbol that
27-
you are replacing or inserting above or below. In particular, keep in mind the description of the `replace_symbol_body` tool. If you want to add some new code at the end of the file, you should
26+
In particular, keep in mind the description of the `replace_symbol_body` tool. If you want to add some new code at the end of the file, you should
2827
use the `insert_after_symbol` tool with the last top-level symbol in the file. If you want to add an import, often a good strategy is to use
2928
`insert_before_symbol` with the first top-level symbol in the file.
3029
You can understand relationships between symbols by using the `find_referencing_symbols` tool. If not explicitly requested otherwise by a user,

src/serena/symbol.py

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import logging
33
import os
4-
from collections.abc import Iterator, Sequence
4+
from collections.abc import Iterable, Iterator, Reversible, Sequence
55
from contextlib import contextmanager
66
from dataclasses import asdict, dataclass, field
77
from difflib import SequenceMatcher
@@ -256,6 +256,13 @@ def kind(self) -> str:
256256
def symbol_kind(self) -> SymbolKind:
257257
return self.symbol_root["kind"]
258258

259+
def is_neighbouring_definition_separated_by_empty_line(self) -> bool:
260+
"""
261+
:return: whether a symbol definition of this symbol's kind is usually separated from the
262+
previous/next definition by at least one empty line.
263+
"""
264+
return self.symbol_kind in (SymbolKind.Function, SymbolKind.Method, SymbolKind.Class, SymbolKind.Interface, SymbolKind.Struct)
265+
259266
@property
260267
def relative_path(self) -> str | None:
261268
location = self.symbol_root.get("location")
@@ -689,26 +696,36 @@ def replace_body_at_location(self, location: SymbolLocation, body: str, *, use_s
689696
if start_pos is None or end_pos is None:
690697
raise ValueError(f"Symbol at {location} does not have a defined body range.")
691698
start_line, start_col = start_pos["line"], start_pos["character"]
699+
692700
if use_same_indentation:
693701
indent = " " * start_col
694702
body_lines = body.splitlines()
695703
body = body_lines[0] + "\n" + "\n".join(indent + line for line in body_lines[1:])
696704

697-
# make sure body always ends with at least one newline
698-
if not body.endswith("\n"):
699-
body += "\n"
705+
# make sure the replacement adds no additional newlines (before or after) - all newlines
706+
# and whitespace before/after should remain the same, so we strip it entirely
707+
body = body.strip()
708+
700709
self._lang_server.delete_text_between_positions(location.relative_path, start_pos, end_pos)
701710
self._lang_server.insert_text_at_position(location.relative_path, start_line, start_col, body)
702711

703-
def insert_after_symbol(
704-
self,
705-
name_path: str,
706-
relative_file_path: str,
707-
body: str,
708-
*,
709-
use_same_indentation: bool = True,
710-
at_new_line: bool = True,
711-
) -> None:
712+
@staticmethod
713+
def _count_leading_newlines(text: Iterable) -> int:
714+
cnt = 0
715+
for c in text:
716+
if c == "\n":
717+
cnt += 1
718+
elif c == "\r":
719+
continue
720+
else:
721+
break
722+
return cnt
723+
724+
@classmethod
725+
def _count_trailing_newlines(cls, text: Reversible) -> int:
726+
return cls._count_leading_newlines(reversed(text))
727+
728+
def insert_after_symbol(self, name_path: str, relative_file_path: str, body: str, *, use_same_indentation: bool = True) -> None:
712729
"""
713730
Inserts content after the symbol with the given name in the given file.
714731
"""
@@ -722,13 +739,9 @@ def insert_after_symbol(
722739
f"Found symbols at locations: \n" + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2)
723740
)
724741
symbol = symbol_candidates[-1]
725-
return self.insert_after_symbol_at_location(
726-
symbol.location, body, at_new_line=at_new_line, use_same_indentation=use_same_indentation
727-
)
742+
return self.insert_after_symbol_at_location(symbol.location, body, use_same_indentation=use_same_indentation)
728743

729-
def insert_after_symbol_at_location(
730-
self, location: SymbolLocation, body: str, *, at_new_line: bool = True, use_same_indentation: bool = True
731-
) -> None:
744+
def insert_after_symbol_at_location(self, location: SymbolLocation, body: str, *, use_same_indentation: bool = True) -> None:
732745
"""
733746
Appends content after the given symbol
734747
@@ -750,12 +763,23 @@ def insert_after_symbol_at_location(
750763
if pos is None:
751764
raise ValueError(f"Symbol at {location} does not have a defined end position.")
752765

753-
line, col = pos["line"], pos["character"]
754-
if at_new_line:
755-
line += 1
756-
col = 0
757-
if not body.startswith("\n"):
758-
body = "\n" + body
766+
# start at the beginning of the next line
767+
col = 0
768+
line = pos["line"] + 1
769+
# make sure a suitable number of leading empty lines is used (at least 0/1 depending on the symbol type,
770+
# otherwise as many as the caller wanted to insert)
771+
original_leading_newlines = self._count_leading_newlines(body)
772+
body = body.lstrip("\r\n")
773+
min_empty_lines = 0
774+
if symbol.is_neighbouring_definition_separated_by_empty_line():
775+
min_empty_lines = 1
776+
num_leading_empty_lines = max(min_empty_lines, original_leading_newlines)
777+
if num_leading_empty_lines:
778+
body = ("\n" * num_leading_empty_lines) + body
779+
# make sure the one line break succeeding the original symbol, which we repurposed as prefix via
780+
# `line += 1`, is replaced
781+
body = body.rstrip("\r\n") + "\n"
782+
759783
if use_same_indentation:
760784
symbol_start_pos = symbol.body_start_position
761785
assert symbol_start_pos is not None, f"Symbol at {location=} does not have a defined start position."
@@ -778,20 +802,11 @@ def insert_after_symbol_at_location(
778802
# > test test
779803
# > second line
780804
# > dataclass_instance.status = "active" # Reassign dataclass field
781-
col = 0
782805

783806
with self._edited_symbol_location(location):
784807
self._lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body)
785808

786-
def insert_before_symbol(
787-
self,
788-
name_path: str,
789-
relative_file_path: str,
790-
body: str,
791-
*,
792-
at_new_line: bool = True,
793-
use_same_indentation: bool = True,
794-
) -> None:
809+
def insert_before_symbol(self, name_path: str, relative_file_path: str, body: str, *, use_same_indentation: bool = True) -> None:
795810
"""
796811
Inserts content before the symbol with the given name in the given file.
797812
"""
@@ -805,11 +820,9 @@ def insert_before_symbol(
805820
f"Found symbols at locations: \n" + json.dumps([s.location.to_dict() for s in symbol_candidates], indent=2)
806821
)
807822
symbol = symbol_candidates[0]
808-
self.insert_before_symbol_at_location(symbol.location, body, at_new_line=at_new_line, use_same_indentation=use_same_indentation)
823+
self.insert_before_symbol_at_location(symbol.location, body, use_same_indentation=use_same_indentation)
809824

810-
def insert_before_symbol_at_location(
811-
self, location: SymbolLocation, body: str, *, at_new_line: bool = True, use_same_indentation: bool = True
812-
) -> None:
825+
def insert_before_symbol_at_location(self, location: SymbolLocation, body: str, *, use_same_indentation: bool = True) -> None:
813826
"""
814827
Inserts content before the given symbol
815828
@@ -820,18 +833,28 @@ def insert_before_symbol_at_location(
820833
symbol_start_pos = symbol.body_start_position
821834
if symbol_start_pos is None:
822835
raise ValueError(f"Symbol at {location} does not have a defined start position.")
823-
line = symbol_start_pos["line"]
824-
col = symbol_start_pos["character"]
836+
825837
if use_same_indentation:
826-
indent = " " * (col)
838+
indent = " " * (symbol_start_pos["character"])
827839
body = "\n".join(indent + line for line in body.splitlines())
828840

829-
# similar problems as in insert_after_symbol_at_location, see comment there
830-
if at_new_line:
831-
col = 0
832-
line -= 1
833-
if not body.endswith("\n"):
834-
body += "\n"
841+
# insert position is the start of line where the symbol is defined
842+
line = symbol_start_pos["line"]
843+
col = 0
844+
845+
original_trailing_empty_lines = self._count_trailing_newlines(body) - 1
846+
847+
# ensure eol is present at end
848+
body = body.rstrip() + "\n"
849+
850+
# add suitable number of trailing empty lines after the body (at least 0/1 depending on the symbol type,
851+
# otherwise as many as the caller wanted to insert)
852+
min_trailing_empty_lines = 0
853+
if symbol.is_neighbouring_definition_separated_by_empty_line():
854+
min_trailing_empty_lines = 1
855+
num_trailing_newlines = max(min_trailing_empty_lines, original_trailing_empty_lines)
856+
body += "\n" * num_trailing_newlines
857+
835858
assert location.relative_path is not None
836859

837860
self._lang_server.insert_text_at_position(location.relative_path, line=line, column=col, text_to_be_inserted=body)

src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
import threading
1111

1212
from solidlsp.ls import SolidLanguageServer
13-
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
14-
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
1513
from solidlsp.ls_config import LanguageServerConfig
1614
from solidlsp.ls_logger import LanguageServerLogger
1715
from solidlsp.ls_utils import FileUtils, PlatformUtils
16+
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
17+
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
1818

1919

2020
class ClangdLanguageServer(SolidLanguageServer):

src/solidlsp/language_servers/dart_language_server/dart_language_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
import stat
66

77
from solidlsp.ls import SolidLanguageServer
8-
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
98
from solidlsp.ls_logger import LanguageServerLogger
109
from solidlsp.ls_utils import FileUtils, PlatformUtils
10+
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
1111

1212

1313
class DartLanguageServer(SolidLanguageServer):

src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
from overrides import override
1717

1818
from solidlsp.ls import SolidLanguageServer
19-
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
20-
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
2119
from solidlsp.ls_config import LanguageServerConfig
2220
from solidlsp.ls_logger import LanguageServerLogger
23-
from solidlsp.settings import SolidLSPSettings
2421
from solidlsp.ls_utils import FileUtils, PlatformUtils
22+
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
23+
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
24+
from solidlsp.settings import SolidLSPSettings
2525

2626

2727
@dataclasses.dataclass

src/solidlsp/language_servers/gopls/gopls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
from overrides import override
99

1010
from solidlsp.ls import SolidLanguageServer
11-
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
12-
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
1311
from solidlsp.ls_config import LanguageServerConfig
1412
from solidlsp.ls_logger import LanguageServerLogger
13+
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
14+
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
1515

1616

1717
class Gopls(SolidLanguageServer):

0 commit comments

Comments
 (0)