Step 120: unify undo/redo snapshots

This commit is contained in:
Bill
2026-02-09 13:12:37 -07:00
parent 4f61953e1a
commit 5834523665
5 changed files with 164 additions and 35 deletions

View File

@@ -638,6 +638,10 @@ add_executable(step119_test tests/step119_test.cpp)
target_include_directories(step119_test PRIVATE src)
target_link_libraries(step119_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step120_test tests/step120_test.cpp)
target_include_directories(step120_test PRIVATE src)
target_link_libraries(step120_test PRIVATE nlohmann_json::nlohmann_json)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

View File

@@ -237,6 +237,56 @@ public:
undoPosition++;
return true;
}
// Record a snapshot (text + AST) for unified undo/redo
void recordSnapshot(const std::string& text, const ASTNode* ast) {
json op;
op["type"] = "snapshot";
op["text"] = text;
if (ast) op["ast"] = toJson(ast);
recordOperation(op);
}
// Undo snapshot
bool undoSnapshot(std::string& outText, std::unique_ptr<Module>& outAst) {
if (undoPosition == 0) return false;
undoPosition--;
json operation = operationJournal[undoPosition];
if (operation.value("type", "") != "snapshot") return false;
outText = operation.value("text", "");
outAst.reset();
if (operation.contains("ast")) {
ASTNode* node = fromJson(operation["ast"]);
if (node && node->conceptType == "Module") {
outAst.reset(static_cast<Module*>(node));
} else {
deleteTree(node);
}
}
return true;
}
// Redo snapshot
bool redoSnapshot(std::string& outText, std::unique_ptr<Module>& outAst) {
if (undoPosition >= operationJournal.size()) return false;
json operation = operationJournal[undoPosition];
if (operation.value("type", "") != "snapshot") return false;
undoPosition++;
outText = operation.value("text", "");
outAst.reset();
if (operation.contains("ast")) {
ASTNode* node = fromJson(operation["ast"]);
if (node && node->conceptType == "Module") {
outAst.reset(static_cast<Module*>(node));
} else {
deleteTree(node);
}
}
return true;
}
int getUndoDepth() const { return (int)undoPosition; }
int getRedoDepth() const { return (int)(operationJournal.size() - undoPosition); }
// Helper function to find a node by ID in the AST
ASTNode* findNodeById(ASTNode* root, const std::string& id) const {
@@ -405,4 +455,4 @@ public:
std::string result = sendToEmacs(command);
return result.find("Error") == std::string::npos;
}
};
};

View File

@@ -99,6 +99,9 @@ struct BufferState {
float splitScrollX = 0.0f;
float splitScrollY = 0.0f;
IncrementalOptimizer incrementalOptimizer;
Orchestrator orchestrator;
bool orchestratorDirty = true;
int undoDepth = 0;
};
struct EditorState {
@@ -165,8 +168,6 @@ struct EditorState {
std::vector<EditorDiagnostic> whetstoneDiagnostics;
SettingsManager settings;
bool showLspSettings = false;
Orchestrator orchestrator;
bool orchestratorDirty = true;
bool showSettingsPanel = false;
double lastAutoSave = 0.0;
ASTMutationAPI mutator;
@@ -343,7 +344,8 @@ struct EditorState {
state->bufferMode = mode;
activeBuffer = state.get();
bufferStates[path] = std::move(state);
orchestratorDirty = true;
active()->orchestratorDirty = true;
recordUndoSnapshot();
if (path.rfind("(untitled", 0) != 0) watcher.watch(path);
if (lsp && path.rfind("(untitled", 0) != 0) {
lsp->didOpen(toFileUri(path), language, content, activeBuffer->lspVersion);
@@ -430,7 +432,7 @@ struct EditorState {
if (!buffers.hasBuffer(path)) return;
buffers.switchToBuffer(path);
activeBuffer = bufferStates[path].get();
orchestratorDirty = true;
if (active()) active()->orchestratorDirty = true;
symbolsPending = true;
symbolsLastChange = ImGui::GetTime();
if (lsp) lsp->clearDocumentSymbols();
@@ -546,6 +548,7 @@ struct EditorState {
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->modified = false;
recordUndoSnapshot();
}
} else {
doOpen(buf.path, bufferModeFromString(buf.mode));
@@ -555,7 +558,7 @@ struct EditorState {
if (!activePath.empty() && buffers.hasBuffer(activePath)) {
switchToBuffer(activePath);
}
orchestratorDirty = true;
if (active()) active()->orchestratorDirty = true;
return true;
} catch (...) {
return false;
@@ -681,28 +684,61 @@ struct EditorState {
if (!isStructured()) return;
Module* ast = active()->sync.getAST();
if (!ast) return;
orchestrator.setAST(cloneModule(ast));
orchestratorDirty = false;
active()->orchestrator.setAST(cloneModule(ast));
active()->orchestratorDirty = false;
}
void applyOrchestratorToActive() {
if (!active()) return;
if (!isStructured()) return;
Module* ast = orchestrator.getAST();
Module* ast = active()->orchestrator.getAST();
if (!ast) return;
active()->sync.setAST(cloneModule(ast));
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
refreshActiveTextFromAST();
orchestratorDirty = false;
active()->orchestratorDirty = false;
recordUndoSnapshot();
}
Module* mutationAST() {
if (!active()) return nullptr;
if (!isStructured()) return nullptr;
if (orchestratorDirty || !orchestrator.getAST()) {
if (active()->orchestratorDirty || !active()->orchestrator.getAST()) {
syncOrchestratorFromActive();
}
return orchestrator.getAST();
return active()->orchestrator.getAST();
}
void recordUndoSnapshot() {
if (!active()) return;
Module* ast = isStructured() ? active()->sync.getAST() : nullptr;
active()->orchestrator.recordSnapshot(active()->editBuf, ast);
active()->undoDepth = active()->orchestrator.getUndoDepth();
}
void applySnapshotToActive(const std::string& text, std::unique_ptr<Module> ast) {
if (!active()) return;
active()->editBuf = text;
active()->editor.setContent(active()->editBuf, active()->language);
if (active()->bufferMode == BufferManager::BufferMode::Structured) {
if (ast) {
active()->orchestrator.setAST(cloneModule(ast.get()));
active()->sync.setAST(std::move(ast));
} else {
active()->sync.setText(active()->editBuf, active()->language);
active()->sync.syncNow();
active()->orchestrator.setAST(cloneModule(active()->sync.getAST()));
}
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
active()->orchestratorDirty = false;
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->modified = true;
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
active()->lspVersion += 1;
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
}
}
void registerCommand(const std::string& id,
@@ -845,7 +881,7 @@ struct EditorState {
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
orchestratorDirty = true;
active()->orchestratorDirty = true;
active()->highlightsDirty = true;
}
@@ -858,7 +894,7 @@ struct EditorState {
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
}
orchestratorDirty = true;
active()->orchestratorDirty = true;
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->modified = true;
@@ -869,34 +905,27 @@ struct EditorState {
active()->lspVersion += 1;
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
}
recordUndoSnapshot();
}
void doUndo() {
if (!active()) return;
active()->editor.undo();
active()->editBuf = active()->editor.getContent();
if (active()->bufferMode == BufferManager::BufferMode::Structured) {
active()->sync.setText(active()->editBuf, active()->language);
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
std::string text;
std::unique_ptr<Module> ast;
if (active()->orchestrator.undoSnapshot(text, ast)) {
applySnapshotToActive(text, std::move(ast));
}
orchestratorDirty = true;
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->undoDepth = active()->orchestrator.getUndoDepth();
}
void doRedo() {
if (!active()) return;
active()->editor.redo();
active()->editBuf = active()->editor.getContent();
if (active()->bufferMode == BufferManager::BufferMode::Structured) {
active()->sync.setText(active()->editBuf, active()->language);
active()->sync.syncNow();
active()->incrementalOptimizer.setRoot(active()->sync.getAST());
std::string text;
std::unique_ptr<Module> ast;
if (active()->orchestrator.redoSnapshot(text, ast)) {
applySnapshotToActive(text, std::move(ast));
}
orchestratorDirty = true;
active()->highlightsDirty = true;
active()->generatedHighlightsDirty = true;
active()->undoDepth = active()->orchestrator.getUndoDepth();
}
void doFind() {
@@ -2098,10 +2127,10 @@ int main(int, char**) {
}
if (ImGui::BeginMenu("Edit")) {
if (ImGui::MenuItem("Undo", state.keys.getBinding("edit.undo").toString().c_str(),
false, state.active() ? state.active()->editor.canUndo() : false))
false, state.active() ? state.active()->orchestrator.getUndoDepth() > 0 : false))
state.doUndo();
if (ImGui::MenuItem("Redo", state.keys.getBinding("edit.redo").toString().c_str(),
false, state.active() ? state.active()->editor.canRedo() : false))
false, state.active() ? state.active()->orchestrator.getRedoDepth() > 0 : false))
state.doRedo();
ImGui::Separator();
if (ImGui::MenuItem("Find/Replace", state.keys.getBinding("search.find").toString().c_str()))
@@ -4027,6 +4056,10 @@ int main(int, char**) {
ImGui::Text("Zoom: %d%%", zoomPercent(state.settings.getFontSize(), baseFontSize));
ImGui::SameLine(0, 30);
// Undo depth
ImGui::Text("Undo: %d", state.active() ? state.active()->undoDepth : 0);
ImGui::SameLine(0, 30);
// Modified indicator
if (state.active() && state.active()->modified)
ImGui::Text("Modified");

View File

@@ -0,0 +1,42 @@
// Step 120 TDD Test: Orchestrator snapshot undo/redo
#include "Orchestrator.h"
#include <iostream>
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
if (cond) {
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
++passed;
} else {
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
++failed;
}
}
int main() {
int passed = 0;
int failed = 0;
Orchestrator orch;
Module a("m1", "A", "python");
Module b("m2", "B", "python");
orch.recordSnapshot("one", &a);
orch.recordSnapshot("two", &b);
std::string text;
std::unique_ptr<Module> ast;
bool undoOk = orch.undoSnapshot(text, ast);
expect(undoOk && text == "two" && ast && ast->name == "B", "undo snapshot", passed, failed);
bool undoOk2 = orch.undoSnapshot(text, ast);
expect(undoOk2 && text == "one" && ast && ast->name == "A", "undo snapshot (second)", passed, failed);
bool redoOk = orch.redoSnapshot(text, ast);
expect(redoOk && text == "one" && ast && ast->name == "A", "redo snapshot (first)", passed, failed);
bool redoOk2 = orch.redoSnapshot(text, ast);
expect(redoOk2 && text == "two" && ast && ast->name == "B", "redo snapshot (second)", passed, failed);
std::cout << "\n=== Step 120 Results: " << passed << " passed, " << failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -385,7 +385,7 @@ AST extensions for Sprint 5.
size dynamically. Status bar shows current zoom level.
*Modifies:* `main.cpp`
- [ ] **Step 120: Undo/Redo rework**
- [x] **Step 120: Undo/Redo rework**
Unify undo stacks: orchestrator journal is the single source of truth.
TextEditor undo feeds into orchestrator. Undo/redo affects both text and
AST atomically. Status bar shows undo depth.