diff --git a/.gitignore b/.gitignore index f4203f1..1164fc7 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,9 @@ dedlin.pyz /.ruff_cache/ node_modules/ + +bot_history/ + +test2.txt + +test3.txt diff --git a/dedlin/basic_types.py b/dedlin/basic_types.py index 0d0bb79..a5c9edc 100644 --- a/dedlin/basic_types.py +++ b/dedlin/basic_types.py @@ -16,7 +16,9 @@ class Commands(Enum): """Enum of commands that can be executed on a document.""" + COMMENT = auto() EMPTY = auto() + NOOP = auto() # ed compatibility # display LIST = auto() PAGE = auto() @@ -252,6 +254,7 @@ class Command: line_range: Optional[LineRange] = None phrases: Optional[Phrases] = None original_text: Optional[str] = dataclasses.field(default=None, compare=False) + comment: Optional[str] = None def validate(self) -> bool: """Check if ranges are sensible""" @@ -267,6 +270,12 @@ def validate(self) -> bool: def format(self) -> str: """Format the command as a string""" + if self.command == Commands.COMMENT: + text = self.comment if self.comment else "" + return f"# {text}" + if self.command == Commands.UNKNOWN: + text = self.original_text if self.original_text else "" + return f"# Unknown: {text}" range_part = self.line_range.format() if self.line_range is not None else "" phrase_part = self.phrases.format() if self.phrases is not None else "" return " ".join([range_part, self.command.name, phrase_part]).strip() diff --git a/dedlin/document.py b/dedlin/document.py index a822e26..86c125a 100644 --- a/dedlin/document.py +++ b/dedlin/document.py @@ -37,7 +37,7 @@ def print(*args, **kwargs): @icontract.invariant(lambda self: all("\n" not in line and "\r" not in line for line in self.lines)) @icontract.invariant( # and not self.lines <-- I'd have to update current line this on every .append() - lambda self: (1 <= self.current_line <= len(self.lines) or self.current_line in (0, 1)), + lambda self: (1 <= self.current_line <= len(self.lines) + 1 or self.current_line in (0, 1)), "Current line must be a valid line", ) class Document: @@ -270,11 +270,11 @@ def edit(self, line_number: int) -> EditStatus: logger.warning("Didn't get an input, nothing changed.") return EditStatus(can_edit_again=False, text=None, line_edited=None) except KeyboardInterrupt: - logger.warning("Cancelling out of edit, line not changed.") + logger.warning("\nCancelling out of edit, line not changed.") return EditStatus(can_edit_again=False, text=None, line_edited=None) if new_line is None: - logger.warning("Cancelling out of edit, line not changed.") + logger.warning("\nCancelling out of edit, line not changed.") return EditStatus(can_edit_again=False, text=None, line_edited=None) self.lines[line_number - 1] = new_line @@ -314,8 +314,10 @@ def insert( for phrase in phrases.as_list(): self.lines.insert(line_number - 1, phrase) self.dirty = True - self.current_line = line_number + self.current_line = line_number + 1 line_number += 1 + # HACK: if you don't do this, sequential scripted INSERT skip lines. + self.current_line -= 1 return phrases user_input_text: Optional[str] = "GO!" diff --git a/dedlin/file_system.py b/dedlin/file_system.py index 16eab81..09caf2e 100644 --- a/dedlin/file_system.py +++ b/dedlin/file_system.py @@ -13,7 +13,6 @@ def read_or_create_file(path: Path) -> list[str]: """Attempt to read file, create if it doesn't exist""" if path: - print(f"Editing {path.absolute()}") if not path.exists(): with open(str(path.absolute()), "w", encoding="utf-8"): pass diff --git a/dedlin/history_feature.py b/dedlin/history_feature.py index 3060705..979a93d 100644 --- a/dedlin/history_feature.py +++ b/dedlin/history_feature.py @@ -42,7 +42,7 @@ def count_files_in_history_folder(self) -> int: if not self.persist: return 0 history_folder = self.initialize_history_folder() - return len(list(history_folder.glob("*.ed"))) + return len(list(history_folder.glob("*.ed"))) + 1 def make_sequential_history_file_name(self) -> str: """ diff --git a/dedlin/main.py b/dedlin/main.py index 079291f..d1277d3 100644 --- a/dedlin/main.py +++ b/dedlin/main.py @@ -62,6 +62,7 @@ def __init__( headless: bool = False, disabled_commands: Optional[list[Commands]] = None, untrusted_user: bool = False, + history: bool = True, ) -> None: """Set up initial state and some dependency injection""" @@ -114,7 +115,7 @@ def __init__( self.file_path: Optional[Path] = None self.history: list[Command] = [] - self.history_log = HistoryLog(persist=not self.headless) + self.history_log = HistoryLog(persist=history) self.macro_file_name: Optional[Path] = None def entry_point(self, file_name: Optional[str] = None, macro_file_name: Optional[str] = None) -> int: @@ -139,6 +140,8 @@ def entry_point(self, file_name: Optional[str] = None, macro_file_name: Optional self.macro_file_name = Path(macro_file_name) if macro_file_name else None self.file_path = Path(file_name) if file_name else None + if self.file_path: + self.feedback(f"Editing {self.file_path.absolute()}") lines = file_system.read_or_create_file(self.file_path) self.doc = Document( @@ -173,9 +176,7 @@ def entry_point(self, file_name: Optional[str] = None, macro_file_name: Optional self.feedback(f"Invalid command {command}") self.print_ai_help(command) - self.history.append(command) - if not self.headless: - self.history_log.write_command_to_history_file(command.format(), self.preferred_line_break) + self.log_history(command) self.echo_if_needed(command.format()) if command.command == Commands.REDO: @@ -184,7 +185,7 @@ def entry_point(self, file_name: Optional[str] = None, macro_file_name: Optional except IndexError: self.feedback("Nothing to redo, not enough history") continue - self.history.append(command) + self.log_history(command) self.echo_if_needed(command.original_text) if command.command == Commands.BROWSE: @@ -200,7 +201,7 @@ def entry_point(self, file_name: Optional[str] = None, macro_file_name: Optional elif command.command == Commands.HISTORY: for command in self.history: # self.feedback(command.original_text.strip("\n\t\r ")) - self.feedback(command.format()) + self.feedback(command.format(), no_comment=True) elif command.command == Commands.EMPTY: pass elif command.command == Commands.LIST and command.line_range: @@ -247,7 +248,9 @@ def entry_point(self, file_name: Optional[str] = None, macro_file_name: Optional line_range=command.line_range, original_text=command.original_text, ) - self.history.append(rewritten_history) + self.log_history(rewritten_history) + else: + self.log_history(command) elif command.command == Commands.PUSH and command.phrases and command.line_range: line_number = command.line_range.start if command.line_range else 1 self.doc.push(line_number, command.phrases.as_list()) @@ -273,7 +276,7 @@ def entry_point(self, file_name: Optional[str] = None, macro_file_name: Optional if edit_status.text is not None: # rewrite history # _ = self.history.pop() - self.history.append( + self.log_history( Command( command=Commands.EDIT, line_range=LineRange(start=edit_status.line_edited, offset=0), @@ -366,6 +369,11 @@ def entry_point(self, file_name: Optional[str] = None, macro_file_name: Optional self.feedback(status) return 0 + def log_history(self, command): + self.history.append(command) + if self.history: + self.history_log.write_command_to_history_file(command.format(), self.preferred_line_break) + def print_ai_help(self, command: str) -> None: if not self.enable_ai_help: return @@ -377,8 +385,13 @@ def print_ai_help(self, command: str) -> None: ask = ChatCompletionMessageParam(content=content, role="user") asyncio.run(client.completion([ask])) - def feedback(self, string, end="\n") -> None: + def feedback(self, string, end="\n", no_comment: bool = False) -> None: """Output feedback to the user""" + if not no_comment: + # prevent infinite loop for HISTORY command + comment = Command(command=Commands.COMMENT, comment=string) + self.log_history(command=comment) + if not (self.vim_mode or self.quiet): self.command_outputter(string, end) return @@ -435,7 +448,7 @@ def save_macro(self): def final_report(self) -> None: """Print out the final report""" - if not self.headless: + if self.history: self.feedback(f"History saved to {self.history_log.history_file_string}") def save_on_crash(self, exception_type: Optional[Exception], value: Any, tb: Any) -> None: diff --git a/dedlin/parsers.py b/dedlin/parsers.py index cc8ca05..9ac86d1 100644 --- a/dedlin/parsers.py +++ b/dedlin/parsers.py @@ -15,6 +15,9 @@ def extract_one_range(value: str, current_line: int, document_length: int) -> Op $ = last line """ value = value.strip() + if value == "": + # Implicit range means different things depending on command... I think + return None if "," in value: parts = value.split(",") start_string = parts[0] @@ -120,7 +123,7 @@ def get_command_length(value: str, suffixes: Iterable[str]) -> int: Commands.HISTORY: ("HISTORY",), Commands.MACRO: ("MACRO",), Commands.BROWSE: ("BROWSE",), - Commands.CURRENT: ("CURRENT",), + Commands.CURRENT: ("C", "CURRENT"), Commands.SHUFFLE: ("SHUFFLE",), Commands.SORT: ("SORT",), Commands.REVERSE: ("REVERSE",), @@ -155,7 +158,9 @@ def parse_range_only( # TODO: the biggest generic parser should replace all of these for command_code, command_forms in RANGE_ONLY.items(): if just_command in command_forms: - if front_part in command_forms: + if front_part and front_part in command_forms: + # Bare command because front part is just the command. + # Incorrectly assuming all commands default to entire document for missing range line_range: Optional[LineRange] = LineRange( start=1, offset=0 if document_length <= 0 else document_length - 1 ) @@ -173,6 +178,12 @@ def parse_range_only( # In interactive mode in means, start accepting input for line 2. # `2 INSERT` phrases = Phrases(("",)) + # override range, because if they specify it, it is meaningless + # if they don't specify, we insert/edit current line + if command_code == Commands.INSERT: + line_range = LineRange(start=current_line + 1 if current_line > 0 else 1, offset=0) + else: + line_range = LineRange(start=current_line if current_line > 0 else 1, offset=0) return Command( command_code, @@ -265,6 +276,12 @@ def parse_command(command: str, current_line: int, document_length: int, headles command=Commands.EMPTY, original_text=original_text, ) + if command == ".": + # This is for "ed" compatibility, where . meant, switch out of input mode back to command mode. + return Command( + command=Commands.NOOP, + original_text=original_text, + ) original_text_upper = command.upper() command = command.upper().strip() diff --git a/tests/sample_headless_scripts/lorem.ed_snapshot.txt b/tests/sample_headless_scripts/lorem.ed_snapshot.txt index 2545831..7227060 100644 --- a/tests/sample_headless_scripts/lorem.ed_snapshot.txt +++ b/tests/sample_headless_scripts/lorem.ed_snapshot.txt @@ -44,3 +44,43 @@ aut rerum necessitatibus saepe eveniet, ut et voluptates repudiandae sint et mol Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. +Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam eaque ipsa, quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt, +explicabo. Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, +qui dolorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do +eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad +minima veniam, quis nostrumd exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid +ex ea commodi consequatur? Quis autem vel eum iure reprehenderit, qui in ea voluptate velit esse, +quam nihil molestiae consequatur, vel illum, qui dolorem eum fugiat, quo voluptas nulla pariatur? + +At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum +deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non +provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum +fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis +est eligendi optio, cumque nihil impedit, quo minus id, quod maxime placeat, facere possimus, omnis +voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis +aut rerum necessitatibus saepe eveniet, ut et voluptates repudiandae sint et molestiae non recusandae. +Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias +consequatur aut perferendis doloribus asperiores repellat. + +Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, +totam rem aperiam eaque ipsa, quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt, +explicabo. Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, +qui dolorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do +eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad +minima veniam, quis nostrumd exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid +ex ea commodi consequatur? Quis autem vel eum iure reprehenderit, qui in ea voluptate velit esse, +quam nihil molestiae consequatur, vel illum, qui dolorem eum fugiat, quo voluptas nulla pariatur? + +At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum +deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non +provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum +fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis +est eligendi optio, cumque nihil impedit, quo minus id, quod maxime placeat, facere possimus, omnis +voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis +aut rerum necessitatibus saepe eveniet, ut et voluptates repudiandae sint et molestiae non recusandae. +Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias +consequatur aut perferendis doloribus asperiores repellat. + diff --git a/tests/sample_headless_scripts/robo.ed_snapshot.txt b/tests/sample_headless_scripts/robo.ed_snapshot.txt index a5981c3..4837bd4 100644 --- a/tests/sample_headless_scripts/robo.ed_snapshot.txt +++ b/tests/sample_headless_scripts/robo.ed_snapshot.txt @@ -5,6 +5,10 @@ Cats Cats Cats Cats +Cats +Cats +Dogs +Dogs Dogs Dogs Dogs @@ -19,6 +23,10 @@ Fish Fish Fish Fish +Fish +Fish +Rabbits +Rabbits Rabbits Rabbits Rabbits diff --git a/tests/sample_headless_scripts/walrus_facts.ed_snapshot.txt b/tests/sample_headless_scripts/walrus_facts.ed_snapshot.txt index a89f91f..610df01 100644 --- a/tests/sample_headless_scripts/walrus_facts.ed_snapshot.txt +++ b/tests/sample_headless_scripts/walrus_facts.ed_snapshot.txt @@ -1,4 +1,10 @@ # Walrus Facts +# Walrus Facts + +Walruses are large aquatic mammals that live in the Arctic. They are known for their long tusks and large bodies. Walruses use their tusks to help them climb out of the water and onto the ice. They have a layer of blubber that keeps them warm in the cold temperatures. Walruses feed on a diet of clams, mussels, and other benthic invertebrates. They can weigh up to 1.5 tons and reach lengths of up to 12 feet. Walruses are highly social animals and can be found in large groups called herds. +# Walrus Facts + +Walruses are large aquatic mammals that live in the Arctic. They are known for their long tusks and large bodies. Walruses use their tusks to help them climb out of the water and onto the ice. They have a layer of blubber that keeps them warm in the cold temperatures. Walruses feed on a diet of clams, mussels, and other benthic invertebrates. They can weigh up to 1.5 tons and reach lengths of up to 12 feet. Walruses are highly social animals and can be found in large groups called herds. Walruses are large aquatic mammals that live in the Arctic. They are known for their long tusks and large bodies. Walruses use their tusks to help them climb out of the water and onto the ice. They have a layer of blubber that keeps them warm in the cold temperatures. Walruses feed on a diet of clams, mussels, and other benthic invertebrates. They can weigh up to 1.5 tons and reach lengths of up to 12 feet. Walruses are highly social animals and can be found in large groups called herds. # Walrus Facts diff --git a/tests/sample_headless_scripts/walrus_facts2.ed_snapshot.txt b/tests/sample_headless_scripts/walrus_facts2.ed_snapshot.txt index 7c3463f..6135e13 100644 --- a/tests/sample_headless_scripts/walrus_facts2.ed_snapshot.txt +++ b/tests/sample_headless_scripts/walrus_facts2.ed_snapshot.txt @@ -1,4 +1,10 @@ # Walrus Fact 1 +# Walrus Fact 1 +# Walrus Fact 2 +# Walrus Fact 3 +# Walrus Fact 1 +# Walrus Fact 2 +# Walrus Fact 3 # Walrus Fact 2 # Walrus Fact 3 # Walrus Fact 1 diff --git a/tests/sample_macros/grep_log.txt b/tests/sample_macros/grep_log.txt index 3f1a268..c492c8d 100644 --- a/tests/sample_macros/grep_log.txt +++ b/tests/sample_macros/grep_log.txt @@ -1,3 +1,4 @@ +Editing E:\github\dedlin\tests\sample_macros\grep_out.txt Current line 1 of 2 1 : This is a cat. Current line 1 of 2 diff --git a/tests/sample_macros/sed_log.txt b/tests/sample_macros/sed_log.txt index b9b4abb..e3c5a75 100644 --- a/tests/sample_macros/sed_log.txt +++ b/tests/sample_macros/sed_log.txt @@ -1,3 +1,4 @@ +Editing E:\github\dedlin\tests\sample_macros\sed_out.txt Current line 1 of 2 Replacing 1 : That is a butt. diff --git a/tests/sample_macros/walrus_facts1_log.txt b/tests/sample_macros/walrus_facts1_log.txt index 0f56a62..291fac2 100644 --- a/tests/sample_macros/walrus_facts1_log.txt +++ b/tests/sample_macros/walrus_facts1_log.txt @@ -1,3 +1,4 @@ +Editing E:\github\dedlin\tests\sample_macros\walrus_facts1_out.txt Current line 1 of 14 1 : They big 2 : They walrus @@ -14,17 +15,17 @@ Current line 1 of 14 1 : Their diet is fish and stuff. Current line 1 of 14 Current line 1 of 14 -Current line 10 of 14 -Current line 10 of 14 +Current line 1 of 14 +Current line 1 of 14 Control C to exit insert mode -Current line 11 of 15 -Current line 11 of 15 -Current line 11 of 15 -Current line 11 of 15 - 8 : They are lazy - 9 : They are funny - 10 : Walruses would eat pizza rosa if they could. - 11 : Walruses primarily feed on benthic bivalve mollusks. +Current line 2 of 15 +Current line 2 of 15 +Current line 2 of 15 +Current line 2 of 15 + 8 : They are fat + 9 : They are lazy + 10 : They are funny + 11 : Their diet is fish and stuff. 12 : They are mammals -Current line 11 of 15 -Current line 11 of 15 +Current line 2 of 15 +Current line 2 of 15 diff --git a/tests/sample_macros/walrus_facts1_out.txt b/tests/sample_macros/walrus_facts1_out.txt index d889402..6fa2c65 100644 --- a/tests/sample_macros/walrus_facts1_out.txt +++ b/tests/sample_macros/walrus_facts1_out.txt @@ -1,4 +1,5 @@ -They big +Walruses would eat pizza rosa if they could. +Walruses primarily feed on benthic bivalve mollusks. They walrus They eat They swim @@ -7,8 +8,7 @@ They are cute They are fat They are lazy They are funny -Walruses would eat pizza rosa if they could. -Walruses primarily feed on benthic bivalve mollusks. +Their diet is fish and stuff. They are mammals They are endangered They are hunted diff --git a/tests/sample_macros/walrus_facts2_log.txt b/tests/sample_macros/walrus_facts2_log.txt index 351014c..c556423 100644 --- a/tests/sample_macros/walrus_facts2_log.txt +++ b/tests/sample_macros/walrus_facts2_log.txt @@ -1,3 +1,4 @@ +Editing E:\github\dedlin\tests\sample_macros\walrus_facts2_out.txt Current line 1 of 24 Current line 1 of 24 Current line 1 of 24 diff --git a/tests/sample_macros/walrus_facts3_log.txt b/tests/sample_macros/walrus_facts3_log.txt index 84eb806..1b66a1c 100644 --- a/tests/sample_macros/walrus_facts3_log.txt +++ b/tests/sample_macros/walrus_facts3_log.txt @@ -1,3 +1,4 @@ +Editing E:\github\dedlin\tests\sample_macros\walrus_facts3_out.txt Current line 1 of 24 1 : They big 2 : They walrus @@ -25,5 +26,6 @@ Current line 1 of 27 Undone Current line 1 of 27 Current line 1 of 27 +Command Commands.COMMENT not implemented Current line 1 of 27 Current line 1 of 27 diff --git a/tests/test_command_parser.py b/tests/test_command_parser.py index 5e45a30..0102f55 100644 --- a/tests/test_command_parser.py +++ b/tests/test_command_parser.py @@ -11,8 +11,10 @@ def test_extract_one_range(): def test_parse_command_insert_default(): for insert in ("I", "Insert", "insert", "i", "INSERT"): + # Insert for whole document doesn't make sense. + # Inserts happen at 1 point. assert parse_command(insert, 1, 3, headless=False) == Command( - Commands.INSERT, LineRange(start=1, offset=2), None + Commands.INSERT, LineRange(start=2, offset=0), None ), insert