Skip to content

Commit 08cc4cb

Browse files
committed
feat: Add support for regex, prefix, and suffix markers in Marker class
1 parent 5175630 commit 08cc4cb

File tree

7 files changed

+142
-27
lines changed

7 files changed

+142
-27
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ classifiers = [
2222
]
2323
keywords = ["cedarscript", "code-editing", "refactoring", "code-analysis", "sql-like", "ai-assisted-development"]
2424
dependencies = [
25-
"cedarscript-ast-parser==0.2.11",
25+
"cedarscript-ast-parser==0.3.0",
2626
"grep-ast==0.3.3",
2727
"tree-sitter-languages==1.10.2",
2828
]

src/cedarscript_editor/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"__version__", "find_commands", "CEDARScriptEditor"
88
]
99

10+
1011
# TODO Move to cedarscript-ast-parser
1112
def find_commands(content: str):
1213
# Regex pattern to match CEDARScript blocks

src/text_manipulation/indentation_kit.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
from typing import NamedTuple
2121

2222

23+
def get_line_indent_count_from_lines(lines: Sequence[str], index: int) -> int:
24+
return get_line_indent_count(lines[index])
25+
26+
2327
def get_line_indent_count(line: str) -> int:
2428
"""
2529
Count the number of leading whitespace characters in a line.

src/text_manipulation/range_spec.py

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from cedarscript_ast_parser import Marker, RelativeMarker, RelativePositionType, MarkerType, BodyOrWhole
1919

20-
from .indentation_kit import get_line_indent_count
20+
from .indentation_kit import get_line_indent_count_from_lines
2121

2222
MATCH_TYPES = ('exact', 'stripped', 'normalized', 'partial')
2323

@@ -190,25 +190,71 @@ def from_line_marker(
190190
f"must be less than or equal to line count ({len(lines)})"
191191
)
192192

193-
for i in range(search_start_index, search_end_index):
194-
line = lines[i]
195-
reference_indent = get_line_indent_count(line)
193+
marker_subtype = (search_term.marker_subtype or "").casefold()
194+
195+
match marker_subtype:
196+
case 'number': # Simple case: a line number
197+
index = int(stripped_search) - 1
198+
assert 0 <= index < len(lines), (
199+
f"Line number {stripped_search} out of bounds "
200+
f"(must be in interval [1, {len(lines)}])"
201+
)
202+
reference_indent = get_line_indent_count_from_lines(lines, index)
203+
index += calc_index_delta_for_relative_position(search_term)
204+
return cls(index, index, reference_indent)
196205

197-
# Check for exact match
198-
if search_line == line:
199-
matches['exact'].append((i, reference_indent))
206+
case 'regex':
207+
try:
208+
pattern = re.compile(search_line)
209+
except re.error as e:
210+
raise ValueError(f"Invalid regex pattern '{search_line}': {e}")
200211

201-
# Check for stripped match
202-
elif stripped_search == line.strip():
203-
matches['stripped'].append((i, reference_indent))
212+
case _:
213+
pattern = None
204214

205-
# Check for normalized match
206-
elif normalized_search_line == cls.normalize_line(line):
207-
matches['normalized'].append((i, reference_indent))
215+
# Not a line number, so we need to find all line matches
216+
for i in range(search_start_index, search_end_index):
217+
reference_indent = get_line_indent_count_from_lines(lines, i)
208218

209-
# Dangerous! Last resort!
210-
elif normalized_search_line.casefold() in cls.normalize_line(line).casefold():
211-
matches['partial'].append((i, reference_indent))
219+
line = lines[i]
220+
match marker_subtype:
221+
222+
case 'regex':
223+
if pattern.search(line) or pattern.search(line.strip()):
224+
matches['exact'].append((i, reference_indent))
225+
226+
case 'prefix':
227+
# Check for stripped prefix match
228+
if line.strip().startswith(stripped_search):
229+
matches['exact'].append((i, reference_indent))
230+
# Check for normalized prefix match
231+
elif cls.normalize_line(line).startswith(normalized_search_line):
232+
matches['normalized'].append((i, reference_indent))
233+
234+
case 'suffix':
235+
# Check for stripped suffix match
236+
if line.strip().endswith(stripped_search):
237+
matches['exact'].append((i, reference_indent))
238+
# Check for normalized suffix match
239+
elif cls.normalize_line(line).endswith(normalized_search_line):
240+
matches['normalized'].append((i, reference_indent))
241+
242+
case _:
243+
# Check for exact match
244+
if search_line == line:
245+
matches['exact'].append((i, reference_indent))
246+
247+
# Check for stripped match
248+
elif stripped_search == line.strip():
249+
matches['stripped'].append((i, reference_indent))
250+
251+
# Check for normalized match
252+
elif normalized_search_line == cls.normalize_line(line):
253+
matches['normalized'].append((i, reference_indent))
254+
255+
# Dangerous! Last resort!
256+
elif normalized_search_line.casefold() in cls.normalize_line(line).casefold():
257+
matches['partial'].append((i, reference_indent))
212258

213259
offset = search_term.offset or 0
214260
max_match_count = max([len(m) for m in matches.values()])
@@ -241,16 +287,7 @@ def from_line_marker(
241287
case 'partial':
242288
print(f"Note: Won't accept {match_type} match at index {index} for {search_term}")
243289
continue
244-
if isinstance(search_term, RelativeMarker):
245-
match search_term.qualifier:
246-
case RelativePositionType.BEFORE:
247-
index += -1
248-
case RelativePositionType.AFTER:
249-
index += 1
250-
case RelativePositionType.AT:
251-
pass
252-
case _ as invalid:
253-
raise ValueError(f"Not implemented: {invalid}")
290+
index += calc_index_delta_for_relative_position(search_term)
254291
return cls(index, index, reference_indent)
255292

256293
return None
@@ -259,12 +296,29 @@ def from_line_marker(
259296
RangeSpec.EMPTY = RangeSpec(0, -1, 0)
260297

261298

299+
def calc_index_delta_for_relative_position(marker: Marker):
300+
match marker:
301+
case RelativeMarker(qualifier=RelativePositionType.BEFORE):
302+
return -1
303+
case RelativeMarker(qualifier=RelativePositionType.AFTER):
304+
return 1
305+
case RelativeMarker(qualifier=RelativePositionType.AT):
306+
pass
307+
case RelativeMarker(qualifier=invalid):
308+
raise ValueError(f"Not implemented: {invalid}")
309+
case _:
310+
pass
311+
return 0
312+
313+
262314
class ParentInfo(NamedTuple):
263315
parent_name: str
264316
parent_type: str
265317

318+
266319
ParentRestriction: TypeAlias = RangeSpec | str | None
267320

321+
268322
class IdentifierBoundaries(NamedTuple):
269323
"""
270324
Represents the boundaries of an identifier in code, including its whole range and body range.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class A:
2+
def a(self):
3+
pass
4+
def calculate(self,
5+
a,
6+
b,
7+
c,
8+
d,
9+
e
10+
):
11+
pass
12+
class B:
13+
def a(self):
14+
pass
15+
def calculate(self,
16+
a,
17+
b,
18+
c,
19+
d,
20+
e
21+
):
22+
pass
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<no-train>
2+
```CEDARScript
3+
UPDATE CLASS "B"
4+
FROM FILE "1.py"
5+
REPLACE LINE 1
6+
WITH CONTENT '''
7+
@0:def calculate(self, line_1,
8+
@1:line_2,
9+
''';
10+
```
11+
</no-train>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class A:
2+
def a(self):
3+
pass
4+
def calculate(self,
5+
a,
6+
b,
7+
c,
8+
d,
9+
e
10+
):
11+
pass
12+
class B:
13+
def a(self):
14+
pass
15+
def calculate(self, line_1,
16+
line_2,
17+
a,
18+
b,
19+
c,
20+
d,
21+
e
22+
):
23+
pass

0 commit comments

Comments
 (0)