diff --git a/PROGRESS.md b/PROGRESS.md index af11da4..df8b4a4 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -197,6 +197,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 94a: **IMPLEMENTED** — AST source span tracking via tree-sitter, serialized in JSON (2/2 tests pass) - [x] Step 95: **IMPLEMENTED** — Diagnostic gutter markers with tooltips and inline squiggles (1/1 tests pass) - [x] Step 96: **IMPLEMENTED** — LSP server settings UI with defaults, auto-detect, and schema validation hook (2/2 tests pass) +- [x] Step 97: **IMPLEMENTED** — Annotation gutter markers with hover details and click-to-open placeholder editor (1/1 tests pass) --- @@ -273,6 +274,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 94a:** Compile and pass (2/2) **Step 95:** Compile and pass (1/1) **Step 96:** Compile and pass (2/2) +**Step 97:** Compile and pass (1/1) --- @@ -378,3 +380,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 2026-02-09 | Codex | Step 94a: AST source span tracking (tree-sitter) with JSON serialization. 2/2 tests pass. | | 2026-02-09 | Codex | Step 95: Diagnostic gutter markers with tooltips and inline squiggles. 1/1 tests pass. | | 2026-02-09 | Codex | Step 96: LSP server settings UI with defaults, auto-detect, and schema validation hook. 2/2 tests pass. | +| 2026-02-09 | Codex | Step 97: Annotation gutter markers with hover details and click-to-open placeholder editor. 1/1 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 7ed5dee..73441f5 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -534,6 +534,10 @@ add_executable(step96_test tests/step96_test.cpp) target_include_directories(step96_test PRIVATE src) target_link_libraries(step96_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step97_test tests/step97_test.cpp) +target_include_directories(step97_test PRIVATE src) +target_link_libraries(step97_test PRIVATE imgui::imgui unofficial::tree-sitter::tree-sitter tree_sitter_python tree_sitter_cpp tree_sitter_elisp) + 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 86734ba..8d42360 100644 --- a/editor/src/CodeEditorWidget.h +++ b/editor/src/CodeEditorWidget.h @@ -24,6 +24,7 @@ struct CodeEditorOptions { const std::vector* errorLines = nullptr; const std::vector* warningLines = nullptr; const std::vector* diagnostics = nullptr; + const std::vector* annotations = nullptr; }; struct CodeEditorResult { @@ -49,6 +50,12 @@ struct DiagnosticRange { std::string message; }; +struct AnnotationMarker { + int line = 0; + ImU32 color = 0; + std::string message; +}; + struct FoldRegion { int startLine = 0; int endLine = 0; @@ -112,6 +119,7 @@ public: lineCount * lineHeight + 4.0f); ImGui::BeginChild(id, size, false, ImGuiWindowFlags_HorizontalScrollbar); + std::string annoPopupId = std::string(id) + "_AnnoPopup"; ImVec2 origin = ImGui::GetCursorScreenPos(); // Dummy to set scroll extents @@ -245,6 +253,26 @@ public: } } + // Annotation marker + if (options.annotations) { + AnnotationMarker marker; + if (annotationAtLine(*options.annotations, ln, marker)) { + ImVec2 center(origin.x + 12.0f, y + lineHeight * 0.5f); + drawList->AddCircleFilled(center, 3.0f, marker.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)) { + annotationPopupMessage_ = marker.message; + ImGui::OpenPopup(annoPopupId.c_str()); + } + ImGui::BeginTooltip(); + ImGui::TextUnformatted(marker.message.c_str()); + ImGui::EndTooltip(); + } + } + } + // Fold indicator const FoldRegion* fold = findFoldAtLine(ln); if (fold) { @@ -330,6 +358,13 @@ public: } } + if (ImGui::BeginPopup(annoPopupId.c_str())) { + ImGui::TextUnformatted("Annotation Editor (TODO)"); + ImGui::Separator(); + ImGui::TextUnformatted(annotationPopupMessage_.c_str()); + ImGui::EndPopup(); + } + // Cursor if (focused) { const double t = ImGui::GetTime(); @@ -408,6 +443,7 @@ private: std::vector folds_; std::string lastFoldText_; std::string lastFoldLang_; + std::string annotationPopupMessage_; bool hasSelection() const { return selStart_ >= 0 && selEnd_ >= 0 && selStart_ != selEnd_; @@ -504,6 +540,18 @@ private: return std::find(lines->begin(), lines->end(), line) != lines->end(); } + static bool annotationAtLine(const std::vector& markers, + int line, + AnnotationMarker& 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 290ea2d..ff6c212 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -29,6 +29,7 @@ #include "Diagnostics.h" #include "SettingsManager.h" #include "ast/Generator.h" +#include "ast/Annotation.h" #include #include @@ -483,6 +484,67 @@ static bool InputTextStr(const char* label, std::string* str, ImGuiInputTextFlag flags, InputTextCallback, &cbData); } +static bool annotationInfo(const ASTNode* anno, ImU32& color, std::string& label) { + if (!anno) return false; + if (anno->conceptType == "ReclaimAnnotation") { + auto* a = static_cast(anno); + if (a->strategy == "Tracing") { + color = IM_COL32(80, 140, 220, 255); + label = "@Reclaim(Tracing)"; + return true; + } + } else if (anno->conceptType == "OwnerAnnotation") { + auto* a = static_cast(anno); + if (a->strategy == "Single") { + color = IM_COL32(220, 160, 60, 255); + label = "@Owner(Single)"; + return true; + } + } else if (anno->conceptType == "DeallocateAnnotation") { + auto* a = static_cast(anno); + if (a->strategy == "Explicit") { + color = IM_COL32(220, 80, 80, 255); + label = "@Deallocate(Explicit)"; + return true; + } + } else if (anno->conceptType == "LifetimeAnnotation") { + auto* a = static_cast(anno); + if (a->strategy == "RAII") { + color = IM_COL32(80, 180, 100, 255); + label = "@Lifetime(RAII)"; + return true; + } + } else if (anno->conceptType == "AllocateAnnotation") { + auto* a = static_cast(anno); + if (a->strategy == "Static") { + color = IM_COL32(160, 160, 160, 255); + label = "@Allocate(Static)"; + return true; + } + } + return false; +} + +static void collectAnnotationMarkers(const ASTNode* node, std::vector& out) { + if (!node) return; + if (node->hasSpan()) { + for (const auto* anno : node->getChildren("annotations")) { + ImU32 color = 0; + std::string label; + if (annotationInfo(anno, color, label)) { + AnnotationMarker marker; + marker.line = node->spanStartLine; + marker.color = color; + marker.message = label; + out.push_back(std::move(marker)); + } + } + } + for (auto* child : node->allChildren()) { + collectAnnotationMarkers(child, out); + } +} + // --------------------------------------------------------------------------- // Theme setup — VSCode Dark-inspired // --------------------------------------------------------------------------- @@ -1053,6 +1115,7 @@ int main(int, char**) { std::vector errorLines; std::vector warningLines; std::vector diagRanges; + std::vector annoMarkers; std::string activeUri = EditorState::toFileUri(buf->path); if (state.lsp) { auto lspDiags = state.lsp->getDiagnosticsForUri(activeUri); @@ -1082,6 +1145,10 @@ int main(int, char**) { dr.message = d.message; diagRanges.push_back(std::move(dr)); } + if (state.active()) { + Module* ast = state.active()->sync.getAST(); + if (ast) collectAnnotationMarkers(ast, annoMarkers); + } CodeEditorOptions opts; opts.showWhitespace = state.showWhitespace; @@ -1091,6 +1158,7 @@ int main(int, char**) { opts.errorLines = &errorLines; opts.warningLines = &warningLines; opts.diagnostics = &diagRanges; + opts.annotations = &annoMarkers; CodeEditorResult res = buf->widget.render("##editor", buf->editBuf, buf->highlights, opts, avail, monoFont); diff --git a/editor/tests/step97_test.cpp b/editor/tests/step97_test.cpp new file mode 100644 index 0000000..1329c6b --- /dev/null +++ b/editor/tests/step97_test.cpp @@ -0,0 +1,59 @@ +// Step 97 TDD Test: Annotation gutter markers +// +// Tests: +// 1. Render with annotation markers without crashing + +#include +#include +#include "imgui.h" +#include "CodeEditorWidget.h" + +static void beginFrame() { + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(800, 600); + io.DeltaTime = 1.0f / 60.0f; + ImGui::NewFrame(); +} + +static void endFrame() { + ImGui::Render(); +} + +int main() { + int passed = 0; + int failed = 0; + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->AddFontDefault(); + io.Fonts->Build(); + + beginFrame(); + ImGui::SetNextWindowFocus(); + ImGui::Begin("TestWindow"); + CodeEditorWidget widget; + std::string text = "def foo():\n return 1\n"; + std::vector spans; + CodeEditorOptions opts; + AnnotationMarker m; + m.line = 0; + m.color = IM_COL32(80, 140, 220, 255); + m.message = "@Reclaim(Tracing)"; + std::vector markers = {m}; + opts.annotations = &markers; + + CodeEditorResult res = widget.render("##editor", text, spans, opts, ImVec2(300, 200), ImGui::GetFont()); + (void)res; + ImGui::End(); + endFrame(); + std::cout << "Test 1 PASS: Render with annotation markers" << std::endl; + ++passed; + + ImGui::DestroyContext(); + + std::cout << "\n=== Step 97 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}