Step 101: annotation conflict highlighting
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
73
editor/src/AnnotationConflict.h
Normal file
73
editor/src/AnnotationConflict.h
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
38
editor/tests/step101_test.cpp
Normal file
38
editor/tests/step101_test.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user