Step 107: add diff preview view
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -31,6 +31,8 @@ struct CodeEditorOptions {
|
||||
const std::vector<struct SuggestionMarker>* suggestions = nullptr;
|
||||
const std::vector<struct AnnotationConflictMarker>* conflicts = nullptr;
|
||||
int highlightLine = -1;
|
||||
const std::vector<int>* 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
|
||||
|
||||
39
editor/src/DiffUtils.h
Normal file
39
editor/src/DiffUtils.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct LineDiff {
|
||||
std::vector<int> beforeLines;
|
||||
std::vector<int> afterLines;
|
||||
};
|
||||
|
||||
inline std::vector<std::string> splitLines(const std::string& text) {
|
||||
std::vector<std::string> 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;
|
||||
}
|
||||
@@ -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<std::string> transformIds;
|
||||
std::vector<int> beforeLines;
|
||||
std::vector<int> 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<std::string>& 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<Module> 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<Module>(static_cast<Module*>(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<std::string> 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<HighlightSpan>{}, 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<HighlightSpan>{}, rightOpts,
|
||||
ImVec2(0, 0), monoFont);
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::PopFont();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
// AST view
|
||||
if (ImGui::BeginTabItem("AST")) {
|
||||
ImGui::PushFont(monoFont);
|
||||
|
||||
27
editor/tests/step107_test.cpp
Normal file
27
editor/tests/step107_test.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
// Step 107 TDD Test: Diff view utilities
|
||||
//
|
||||
// Tests:
|
||||
// 1. buildLineDiff marks changed lines on both sides
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user