From 4c2b5fffcddfaa0ca4d1f879c8bfd8b1b083ac24 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 09:11:52 -0700 Subject: [PATCH] Step 356: Breadcrumb Navigation (12/12 tests) --- editor/CMakeLists.txt | 4 + editor/src/panels/BreadcrumbBar.h | 107 ++++++++++++++++++ editor/tests/step356_test.cpp | 181 ++++++++++++++++++++++++++++++ progress.md | 36 ++++++ 4 files changed, 328 insertions(+) create mode 100644 editor/src/panels/BreadcrumbBar.h create mode 100644 editor/tests/step356_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index bb656c0..5b6ac5b 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2118,4 +2118,8 @@ add_executable(step355_test tests/step355_test.cpp) target_include_directories(step355_test PRIVATE src) target_link_libraries(step355_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step356_test tests/step356_test.cpp) +target_include_directories(step356_test PRIVATE src) +target_link_libraries(step356_test PRIVATE nlohmann_json::nlohmann_json) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/panels/BreadcrumbBar.h b/editor/src/panels/BreadcrumbBar.h new file mode 100644 index 0000000..944efa7 --- /dev/null +++ b/editor/src/panels/BreadcrumbBar.h @@ -0,0 +1,107 @@ +#pragma once +// Step 356: Breadcrumb bar model for AST navigation. +// Headless model used for unit/integration testing and UI adapter layers. + +#include +#include +#include +#include "../ThemeData.h" +#include "../CompactAST.h" +#include "../ast/ASTNode.h" +#include "../ast/Module.h" + +struct BreadcrumbSegment { + std::string label; + const ASTNode* node = nullptr; + std::vector siblings; +}; + +struct BreadcrumbBarModel { + std::vector segments; + bool overflow = false; + Color4 bgColor; + Color4 textColor; + Color4 borderColor; +}; + +inline std::string breadcrumbDisplayName(const ASTNode* node) { + if (!node) return ""; + std::string name = getNodeName(node); + if (!name.empty()) return name; + return node->conceptType; +} + +inline std::vector breadcrumbSiblings(const ASTNode* node) { + if (!node || !node->parent) return {}; + const ASTNode* parent = node->parent; + for (const auto& role : parent->childRoles()) { + const auto& kids = parent->getChildren(role); + bool found = false; + for (auto* kid : kids) { + if (kid == node) { + found = true; + break; + } + } + if (found) { + std::vector siblings; + for (auto* kid : kids) siblings.push_back(kid); + return siblings; + } + } + return {}; +} + +inline BreadcrumbBarModel buildBreadcrumbBar(const Module* module, + const ASTNode* cursorNode, + const WhetstoneTheme& theme, + size_t maxSegments = 8) { + BreadcrumbBarModel model; + model.bgColor = theme.bgPanel; + model.textColor = theme.text; + model.borderColor = theme.border; + + if (!module) return model; + + std::vector chain; + if (!cursorNode) { + chain.push_back(module); + } else { + const ASTNode* cur = cursorNode; + while (cur) { + chain.push_back(cur); + if (cur == module) break; + cur = cur->parent; + } + if (chain.empty() || chain.back() != module) { + chain.clear(); + chain.push_back(module); + } else { + std::reverse(chain.begin(), chain.end()); + } + } + + for (auto* node : chain) { + BreadcrumbSegment seg; + seg.node = node; + seg.label = breadcrumbDisplayName(node); + seg.siblings = breadcrumbSiblings(node); + model.segments.push_back(seg); + } + + model.overflow = model.segments.size() > maxSegments; + return model; +} + +inline const ASTNode* navigateBreadcrumbClick(const BreadcrumbBarModel& model, size_t index) { + if (index >= model.segments.size()) return nullptr; + return model.segments[index].node; +} + +inline std::vector breadcrumbSiblingNames(const BreadcrumbSegment& segment) { + std::vector names; + for (auto* sib : segment.siblings) { + names.push_back(breadcrumbDisplayName(sib)); + } + return names; +} diff --git a/editor/tests/step356_test.cpp b/editor/tests/step356_test.cpp new file mode 100644 index 0000000..9d5337f --- /dev/null +++ b/editor/tests/step356_test.cpp @@ -0,0 +1,181 @@ +// Step 356: Breadcrumb Navigation (12 tests) + +#include +#include +#include +#include "panels/BreadcrumbBar.h" +#include "ast/ClassDeclaration.h" +#include "ast/Function.h" +#include "ast/Variable.h" +#include "ast/Annotation.h" + +int main() { + int passed = 0; + + auto theme = getDefaultDarkTheme(); + + // Build shared AST: + // module app > class Engine > methods start/stop > variable counter + Module module("mod1", "app", "python"); + auto* cls = new ClassDeclaration("cls1", "Engine"); + auto* start = new MethodDeclaration("m1", "start"); + auto* stop = new MethodDeclaration("m2", "stop"); + auto* var = new Variable("v1", "counter"); + auto* anno = new IntentAnnotation(); + anno->id = "a1"; + anno->summary = "log"; + + start->addChild("body", var); + start->addChild("annotations", anno); + cls->addChild("methods", start); + cls->addChild("methods", stop); + module.addChild("classes", cls); + + // Test 1: breadcrumb shows module name + { + auto model = buildBreadcrumbBar(&module, &module, theme); + assert(!model.segments.empty()); + assert(model.segments.front().label == "app"); + std::cout << "Test 1 PASSED: module name shown\n"; + passed++; + } + + // Test 2: nested path updates with cursor + { + auto modelA = buildBreadcrumbBar(&module, start, theme); + auto modelB = buildBreadcrumbBar(&module, var, theme); + assert(modelA.segments.size() >= 3); + assert(modelB.segments.size() > modelA.segments.size()); + assert(modelA.segments.back().label == "start"); + assert(modelB.segments.back().label == "counter"); + std::cout << "Test 2 PASSED: nested path updates\n"; + passed++; + } + + // Test 3: click navigates to node + { + auto model = buildBreadcrumbBar(&module, var, theme); + const ASTNode* clicked = navigateBreadcrumbClick(model, 1); // likely class + assert(clicked != nullptr); + std::cout << "Test 3 PASSED: click navigates to node\n"; + passed++; + } + + // Test 4: dropdown shows siblings + { + auto model = buildBreadcrumbBar(&module, start, theme); + bool foundMethodSegment = false; + for (const auto& seg : model.segments) { + if (seg.label == "start") { + auto names = breadcrumbSiblingNames(seg); + assert(names.size() == 2); + bool hasStop = false; + for (const auto& n : names) { + if (n == "stop") hasStop = true; + } + assert(hasStop); + foundMethodSegment = true; + } + } + assert(foundMethodSegment); + std::cout << "Test 4 PASSED: siblings shown\n"; + passed++; + } + + // Test 5: empty file shows module only + { + Module empty("m2", "empty_mod", "python"); + auto model = buildBreadcrumbBar(&empty, nullptr, theme); + assert(model.segments.size() == 1); + assert(model.segments[0].label == "empty_mod"); + std::cout << "Test 5 PASSED: empty file module only\n"; + passed++; + } + + // Test 6: deep nesting renders with overflow signal + { + auto* deep1 = new Function("d1", "d1"); + auto* deep2 = new Function("d2", "d2"); + auto* deep3 = new Function("d3", "d3"); + auto* deep4 = new Function("d4", "d4"); + deep1->addChild("body", deep2); + deep2->addChild("body", deep3); + deep3->addChild("body", deep4); + module.addChild("functions", deep1); + auto model = buildBreadcrumbBar(&module, deep4, theme, 4); + assert(model.segments.size() >= 5); + assert(model.overflow); + std::cout << "Test 6 PASSED: deep nesting overflow handled\n"; + passed++; + } + + // Test 7: breadcrumb uses theme colors + { + auto model = buildBreadcrumbBar(&module, start, theme); + assert(model.bgColor.toHex() == theme.bgPanel.toHex()); + assert(model.textColor.toHex() == theme.text.toHex()); + assert(model.borderColor.toHex() == theme.border.toHex()); + std::cout << "Test 7 PASSED: theme colors used\n"; + passed++; + } + + // Test 8: annotation nodes appear in path + { + auto model = buildBreadcrumbBar(&module, anno, theme); + assert(!model.segments.empty()); + bool foundAnnotation = false; + for (const auto& seg : model.segments) { + if (seg.label == "IntentAnnotation" || seg.label == "log") { + foundAnnotation = true; + } + } + assert(foundAnnotation); + std::cout << "Test 8 PASSED: annotation appears in path\n"; + passed++; + } + + // Test 9: breadcrumb labels use compact names for class/function + { + auto model = buildBreadcrumbBar(&module, start, theme); + bool hasClass = false, hasMethod = false; + for (const auto& seg : model.segments) { + if (seg.label == "Engine") hasClass = true; + if (seg.label == "start") hasMethod = true; + } + assert(hasClass && hasMethod); + std::cout << "Test 9 PASSED: compact node names\n"; + passed++; + } + + // Test 10: invalid click index returns null + { + auto model = buildBreadcrumbBar(&module, start, theme); + assert(navigateBreadcrumbClick(model, 999) == nullptr); + std::cout << "Test 10 PASSED: invalid click safe\n"; + passed++; + } + + // Test 11: sibling list empty for root segment + { + auto model = buildBreadcrumbBar(&module, start, theme); + assert(!model.segments.empty()); + auto rootNames = breadcrumbSiblingNames(model.segments[0]); + assert(rootNames.empty()); + std::cout << "Test 11 PASSED: root has no siblings\n"; + passed++; + } + + // Test 12: cursor outside module falls back to module only + { + Function foreign("f1", "foreign"); + auto model = buildBreadcrumbBar(&module, &foreign, theme); + assert(model.segments.size() == 1); + assert(model.segments[0].label == "app"); + std::cout << "Test 12 PASSED: foreign cursor fallback\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +} diff --git a/progress.md b/progress.md index 95277c0..3d4531e 100644 --- a/progress.md +++ b/progress.md @@ -1863,6 +1863,42 @@ a full keyboard-driven command execution path. - `step354_test` — PASS (12/12) regression coverage - `step355_test` — PASS (8/8) new integration coverage +### Step 356: Breadcrumb Navigation +**Status:** PASS (12/12 tests) + +Implemented a breadcrumb bar model that tracks AST path from module root to +cursor node, supports click-based navigation targets, and exposes sibling lists +for dropdown navigation. The model uses compact node names (`getNodeName`) and +theme-derived colors for consistent visual integration. + +**Files created:** +- `editor/src/panels/BreadcrumbBar.h` — headless breadcrumb model: + - `buildBreadcrumbBar(...)` path generation from module/cursor + - clickable navigation helper (`navigateBreadcrumbClick`) + - sibling dropdown helpers (`breadcrumbSiblings`, `breadcrumbSiblingNames`) + - theme color wiring + overflow signal for deep paths +- `editor/tests/step356_test.cpp` — 12 tests covering: + 1. module name segment + 2. cursor-driven nested updates + 3. click navigation + 4. sibling dropdown population + 5. empty-file fallback + 6. deep path overflow handling + 7. theme color usage + 8. annotation nodes in path + 9. compact naming for class/method + 10. out-of-range click safety + 11. root sibling behavior + 12. foreign-node fallback to module-only + +**Files modified:** +- `editor/CMakeLists.txt` — `step356_test` target + +**Verification run:** +- `step354_test` — PASS (12/12) regression coverage +- `step355_test` — PASS (8/8) regression coverage +- `step356_test` — PASS (12/12) new step coverage + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)