Step 112: breadcrumb navigation

This commit is contained in:
Bill
2026-02-09 12:26:13 -07:00
parent b474ad2752
commit 8b33cd05d5
6 changed files with 162 additions and 2 deletions

View File

@@ -215,6 +215,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1
- [x] Step 109: **IMPLEMENTED** — Command palette with fuzzy search and MRU ranking (2/2 tests pass)
- [x] Step 110: **IMPLEMENTED** — Go-to-definition (LSP + Whetstone) with hover preview (2/2 tests pass)
- [x] Step 111: **IMPLEMENTED** — Symbol outline panel with LSP + AST fallback (2/2 tests pass)
- [x] Step 112: **IMPLEMENTED** — Breadcrumb navigation with scope roles (1/1 tests pass)
---
@@ -309,6 +310,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
**Step 109:** Compile and pass (2/2)
**Step 110:** Compile and pass (2/2)
**Step 111:** Compile and pass (2/2)
**Step 112:** Compile and pass (1/1)
---
@@ -360,7 +362,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
## What's Next
Sprint 4 in progress. Step 111 (symbol outline panel) done. Next: Step 112 (breadcrumb navigation).
Sprint 4 in progress. Step 112 (breadcrumb navigation) done. Next: Step 113 (project-wide search).
---
@@ -433,3 +435,4 @@ Sprint 4 in progress. Step 111 (symbol outline panel) done. Next: Step 112 (brea
| 2026-02-09 | Codex | Step 109: Command palette with fuzzy search and MRU ranking. 2/2 tests pass. |
| 2026-02-09 | Codex | Step 110: Go-to-definition (LSP + Whetstone) with hover preview. 2/2 tests pass. |
| 2026-02-09 | Codex | Step 111: Symbol outline panel with LSP + AST fallback. 2/2 tests pass. |
| 2026-02-09 | Codex | Step 112: Breadcrumb navigation with scope roles. 1/1 tests pass. |

View File

@@ -606,6 +606,10 @@ add_executable(step111_test tests/step111_test.cpp)
target_include_directories(step111_test PRIVATE src)
target_link_libraries(step111_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step112_test tests/step112_test.cpp)
target_include_directories(step112_test PRIVATE src)
target_link_libraries(step112_test PRIVATE nlohmann_json::nlohmann_json)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

75
editor/src/Breadcrumbs.h Normal file
View File

@@ -0,0 +1,75 @@
#pragma once
// Step 112: Breadcrumb navigation helpers
#include <string>
#include <vector>
#include <algorithm>
#include "ast/ASTNode.h"
#include "ast/Module.h"
#include "ast/Function.h"
#include "ast/Variable.h"
#include "ast/Parameter.h"
struct BreadcrumbItem {
std::string label;
const ASTNode* node = nullptr;
bool isRole = false;
};
inline std::string breadcrumbNodeLabel(const ASTNode* node) {
if (!node) return "";
if (node->conceptType == "Function") {
auto* fn = static_cast<const Function*>(node);
return std::string("Function:") + fn->name;
}
if (node->conceptType == "Variable") {
auto* var = static_cast<const Variable*>(node);
return std::string("Variable:") + var->name;
}
if (node->conceptType == "Parameter") {
auto* param = static_cast<const Parameter*>(node);
return std::string("Parameter:") + param->name;
}
return node->conceptType;
}
inline std::string breadcrumbRoleLabel(const ASTNode* parent, const ASTNode* child) {
if (!parent || !child) return "";
for (const auto& role : parent->childRoles()) {
const auto& kids = parent->getChildren(role);
for (auto* k : kids) {
if (k == child) return role;
}
}
return "";
}
inline std::vector<BreadcrumbItem> buildBreadcrumbTrail(const ASTNode* node) {
std::vector<const ASTNode*> chain;
const ASTNode* cur = node;
while (cur) {
chain.push_back(cur);
cur = cur->parent;
}
std::reverse(chain.begin(), chain.end());
std::vector<BreadcrumbItem> items;
for (size_t i = 0; i < chain.size(); ++i) {
BreadcrumbItem item;
item.label = breadcrumbNodeLabel(chain[i]);
item.node = chain[i];
items.push_back(std::move(item));
if (i + 1 < chain.size()) {
std::string role = breadcrumbRoleLabel(chain[i], chain[i + 1]);
if (!role.empty()) {
BreadcrumbItem roleItem;
roleItem.label = role;
roleItem.node = nullptr;
roleItem.isRole = true;
items.push_back(std::move(roleItem));
}
}
}
return items;
}

View File

@@ -42,6 +42,7 @@
#include "RefactorActions.h"
#include "CommandPalette.h"
#include "ContextAPI.h"
#include "Breadcrumbs.h"
#include "ast/Serialization.h"
#include "ast/Generator.h"
#include "ast/Annotation.h"
@@ -2140,6 +2141,45 @@ int main(int, char**) {
// Editor (center) — editable text area
// ---------------------------------------------------------------
ImGui::Begin("Editor");
ImGui::PushFont(uiFont);
ImGui::BeginChild("##breadcrumbs", ImVec2(0, 26.0f), false, ImGuiWindowFlags_NoScrollbar);
if (!state.active()) {
ImGui::TextDisabled("(no file)");
} else if (!state.isStructured()) {
ImGui::TextDisabled("Text mode");
} else {
Module* ast = state.activeAST();
int lineZero = std::max(0, state.active()->cursorLine - 1);
int colZero = std::max(0, state.active()->cursorCol - 1);
ASTNode* node = ast ? findNodeAtPosition(ast, lineZero, colZero) : nullptr;
if (!node) {
ImGui::TextDisabled("(no scope)");
} else {
auto crumbs = buildBreadcrumbTrail(node);
for (size_t i = 0; i < crumbs.size(); ++i) {
if (i > 0) {
ImGui::SameLine();
ImGui::TextUnformatted(">");
ImGui::SameLine();
}
ImGui::PushID((int)i);
const auto& item = crumbs[i];
if (item.isRole) {
ImGui::TextDisabled("%s", item.label.c_str());
} else if (item.node && item.node->hasSpan()) {
if (ImGui::SmallButton(item.label.c_str())) {
state.jumpTo(state.active(), item.node->spanStartLine, item.node->spanStartCol);
}
} else {
ImGui::TextUnformatted(item.label.c_str());
}
ImGui::PopID();
}
}
}
ImGui::EndChild();
ImGui::PopFont();
ImGui::PushFont(monoFont);
// Tab bar for the file

View File

@@ -0,0 +1,38 @@
// Step 112 TDD Test: Breadcrumb trail
#include "Breadcrumbs.h"
#include "ast/Statement.h"
#include <iostream>
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
if (cond) {
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
++passed;
} else {
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
++failed;
}
}
int main() {
int passed = 0;
int failed = 0;
Module mod("m1", "Main", "python");
Function fn("f1", "add");
Return ret;
mod.addChild("functions", &fn);
fn.addChild("body", &ret);
auto trail = buildBreadcrumbTrail(&ret);
bool ok = trail.size() == 5 &&
trail[0].label == "Module" &&
trail[1].label == "functions" && trail[1].isRole &&
trail[2].label == "Function:add" &&
trail[3].label == "body" && trail[3].isRole &&
trail[4].label == "Return";
expect(ok, "breadcrumb path with roles", passed, failed);
std::cout << "\n=== Step 112 Results: " << passed << " passed, " << failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -327,7 +327,7 @@ Whetstone-specific AST navigation.
Click to navigate. Icons by symbol kind. Search/filter box.
*Wires:* `ContextAPI.h`, `LSPClient.h`
- [ ] **Step 112: Breadcrumb navigation**
- [x] **Step 112: Breadcrumb navigation**
Bar above the editor showing the current scope path:
`Module > Function:add > body > Return`. Click any segment to jump.
Updates as cursor moves. Uses Whetstone AST parent chain (our unique feature —