diff --git a/PROGRESS.md b/PROGRESS.md index c3e8d97..deb6436 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 80629c8..41680eb 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/AnnotationConflict.h b/editor/src/AnnotationConflict.h new file mode 100644 index 0000000..06c3063 --- /dev/null +++ b/editor/src/AnnotationConflict.h @@ -0,0 +1,73 @@ +#pragma once +// Step 101: Annotation conflict detection helpers + +#include +#include +#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(anno)->strategy; + if (anno->conceptType == "LifetimeAnnotation") + return static_cast(anno)->strategy; + if (anno->conceptType == "ReclaimAnnotation") + return static_cast(anno)->strategy; + if (anno->conceptType == "OwnerAnnotation") + return static_cast(anno)->strategy; + if (anno->conceptType == "AllocateAnnotation") + return static_cast(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& 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); + } +} diff --git a/editor/src/CodeEditorWidget.h b/editor/src/CodeEditorWidget.h index 59e6c3f..a31672c 100644 --- a/editor/src/CodeEditorWidget.h +++ b/editor/src/CodeEditorWidget.h @@ -28,6 +28,7 @@ struct CodeEditorOptions { const std::vector* diagnostics = nullptr; const std::vector* annotations = nullptr; const std::vector* suggestions = nullptr; + const std::vector* 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& 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& 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 c65eb52..58e234f 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -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 diagRanges; std::vector annoMarkers; std::vector suggestionMarkers; + std::vector 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 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 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(); } diff --git a/editor/tests/step101_test.cpp b/editor/tests/step101_test.cpp new file mode 100644 index 0000000..0053606 --- /dev/null +++ b/editor/tests/step101_test.cpp @@ -0,0 +1,38 @@ +// Step 101 TDD Test: Annotation conflict detection +// +// Tests: +// 1. Conflict detected between parent/child annotations + +#include +#include +#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 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; +}