Step 100: memory strategy suggestions

This commit is contained in:
Bill
2026-02-09 10:36:06 -07:00
parent b987d45939
commit 865cb3efa6
5 changed files with 173 additions and 1 deletions

View File

@@ -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. |

View File

@@ -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)

View File

@@ -27,6 +27,7 @@ struct CodeEditorOptions {
const std::vector<int>* warningLines = nullptr;
const std::vector<struct DiagnosticRange>* diagnostics = nullptr;
const std::vector<struct AnnotationMarker>* annotations = nullptr;
const std::vector<struct SuggestionMarker>* 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<SuggestionMarker>& 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<DiagnosticRange>& diags, int line) {
for (const auto& d : diags) {
if (line >= d.startLine && line <= d.endLine) return d.message;

View File

@@ -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<MemoryStrategyInference::Suggestion> 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<AnnotationMarker>& out) {
if (!node) return;
if (node->hasSpan()) {
@@ -1217,6 +1230,7 @@ int main(int, char**) {
std::vector<int> warningLines;
std::vector<DiagnosticRange> diagRanges;
std::vector<AnnotationMarker> annoMarkers;
std::vector<SuggestionMarker> 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();

View File

@@ -0,0 +1,31 @@
// Step 100 TDD Test: Memory strategy suggestions
//
// Tests:
// 1. Inference suggests tracing for Python module
#include <cassert>
#include <iostream>
#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;
}