Step 114: go-to-line

This commit is contained in:
Bill
2026-02-09 12:33:15 -07:00
parent ea51a2c135
commit 1d2828f5de
6 changed files with 166 additions and 2 deletions

View File

@@ -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. |

View File

@@ -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)

45
editor/src/GoToLine.h Normal file
View File

@@ -0,0 +1,45 @@
#pragma once
// Step 114: Go-to-line parsing helpers
#include <string>
#include <cctype>
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;
}

View File

@@ -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<ProjectSearch::FileResult> 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
// ---------------------------------------------------------------

View File

@@ -0,0 +1,45 @@
// Step 114 TDD Test: Go-to-line parsing
#include "GoToLine.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;
{
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;
}

View File

@@ -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`