Skip to content

Commit 1f1b043

Browse files
committed
Many improvements
1 parent 6272d08 commit 1f1b043

10 files changed

+594
-480
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ version = {attr = "cedarscript_editor.__version__"}
5454

5555
[tool.setuptools.packages.find]
5656
where = ["src"]
57-
include = ["cedarscript_editor*", "text_editor*"]
57+
include = ["cedarscript_editor*", "text_editor*", "identifier_selector*", "*identifier_finder*"]
5858
namespaces = false
5959

6060
[tool.setuptools.package-data]

src/cedarscript_editor/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
from .cedarscript_editor_java import JavaCEDARScriptEditor
2-
from .cedarscript_editor_kotlin import KotlinCEDARScriptEditor
3-
from .cedarscript_editor_python import PythonCEDARScriptEditor
1+
from cedarscript_editor.cedarscript_editor import CEDARScriptEditor
42

5-
__version__ = "0.1.10"
3+
__version__ = "0.2.0"
4+
5+
__all__ = ["CEDARScriptEditor"]
66

7-
__all__ = ["PythonCEDARScriptEditor"]
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
import os
2+
from typing import Callable
3+
from collections.abc import Sequence
4+
5+
from cedarscript_ast_parser import Command, CreateCommand, RmFileCommand, MvFileCommand, UpdateCommand, \
6+
SelectCommand, IdentifierFromFile, Segment, Marker, MoveClause, DeleteClause, \
7+
InsertClause, ReplaceClause, EditingAction, BodyOrWhole, RegionClause, MarkerType
8+
from cedarscript_ast_parser.cedarscript_ast_parser import MarkerCompatible, RelativeMarker, RelativePositionType
9+
10+
from .identifier_selector import select_finder
11+
from .python_identifier_finder import find_python_identifier
12+
from .text_editor_kit import \
13+
normalize_indent, write_file, read_file, bow_to_search_range, \
14+
IdentifierBoundaries, RangeSpec, analyze_and_adjust_indentation, analyze_and_normalize_indentation, IndentationInfo
15+
16+
17+
class CEDARScriptEditorException(Exception):
18+
def __init__(self, command_ordinal: int, description: str):
19+
match command_ordinal:
20+
case 0 | 1:
21+
items = ''
22+
case 2:
23+
items = "#1"
24+
case 3:
25+
items = "#1 and #2"
26+
case _:
27+
sequence = ", ".join(f'#{i}' for i in range(1, command_ordinal - 1))
28+
items = f"{sequence} and #{command_ordinal - 1}"
29+
if command_ordinal <= 1:
30+
note = ''
31+
plural_indicator=''
32+
previous_cmd_notes = ''
33+
else:
34+
35+
plural_indicator='s'
36+
previous_cmd_notes = f", bearing in mind the file was updated and now contains all changes expressed in command{plural_indicator} {items}"
37+
if 'syntax' in description.casefold():
38+
probability_indicator = "most probably"
39+
else:
40+
probability_indicator= "might have"
41+
42+
note = (
43+
f"<note>*ALL* commands *before* command #{command_ordinal} were applied and *their changes are already committed*. "
44+
f"Re-read the file to catch up with the applied changes."
45+
f"ATTENTION: The previous command (#{command_ordinal - 1}) {probability_indicator} caused command #{command_ordinal} to fail "
46+
f"due to changes that left the file in an invalid state (check that by re-analyzing the file!)</note>"
47+
)
48+
super().__init__(
49+
f"<error-details><error-location>COMMAND #{command_ordinal}</error-location>{note}"
50+
f"<description>{description}</description>"
51+
"<suggestion>NEVER apologize; just relax, take a deep breath, think step-by-step and write an in-depth analysis of what went wrong "
52+
"(specifying which command ordinal failed), then acknowledge which commands were already applied and concisely describe the state at which the file was left "
53+
"(saying what needs to be done now), "
54+
f"then write new commands that will fix the problem{previous_cmd_notes} "
55+
"(you'll get a one-million dollar tip if you get it right!) "
56+
"Use descriptive comment before each command.</suggestion></error-details>"
57+
)
58+
59+
60+
class CEDARScriptEditor:
61+
def __init__(self, root_path):
62+
self.root_path = os.path.abspath(root_path)
63+
print(f'[{self.__class__}] root: {self.root_path}')
64+
65+
# TODO Add 'target_search_range: RangeSpec' parameter
66+
def find_identifier(self, source_info: tuple[str, str | Sequence[str]], marker: Marker) -> IdentifierBoundaries:
67+
file_path = source_info[0]
68+
source = source_info[1]
69+
if not isinstance(source, str):
70+
source = '\n'.join(source)
71+
return (
72+
select_finder(self.root_path, file_path, source)
73+
(self.root_path, file_path, source, marker)
74+
)
75+
76+
def apply_commands(self, commands: Sequence[Command]):
77+
result = []
78+
for i, command in enumerate(commands):
79+
try:
80+
match command:
81+
case UpdateCommand() as cmd:
82+
result.append(self._update_command(cmd))
83+
case CreateCommand() as cmd:
84+
result.append(self._create_command(cmd))
85+
case RmFileCommand() as cmd:
86+
result.append(self._rm_command(cmd))
87+
case MvFileCommand() as cmd:
88+
raise ValueError('Noy implemented: MV')
89+
case SelectCommand() as cmd:
90+
raise ValueError('Noy implemented: SELECT')
91+
case _ as invalid:
92+
raise ValueError(f"Unknown command '{type(invalid)}'")
93+
except Exception as e:
94+
print(f'[apply_commands] (command #{i+1}) Failed: {command}')
95+
if isinstance(command, UpdateCommand):
96+
print(f'CMD CONTENT: ***{command.content}***')
97+
raise CEDARScriptEditorException(i + 1, str(e)) from e
98+
return result
99+
100+
def _update_command(self, cmd: UpdateCommand):
101+
action: EditingAction = cmd.action
102+
target = cmd.target
103+
content = cmd.content or []
104+
file_path = os.path.join(self.root_path, target.file_path)
105+
106+
# Example 1:
107+
# UPDATE FILE "tmp.benchmarks/2024-10-04-22-59-58--CEDARScript-Gemini-small/bowling/bowling.py"
108+
# INSERT INSIDE FUNCTION "__init__" TOP
109+
# WITH CONTENT '''
110+
# @0:print("This line will be inserted at the top")
111+
# ''';
112+
# After parsing ->
113+
# UpdateCommand(
114+
# type='update',
115+
# target=SingleFileClause(file_path='tmp.benchmarks/2024-10-04-22-59-58--CEDARScript-Gemini-small/bowling/bowling.py'),
116+
# action=InsertClause(insert_position=RelativeMarker(type=<MarkerType.FUNCTION: 'function'>, value='__init__', offset=None)),
117+
# content='\n @0:print("This line will be inserted at the top")\n '
118+
# )
119+
120+
121+
# Example 2:
122+
# UPDATE FUNCTION
123+
# FROM FILE "tmp.benchmarks/2024-10-04-22-59-58--CEDARScript-Gemini-small/bowling/bowling.py"
124+
# WHERE NAME = "__init__"
125+
# REPLACE SEGMENT
126+
# STARTING AFTER LINE "def __init__(self):"
127+
# ENDING AFTER LINE "def __init__(self):"
128+
# WITH CONTENT '''
129+
# @0:print("This line will be inserted at the top")
130+
# ''';
131+
# After parsing ->
132+
# UpdateCommand(
133+
# type='update',
134+
# target=IdentifierFromFile(file_path='bowling.py',
135+
# where_clause=WhereClause(field='NAME', operator='=', value='__init__'),
136+
# identifier_type='FUNCTION', offset=None
137+
# ),
138+
# action=ReplaceClause(
139+
# region=Segment(
140+
# start=RelativeMarker(type=<MarkerType.LINE: 'line'>, value='def __init__(self):', offset=None),
141+
# end=RelativeMarker(type=<MarkerType.LINE: 'line'>, value='def __init__(self):', offset=None)
142+
# )),
143+
# content='\n @0:print("This line will be inserted at the top")\n '
144+
# )
145+
146+
src = read_file(file_path)
147+
lines = src.splitlines()
148+
149+
source_info: tuple[str, str | Sequence[str]] = (file_path, src)
150+
151+
def identifier_resolver(marker: Marker):
152+
return self.find_identifier(source_info, marker)
153+
154+
# Set range_spec to cover the identifier
155+
search_range = restrict_search_range(action, target, identifier_resolver)
156+
157+
marker, search_range = find_marker_or_segment(action, lines, search_range)
158+
159+
search_range = restrict_search_range_for_marker(
160+
marker, action, lines, search_range, identifier_resolver
161+
)
162+
163+
match content:
164+
case (region, relindent):
165+
dest_indent = search_range.indent
166+
content_range = restrict_search_range_for_marker(
167+
region, action, lines, search_range, identifier_resolver
168+
)
169+
content = content_range.read(lines)
170+
content = analyze_and_adjust_indentation(
171+
src_content_to_adjust=content,
172+
target_context_for_analysis=lines,
173+
base_indentation_count=dest_indent + (relindent or 0)
174+
)
175+
case str() | [str(), *_] | (str(), *_):
176+
pass
177+
case _:
178+
raise ValueError(f'Invalid content: {content}')
179+
180+
181+
self._apply_action(action, lines, search_range, content)
182+
183+
write_file(file_path, lines)
184+
185+
return f"Updated {target if target else 'file'} in {file_path}\n -> {action}"
186+
187+
def _apply_action(self, action: EditingAction, lines: Sequence[str], range_spec: RangeSpec, content: str | None = None):
188+
match action:
189+
190+
case MoveClause(insert_position=insert_position, to_other_file=other_file, relative_indentation=relindent):
191+
saved_content = range_spec.delete(lines)
192+
# TODO Move from 'lines' to the same file or to 'other_file'
193+
dest_range = self._get_index_range(InsertClause(insert_position), lines)
194+
saved_content = analyze_and_adjust_indentation(
195+
src_content_to_adjust=saved_content,
196+
target_context_for_analysis=lines,
197+
base_indentation_count= dest_range.indent + (relindent or 0)
198+
)
199+
dest_range.write(saved_content, lines)
200+
201+
case DeleteClause():
202+
range_spec.delete(lines)
203+
204+
case ReplaceClause() | InsertClause():
205+
content = analyze_and_normalize_indentation(
206+
src_content_to_adjust=content,
207+
target_context_for_analysis=lines,
208+
context_indent_count=range_spec.indent
209+
)
210+
range_spec.write(content, lines)
211+
212+
case _ as invalid:
213+
raise ValueError(f"Unsupported action type: {type(invalid)}")
214+
215+
def _rm_command(self, cmd: RmFileCommand):
216+
file_path = os.path.join(self.root_path, cmd.file_path)
217+
218+
def _delete_function(self, cmd): # TODO
219+
file_path = os.path.join(self.root_path, cmd.file_path)
220+
221+
# def _create_command(self, cmd: CreateCommand):
222+
# file_path = os.path.join(self.root_path, cmd.file_path)
223+
#
224+
# os.makedirs(os.path.dirname(file_path), exist_ok=False)
225+
# with open(file_path, 'w') as file:
226+
# file.write(content)
227+
#
228+
# return f"Created file: {command['file']}"
229+
230+
def find_index_range_for_region(self,
231+
region: BodyOrWhole | Marker | Segment | RelativeMarker,
232+
lines: Sequence[str],
233+
identifier_resolver: Callable[[Marker], IdentifierBoundaries],
234+
search_range: RangeSpec | IdentifierBoundaries | None = None,
235+
) -> RangeSpec:
236+
# BodyOrWhole | RelativeMarker | MarkerOrSegment
237+
# marker_or_segment_to_index_range_impl
238+
# IdentifierBoundaries.location_to_search_range(self, location: BodyOrWhole | RelativePositionType) -> RangeSpec
239+
match region:
240+
case BodyOrWhole() as bow:
241+
# TODO Set indent char count
242+
index_range = bow_to_search_range(bow, search_range)
243+
case Marker() | Segment() as mos:
244+
if isinstance(search_range, IdentifierBoundaries):
245+
search_range = search_range.whole
246+
match mos:
247+
case Marker(type=marker_type):
248+
match marker_type:
249+
case MarkerType.LINE:
250+
pass
251+
case _:
252+
# TODO transform to RangeSpec
253+
mos = self.find_identifier(lines, f'for:{region}', mos).body
254+
index_range = mos.to_search_range(
255+
lines,
256+
search_range.start if search_range else 0,
257+
search_range.end if search_range else -1,
258+
)
259+
case _ as invalid:
260+
raise ValueError(f"Invalid: {invalid}")
261+
return index_range
262+
263+
264+
def find_marker_or_segment(action: EditingAction, lines: Sequence[str], search_range: RangeSpec) -> tuple[Marker, RangeSpec]:
265+
marker: Marker | Segment | None = None
266+
match action:
267+
case MarkerCompatible() as marker_compatible:
268+
marker = marker_compatible.as_marker
269+
case RegionClause(region=region):
270+
match region:
271+
case MarkerCompatible():
272+
marker = region.as_marker
273+
case Segment() as segment:
274+
# TODO Handle segment's start and end as a marker and support identifier markers
275+
search_range = segment.to_search_range(lines, search_range)
276+
marker = None
277+
return marker, search_range
278+
279+
280+
def restrict_search_range(action, target, identifier_resolver: Callable[[Marker], IdentifierBoundaries]) -> RangeSpec:
281+
search_range = RangeSpec(0, -1, 0)
282+
match target:
283+
case IdentifierFromFile() as identifier_from_file:
284+
identifier_marker = identifier_from_file.as_marker
285+
identifier_boundaries = identifier_resolver(identifier_marker)
286+
if not identifier_boundaries:
287+
raise ValueError(f"'{identifier_marker}' not found")
288+
match action:
289+
case RegionClause(region=region):
290+
match region: # BodyOrWhole | Marker | Segment
291+
case BodyOrWhole():
292+
search_range = identifier_boundaries.location_to_search_range(region)
293+
case _:
294+
search_range = identifier_boundaries.location_to_search_range(BodyOrWhole.WHOLE)
295+
return search_range
296+
297+
298+
def restrict_search_range_for_marker(
299+
marker: Marker,
300+
action: EditingAction,
301+
lines: Sequence[str],
302+
search_range: RangeSpec,
303+
identifier_resolver: Callable[[Marker], IdentifierBoundaries]
304+
) -> RangeSpec:
305+
if marker is None:
306+
return search_range
307+
308+
match marker:
309+
case Marker():
310+
match marker.type:
311+
case MarkerType.LINE:
312+
search_range = marker.to_search_range(lines, search_range)
313+
match action:
314+
case InsertClause():
315+
if action.insert_position.qualifier == RelativePositionType.BEFORE:
316+
search_range = search_range.inc()
317+
case DeleteClause():
318+
search_range = search_range.set_length(1)
319+
case _:
320+
identifier_boundaries = identifier_resolver(marker)
321+
if not identifier_boundaries:
322+
raise ValueError(f"'{marker}' not found")
323+
qualifier: RelativePositionType = marker.qualifier if isinstance(
324+
marker, RelativeMarker
325+
) else RelativePositionType.AT
326+
search_range = identifier_boundaries.location_to_search_range(qualifier)
327+
case Segment():
328+
pass # TODO
329+
return search_range

0 commit comments

Comments
 (0)