{"record":{"id":"d497aec378624ef8","repo":"Textualize/textual","slug":"cannot-add-an-edit-to-history-before-it-has-been-p","errorCode":null,"errorMessage":"Cannot add an edit to history before it has been performed using `Edit.do`.","messagePattern":"Cannot add an edit to history before it has been performed using `Edit\\.do`\\.","errorType":"exception","errorClass":"HistoryException","httpStatus":null,"severity":"error","filePath":"src/textual/document/_history.py","lineNumber":78,"sourceCode":"        - The checkpoint timer expires.\n        - The maximum number of characters permitted in a checkpoint is reached.\n        - A redo is performed (we should not add new edits to a batch that has been redone).\n        - The programmer has requested a new batch via a call to `force_new_batch`.\n            - e.g. the TextArea widget may call this method in some circumstances.\n            - Clicking to move the cursor elsewhere in the document should create a new batch.\n            - Movement of the cursor via a keyboard action that is NOT an edit.\n            - Blurring the TextArea creates a new checkpoint.\n        - The current edit involves a deletion/replacement and the previous edit did not.\n        - The current edit is a pure insertion and the previous edit was not.\n        - The edit involves insertion or deletion of one or more newline characters.\n        - An edit which inserts more than a single character (a paste) gets an isolated batch.\n\n        Args:\n            edit: The edit to record.\n        \"\"\"\n        edit_result = edit._edit_result\n        if edit_result is None:\n            raise HistoryException(\n                \"Cannot add an edit to history before it has been performed using `Edit.do`.\"\n            )\n\n        if edit.text == \"\" and edit_result.replaced_text == \"\":\n            return None\n\n        is_replacement = bool(edit_result.replaced_text)\n        undo_stack = self._undo_stack\n        current_time = self._get_time()\n        edit_characters = len(edit.text)\n        contains_newline = \"\\n\" in edit.text or \"\\n\" in edit_result.replaced_text\n\n        # Determine whether to create a new batch, or add to the latest batch.\n        if (\n            not undo_stack\n            or self._force_end_batch\n            or edit_characters > 1\n            or contains_newline","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/Textualize/textual/blob/06dbeef4bb70fb718236aa418ed658ef4667a126/src/textual/document/_history.py#L60-L96","documentation":"History.record raises HistoryException when asked to record an Edit that has not been performed yet — the edit carries no _edit_result because Edit.do was never called. History only accepts edits that actually happened, so it can store the replaced text for undo.","triggerScenarios":"Creating an Edit(edit) and passing it to history.record(edit) directly without first calling edit.do(document). Typically hit when writing custom edit flows in TextArea/document code.","commonSituations":"Custom undo/redo integrations or scripted transformations where the developer builds the edit and records it manually, forgetting the do step.","solutions":["Call edit.do(document) before history.record(edit)","Or use the higher-level Document.edit API, which performs and records atomically","Skip recording for no-op edits (record already ignores empty edits after do)"],"exampleFix":"# before\nedit = Edit(from_loc, to_loc, text)\nhistory.record(edit)  # raises\n# after\nedit = Edit(from_loc, to_loc, text)\nedit.do(document)\nhistory.record(edit)","handlingStrategy":"validation","validationCode":"def perform_and_record(history, edit, document):\n    if edit._edit_result is None:\n        edit.do(document)\n    history.record(edit)","typeGuard":null,"tryCatchPattern":"from textual.document._history import HistoryException\ntry:\n    history.record(edit)\nexcept HistoryException:\n    edit.do(document)\n    history.record(edit)","preventionTips":["Always call edit.do() before history.record()","Prefer Document.edit() which does both atomically","Skip recording for no-op edits"],"tags":["textual","document","history","undo","edit-ordering"],"backgroundTag":"operation-before-validation","analyzedSha":"06dbeef4bb70fb718236aa418ed658ef4667a126","analyzedAt":"2026-08-27T02:36:57.214Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}