diff --git a/PROGRESS.md b/PROGRESS.md index 74a8f46..b0a0e8b 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index cb2e536..c563f5c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/Breadcrumbs.h b/editor/src/Breadcrumbs.h new file mode 100644 index 0000000..42a6a32 --- /dev/null +++ b/editor/src/Breadcrumbs.h @@ -0,0 +1,75 @@ +#pragma once +// Step 112: Breadcrumb navigation helpers + +#include +#include +#include +#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(node); + return std::string("Function:") + fn->name; + } + if (node->conceptType == "Variable") { + auto* var = static_cast(node); + return std::string("Variable:") + var->name; + } + if (node->conceptType == "Parameter") { + auto* param = static_cast(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 buildBreadcrumbTrail(const ASTNode* node) { + std::vector chain; + const ASTNode* cur = node; + while (cur) { + chain.push_back(cur); + cur = cur->parent; + } + std::reverse(chain.begin(), chain.end()); + + std::vector 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; +} diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 1322505..a851994 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -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 diff --git a/editor/tests/step112_test.cpp b/editor/tests/step112_test.cpp new file mode 100644 index 0000000..80873a8 --- /dev/null +++ b/editor/tests/step112_test.cpp @@ -0,0 +1,38 @@ +// Step 112 TDD Test: Breadcrumb trail +#include "Breadcrumbs.h" +#include "ast/Statement.h" +#include + +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; +} diff --git a/sprint4_plan.md b/sprint4_plan.md index 85f5d4a..6e8aa83 100644 --- a/sprint4_plan.md +++ b/sprint4_plan.md @@ -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 —