diff --git a/PROGRESS.md b/PROGRESS.md index e92641a..c3e8d97 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -200,6 +200,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 97: **IMPLEMENTED** — Annotation gutter markers with hover details and click-to-open placeholder editor (1/1 tests pass) - [x] Step 98: **IMPLEMENTED** — Inline annotation tags with layout-aware placement and view toggle (1/1 tests pass) - [x] Step 99: **IMPLEMENTED** — Annotation context menu for add/edit/remove via ASTMutationAPI (2/2 tests pass) +- [x] Step 100: **IMPLEMENTED** — Memory strategy suggestions with lightbulb gutter icons and apply popup (1/1 tests pass) --- @@ -279,6 +280,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 97:** Compile and pass (1/1) **Step 98:** Compile and pass (1/1) **Step 99:** Compile and pass (2/2) +**Step 100:** Compile and pass (1/1) --- @@ -387,3 +389,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 2026-02-09 | Codex | Step 97: Annotation gutter markers with hover details and click-to-open placeholder editor. 1/1 tests pass. | | 2026-02-09 | Codex | Step 98: Inline annotation tags with layout-aware placement and view toggle. 1/1 tests pass. | | 2026-02-09 | Codex | Step 99: Annotation context menu (add/edit/remove) via ASTMutationAPI. 2/2 tests pass. | +| 2026-02-09 | Codex | Step 100: Memory strategy suggestions with lightbulb icons and apply popup. 1/1 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index f376e16..80629c8 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -546,6 +546,10 @@ add_executable(step99_test tests/step99_test.cpp) target_include_directories(step99_test PRIVATE src) target_link_libraries(step99_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step100_test tests/step100_test.cpp) +target_include_directories(step100_test PRIVATE src) +target_link_libraries(step100_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 162e83d..59e6c3f 100644 --- a/editor/src/CodeEditorWidget.h +++ b/editor/src/CodeEditorWidget.h @@ -27,6 +27,7 @@ struct CodeEditorOptions { const std::vector* warningLines = nullptr; const std::vector* diagnostics = nullptr; const std::vector* annotations = nullptr; + const std::vector* suggestions = nullptr; }; struct CodeEditorResult { @@ -41,6 +42,8 @@ struct CodeEditorResult { float minimapWidth = 0.0f; float minimapViewportStart = 0.0f; float minimapViewportEnd = 0.0f; + bool suggestionClicked = false; + struct SuggestionMarker clickedSuggestion; }; struct DiagnosticRange { @@ -58,6 +61,16 @@ struct AnnotationMarker { std::string message; }; +struct SuggestionMarker { + int line = 0; + double confidence = 0.0; + std::string label; + std::string reason; + std::string annotationType; + std::string strategy; + std::string nodeId; +}; + struct FoldRegion { int startLine = 0; int endLine = 0; @@ -275,6 +288,29 @@ public: } } + // Suggestion marker (lightbulb) + if (options.suggestions) { + SuggestionMarker suggestion; + if (suggestionAtLine(*options.suggestions, ln, suggestion)) { + ImVec2 center(origin.x + 20.0f, y + lineHeight * 0.5f); + ImU32 color = IM_COL32(240, 200, 40, 255); + drawList->AddCircleFilled(center, 3.0f, color); + ImVec2 a(center.x - 4.0f, center.y - 4.0f); + ImVec2 b(center.x + 4.0f, center.y + 4.0f); + if (ImGui::IsMouseHoveringRect(a, b)) { + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + result.suggestionClicked = true; + result.clickedSuggestion = suggestion; + } + ImGui::BeginTooltip(); + ImGui::Text("%s (%.2f)", suggestion.label.c_str(), suggestion.confidence); + ImGui::Separator(); + ImGui::TextUnformatted(suggestion.reason.c_str()); + ImGui::EndTooltip(); + } + } + } + // Fold indicator const FoldRegion* fold = findFoldAtLine(ln); if (fold) { @@ -575,6 +611,18 @@ private: return false; } + static bool suggestionAtLine(const std::vector& markers, + int line, + SuggestionMarker& out) { + for (const auto& m : markers) { + if (m.line == line) { + out = m; + return true; + } + } + return false; + } + static std::string diagnosticMessageAtLine(const std::vector& diags, int line) { for (const auto& d : diags) { if (line >= d.startLine && line <= d.endLine) return d.message; diff --git a/editor/src/main.cpp b/editor/src/main.cpp index ff9b362..c65eb52 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -30,6 +30,7 @@ #include "SettingsManager.h" #include "LayoutManager.h" #include "ASTMutationAPI.h" +#include "MemoryStrategyInference.h" #include "ast/Generator.h" #include "ast/Annotation.h" @@ -110,6 +111,9 @@ struct EditorState { SettingsManager settings; bool showLspSettings = false; ASTMutationAPI mutator; + std::vector suggestions; + bool showSuggestionPopup = false; + MemoryStrategyInference::Suggestion suggestionPopup; BufferState* active() { return activeBuffer; } @@ -616,6 +620,15 @@ static ASTNode* findAnnotationTarget(ASTNode* node, int line) { return best; } +static ASTNode* findNodeById(ASTNode* node, const std::string& id) { + if (!node) return nullptr; + if (node->id == id) return node; + for (auto* child : node->allChildren()) { + if (auto* found = findNodeById(child, id)) return found; + } + return nullptr; +} + static void collectAnnotationMarkers(const ASTNode* node, std::vector& out) { if (!node) return; if (node->hasSpan()) { @@ -1217,6 +1230,7 @@ int main(int, char**) { std::vector warningLines; std::vector diagRanges; std::vector annoMarkers; + std::vector suggestionMarkers; std::string activeUri = EditorState::toFileUri(buf->path); if (state.lsp) { auto lspDiags = state.lsp->getDiagnosticsForUri(activeUri); @@ -1248,7 +1262,24 @@ int main(int, char**) { } if (state.active()) { Module* ast = state.active()->sync.getAST(); - if (ast) collectAnnotationMarkers(ast, annoMarkers); + if (ast) { + collectAnnotationMarkers(ast, annoMarkers); + for (const auto& s : state.suggestions) { + if (s.confidence <= 0.5) continue; + ASTNode* target = findNodeById(ast, s.nodeId); + if (!target || !target->hasSpan()) continue; + SuggestionMarker sm; + sm.line = target->spanStartLine; + sm.confidence = s.confidence; + sm.reason = s.reason; + sm.annotationType = s.annotationType; + sm.strategy = s.strategy; + sm.nodeId = s.nodeId; + sm.label = "@" + s.annotationType.substr(0, s.annotationType.find("Annotation")) + + "(" + s.strategy + ")"; + suggestionMarkers.push_back(std::move(sm)); + } + } } CodeEditorOptions opts; @@ -1264,6 +1295,7 @@ int main(int, char**) { opts.warningLines = &warningLines; opts.diagnostics = &diagRanges; opts.annotations = &annoMarkers; + opts.suggestions = &suggestionMarkers; CodeEditorResult res = buf->widget.render("##editor", buf->editBuf, buf->highlights, opts, avail, monoFont); @@ -1290,6 +1322,16 @@ int main(int, char**) { state.analysisLastChange = ImGui::GetTime(); } + if (res.suggestionClicked) { + state.suggestionPopup.annotationType = res.clickedSuggestion.annotationType; + state.suggestionPopup.strategy = res.clickedSuggestion.strategy; + state.suggestionPopup.reason = res.clickedSuggestion.reason; + state.suggestionPopup.confidence = res.clickedSuggestion.confidence; + state.suggestionPopup.nodeId = res.clickedSuggestion.nodeId; + state.showSuggestionPopup = true; + ImGui::OpenPopup("SuggestionPopup"); + } + double now = ImGui::GetTime(); if (state.completionPending && (now - state.completionLastChange) > 0.2) { if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) { @@ -1314,6 +1356,13 @@ int main(int, char**) { } else { state.whetstoneDiagnostics.clear(); } + Module* ast = state.active()->sync.getAST(); + if (ast) { + MemoryStrategyInference inf; + state.suggestions = inf.inferAnnotations(ast); + } else { + state.suggestions.clear(); + } } state.analysisPending = false; } @@ -1372,6 +1421,43 @@ int main(int, char**) { } } + if (state.showSuggestionPopup) { + if (ImGui::BeginPopupModal("SuggestionPopup", &state.showSuggestionPopup, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("%s(%s)", state.suggestionPopup.annotationType.c_str(), + state.suggestionPopup.strategy.c_str()); + ImGui::Separator(); + ImGui::TextWrapped("%s", state.suggestionPopup.reason.c_str()); + ImGui::Text("Confidence: %.2f", state.suggestionPopup.confidence); + if (ImGui::Button("Apply")) { + Module* ast = state.active() ? state.active()->sync.getAST() : nullptr; + if (ast) { + ASTNode* target = findNodeById(ast, state.suggestionPopup.nodeId); + if (target) { + auto* anno = createAnnotationNode(state.suggestionPopup.annotationType, + state.suggestionPopup.strategy); + if (anno) { + if (target->hasSpan()) { + anno->setSpan(target->spanStartLine, target->spanStartCol, + target->spanEndLine, target->spanEndCol); + } + state.mutator.setRoot(ast); + auto res2 = state.mutator.insertNode(target->id, "annotations", anno); + if (!res2.error.empty()) state.outputLog += res2.error + "\n"; + } + } + } + state.showSuggestionPopup = false; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + state.showSuggestionPopup = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + if (state.lsp && state.active()) { if (ImGui::IsWindowHovered()) { ImVec2 mouse = ImGui::GetMousePos(); diff --git a/editor/tests/step100_test.cpp b/editor/tests/step100_test.cpp new file mode 100644 index 0000000..80818a9 --- /dev/null +++ b/editor/tests/step100_test.cpp @@ -0,0 +1,31 @@ +// Step 100 TDD Test: Memory strategy suggestions +// +// Tests: +// 1. Inference suggests tracing for Python module + +#include +#include +#include "MemoryStrategyInference.h" +#include "ast/Module.h" + +int main() { + int passed = 0; + int failed = 0; + + Module mod("m1", "mod", "python"); + MemoryStrategyInference inf; + auto suggestions = inf.inferAnnotations(&mod); + bool found = false; + for (const auto& s : suggestions) { + if (s.annotationType == "ReclaimAnnotation" && s.strategy == "Tracing" && s.confidence > 0.5) { + found = true; + break; + } + } + assert(found); + std::cout << "Test 1 PASS: tracing suggestion for python" << std::endl; + ++passed; + + std::cout << "\n=== Step 100 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}