From 1d2828f5de3185b0ed49212ed909af6b9a24da84 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 9 Feb 2026 12:33:15 -0700 Subject: [PATCH] Step 114: go-to-line --- PROGRESS.md | 5 ++- editor/CMakeLists.txt | 4 +++ editor/src/GoToLine.h | 45 +++++++++++++++++++++++ editor/src/main.cpp | 67 +++++++++++++++++++++++++++++++++++ editor/tests/step114_test.cpp | 45 +++++++++++++++++++++++ sprint4_plan.md | 2 +- 6 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 editor/src/GoToLine.h create mode 100644 editor/tests/step114_test.cpp diff --git a/PROGRESS.md b/PROGRESS.md index e37858a..5d1df46 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -217,6 +217,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [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) - [x] Step 113: **IMPLEMENTED** — Project-wide search panel with regex and glob filters (3/3 tests pass) +- [x] Step 114: **IMPLEMENTED** — Go-to-line popup with :line:col parsing (4/4 tests pass) --- @@ -313,6 +314,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 111:** Compile and pass (2/2) **Step 112:** Compile and pass (1/1) **Step 113:** Compile and pass (3/3) +**Step 114:** Compile and pass (4/4) --- @@ -364,7 +366,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi ## What's Next -Sprint 4 in progress. Step 113 (project-wide search) done. Next: Step 114 (go-to-line). +Sprint 4 in progress. Step 114 (go-to-line) done. Next: Step 115 (wire orchestrator). --- @@ -439,3 +441,4 @@ Sprint 4 in progress. Step 113 (project-wide search) done. Next: Step 114 (go-to | 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. | | 2026-02-09 | Codex | Step 113: Project-wide search panel with regex and glob filters. 3/3 tests pass. | +| 2026-02-09 | Codex | Step 114: Go-to-line popup with :line:col parsing. 4/4 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 94275bc..2890bc0 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -614,6 +614,10 @@ add_executable(step113_test tests/step113_test.cpp) target_include_directories(step113_test PRIVATE src) target_link_libraries(step113_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step114_test tests/step114_test.cpp) +target_include_directories(step114_test PRIVATE src) +target_link_libraries(step114_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/GoToLine.h b/editor/src/GoToLine.h new file mode 100644 index 0000000..ced6572 --- /dev/null +++ b/editor/src/GoToLine.h @@ -0,0 +1,45 @@ +#pragma once +// Step 114: Go-to-line parsing helpers + +#include +#include + +inline std::string trimCopy(const std::string& s) { + size_t start = 0; + while (start < s.size() && std::isspace((unsigned char)s[start])) ++start; + size_t end = s.size(); + while (end > start && std::isspace((unsigned char)s[end - 1])) --end; + return s.substr(start, end - start); +} + +inline bool parseLineColInput(const std::string& input, int& outLine, int& outCol) { + std::string s = trimCopy(input); + if (s.empty()) return false; + if (!s.empty() && s[0] == ':') s = s.substr(1); + + size_t colon = s.find(':'); + std::string lineStr = (colon == std::string::npos) ? s : s.substr(0, colon); + std::string colStr = (colon == std::string::npos) ? "" : s.substr(colon + 1); + + if (lineStr.empty()) return false; + int line = 0; + for (char c : lineStr) { + if (!std::isdigit((unsigned char)c)) return false; + line = line * 10 + (c - '0'); + } + if (line <= 0) return false; + + int col = 1; + if (!colStr.empty()) { + col = 0; + for (char c : colStr) { + if (!std::isdigit((unsigned char)c)) return false; + col = col * 10 + (c - '0'); + } + if (col <= 0) return false; + } + + outLine = line; + outCol = col; + return true; +} diff --git a/editor/src/main.cpp b/editor/src/main.cpp index a13ec31..b868bea 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -44,6 +44,7 @@ #include "ContextAPI.h" #include "Breadcrumbs.h" #include "ProjectSearch.h" +#include "GoToLine.h" #include "ast/Serialization.h" #include "ast/Generator.h" #include "ast/Annotation.h" @@ -113,6 +114,9 @@ struct EditorState { char searchExclude[256] = {}; bool searchUseRegex = true; std::vector searchResults; + bool showGoToLine = false; + char goToLineBuf[64] = {}; + bool goToLineError = false; // Bottom panel int bottomTab = 0; // 0=Output, 1=AST, 2=Highlighted @@ -510,6 +514,13 @@ struct EditorState { registerCommand("search.findInFiles", "Search: Find in Files", keys.getBinding("search.findInFiles").toString(), [this]() { showProjectSearch = !showProjectSearch; }); + registerCommand("nav.goToLine", "Navigate: Go to Line", + keys.getBinding("nav.goToLine").toString(), + [this]() { + showGoToLine = true; + goToLineBuf[0] = '\0'; + goToLineError = false; + }); registerCommand("view.whitespace", "View: Toggle Whitespace", "", [this]() { showWhitespace = !showWhitespace; }); registerCommand("view.minimap", "View: Toggle Minimap", "", @@ -1644,6 +1655,11 @@ int main(int, char**) { else if (action == "edit.redo") state.doRedo(); else if (action == "search.find") state.showFind = !state.showFind; else if (action == "search.findInFiles") state.showProjectSearch = !state.showProjectSearch; + else if (action == "nav.goToLine") { + state.showGoToLine = true; + state.goToLineBuf[0] = '\0'; + state.goToLineError = false; + } else if (action == "file.save") state.doSave(); else if (action == "file.new") { std::string lang = state.active() ? state.active()->language : "python"; @@ -1778,6 +1794,14 @@ int main(int, char**) { } ImGui::EndMenu(); } + if (ImGui::BeginMenu("Navigate")) { + if (ImGui::MenuItem("Go to Line...", state.keys.getBinding("nav.goToLine").toString().c_str())) { + state.showGoToLine = true; + state.goToLineBuf[0] = '\0'; + state.goToLineError = false; + } + ImGui::EndMenu(); + } if (ImGui::BeginMenu("Language")) { if (ImGui::MenuItem("Python", nullptr, state.active() && state.active()->language == "python")) state.setLanguage("python"); @@ -2041,6 +2065,49 @@ int main(int, char**) { ImGui::End(); } + // --------------------------------------------------------------- + // Go To Line (Ctrl+G) + // --------------------------------------------------------------- + if (state.showGoToLine) { + ImGui::OpenPopup("GoToLine"); + } + if (ImGui::BeginPopupModal("GoToLine", &state.showGoToLine, ImGuiWindowFlags_AlwaysAutoResize)) { + int totalLines = state.active() ? countLines(state.active()->editBuf) : 0; + ImGui::Text("Enter line or :line:col"); + ImGui::TextDisabled("Total lines: %d", totalLines); + ImGui::SetNextItemWidth(240); + bool submit = ImGui::InputText("##gotoLineInput", state.goToLineBuf, + sizeof(state.goToLineBuf), + ImGuiInputTextFlags_EnterReturnsTrue); + if (ImGui::Button("Go")) submit = true; + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + state.showGoToLine = false; + state.goToLineError = false; + ImGui::CloseCurrentPopup(); + } + if (submit) { + int line = 0; + int col = 0; + if (parseLineColInput(state.goToLineBuf, line, col)) { + if (totalLines > 0) line = std::max(1, std::min(line, totalLines)); + col = std::max(1, col); + if (state.active()) { + state.jumpTo(state.active(), line - 1, col - 1); + } + state.showGoToLine = false; + state.goToLineError = false; + ImGui::CloseCurrentPopup(); + } else { + state.goToLineError = true; + } + } + if (state.goToLineError) { + ImGui::TextColored(ImVec4(0.9f, 0.4f, 0.4f, 1.0f), "Invalid format."); + } + ImGui::EndPopup(); + } + // --------------------------------------------------------------- // LSP Server Settings // --------------------------------------------------------------- diff --git a/editor/tests/step114_test.cpp b/editor/tests/step114_test.cpp new file mode 100644 index 0000000..0acbcb4 --- /dev/null +++ b/editor/tests/step114_test.cpp @@ -0,0 +1,45 @@ +// Step 114 TDD Test: Go-to-line parsing +#include "GoToLine.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; + + { + int line = 0, col = 0; + bool ok = parseLineColInput("10", line, col) && line == 10 && col == 1; + expect(ok, "line only", passed, failed); + } + + { + int line = 0, col = 0; + bool ok = parseLineColInput(":7:3", line, col) && line == 7 && col == 3; + expect(ok, "colon prefix", passed, failed); + } + + { + int line = 0, col = 0; + bool ok = parseLineColInput("5:9", line, col) && line == 5 && col == 9; + expect(ok, "line and col", passed, failed); + } + + { + int line = 0, col = 0; + bool ok = !parseLineColInput("abc", line, col); + expect(ok, "invalid input", passed, failed); + } + + std::cout << "\n=== Step 114 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint4_plan.md b/sprint4_plan.md index 13f193d..25127c2 100644 --- a/sprint4_plan.md +++ b/sprint4_plan.md @@ -340,7 +340,7 @@ Whetstone-specific AST navigation. Regex support. Include/exclude glob filters. *New:* `ProjectSearch.h` -- [ ] **Step 114: Go-to-line** +- [x] **Step 114: Go-to-line** Ctrl+G opens a quick input: type line number → jump. Supports `:line:col` format. Shows total line count. *Modifies:* `main.cpp`