From 5f30fd5044e91bd91b4ae6368b2e29584de4b2be Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 9 Feb 2026 10:10:25 -0700 Subject: [PATCH] Step 95: diagnostic squiggles --- PROGRESS.md | 3 ++ editor/CMakeLists.txt | 4 ++ editor/src/CodeEditorWidget.h | 93 +++++++++++++++++++++++++++++++++++ editor/src/main.cpp | 18 +++++++ editor/tests/step95_test.cpp | 62 +++++++++++++++++++++++ 5 files changed, 180 insertions(+) create mode 100644 editor/tests/step95_test.cpp diff --git a/PROGRESS.md b/PROGRESS.md index 4a6278d..6af517f 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -194,6 +194,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 92: **IMPLEMENTED** — LSP completion requests, response parsing, and completion popup with filtering/acceptance (1/1 tests pass) - [x] Step 93: **IMPLEMENTED** — LSP hover and signature help requests, response parsing, and inline tooltip/popup (2/2 tests pass) - [x] Step 94: **IMPLEMENTED** — Whetstone diagnostics aggregation via Pipeline, merged Problems panel, and gutter markers (1/1 tests pass) +- [x] Step 95: **IMPLEMENTED** — Diagnostic gutter markers with tooltips and inline squiggles (1/1 tests pass) --- @@ -267,6 +268,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 92:** Compile and pass (1/1) **Step 93:** Compile and pass (2/2) **Step 94:** Compile and pass (1/1) +**Step 95:** Compile and pass (1/1) --- @@ -369,3 +371,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 2026-02-09 | Codex | Step 92: LSP completion requests/response parsing; completion popup with filtering and acceptance. 1/1 tests pass. | | 2026-02-09 | Codex | Step 93: LSP hover and signature help requests/response parsing; tooltip and signature popup. 2/2 tests pass. | | 2026-02-09 | Codex | Step 94: Whetstone diagnostics aggregation via Pipeline; merged Problems panel and gutter markers. 1/1 tests pass. | +| 2026-02-09 | Codex | Step 95: Diagnostic gutter markers with tooltips and inline squiggles. 1/1 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 72b9536..104fef4 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -522,6 +522,10 @@ add_executable(step94_test tests/step94_test.cpp) target_include_directories(step94_test PRIVATE src) target_link_libraries(step94_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step95_test tests/step95_test.cpp) +target_include_directories(step95_test PRIVATE src) +target_link_libraries(step95_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 2d1299a..86734ba 100644 --- a/editor/src/CodeEditorWidget.h +++ b/editor/src/CodeEditorWidget.h @@ -23,6 +23,7 @@ struct CodeEditorOptions { bool showMinimap = false; const std::vector* errorLines = nullptr; const std::vector* warningLines = nullptr; + const std::vector* diagnostics = nullptr; }; struct CodeEditorResult { @@ -39,6 +40,15 @@ struct CodeEditorResult { float minimapViewportEnd = 0.0f; }; +struct DiagnosticRange { + int startLine = 0; + int startCol = 0; + int endLine = 0; + int endCol = 0; + int severity = 0; // 1=error, 2=warning + std::string message; +}; + struct FoldRegion { int startLine = 0; int endLine = 0; @@ -224,6 +234,16 @@ public: ImVec2 center(origin.x + 3.0f, y + lineHeight * 0.5f); drawList->AddCircleFilled(center, 3.0f, color); } + if (options.diagnostics) { + std::string msg = diagnosticMessageAtLine(*options.diagnostics, ln); + ImVec2 gutterA(origin.x, y); + ImVec2 gutterB(origin.x + gutterWidth, y + lineHeight); + if (!msg.empty() && ImGui::IsMouseHoveringRect(gutterA, gutterB)) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(msg.c_str()); + ImGui::EndTooltip(); + } + } // Fold indicator const FoldRegion* fold = findFoldAtLine(ln); @@ -293,6 +313,21 @@ public: ImVec2 p(textBase.x + len * charAdvance + 6.0f, y); drawList->AddText(font, font->FontSize, p, IM_COL32(140, 140, 140, 255), "{...}"); } + + if (options.diagnostics) { + renderSquiggles(*options.diagnostics, ln, y, lineHeight, charAdvance, + textBase.x, len, drawList); + if (hovered) { + ImVec2 mouse = ImGui::GetMousePos(); + std::string msg = diagnosticMessageAtPoint(*options.diagnostics, ln, mouse, + y, lineHeight, charAdvance, textBase.x); + if (!msg.empty()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(msg.c_str()); + ImGui::EndTooltip(); + } + } + } } // Cursor @@ -469,6 +504,64 @@ private: return std::find(lines->begin(), lines->end(), line) != lines->end(); } + 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; + } + return ""; + } + + static void renderSquiggles(const std::vector& diags, + int line, + float y, + float lineHeight, + float charAdvance, + float textBaseX, + int lineLen, + ImDrawList* drawList) { + float baseY = y + lineHeight - 2.0f; + for (const auto& d : diags) { + if (line < d.startLine || line > d.endLine) continue; + int startCol = (line == d.startLine) ? d.startCol : 0; + int endCol = (line == d.endLine) ? d.endCol : lineLen; + startCol = std::max(0, startCol); + endCol = std::max(startCol, endCol); + float xStart = textBaseX + startCol * charAdvance; + float xEnd = textBaseX + endCol * charAdvance; + ImU32 color = (d.severity == 1) ? IM_COL32(220, 80, 80, 255) : IM_COL32(220, 160, 60, 255); + float x = xStart; + float amp = 2.0f; + bool up = true; + while (x < xEnd) { + float x2 = std::min(x + 4.0f, xEnd); + float y1 = baseY + (up ? -amp : amp); + float y2 = baseY + (up ? amp : -amp); + drawList->AddLine(ImVec2(x, y1), ImVec2(x2, y2), color, 1.0f); + up = !up; + x = x2; + } + } + } + + static std::string diagnosticMessageAtPoint(const std::vector& diags, + int line, + const ImVec2& mouse, + float y, + float lineHeight, + float charAdvance, + float textBaseX) { + if (mouse.y < y || mouse.y > y + lineHeight) return ""; + for (const auto& d : diags) { + if (line < d.startLine || line > d.endLine) continue; + int startCol = (line == d.startLine) ? d.startCol : 0; + int endCol = (line == d.endLine) ? d.endCol : startCol + 1; + float xStart = textBaseX + startCol * charAdvance; + float xEnd = textBaseX + endCol * charAdvance; + if (mouse.x >= xStart && mouse.x <= xEnd) return d.message; + } + return ""; + } + void updateFolds(const std::string& text, const std::string& language) { if (text == lastFoldText_ && language == lastFoldLang_) return; lastFoldText_ = text; diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 5ce1e2d..40ee030 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -1011,18 +1011,35 @@ int main(int, char**) { state.updateHighlights(); std::vector errorLines; std::vector warningLines; + std::vector diagRanges; std::string activeUri = EditorState::toFileUri(buf->path); if (state.lsp) { auto lspDiags = state.lsp->getDiagnosticsForUri(activeUri); for (const auto& d : lspDiags) { if (d.severity == 1) errorLines.push_back(d.range.start.line); else if (d.severity == 2) warningLines.push_back(d.range.start.line); + DiagnosticRange dr; + dr.startLine = d.range.start.line; + dr.startCol = d.range.start.character; + dr.endLine = d.range.end.line; + dr.endCol = d.range.end.character; + dr.severity = d.severity; + dr.message = d.message; + diagRanges.push_back(std::move(dr)); } } for (const auto& d : state.whetstoneDiagnostics) { if (d.uri != activeUri) continue; if (d.severity == 1) errorLines.push_back(d.line); else if (d.severity == 2) warningLines.push_back(d.line); + DiagnosticRange dr; + dr.startLine = d.line; + dr.startCol = d.character; + dr.endLine = d.line; + dr.endCol = d.character + 1; + dr.severity = d.severity; + dr.message = d.message; + diagRanges.push_back(std::move(dr)); } CodeEditorOptions opts; @@ -1032,6 +1049,7 @@ int main(int, char**) { opts.showMinimap = state.showMinimap; opts.errorLines = &errorLines; opts.warningLines = &warningLines; + opts.diagnostics = &diagRanges; CodeEditorResult res = buf->widget.render("##editor", buf->editBuf, buf->highlights, opts, avail, monoFont); diff --git a/editor/tests/step95_test.cpp b/editor/tests/step95_test.cpp new file mode 100644 index 0000000..2c4b339 --- /dev/null +++ b/editor/tests/step95_test.cpp @@ -0,0 +1,62 @@ +// Step 95 TDD Test: Gutter markers and squiggles +// +// Tests: +// 1. Render with diagnostics ranges 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 = "int main() {\n return 0;\n}\n"; + std::vector spans; + CodeEditorOptions opts; + DiagnosticRange diag; + diag.startLine = 1; + diag.startCol = 2; + diag.endLine = 1; + diag.endCol = 8; + diag.severity = 1; + diag.message = "Example error"; + std::vector diags = {diag}; + opts.diagnostics = &diags; + + 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 diagnostics" << std::endl; + ++passed; + + ImGui::DestroyContext(); + + std::cout << "\n=== Step 95 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}