Step 356: Breadcrumb Navigation (12/12 tests)
This commit is contained in:
@@ -2118,4 +2118,8 @@ add_executable(step355_test tests/step355_test.cpp)
|
|||||||
target_include_directories(step355_test PRIVATE src)
|
target_include_directories(step355_test PRIVATE src)
|
||||||
target_link_libraries(step355_test PRIVATE nlohmann_json::nlohmann_json)
|
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)
|
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||||
|
|||||||
107
editor/src/panels/BreadcrumbBar.h
Normal file
107
editor/src/panels/BreadcrumbBar.h
Normal file
@@ -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 <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#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<const ASTNode*> siblings;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BreadcrumbBarModel {
|
||||||
|
std::vector<BreadcrumbSegment> 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<const ASTNode*> 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<const ASTNode*> 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<const ASTNode*> 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<std::string> breadcrumbSiblingNames(const BreadcrumbSegment& segment) {
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (auto* sib : segment.siblings) {
|
||||||
|
names.push_back(breadcrumbDisplayName(sib));
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
181
editor/tests/step356_test.cpp
Normal file
181
editor/tests/step356_test.cpp
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
// Step 356: Breadcrumb Navigation (12 tests)
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
36
progress.md
36
progress.md
@@ -1863,6 +1863,42 @@ a full keyboard-driven command execution path.
|
|||||||
- `step354_test` — PASS (12/12) regression coverage
|
- `step354_test` — PASS (12/12) regression coverage
|
||||||
- `step355_test` — PASS (8/8) new integration 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+
|
# Roadmap Planning — Sprints 12-25+
|
||||||
|
|
||||||
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)
|
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)
|
||||||
|
|||||||
Reference in New Issue
Block a user