diff --git a/PROGRESS.md b/PROGRESS.md index 66c07e5..e962c6d 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -209,7 +209,8 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 106: **IMPLEMENTED** — Transform history panel with undo controls and provenance coloring (1/1 tests pass) - [x] Step 107: **IMPLEMENTED** — Before/after diff view with preview and accept/reject (1/1 tests pass) - [x] Step 107a: **IMPLEMENTED** — Text-Editor Mode toggle and per-buffer mode tracking (2/2 tests pass) -- [ ] Step 107b–107c: **PLANNED** — Mode-specific UI behavior and per-buffer persistence (not started) +- [x] Step 107b: **IMPLEMENTED** — Mode-specific UI behavior and feature gating (2/2 tests pass) +- [ ] Step 107c: **PLANNED** — Per-buffer persistence for Text/Structured modes (not started) --- @@ -298,6 +299,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 106:** Compile and pass (1/1) **Step 107:** Compile and pass (1/1) **Step 107a:** Compile and pass (2/2) +**Step 107b:** Compile and pass (2/2) --- @@ -416,3 +418,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 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. | | 2026-02-09 | Codex | Step 107a: Text-Editor Mode toggle and per-buffer mode tracking. 2/2 tests pass. | +| 2026-02-09 | Codex | Step 107b: Mode-specific UI behavior and feature gating. 2/2 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index ded374a..9f1a65a 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -582,6 +582,10 @@ add_executable(step107a_test tests/step107a_test.cpp) target_include_directories(step107a_test PRIVATE src) target_link_libraries(step107a_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step107b_test tests/step107b_test.cpp) +target_include_directories(step107b_test PRIVATE src) +target_link_libraries(step107b_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/EditorModePolicy.h b/editor/src/EditorModePolicy.h new file mode 100644 index 0000000..748fd15 --- /dev/null +++ b/editor/src/EditorModePolicy.h @@ -0,0 +1,10 @@ +#pragma once +#include "BufferManager.h" + +inline bool isTextMode(BufferManager::BufferMode mode) { + return mode == BufferManager::BufferMode::Text; +} + +inline bool allowStructuredFeatures(BufferManager::BufferMode mode) { + return mode == BufferManager::BufferMode::Structured; +} diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 226f742..712d713 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -38,6 +38,7 @@ #include "StrategyAwareOptimizer.h" #include "CrossLanguageProjector.h" #include "DiffUtils.h" +#include "EditorModePolicy.h" #include "ast/Serialization.h" #include "ast/Generator.h" #include "ast/Annotation.h" @@ -71,7 +72,7 @@ struct BufferState { std::string generatedLanguage = "python"; std::string path = "(untitled)"; bool readOnly = false; - BufferManager::BufferMode mode = BufferManager::BufferMode::Structured; + BufferManager::BufferMode bufferMode = BufferManager::BufferMode::Structured; bool modified = false; int cursorLine = 1; int cursorCol = 1; @@ -154,6 +155,14 @@ struct EditorState { float diffScrollY = 0.0f; BufferState* active() { return activeBuffer; } + bool isStructured() const { + return activeBuffer && allowStructuredFeatures(activeBuffer->bufferMode); + } + Module* activeAST() { + if (!activeBuffer) return nullptr; + if (!allowStructuredFeatures(activeBuffer->bufferMode)) return nullptr; + return activeBuffer->sync.getAST(); + } static std::string toFileUri(const std::string& path) { std::filesystem::path p(path); @@ -254,7 +263,7 @@ struct EditorState { state->modified = false; state->lspVersion = 1; buffers.openBuffer(path, content, language, BufferManager::BufferMode::Structured); - state->mode = BufferManager::BufferMode::Structured; + state->bufferMode = BufferManager::BufferMode::Structured; activeBuffer = state.get(); bufferStates[path] = std::move(state); if (path.rfind("(untitled", 0) != 0) watcher.watch(path); @@ -325,9 +334,11 @@ struct EditorState { active()->generatedHighlightsDirty = true; } active()->editor.setContent(active()->editBuf, lang); - active()->sync.setText(active()->editBuf, lang); - active()->sync.syncNow(); - active()->incrementalOptimizer.setRoot(active()->sync.getAST()); + if (active()->bufferMode == BufferManager::BufferMode::Structured) { + active()->sync.setText(active()->editBuf, lang); + active()->sync.syncNow(); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); + } active()->highlightsDirty = true; } @@ -335,9 +346,11 @@ struct EditorState { void onTextChanged() { if (!active()) return; active()->editor.setContent(active()->editBuf, active()->language); - active()->sync.setText(active()->editBuf, active()->language); - active()->sync.syncNow(); - active()->incrementalOptimizer.setRoot(active()->sync.getAST()); + if (active()->bufferMode == BufferManager::BufferMode::Structured) { + active()->sync.setText(active()->editBuf, active()->language); + active()->sync.syncNow(); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); + } active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; active()->modified = true; @@ -351,9 +364,11 @@ struct EditorState { if (!active()) return; active()->editor.undo(); active()->editBuf = active()->editor.getContent(); - active()->sync.setText(active()->editBuf, active()->language); - active()->sync.syncNow(); - active()->incrementalOptimizer.setRoot(active()->sync.getAST()); + if (active()->bufferMode == BufferManager::BufferMode::Structured) { + active()->sync.setText(active()->editBuf, active()->language); + active()->sync.syncNow(); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); + } active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; } @@ -362,9 +377,11 @@ struct EditorState { if (!active()) return; active()->editor.redo(); active()->editBuf = active()->editor.getContent(); - active()->sync.setText(active()->editBuf, active()->language); - active()->sync.syncNow(); - active()->incrementalOptimizer.setRoot(active()->sync.getAST()); + if (active()->bufferMode == BufferManager::BufferMode::Structured) { + active()->sync.setText(active()->editBuf, active()->language); + active()->sync.syncNow(); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); + } active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; } @@ -395,9 +412,11 @@ struct EditorState { int count = active()->editor.replaceAll(findBuf, replaceBuf); if (count > 0) { active()->editBuf = active()->editor.getContent(); - active()->sync.setText(active()->editBuf, active()->language); - active()->sync.syncNow(); - active()->incrementalOptimizer.setRoot(active()->sync.getAST()); + if (active()->bufferMode == BufferManager::BufferMode::Structured) { + active()->sync.setText(active()->editBuf, active()->language); + active()->sync.syncNow(); + active()->incrementalOptimizer.setRoot(active()->sync.getAST()); + } active()->highlightsDirty = true; active()->generatedHighlightsDirty = true; active()->modified = true; @@ -464,9 +483,11 @@ struct EditorState { auto* buf = it->second.get(); buf->editBuf = ss.str(); buf->editor.setContent(buf->editBuf, buf->language); - buf->sync.setText(buf->editBuf, buf->language); - buf->sync.syncNow(); - buf->incrementalOptimizer.setRoot(buf->sync.getAST()); + if (buf->bufferMode == BufferManager::BufferMode::Structured) { + buf->sync.setText(buf->editBuf, buf->language); + buf->sync.syncNow(); + buf->incrementalOptimizer.setRoot(buf->sync.getAST()); + } buf->highlightsDirty = true; buf->generatedHighlightsDirty = true; buf->modified = false; @@ -495,6 +516,7 @@ struct EditorState { void updateGenerated() { if (!active()) return; + if (active()->bufferMode == BufferManager::BufferMode::Text) return; Module* ast = active()->sync.getAST(); std::string generated = generateForLanguage(ast, active()->generatedLanguage); if (generated != active()->generatedBuf) { @@ -510,7 +532,7 @@ struct EditorState { void projectToLanguage(const std::string& targetLanguage) { if (!active()) return; - Module* ast = active()->sync.getAST(); + Module* ast = activeAST(); if (!ast) { outputLog += "Project to " + targetLanguage + ": no AST available.\n"; return; @@ -559,6 +581,7 @@ struct EditorState { void refreshActiveTextFromAST() { if (!active()) return; + if (!isStructured()) return; active()->editBuf = active()->sync.getText(); active()->editor.setContent(active()->editBuf, active()->language); active()->highlightsDirty = true; @@ -1332,12 +1355,19 @@ int main(int, char**) { ImGui::MenuItem("Show Annotations", nullptr, &state.showAnnotations); ImGui::MenuItem("LSP Servers...", nullptr, &state.showLspSettings); if (state.active()) { - bool textMode = state.active()->mode == BufferManager::BufferMode::Text; + bool textMode = state.active()->bufferMode == BufferManager::BufferMode::Text; if (ImGui::MenuItem("Text-Editor Mode", nullptr, textMode)) { - state.active()->mode = textMode ? + state.active()->bufferMode = textMode ? BufferManager::BufferMode::Structured : BufferManager::BufferMode::Text; - state.buffers.setBufferMode(state.active()->path, state.active()->mode); + state.buffers.setBufferMode(state.active()->path, state.active()->bufferMode); + if (state.active()->bufferMode == BufferManager::BufferMode::Text) { + state.suggestions.clear(); + state.whetstoneDiagnostics.clear(); + state.analysisPending = false; + } else { + state.onTextChanged(); + } } } if (ImGui::BeginMenu("Layout")) { @@ -1379,7 +1409,7 @@ int main(int, char**) { ImGui::EndMenu(); } - const bool canProject = state.active() && state.active()->sync.getAST(); + const bool canProject = state.activeAST() != nullptr; ImGui::SameLine(); ImGui::Dummy(ImVec2(12.0f, 0.0f)); ImGui::SameLine(); @@ -1508,7 +1538,9 @@ int main(int, char**) { avail.y -= 4; // small margin state.updateHighlights(); - state.updateGenerated(); + if (buf->bufferMode == BufferManager::BufferMode::Structured) { + state.updateGenerated(); + } std::vector errorLines; std::vector warningLines; std::vector diagRanges; @@ -1544,8 +1576,8 @@ int main(int, char**) { dr.message = d.message; diagRanges.push_back(std::move(dr)); } - if (state.active()) { - Module* ast = state.active()->sync.getAST(); + if (state.isStructured()) { + Module* ast = state.activeAST(); if (ast) { collectAnnotationMarkers(ast, annoMarkers); std::vector conflicts; @@ -1594,60 +1626,67 @@ int main(int, char**) { opts.annotations = &annoMarkers; opts.suggestions = &suggestionMarkers; opts.conflicts = &conflictMarkers; - opts.syncScrollX = &buf->splitScrollX; - opts.syncScrollY = &buf->splitScrollY; - opts.scrollMaster = true; - ImGui::BeginTable("##editorSplit", 2, - ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp); - ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch, 0.55f); - ImGui::TableSetupColumn("Generated", ImGuiTableColumnFlags_WidthStretch, 0.45f); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - CodeEditorResult res = buf->widget.render("##editor", - buf->editBuf, buf->highlights, opts, ImVec2(0, avail.y), monoFont); + CodeEditorResult res; + if (buf->bufferMode == BufferManager::BufferMode::Structured) { + opts.syncScrollX = &buf->splitScrollX; + opts.syncScrollY = &buf->splitScrollY; + opts.scrollMaster = true; - ImGui::TableSetColumnIndex(1); - ImGui::BeginGroup(); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Generated"); - ImGui::SameLine(); - const char* targetLabels[] = {"Python", "C++", "Elisp"}; - const char* targetValues[] = {"python", "cpp", "elisp"}; - int targetIndex = 0; - for (int i = 0; i < IM_ARRAYSIZE(targetValues); ++i) { - if (buf->generatedLanguage == targetValues[i]) { - targetIndex = i; - break; + ImGui::BeginTable("##editorSplit", 2, + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp); + ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch, 0.55f); + ImGui::TableSetupColumn("Generated", ImGuiTableColumnFlags_WidthStretch, 0.45f); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + res = buf->widget.render("##editor", + buf->editBuf, buf->highlights, opts, ImVec2(0, avail.y), monoFont); + + ImGui::TableSetColumnIndex(1); + ImGui::BeginGroup(); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Generated"); + ImGui::SameLine(); + const char* targetLabels[] = {"Python", "C++", "Elisp"}; + const char* targetValues[] = {"python", "cpp", "elisp"}; + int targetIndex = 0; + for (int i = 0; i < IM_ARRAYSIZE(targetValues); ++i) { + if (buf->generatedLanguage == targetValues[i]) { + targetIndex = i; + break; + } } + ImGui::SetNextItemWidth(120.0f); + if (ImGui::Combo("##targetLang", &targetIndex, + targetLabels, IM_ARRAYSIZE(targetLabels))) { + buf->generatedLanguage = targetValues[targetIndex]; + buf->generatedMode.setLanguage(buf->generatedLanguage); + buf->generatedHighlightsDirty = true; + state.updateGenerated(); + } + ImVec2 genAvail = ImGui::GetContentRegionAvail(); + CodeEditorOptions genOpts; + genOpts.readOnly = true; + genOpts.showWhitespace = state.showWhitespace; + genOpts.mode = &buf->generatedMode; + genOpts.showCurrentLine = false; + genOpts.highlightLine = buf->generatedHighlightLine; + genOpts.syncScrollX = &buf->splitScrollX; + genOpts.syncScrollY = &buf->splitScrollY; + genOpts.scrollMaster = false; + buf->generatedWidget.render("##generated", + buf->generatedBuf, buf->generatedHighlights, genOpts, + ImVec2(0, genAvail.y), monoFont); + ImGui::EndGroup(); + ImGui::EndTable(); + } else { + res = buf->widget.render("##editor", + buf->editBuf, buf->highlights, opts, avail, monoFont); } - ImGui::SetNextItemWidth(120.0f); - if (ImGui::Combo("##targetLang", &targetIndex, - targetLabels, IM_ARRAYSIZE(targetLabels))) { - buf->generatedLanguage = targetValues[targetIndex]; - buf->generatedMode.setLanguage(buf->generatedLanguage); - buf->generatedHighlightsDirty = true; - state.updateGenerated(); - } - ImVec2 genAvail = ImGui::GetContentRegionAvail(); - CodeEditorOptions genOpts; - genOpts.readOnly = true; - genOpts.showWhitespace = state.showWhitespace; - genOpts.mode = &buf->generatedMode; - genOpts.showCurrentLine = false; - genOpts.highlightLine = buf->generatedHighlightLine; - genOpts.syncScrollX = &buf->splitScrollX; - genOpts.syncScrollY = &buf->splitScrollY; - genOpts.scrollMaster = false; - buf->generatedWidget.render("##generated", - buf->generatedBuf, buf->generatedHighlights, genOpts, - ImVec2(0, genAvail.y), monoFont); - ImGui::EndGroup(); - ImGui::EndTable(); state.updateCursorPos(res.cursorByte); - if (res.lineClicked) { - Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; + if (res.lineClicked && buf->bufferMode == BufferManager::BufferMode::Structured) { + Module* ast = state.activeAST(); if (ast) { int genLines = countLines(buf->generatedBuf); int targetLine = std::max(0, std::min(res.clickedLine, genLines - 1)); @@ -1674,8 +1713,10 @@ int main(int, char**) { state.lsp->clearSignatureHelp(); } } - state.analysisPending = true; - state.analysisLastChange = ImGui::GetTime(); + if (buf->bufferMode == BufferManager::BufferMode::Structured) { + state.analysisPending = true; + state.analysisLastChange = ImGui::GetTime(); + } } if (res.suggestionClicked) { @@ -1700,7 +1741,7 @@ int main(int, char**) { } if (state.analysisPending && (now - state.analysisLastChange) > 0.5) { - if (state.active()) { + if (state.isStructured()) { auto result = state.pipeline.run(state.active()->editBuf, state.active()->language, state.active()->language); @@ -1712,13 +1753,16 @@ int main(int, char**) { } else { state.whetstoneDiagnostics.clear(); } - Module* ast = state.active()->sync.getAST(); + Module* ast = state.activeAST(); if (ast) { MemoryStrategyInference inf; state.suggestions = inf.inferAnnotations(ast); } else { state.suggestions.clear(); } + } else { + state.whetstoneDiagnostics.clear(); + state.suggestions.clear(); } state.analysisPending = false; } @@ -2092,6 +2136,8 @@ int main(int, char**) { Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; if (!state.active()) { ImGui::TextDisabled("(no active buffer)"); + } else if (state.active()->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(disabled in Text mode)"); } else if (!ast) { ImGui::TextDisabled("(no AST)"); } else if (state.active()->readOnly) { @@ -2261,6 +2307,8 @@ int main(int, char**) { Module* ast = buf ? buf->sync.getAST() : nullptr; if (!buf) { ImGui::TextDisabled("(no active buffer)"); + } else if (buf->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(disabled in Text mode)"); } else if (!ast) { ImGui::TextDisabled("(no AST)"); } else { @@ -2320,6 +2368,8 @@ int main(int, char**) { Module* ast = buf ? buf->sync.getAST() : nullptr; if (!buf) { ImGui::TextDisabled("(no active buffer)"); + } else if (buf->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(diff view disabled in Text mode)"); } else if (!state.diff.active) { ImGui::TextDisabled("(no diff)"); } else { @@ -2406,7 +2456,9 @@ int main(int, char**) { ImGui::PushFont(monoFont); ImGui::BeginChild("##astScroll", ImVec2(0, 0), false); Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; - if (ast) { + if (state.active() && state.active()->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(disabled in Text mode)"); + } else if (ast) { std::map transformNames; if (state.active()) { auto history = state.active()->incrementalOptimizer.getTransformHistory(); @@ -2491,11 +2543,15 @@ int main(int, char**) { ImGui::PushFont(monoFont); ImGui::BeginChild("##genScroll", ImVec2(0, 0), false); Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; - state.updateGenerated(); - if (ast && state.active()) { - ImGui::TextUnformatted(state.active()->generatedBuf.c_str()); + if (state.active() && state.active()->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(disabled in Text mode)"); } else { - ImGui::TextDisabled("(no AST)"); + state.updateGenerated(); + if (ast && state.active()) { + ImGui::TextUnformatted(state.active()->generatedBuf.c_str()); + } else { + ImGui::TextDisabled("(no AST)"); + } } ImGui::EndChild(); ImGui::PopFont(); @@ -2513,7 +2569,9 @@ int main(int, char**) { ImGui::Begin("Memory Strategies"); ImGui::PushFont(uiFont); Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; - if (!ast) { + if (state.active() && state.active()->bufferMode == BufferManager::BufferMode::Text) { + ImGui::TextDisabled("(disabled in Text mode)"); + } else if (!ast) { ImGui::TextDisabled("(no AST)"); } else { std::vector entries; @@ -2590,7 +2648,7 @@ int main(int, char**) { // Mode if (state.active()) { const char* modeLabel = - state.active()->mode == BufferManager::BufferMode::Text ? "Text" : "Structured"; + state.active()->bufferMode == BufferManager::BufferMode::Text ? "Text" : "Structured"; ImGui::Text("Mode: %s", modeLabel); } else { ImGui::Text("Mode: -"); diff --git a/editor/tests/step107b_test.cpp b/editor/tests/step107b_test.cpp new file mode 100644 index 0000000..8cd3f1e --- /dev/null +++ b/editor/tests/step107b_test.cpp @@ -0,0 +1,27 @@ +// Step 107b TDD Test: Editor mode policy +// +// Tests: +// 1. Structured mode allows structured features +// 2. Text mode disables structured features + +#include +#include +#include "EditorModePolicy.h" + +int main() { + int passed = 0; + int failed = 0; + + assert(allowStructuredFeatures(BufferManager::BufferMode::Structured)); + assert(!isTextMode(BufferManager::BufferMode::Structured)); + std::cout << "Test 1 PASS: structured mode allowed" << std::endl; + ++passed; + + assert(!allowStructuredFeatures(BufferManager::BufferMode::Text)); + assert(isTextMode(BufferManager::BufferMode::Text)); + std::cout << "Test 2 PASS: text mode disabled" << std::endl; + ++passed; + + std::cout << "\n=== Step 107b Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}