Skip to content

Commit 95bd99f

Browse files
authored
feat: Add different kind of TextEdit for convenience (#1641)
1 parent eb1dcab commit 95bd99f

File tree

2 files changed

+113
-19
lines changed

2 files changed

+113
-19
lines changed

src/robocop/linter/fix.py

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@
1313
from robocop.source_file import SourceFile
1414

1515

16+
class TextEditKind(Enum):
17+
"""
18+
Enumeration of edit operation types.
19+
20+
Attributes:
21+
REPLACEMENT (str): Replace an existing line or part of it with the new content.
22+
INSERTION (str): Insert a new line or lines without changing existing lines.
23+
DELETION (str): Delete line or lines.
24+
25+
"""
26+
27+
REPLACEMENT = "replace"
28+
INSERTION = "insert"
29+
DELETION = "delete"
30+
31+
1632
@dataclass
1733
class TextEdit:
1834
"""
@@ -38,11 +54,18 @@ class TextEdit:
3854
rule_id: str
3955
rule_name: str
4056
start_line: int
41-
start_col: int
42-
end_line: int
43-
start_line: int
44-
end_col: int
45-
replacement: str
57+
start_col: int | None
58+
end_line: int | None
59+
end_col: int | None
60+
replacement: str | None
61+
62+
@property
63+
def kind(self) -> TextEditKind:
64+
if self.replacement is None:
65+
return TextEditKind.DELETION
66+
if self.end_line is None:
67+
return TextEditKind.INSERTION
68+
return TextEditKind.REPLACEMENT
4669

4770
@classmethod
4871
def replace_at_range(cls, rule_id: str, rule_name: str, diag_range: Range, replacement: str) -> TextEdit:
@@ -56,6 +79,32 @@ def replace_at_range(cls, rule_id: str, rule_name: str, diag_range: Range, repla
5679
replacement=replacement,
5780
)
5881

82+
@classmethod
83+
def remove_at_range(cls, rule_id: str, rule_name: str, diag_range: Range) -> TextEdit:
84+
"""Remove lines between start_line and end_line from the edit range."""
85+
return cls(
86+
rule_id=rule_id,
87+
rule_name=rule_name,
88+
start_line=diag_range.start.line,
89+
start_col=None,
90+
end_line=diag_range.end.line,
91+
end_col=None,
92+
replacement=None,
93+
)
94+
95+
@classmethod
96+
def insert_at_range(cls, rule_id: str, rule_name: str, diag_range: Range, replacement: str) -> TextEdit:
97+
"""Insert new content at start_line."""
98+
return cls(
99+
rule_id=rule_id,
100+
rule_name=rule_name,
101+
start_line=diag_range.start.line,
102+
start_col=None,
103+
end_line=None,
104+
end_col=None,
105+
replacement=replacement,
106+
)
107+
59108

60109
class FixApplicability(Enum):
61110
"""
@@ -220,17 +269,24 @@ def _apply_edit(lines: list[str], edit: TextEdit) -> None:
220269
edit: The edit to apply (uses 1-indexed line/col numbers).
221270
222271
"""
223-
if edit.end_line > len(lines) or edit.start_line < 1:
224-
return
225-
start_line_idx = edit.start_line - 1
226-
end_line_idx = edit.end_line - 1
227-
start_col_idx = edit.start_col - 1
228-
end_col_idx = edit.end_col - 1
229-
230-
if start_line_idx == end_line_idx: # single line
231-
line = lines[edit.start_line - 1] # TODO: store for diff view, + surrounding lines
232-
new_line = line[:start_col_idx] + edit.replacement + line[end_col_idx:]
233-
lines[start_line_idx] = new_line
234-
else: # Multi-line edit
235-
# When edit is multiline, we replace the lines fully
236-
lines[start_line_idx : end_line_idx + 2] = [edit.replacement]
272+
if edit.kind == TextEditKind.REPLACEMENT:
273+
if edit.end_line > len(lines) or edit.start_line < 1:
274+
return
275+
start_line_idx = edit.start_line - 1
276+
end_line_idx = edit.end_line - 1
277+
start_col_idx = edit.start_col - 1
278+
end_col_idx = edit.end_col - 1
279+
280+
if start_line_idx == end_line_idx: # single line
281+
line = lines[edit.start_line - 1]
282+
new_line = line[:start_col_idx] + edit.replacement + line[end_col_idx:]
283+
lines[start_line_idx] = new_line
284+
else: # Multi-line edit
285+
# When edit is multiline, we replace the lines fully
286+
lines[start_line_idx : end_line_idx + 2] = [edit.replacement]
287+
elif edit.kind == TextEditKind.INSERTION:
288+
start_line_idx = edit.start_line - 1
289+
lines.insert(start_line_idx, edit.replacement)
290+
else: # edit.kind == TextEditKind.DELETION
291+
start_line_idx = edit.start_line - 1
292+
del lines[start_line_idx : edit.end_line]

tests/linter/test_fix.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pytest
66

7+
from robocop.linter.diagnostics import Position, Range
78
from robocop.linter.fix import Fix, FixApplicability, FixApplier, FixStats, TextEdit
89
from robocop.source_file import SourceFile
910

@@ -686,3 +687,40 @@ def test_diff_mode_does_not_modify_file(sample_source_file):
686687
# But original should be preserved
687688
assert "Hello World" in sample_source_file.original_source_lines[7]
688689
assert sample_source_file.original_source_lines == original_content
690+
691+
692+
def test_edit_kind(sample_source_file):
693+
applier = FixApplier()
694+
695+
replace_range = Range(start=Position(line=3, character=12), end=Position(line=3, character=23))
696+
remove_range = Range(start=Position(line=4, character=1), end=Position(line=4, character=1000))
697+
insert_range = Range(start=Position(line=5, character=1), end=Position(line=5, character=1))
698+
fixes = [
699+
Fix(
700+
edits=[
701+
TextEdit.replace_at_range(
702+
rule_id="W001", rule_name="update-library", diag_range=replace_range, replacement="Replaced"
703+
),
704+
TextEdit.remove_at_range(rule_id="W001", rule_name="update-library", diag_range=remove_range),
705+
],
706+
message="Range helpers",
707+
applicability=FixApplicability.SAFE,
708+
),
709+
Fix(
710+
edits=[
711+
TextEdit.insert_at_range(
712+
rule_id="W001", rule_name="update-library", diag_range=insert_range, replacement="Library Name\n"
713+
)
714+
],
715+
message="Range helpers",
716+
applicability=FixApplicability.SAFE,
717+
),
718+
]
719+
720+
applier.apply_fixes(sample_source_file, fixes)
721+
722+
assert applier.fix_stats.total_fixes == 0
723+
assert "Replaced" in sample_source_file.source_lines[2]
724+
assert "String" not in sample_source_file.source_lines[3]
725+
assert "Name" in sample_source_file.source_lines[3]
726+
assert applier.fix_stats.by_file[sample_source_file.path][("W001", "update-library")] == 3

0 commit comments

Comments
 (0)