diff --git a/PROGRESS.md b/PROGRESS.md index 56a8303..68c6f14 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -208,6 +208,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 105: **IMPLEMENTED** — Optimization controls panel with constraint-aware buttons and summaries (2/2 tests pass) - [x] Step 106: **IMPLEMENTED** — Transform history panel with undo controls and provenance coloring (1/1 tests pass) - [ ] Step 107a–107c: **PLANNED** — Text-Editor Mode toggle, UI behavior by mode, and per-buffer persistence (not started) +- [x] Step 107: **IMPLEMENTED** — Before/after diff view with preview and accept/reject (1/1 tests pass) --- @@ -294,6 +295,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 104:** Compile and pass (2/2) **Step 105:** Compile and pass (2/2) **Step 106:** Compile and pass (1/1) +**Step 107:** Compile and pass (1/1) --- @@ -410,3 +412,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 2026-02-09 | Codex | Step 105: Optimization controls panel with constraint-aware buttons and summaries. 2/2 tests pass. | | 2026-02-09 | Codex | Step 106: Transform history panel with undo controls and provenance coloring. 1/1 tests pass. | | 2026-02-09 | Codex | Planned Step 107a–107c: add Text-Editor Mode toggle, mode-specific UI behavior, and per-buffer persistence. | +| 2026-02-09 | Codex | Step 107: Before/after diff view with preview and accept/reject. 1/1 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 124345d..1ce9e05 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -574,6 +574,10 @@ add_executable(step106_test tests/step106_test.cpp) target_include_directories(step106_test PRIVATE src) target_link_libraries(step106_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step107_test tests/step107_test.cpp) +target_include_directories(step107_test PRIVATE src) +target_link_libraries(step107_test PRIVATE nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/CodeEditorWidget.h b/editor/src/CodeEditorWidget.h index 4d26b42..f6476de 100644 --- a/editor/src/CodeEditorWidget.h +++ b/editor/src/CodeEditorWidget.h @@ -31,6 +31,8 @@ struct CodeEditorOptions { const std::vector* suggestions = nullptr; const std::vector* conflicts = nullptr; int highlightLine = -1; + const std::vector* highlightLines = nullptr; + ImU32 highlightLineColor = IM_COL32(90, 70, 30, 120); float* syncScrollX = nullptr; float* syncScrollY = nullptr; bool scrollMaster = false; @@ -388,7 +390,15 @@ public: if (options.highlightLine >= 0 && ln == options.highlightLine) { ImVec2 hlA(textBase.x, y); ImVec2 hlB(textBase.x + textAreaWidth, y + lineHeight); - drawList->AddRectFilled(hlA, hlB, IM_COL32(90, 70, 30, 120)); + drawList->AddRectFilled(hlA, hlB, options.highlightLineColor); + } + if (options.highlightLines) { + if (std::find(options.highlightLines->begin(), + options.highlightLines->end(), ln) != options.highlightLines->end()) { + ImVec2 hlA(textBase.x, y); + ImVec2 hlB(textBase.x + textAreaWidth, y + lineHeight); + drawList->AddRectFilled(hlA, hlB, options.highlightLineColor); + } } // Selection background diff --git a/editor/src/DiffUtils.h b/editor/src/DiffUtils.h new file mode 100644 index 0000000..0858b50 --- /dev/null +++ b/editor/src/DiffUtils.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include + +struct LineDiff { + std::vector beforeLines; + std::vector afterLines; +}; + +inline std::vector splitLines(const std::string& text) { + std::vector lines; + std::string current; + for (char c : text) { + if (c == '\n') { + lines.push_back(current); + current.clear(); + } else { + current.push_back(c); + } + } + lines.push_back(current); + return lines; +} + +inline LineDiff buildLineDiff(const std::string& beforeText, const std::string& afterText) { + LineDiff diff; + auto before = splitLines(beforeText); + auto after = splitLines(afterText); + size_t maxLines = before.size() > after.size() ? before.size() : after.size(); + for (size_t i = 0; i < maxLines; ++i) { + const std::string* b = i < before.size() ? &before[i] : nullptr; + const std::string* a = i < after.size() ? &after[i] : nullptr; + if (!b || !a || *b != *a) { + if (b) diff.beforeLines.push_back((int)i); + if (a) diff.afterLines.push_back((int)i); + } + } + return diff; +} diff --git a/editor/src/main.cpp b/editor/src/main.cpp index ef22acf..c1e3bd8 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -36,6 +36,8 @@ #include "TransformEngine.h" #include "StrategyAwareOptimizer.h" #include "CrossLanguageProjector.h" +#include "DiffUtils.h" +#include "ast/Serialization.h" #include "ast/Generator.h" #include "ast/Annotation.h" @@ -133,6 +135,21 @@ struct EditorState { std::string optimizeFoldSummary; std::string optimizeDeadCodeSummary; std::string optimizeApplySummary; + bool optimizePreview = false; + struct DiffState { + bool active = false; + bool preview = false; + int action = 0; // 1=constant-fold, 2=dead-code-elim, 3=apply-all + std::string beforeText; + std::string afterText; + std::vector transformIds; + std::vector beforeLines; + std::vector afterLines; + } diff; + CodeEditorWidget diffLeftWidget; + CodeEditorWidget diffRightWidget; + float diffScrollX = 0.0f; + float diffScrollY = 0.0f; BufferState* active() { return activeBuffer; } @@ -551,6 +568,22 @@ struct EditorState { analysisLastChange = ImGui::GetTime(); } + void openDiff(const std::string& beforeText, + const std::string& afterText, + bool preview, + int action, + const std::vector& transformIds) { + diff.active = true; + diff.preview = preview; + diff.action = action; + diff.beforeText = beforeText; + diff.afterText = afterText; + diff.transformIds = transformIds; + LineDiff lineDiff = buildLineDiff(beforeText, afterText); + diff.beforeLines = std::move(lineDiff.beforeLines); + diff.afterLines = std::move(lineDiff.afterLines); + } + void updateCursorPos(int bytePos) { if (!active()) return; active()->cursorLine = 1; @@ -601,6 +634,14 @@ static std::string generateForLanguage(const Module* ast, const std::string& lan return gen.generate(ast); } +static std::unique_ptr cloneModule(const Module* ast) { + if (!ast) return nullptr; + json j = toJson(ast); + ASTNode* node = fromJson(j); + if (!node || node->conceptType != "Module") return nullptr; + return std::unique_ptr(static_cast(node)); +} + static bool isAnnotationNode(const ASTNode* node) { if (!node) return false; if (node->conceptType.find("Annotation") != std::string::npos) return true; @@ -2044,6 +2085,7 @@ int main(int, char**) { } else if (state.active()->readOnly) { ImGui::TextDisabled("(read-only buffer)"); } else { + ImGui::Checkbox("Preview changes", &state.optimizePreview); std::string blockReason = findOptimizationBlockReason(ast); bool blocked = !blockReason.empty(); @@ -2058,31 +2100,44 @@ int main(int, char**) { if (blocked) ImGui::BeginDisabled(); if (ImGui::Button("Constant Fold")) { - auto& inc = state.active()->incrementalOptimizer; - inc.setRoot(ast); - std::string tid = inc.applyTransform("constant-fold"); - auto history = inc.getTransformHistory(); - size_t nodes = 0; - for (const auto& h : history) { - if (h.transformId == tid) { - nodes = h.affectedNodeIds.size(); - break; - } - } - std::string warning = findOptimizationLockWarning(ast); - std::string summary = "Constant Fold: "; - if (nodes > 0) { - summary += "applied (" + std::to_string(nodes) + " nodes)"; + std::string beforeText = state.active()->editBuf; + if (state.optimizePreview) { + auto previewAst = cloneModule(ast); + TransformEngine engine; + engine.setRoot(previewAst.get()); + engine.constantFolding(); + std::string afterText = generateForLanguage(previewAst.get(), + state.active()->language); + state.openDiff(beforeText, afterText, true, 1, {}); } else { - summary += "no changes"; + auto& inc = state.active()->incrementalOptimizer; + inc.setRoot(ast); + std::string tid = inc.applyTransform("constant-fold"); + auto history = inc.getTransformHistory(); + size_t nodes = 0; + for (const auto& h : history) { + if (h.transformId == tid) { + nodes = h.affectedNodeIds.size(); + break; + } + } + std::string warning = findOptimizationLockWarning(ast); + std::string summary = "Constant Fold: "; + if (nodes > 0) { + summary += "applied (" + std::to_string(nodes) + " nodes)"; + } else { + summary += "no changes"; + } + if (!warning.empty()) { + summary += " — warning: " + warning; + state.outputLog += warning + "\n"; + } + state.optimizeFoldSummary = summary; + state.outputLog += summary + "\n"; + if (nodes > 0) state.refreshActiveTextFromAST(); + std::string afterText = state.active()->editBuf; + state.openDiff(beforeText, afterText, false, 1, {tid}); } - if (!warning.empty()) { - summary += " — warning: " + warning; - state.outputLog += warning + "\n"; - } - state.optimizeFoldSummary = summary; - state.outputLog += summary + "\n"; - if (nodes > 0) state.refreshActiveTextFromAST(); } showBlockedTooltip(blockReason); if (blocked) ImGui::EndDisabled(); @@ -2094,26 +2149,39 @@ int main(int, char**) { if (blocked) ImGui::BeginDisabled(); if (ImGui::Button("Dead Code Elimination")) { - auto& inc = state.active()->incrementalOptimizer; - inc.setRoot(ast); - std::string tid = inc.applyTransform("dead-code-elim"); - auto history = inc.getTransformHistory(); - size_t nodes = 0; - for (const auto& h : history) { - if (h.transformId == tid) { - nodes = h.affectedNodeIds.size(); - break; - } - } - std::string summary = "Dead Code Elimination: "; - if (nodes > 0) { - summary += "applied (" + std::to_string(nodes) + " nodes)"; + std::string beforeText = state.active()->editBuf; + if (state.optimizePreview) { + auto previewAst = cloneModule(ast); + TransformEngine engine; + engine.setRoot(previewAst.get()); + engine.deadCodeElimination(); + std::string afterText = generateForLanguage(previewAst.get(), + state.active()->language); + state.openDiff(beforeText, afterText, true, 2, {}); } else { - summary += "no changes"; + auto& inc = state.active()->incrementalOptimizer; + inc.setRoot(ast); + std::string tid = inc.applyTransform("dead-code-elim"); + auto history = inc.getTransformHistory(); + size_t nodes = 0; + for (const auto& h : history) { + if (h.transformId == tid) { + nodes = h.affectedNodeIds.size(); + break; + } + } + std::string summary = "Dead Code Elimination: "; + if (nodes > 0) { + summary += "applied (" + std::to_string(nodes) + " nodes)"; + } else { + summary += "no changes"; + } + state.optimizeDeadCodeSummary = summary; + state.outputLog += summary + "\n"; + if (nodes > 0) state.refreshActiveTextFromAST(); + std::string afterText = state.active()->editBuf; + state.openDiff(beforeText, afterText, false, 2, {tid}); } - state.optimizeDeadCodeSummary = summary; - state.outputLog += summary + "\n"; - if (nodes > 0) state.refreshActiveTextFromAST(); } showBlockedTooltip(blockReason); if (blocked) ImGui::EndDisabled(); @@ -2125,31 +2193,44 @@ int main(int, char**) { if (blocked) ImGui::BeginDisabled(); if (ImGui::Button("Apply All")) { - auto& inc = state.active()->incrementalOptimizer; - inc.setRoot(ast); - std::string tidFold = inc.applyTransform("constant-fold"); - std::string tidDce = inc.applyTransform("dead-code-elim"); - auto history = inc.getTransformHistory(); - size_t totalNodes = 0; - for (const auto& h : history) { - if (h.transformId == tidFold || h.transformId == tidDce) { - totalNodes += h.affectedNodeIds.size(); - } - } - std::string warning = findOptimizationLockWarning(ast); - std::string summary = "Apply All: "; - if (totalNodes > 0) { - summary += "applied (" + std::to_string(totalNodes) + " nodes)"; + std::string beforeText = state.active()->editBuf; + if (state.optimizePreview) { + auto previewAst = cloneModule(ast); + TransformEngine engine; + engine.setRoot(previewAst.get()); + engine.applyAll(); + std::string afterText = generateForLanguage(previewAst.get(), + state.active()->language); + state.openDiff(beforeText, afterText, true, 3, {}); } else { - summary += "no changes"; + auto& inc = state.active()->incrementalOptimizer; + inc.setRoot(ast); + std::string tidFold = inc.applyTransform("constant-fold"); + std::string tidDce = inc.applyTransform("dead-code-elim"); + auto history = inc.getTransformHistory(); + size_t totalNodes = 0; + for (const auto& h : history) { + if (h.transformId == tidFold || h.transformId == tidDce) { + totalNodes += h.affectedNodeIds.size(); + } + } + std::string warning = findOptimizationLockWarning(ast); + std::string summary = "Apply All: "; + if (totalNodes > 0) { + summary += "applied (" + std::to_string(totalNodes) + " nodes)"; + } else { + summary += "no changes"; + } + if (!warning.empty()) { + summary += " — warning: " + warning; + state.outputLog += warning + "\n"; + } + state.optimizeApplySummary = summary; + state.outputLog += summary + "\n"; + if (totalNodes > 0) state.refreshActiveTextFromAST(); + std::string afterText = state.active()->editBuf; + state.openDiff(beforeText, afterText, false, 3, {tidFold, tidDce}); } - if (!warning.empty()) { - summary += " — warning: " + warning; - state.outputLog += warning + "\n"; - } - state.optimizeApplySummary = summary; - state.outputLog += summary + "\n"; - if (totalNodes > 0) state.refreshActiveTextFromAST(); } showBlockedTooltip(blockReason); if (blocked) ImGui::EndDisabled(); @@ -2220,6 +2301,94 @@ int main(int, char**) { ImGui::EndTabItem(); } + // Diff view + if (ImGui::BeginTabItem("Diff")) { + ImGui::PushFont(uiFont); + auto* buf = state.active(); + Module* ast = buf ? buf->sync.getAST() : nullptr; + if (!buf) { + ImGui::TextDisabled("(no active buffer)"); + } else if (!state.diff.active) { + ImGui::TextDisabled("(no diff)"); + } else { + if (state.diff.preview) { + if (ImGui::Button("Apply")) { + if (ast && !buf->readOnly) { + buf->incrementalOptimizer.setRoot(ast); + std::vector ids; + if (state.diff.action == 1) { + ids.push_back(buf->incrementalOptimizer.applyTransform("constant-fold")); + } else if (state.diff.action == 2) { + ids.push_back(buf->incrementalOptimizer.applyTransform("dead-code-elim")); + } else if (state.diff.action == 3) { + ids.push_back(buf->incrementalOptimizer.applyTransform("constant-fold")); + ids.push_back(buf->incrementalOptimizer.applyTransform("dead-code-elim")); + } + state.refreshActiveTextFromAST(); + } + state.diff.active = false; + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + state.diff.active = false; + } + } else { + if (ImGui::Button("Keep")) { + state.diff.active = false; + } + ImGui::SameLine(); + if (ImGui::Button("Undo")) { + if (ast && !buf->readOnly) { + buf->incrementalOptimizer.setRoot(ast); + for (auto it = state.diff.transformIds.rbegin(); + it != state.diff.transformIds.rend(); ++it) { + buf->incrementalOptimizer.undoTransform(*it); + } + state.refreshActiveTextFromAST(); + } + state.diff.active = false; + } + } + + ImGui::Separator(); + ImGui::BeginTable("##diffSplit", 2, + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp); + ImGui::TableSetupColumn("Before", ImGuiTableColumnFlags_WidthStretch, 0.5f); + ImGui::TableSetupColumn("After", ImGuiTableColumnFlags_WidthStretch, 0.5f); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + CodeEditorOptions leftOpts; + leftOpts.readOnly = true; + leftOpts.showWhitespace = state.showWhitespace; + leftOpts.showCurrentLine = false; + leftOpts.highlightLines = &state.diff.beforeLines; + leftOpts.highlightLineColor = IM_COL32(160, 80, 80, 120); + leftOpts.syncScrollX = &state.diffScrollX; + leftOpts.syncScrollY = &state.diffScrollY; + leftOpts.scrollMaster = true; + state.diffLeftWidget.render("##diffBefore", + state.diff.beforeText, std::vector{}, leftOpts, + ImVec2(0, 0), monoFont); + + ImGui::TableSetColumnIndex(1); + CodeEditorOptions rightOpts; + rightOpts.readOnly = true; + rightOpts.showWhitespace = state.showWhitespace; + rightOpts.showCurrentLine = false; + rightOpts.highlightLines = &state.diff.afterLines; + rightOpts.highlightLineColor = IM_COL32(80, 160, 80, 120); + rightOpts.syncScrollX = &state.diffScrollX; + rightOpts.syncScrollY = &state.diffScrollY; + rightOpts.scrollMaster = false; + state.diffRightWidget.render("##diffAfter", + state.diff.afterText, std::vector{}, rightOpts, + ImVec2(0, 0), monoFont); + ImGui::EndTable(); + } + ImGui::PopFont(); + ImGui::EndTabItem(); + } + // AST view if (ImGui::BeginTabItem("AST")) { ImGui::PushFont(monoFont); diff --git a/editor/tests/step107_test.cpp b/editor/tests/step107_test.cpp new file mode 100644 index 0000000..90d93f2 --- /dev/null +++ b/editor/tests/step107_test.cpp @@ -0,0 +1,27 @@ +// Step 107 TDD Test: Diff view utilities +// +// Tests: +// 1. buildLineDiff marks changed lines on both sides + +#include +#include +#include "DiffUtils.h" + +int main() { + int passed = 0; + int failed = 0; + + std::string before = "a\nb\nc"; + std::string after = "a\nx\nc\nd"; + auto diff = buildLineDiff(before, after); + assert(diff.beforeLines.size() == 1); + assert(diff.beforeLines[0] == 1); + assert(diff.afterLines.size() == 2); + assert(diff.afterLines[0] == 1); + assert(diff.afterLines[1] == 3); + std::cout << "Test 1 PASS: diff lines detected" << std::endl; + ++passed; + + std::cout << "\n=== Step 107 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}