Step 101: annotation conflict highlighting

This commit is contained in:
Bill
2026-02-09 10:39:48 -07:00
parent 865cb3efa6
commit 7738c3d6b4
6 changed files with 208 additions and 0 deletions

View File

@@ -201,6 +201,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1
- [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)
- [x] Step 101: **IMPLEMENTED** — Annotation conflict highlighting with gutter linkage and quick-fix actions (1/1 tests pass)
---
@@ -281,6 +282,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
**Step 98:** Compile and pass (1/1)
**Step 99:** Compile and pass (2/2)
**Step 100:** Compile and pass (1/1)
**Step 101:** Compile and pass (1/1)
---
@@ -390,3 +392,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e
| 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. |
| 2026-02-09 | Codex | Step 101: Annotation conflict highlighting with quick-fix actions. 1/1 tests pass. |

View File

@@ -550,6 +550,10 @@ 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)
add_executable(step101_test tests/step101_test.cpp)
target_include_directories(step101_test PRIVATE src)
target_link_libraries(step101_test PRIVATE nlohmann_json::nlohmann_json)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

View File

@@ -0,0 +1,73 @@
#pragma once
// Step 101: Annotation conflict detection helpers
#include <string>
#include <vector>
#include "ast/ASTNode.h"
#include "ast/Annotation.h"
struct AnnotationConflict {
std::string childAnnoId;
std::string parentAnnoId;
std::string conceptType;
std::string parentStrategy;
std::string message;
int childLine = -1;
int parentLine = -1;
};
inline std::string annotationStrategy(const ASTNode* anno) {
if (!anno) return "";
if (anno->conceptType == "DeallocateAnnotation")
return static_cast<const DeallocateAnnotation*>(anno)->strategy;
if (anno->conceptType == "LifetimeAnnotation")
return static_cast<const LifetimeAnnotation*>(anno)->strategy;
if (anno->conceptType == "ReclaimAnnotation")
return static_cast<const ReclaimAnnotation*>(anno)->strategy;
if (anno->conceptType == "OwnerAnnotation")
return static_cast<const OwnerAnnotation*>(anno)->strategy;
if (anno->conceptType == "AllocateAnnotation")
return static_cast<const AllocateAnnotation*>(anno)->strategy;
return "";
}
inline int nodeLineOrFallback(const ASTNode* node, const ASTNode* fallback) {
if (node && node->hasSpan()) return node->spanStartLine;
if (fallback && fallback->hasSpan()) return fallback->spanStartLine;
return -1;
}
inline void collectAnnotationConflicts(const ASTNode* node,
std::vector<AnnotationConflict>& out) {
if (!node) return;
for (auto* anno : node->getChildren("annotations")) {
std::string strategy = annotationStrategy(anno);
if (strategy.empty()) continue;
const ASTNode* cur = node->parent;
while (cur) {
for (auto* parentAnno : cur->getChildren("annotations")) {
if (parentAnno->conceptType == anno->conceptType) {
std::string parentStrategy = annotationStrategy(parentAnno);
if (!parentStrategy.empty() && parentStrategy != strategy) {
AnnotationConflict c;
c.childAnnoId = anno->id;
c.parentAnnoId = parentAnno->id;
c.conceptType = anno->conceptType;
c.parentStrategy = parentStrategy;
c.childLine = nodeLineOrFallback(anno, node);
c.parentLine = nodeLineOrFallback(parentAnno, cur);
c.message = "Annotation conflict: " + anno->conceptType +
"(" + strategy + ") vs " +
parentAnno->conceptType + "(" + parentStrategy + ")";
out.push_back(std::move(c));
return;
}
}
}
cur = cur->parent;
}
}
for (auto* child : node->allChildren()) {
collectAnnotationConflicts(child, out);
}
}

View File

@@ -28,6 +28,7 @@ struct CodeEditorOptions {
const std::vector<struct DiagnosticRange>* diagnostics = nullptr;
const std::vector<struct AnnotationMarker>* annotations = nullptr;
const std::vector<struct SuggestionMarker>* suggestions = nullptr;
const std::vector<struct AnnotationConflictMarker>* conflicts = nullptr;
};
struct CodeEditorResult {
@@ -71,6 +72,15 @@ struct SuggestionMarker {
std::string nodeId;
};
struct AnnotationConflictMarker {
int childLine = -1;
int parentLine = -1;
std::string message;
std::string childAnnoId;
std::string parentAnnoId;
std::string parentStrategy;
};
struct FoldRegion {
int startLine = 0;
int endLine = 0;
@@ -311,6 +321,30 @@ public:
}
}
// Conflict markers: highlight and connecting line
if (options.conflicts) {
AnnotationConflictMarker conflict;
if (conflictAtLine(*options.conflicts, ln, conflict)) {
ImVec2 center(origin.x + 12.0f, y + lineHeight * 0.5f);
drawList->AddCircle(center, 5.0f, IM_COL32(220, 80, 80, 255), 12, 1.5f);
if (ln == std::min(conflict.childLine, conflict.parentLine) &&
conflict.childLine >= 0 && conflict.parentLine >= 0) {
float y1 = textBase.y + conflict.childLine * lineHeight + lineHeight * 0.5f;
float y2 = textBase.y + conflict.parentLine * lineHeight + lineHeight * 0.5f;
drawList->AddLine(ImVec2(origin.x + 12.0f, y1),
ImVec2(origin.x + 12.0f, y2),
IM_COL32(220, 80, 80, 180), 1.0f);
}
ImVec2 a(center.x - 5.0f, center.y - 5.0f);
ImVec2 b(center.x + 5.0f, center.y + 5.0f);
if (ImGui::IsMouseHoveringRect(a, b)) {
ImGui::BeginTooltip();
ImGui::TextUnformatted(conflict.message.c_str());
ImGui::EndTooltip();
}
}
}
// Fold indicator
const FoldRegion* fold = findFoldAtLine(ln);
if (fold) {
@@ -623,6 +657,18 @@ private:
return false;
}
static bool conflictAtLine(const std::vector<AnnotationConflictMarker>& markers,
int line,
AnnotationConflictMarker& out) {
for (const auto& m : markers) {
if (m.childLine == line || m.parentLine == 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

@@ -31,6 +31,7 @@
#include "LayoutManager.h"
#include "ASTMutationAPI.h"
#include "MemoryStrategyInference.h"
#include "AnnotationConflict.h"
#include "ast/Generator.h"
#include "ast/Annotation.h"
@@ -1231,6 +1232,7 @@ int main(int, char**) {
std::vector<DiagnosticRange> diagRanges;
std::vector<AnnotationMarker> annoMarkers;
std::vector<SuggestionMarker> suggestionMarkers;
std::vector<AnnotationConflictMarker> conflictMarkers;
std::string activeUri = EditorState::toFileUri(buf->path);
if (state.lsp) {
auto lspDiags = state.lsp->getDiagnosticsForUri(activeUri);
@@ -1264,6 +1266,18 @@ int main(int, char**) {
Module* ast = state.active()->sync.getAST();
if (ast) {
collectAnnotationMarkers(ast, annoMarkers);
std::vector<AnnotationConflict> conflicts;
collectAnnotationConflicts(ast, conflicts);
for (const auto& c : conflicts) {
AnnotationConflictMarker m;
m.childLine = c.childLine;
m.parentLine = c.parentLine;
m.message = c.message;
m.childAnnoId = c.childAnnoId;
m.parentAnnoId = c.parentAnnoId;
m.parentStrategy = c.parentStrategy;
conflictMarkers.push_back(std::move(m));
}
for (const auto& s : state.suggestions) {
if (s.confidence <= 0.5) continue;
ASTNode* target = findNodeById(ast, s.nodeId);
@@ -1296,6 +1310,7 @@ int main(int, char**) {
opts.diagnostics = &diagRanges;
opts.annotations = &annoMarkers;
opts.suggestions = &suggestionMarkers;
opts.conflicts = &conflictMarkers;
CodeEditorResult res = buf->widget.render("##editor",
buf->editBuf, buf->highlights, opts, avail, monoFont);
@@ -1606,6 +1621,35 @@ int main(int, char**) {
}
ImGui::EndMenu();
}
std::vector<AnnotationConflict> conflicts;
collectAnnotationConflicts(ast, conflicts);
bool conflictOnLine = false;
AnnotationConflict lineConflict;
for (const auto& c : conflicts) {
if (c.childLine == lineZero || c.parentLine == lineZero) {
lineConflict = c;
conflictOnLine = true;
break;
}
}
if (ImGui::BeginMenu("Resolve Conflict", conflictOnLine)) {
if (conflictOnLine) {
if (ImGui::MenuItem("Remove child annotation")) {
state.mutator.setRoot(ast);
auto res = state.mutator.deleteNode(lineConflict.childAnnoId);
if (!res.error.empty()) state.outputLog += res.error + "\n";
}
if (ImGui::MenuItem("Match parent strategy")) {
state.mutator.setRoot(ast);
auto res = state.mutator.setProperty(lineConflict.childAnnoId,
"strategy",
lineConflict.parentStrategy);
if (!res.error.empty()) state.outputLog += res.error + "\n";
}
}
ImGui::EndMenu();
}
}
ImGui::EndPopup();
}

View File

@@ -0,0 +1,38 @@
// Step 101 TDD Test: Annotation conflict detection
//
// Tests:
// 1. Conflict detected between parent/child annotations
#include <cassert>
#include <iostream>
#include "AnnotationConflict.h"
#include "ast/Module.h"
#include "ast/Function.h"
#include "ast/Annotation.h"
int main() {
int passed = 0;
int failed = 0;
auto* mod = new Module("m1", "mod", "cpp");
mod->setSpan(0, 0, 10, 0);
auto* fn = new Function("f1", "foo");
fn->setSpan(2, 0, 5, 0);
mod->addChild("functions", fn);
auto* parentAnno = new OwnerAnnotation("a1", "Shared_ARC");
auto* childAnno = new OwnerAnnotation("a2", "Single");
mod->addChild("annotations", parentAnno);
fn->addChild("annotations", childAnno);
std::vector<AnnotationConflict> conflicts;
collectAnnotationConflicts(mod, conflicts);
assert(conflicts.size() == 1);
assert(conflicts[0].childAnnoId == "a2");
assert(conflicts[0].parentAnnoId == "a1");
std::cout << "Test 1 PASS: conflict detected" << std::endl;
++passed;
std::cout << "\n=== Step 101 Results: " << passed << " passed, " << failed << " failed ===" << std::endl;
return failed > 0 ? 1 : 0;
}