Step 97: annotation gutter markers
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -24,6 +24,7 @@ struct CodeEditorOptions {
|
||||
const std::vector<int>* errorLines = nullptr;
|
||||
const std::vector<int>* warningLines = nullptr;
|
||||
const std::vector<struct DiagnosticRange>* diagnostics = nullptr;
|
||||
const std::vector<struct AnnotationMarker>* 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<FoldRegion> 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<AnnotationMarker>& 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<DiagnosticRange>& diags, int line) {
|
||||
for (const auto& d : diags) {
|
||||
if (line >= d.startLine && line <= d.endLine) return d.message;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "Diagnostics.h"
|
||||
#include "SettingsManager.h"
|
||||
#include "ast/Generator.h"
|
||||
#include "ast/Annotation.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
@@ -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<const ReclaimAnnotation*>(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<const OwnerAnnotation*>(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<const DeallocateAnnotation*>(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<const LifetimeAnnotation*>(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<const AllocateAnnotation*>(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<AnnotationMarker>& 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<int> errorLines;
|
||||
std::vector<int> warningLines;
|
||||
std::vector<DiagnosticRange> diagRanges;
|
||||
std::vector<AnnotationMarker> 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);
|
||||
|
||||
59
editor/tests/step97_test.cpp
Normal file
59
editor/tests/step97_test.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
// Step 97 TDD Test: Annotation gutter markers
|
||||
//
|
||||
// Tests:
|
||||
// 1. Render with annotation markers without crashing
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#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<HighlightSpan> spans;
|
||||
CodeEditorOptions opts;
|
||||
AnnotationMarker m;
|
||||
m.line = 0;
|
||||
m.color = IM_COL32(80, 140, 220, 255);
|
||||
m.message = "@Reclaim(Tracing)";
|
||||
std::vector<AnnotationMarker> 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;
|
||||
}
|
||||
Reference in New Issue
Block a user