Step 95: diagnostic squiggles
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -23,6 +23,7 @@ struct CodeEditorOptions {
|
||||
bool showMinimap = false;
|
||||
const std::vector<int>* errorLines = nullptr;
|
||||
const std::vector<int>* warningLines = nullptr;
|
||||
const std::vector<struct DiagnosticRange>* 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<DiagnosticRange>& 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<DiagnosticRange>& 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<DiagnosticRange>& 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;
|
||||
|
||||
@@ -1011,18 +1011,35 @@ int main(int, char**) {
|
||||
state.updateHighlights();
|
||||
std::vector<int> errorLines;
|
||||
std::vector<int> warningLines;
|
||||
std::vector<DiagnosticRange> 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);
|
||||
|
||||
62
editor/tests/step95_test.cpp
Normal file
62
editor/tests/step95_test.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
// Step 95 TDD Test: Gutter markers and squiggles
|
||||
//
|
||||
// Tests:
|
||||
// 1. Render with diagnostics ranges 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 = "int main() {\n return 0;\n}\n";
|
||||
std::vector<HighlightSpan> 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<DiagnosticRange> 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;
|
||||
}
|
||||
Reference in New Issue
Block a user