Skip to content

Commit e46b492

Browse files
committed
Added files
1 parent 0b86a2d commit e46b492

9 files changed

+847
-1
lines changed

LICENSE

Whitespace-only changes.

README.md

+65-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,65 @@
1-
# cedarscript-editor
1+
# CEDARScript Editor (Python)
2+
3+
[![PyPI version](https://badge.fury.io/py/cedarscript-editor-python.svg)](https://pypi.org/project/cedarscript-editor/)
4+
[![Python Versions](https://img.shields.io/pypi/pyversions/cedarscript-editor.svg)](https://pypi.org/project/cedarscript-editor/)
5+
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
6+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7+
8+
`CEDARScript Editor (Python)` is a Python library for interpreting `CEDARScript` scripts and
9+
performing code analysis and modification operations on a codebase.
10+
11+
## What is CEDARScript?
12+
13+
[CEDARScript](https://github.com/CEDARScript/cedarscript-grammar#readme) (_Concise Examination, Development, And Refactoring Script_)
14+
is a domain-specific language that aims to improve how AI coding assistants interact with codebases and communicate their code modification intentions.
15+
It provides a standardized way to express complex code modification and analysis operations, making it easier for
16+
AI-assisted development tools to understand and execute these tasks.
17+
18+
## Features
19+
20+
- Given a `CEDARScript` script and a base direcotry, executes the script commands on files inside the base directory;
21+
- Return results in `XML` format for easier parsing and processing by LLM systems
22+
23+
## Installation
24+
25+
You can install `CEDARScript` Editor using pip:
26+
27+
```
28+
pip install cedarscript_editor
29+
```
30+
31+
## Usage
32+
33+
Here's a quick example of how to use `CEDARScript` Editor:
34+
35+
```python
36+
from cedarscript_editor import CEDARScriptEdior
37+
38+
parser = CEDARScriptEdior()
39+
code = """
40+
CREATE FILE "example.py"
41+
UPDATE FILE "example.py"
42+
INSERT AT END OF FILE
43+
CONTENT
44+
print("Hello, World!")
45+
END CONTENT
46+
END UPDATE
47+
"""
48+
49+
commands, errors = parser.parse_script(code)
50+
51+
if errors:
52+
for error in errors:
53+
print(f"Error: {error}")
54+
else:
55+
for command in commands:
56+
print(f"Parsed command: {command}")
57+
```
58+
59+
## Contributing
60+
61+
Contributions are welcome! Please feel free to submit a Pull Request.
62+
63+
## License
64+
65+
This project is licensed under the MIT License.

pyproject.toml

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "cedarscript-editor"
7+
version = "0.1.2"
8+
description = "A library for executing CEDARScript, a SQL-like language for code analysis and transformations"
9+
readme = "README.md"
10+
authors = [{ name = "Elifarley", email = "elifarley@example.com" }]
11+
license = { file = "LICENSE" }
12+
classifiers = [
13+
"Development Status :: 3 - Alpha",
14+
"Programming Language :: Python :: 3",
15+
"Programming Language :: Python :: 3.12",
16+
"License :: OSI Approved :: MIT License",
17+
"Operating System :: OS Independent",
18+
"Intended Audience :: Developers",
19+
"Topic :: Software Development :: Libraries :: Python Modules",
20+
"Topic :: Text Processing :: Linguistic",
21+
"Topic :: Software Development :: Code Generators",
22+
"Topic :: Software Development :: Compilers",
23+
]
24+
keywords = ["cedarscript", "code-editing", "refactoring", "code-analysis", "sql-like", "ai-assisted-development"]
25+
dependencies = [
26+
"cedarscript_ast_parser>=0.1.2",
27+
]
28+
requires-python = ">=3.12"
29+
30+
[project.urls]
31+
Homepage = "https://github.com/CEDARScript/cedarscript-editor-python"
32+
Documentation = "https://github.com/CEDARScript/cedarscript-editor-python#readme"
33+
Repository = "https://github.com/CEDARScript/cedarscript-editor-python.git"
34+
"Bug Tracker" = "https://github.com/CEDARScript/cedarscript-editor-python/issues"
35+
36+
[project.optional-dependencies]
37+
dev = [
38+
"pytest>=7.0",
39+
"black>=22.0",
40+
"isort>=5.0",
41+
"flake8>=4.0",
42+
"mypy>=0.900",
43+
"coverage>=6.0",
44+
"tox>=3.24",
45+
]
46+
47+
[tool.setuptools]
48+
package-dir = {"" = "src"}
49+
include-package-data = true
50+
51+
[tool.setuptools.packages.find]
52+
where = ["src"]
53+
include = ["cedarscript_editor*", "text_editor*"]
54+
exclude = ["cedarscript_editor.tests*"]
55+
56+
[tool.black]
57+
line-length = 100
58+
target-version = ['py310', 'py311', 'py312']
59+
60+
[tool.isort]
61+
profile = "black"
62+
line_length = 100
63+
64+
[tool.mypy]
65+
ignore_missing_imports = true
66+
strict = true
67+
68+
[tool.pytest.ini_options]
69+
minversion = "6.0"
70+
addopts = "-ra -q"
71+
testpaths = [
72+
"tests",
73+
]

src/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .cedar_editor_java import JavaCedarEditor
2+
from .cedar_editor_kotlin import KotlinCedarEditor
3+
from .cedar_editor_python import PythonCedarEditor
4+
5+
__all__ = ("PythonCedarEditor", "KotlinCedarEditor", "JavaCedarEditor")

src/cedar_editor_base.py

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import os
2+
from abc import ABC, abstractmethod
3+
4+
from cedarscript_ast_parser import Command, CreateCommand, RmFileCommand, MvFileCommand, UpdateCommand, \
5+
SelectCommand, IdentifierFromFile, SingleFileClause, Segment, Marker, MoveClause, DeleteClause, \
6+
InsertClause, ReplaceClause, EditingAction, Region, BodyOrWhole, WhereClause, RegionClause
7+
from .text_editor_kit import \
8+
normalize_indent, write_file, read_file, bow_to_search_range, \
9+
FunctionBoundaries, SearchRange, analyze_indentation, IndentationInfo
10+
11+
class CedarEditorException(Exception):
12+
def __init__(self, command_ordinal: int, description: str):
13+
match command_ordinal:
14+
case 0 | 1:
15+
items = ''
16+
case 2:
17+
items = "#1"
18+
case 3:
19+
items = "#1 and #2"
20+
case _:
21+
sequence = ", ".join(f'#{i}' for i in range(1, command_ordinal - 1))
22+
items = f"{sequence} and #{command_ordinal - 1}"
23+
if command_ordinal <= 1:
24+
note = ''
25+
plural_indicator=''
26+
previous_cmd_notes = ''
27+
else:
28+
29+
plural_indicator='s'
30+
previous_cmd_notes = f", bearing in mind the file was updated and now contains all changes expressed in command{plural_indicator} {items}"
31+
if 'syntax' in description.casefold():
32+
probability_indicator = "most probably"
33+
else:
34+
probability_indicator= "might have"
35+
36+
note = (
37+
f"<note>*ALL* commands *before* command #{command_ordinal} were applied and *their changes are already committed*. "
38+
f"Re-read the file to catch up with the applied changes."
39+
f"ATTENTION: The previous command (#{command_ordinal - 1}) {probability_indicator} caused command #{command_ordinal} to fail "
40+
f"due to changes that left the file in an invalid state (check that by re-analyzing the file!)</note>"
41+
)
42+
super().__init__(
43+
f"<error-details><error-location>COMMAND #{command_ordinal}</error-location>{note}"
44+
f"<description>{description}</description>"
45+
"<suggestion>NEVER apologize; just relax, take a deep breath, think step-by-step and write an in-depth analysis of what went wrong "
46+
"(specifying which command ordinal failed), then acknowledge which commands were already applied and concisely describe the state at which the file was left "
47+
"(saying what needs to be done now), "
48+
f"then write new commands that will fix the problem{previous_cmd_notes} "
49+
"(you'll get a one-million dollar tip if you get it right!) "
50+
"Use descriptive comment before each command.</suggestion></error-details>"
51+
)
52+
53+
54+
class CedarEditorBase(ABC):
55+
def __init__(self, root_path):
56+
self.root_path = os.path.abspath(root_path)
57+
print(f'[{self.__class__}] root: {self.root_path}')
58+
59+
# TODO Add search_range: SearchRange parameter
60+
def find_function(self, source: str | list[str], file_name: str, function_name: str, offset: int | None = None) -> FunctionBoundaries:
61+
if not isinstance(source, str):
62+
source = '\n'.join(source)
63+
return self._find_function(source, file_name, function_name, offset)
64+
65+
@abstractmethod
66+
def _find_function(self, source: str, file_name: str, function_name: str, offset: int | None = None) -> FunctionBoundaries | None:
67+
pass
68+
69+
def apply_commands(self, commands: list[Command]):
70+
result = []
71+
for i, command in enumerate(commands):
72+
try:
73+
match command:
74+
case UpdateCommand() as cmd:
75+
result.append(self._update_command(cmd))
76+
case CreateCommand() as cmd:
77+
result.append(self._create_command(cmd))
78+
case RmFileCommand() as cmd:
79+
result.append(self._rm_command(cmd))
80+
case MvFileCommand() as cmd:
81+
raise ValueError('Noy implemented: MV')
82+
case SelectCommand() as cmd:
83+
raise ValueError('Noy implemented: SELECT')
84+
case _ as invalid:
85+
raise ValueError(f"Unknown command '{type(invalid)}'")
86+
except Exception as e:
87+
print(f'[apply_commands] (command #{i+1}) Failed: {command}')
88+
if isinstance(command, UpdateCommand):
89+
print(f'CMD CONTENT: ***{command.content}***')
90+
raise CedarEditorException(i+1, str(e)) from e
91+
return result
92+
93+
def _update_command(self, cmd: UpdateCommand):
94+
file_path = os.path.join(self.root_path, cmd.target.file_path)
95+
content = cmd.content or []
96+
97+
match cmd.target:
98+
99+
case IdentifierFromFile(
100+
identifier_type='FUNCTION', where_clause=WhereClause(field='NAME', operator='=', value=function_name)
101+
):
102+
try:
103+
return self._update_content(file_path, cmd.action, content, function_name=function_name, offset = cmd.target.offset)
104+
except IOError as e:
105+
msg = f"function `{function_name}` in `{cmd.target.file_path}`"
106+
raise IOError(f"Error updating {msg}: {e}")
107+
108+
case SingleFileClause():
109+
try:
110+
return self._update_content(file_path, cmd.action, content)
111+
except IOError as e:
112+
msg = f"file `{cmd.target.file_path}`"
113+
raise IOError(f"Error updating {msg}: {e}")
114+
115+
case _ as invalid:
116+
raise ValueError(f"Not implemented: {invalid}")
117+
118+
def _update_content(self, file_path: str, action: EditingAction, content: str | None,
119+
search_range: SearchRange | None = None, function_name: str | None = None, offset: int | None = None) -> str:
120+
src = read_file(file_path)
121+
lines = src.splitlines()
122+
123+
if function_name:
124+
function_boundaries = self.find_function(src, file_path, function_name, offset)
125+
if not function_boundaries:
126+
raise ValueError(f"Function '{function_name}' not found in {file_path}")
127+
if search_range:
128+
print(f'Discarding search range to use function range...')
129+
search_range = _get_index_range(action, lines, function_boundaries)
130+
else:
131+
search_range = _get_index_range(action, lines)
132+
133+
self._apply_action(action, lines, search_range, content)
134+
135+
write_file(file_path, lines)
136+
137+
return f"Updated {'function ' + function_name if function_name else 'file'} in {file_path}\n -> {action}"
138+
139+
def _apply_action(self, action: EditingAction, lines: list[str], search_range: SearchRange, content: str | None = None):
140+
index_start, index_end, reference_indent = search_range
141+
142+
match action:
143+
144+
case MoveClause(insert_position=insert_position, to_other_file=other_file, relative_indentation=relindent):
145+
saved_content = lines[index_start:index_end]
146+
lines[index_start:index_end] = []
147+
# TODO Move from 'lines' to the same file or to 'other_file'
148+
dest_range = _get_index_range(InsertClause(insert_position), lines)
149+
indentation_info: IndentationInfo = analyze_indentation(saved_content)
150+
lines[dest_range.start:dest_range.end] = indentation_info.adjust_indentation(saved_content, dest_range.indent + (relindent or 0))
151+
152+
case DeleteClause():
153+
lines[index_start:index_end] = []
154+
155+
case ReplaceClause() | InsertClause():
156+
indentation_info: IndentationInfo = analyze_indentation(lines)
157+
lines[index_start:index_end] = normalize_indent(content, reference_indent, indentation_info)
158+
159+
case _ as invalid:
160+
raise ValueError(f"Unsupported action type: {type(invalid)}")
161+
162+
def _rm_command(self, cmd: RmFileCommand):
163+
file_path = os.path.join(self.root_path, cmd.file_path)
164+
165+
def _delete_function(self, cmd): # TODO
166+
file_path = os.path.join(self.root_path, cmd.file_path)
167+
168+
# def _create_command(self, cmd: CreateCommand):
169+
# file_path = os.path.join(self.root_path, cmd.file_path)
170+
#
171+
# os.makedirs(os.path.dirname(file_path), exist_ok=False)
172+
# with open(file_path, 'w') as file:
173+
# file.write(content)
174+
#
175+
# return f"Created file: {command['file']}"
176+
177+
178+
def _get_index_range(action: EditingAction, lines: list[str], search_range: SearchRange | FunctionBoundaries | None = None) -> SearchRange:
179+
match action:
180+
case RegionClause(region=r) | InsertClause(insert_position=r):
181+
return find_index_range_for_region(r, lines, search_range)
182+
case _ as invalid:
183+
raise ValueError(f"Unsupported action type: {type(invalid)}")
184+
185+
def find_index_range_for_region(region: Region, lines: list[str], search_range: SearchRange | FunctionBoundaries | None = None) -> SearchRange:
186+
match region:
187+
case BodyOrWhole() as bow:
188+
# TODO Set indent char count
189+
index_range = bow_to_search_range(bow, search_range)
190+
case Marker() | Segment() as mos:
191+
if isinstance(search_range, FunctionBoundaries):
192+
search_range = search_range.whole
193+
index_range = mos.marker_or_segment_to_index_range(
194+
lines,
195+
search_range.start if search_range else 0,
196+
search_range.end if search_range else -1,
197+
)
198+
case _ as invalid:
199+
raise ValueError(f"Invalid: {invalid}")
200+
return index_range

0 commit comments

Comments
 (0)