Step 358: Go-to-Definition and Symbol Navigation (12/12 tests)
This commit is contained in:
@@ -2126,4 +2126,8 @@ add_executable(step357_test tests/step357_test.cpp)
|
||||
target_include_directories(step357_test PRIVATE src)
|
||||
target_link_libraries(step357_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step358_test tests/step358_test.cpp)
|
||||
target_include_directories(step358_test PRIVATE src)
|
||||
target_link_libraries(step358_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
127
editor/src/panels/NavigationTools.h
Normal file
127
editor/src/panels/NavigationTools.h
Normal file
@@ -0,0 +1,127 @@
|
||||
#pragma once
|
||||
// Step 358: Go-to-Definition + Go-to-Symbol headless navigation tools.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "../CompactAST.h"
|
||||
#include "../Icons.h"
|
||||
#include "../ast/Expression.h"
|
||||
#include "../ast/Module.h"
|
||||
|
||||
struct SymbolLocation {
|
||||
std::string name;
|
||||
std::string type; // function/class/variable/method/annotation/...
|
||||
std::string icon;
|
||||
std::string filePath;
|
||||
int line = -1;
|
||||
int column = -1;
|
||||
const ASTNode* node = nullptr;
|
||||
};
|
||||
|
||||
inline std::string navLower(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return s;
|
||||
}
|
||||
|
||||
inline void collectSymbolsRecursive(const ASTNode* node,
|
||||
const std::string& filePath,
|
||||
std::vector<SymbolLocation>& out) {
|
||||
if (!node) return;
|
||||
const std::string ct = node->conceptType;
|
||||
const bool isSymbol =
|
||||
ct == "Function" || ct == "MethodDeclaration" || ct == "ClassDeclaration" ||
|
||||
ct == "Variable" || ct == "InterfaceDeclaration" || ct == "EnumDeclaration";
|
||||
if (isSymbol) {
|
||||
SymbolLocation s;
|
||||
s.name = getNodeName(node);
|
||||
s.type = navLower(ct);
|
||||
s.icon = WhetstoneIcons::iconForNodeType(ct);
|
||||
s.filePath = filePath;
|
||||
s.line = node->spanStartLine;
|
||||
s.column = node->spanStartCol;
|
||||
s.node = node;
|
||||
out.push_back(s);
|
||||
}
|
||||
for (auto* child : node->allChildren()) {
|
||||
collectSymbolsRecursive(child, filePath, out);
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<SymbolLocation> collectFileSymbols(const Module* module,
|
||||
const std::string& filePath) {
|
||||
std::vector<SymbolLocation> out;
|
||||
collectSymbolsRecursive(module, filePath, out);
|
||||
std::sort(out.begin(), out.end(), [](const SymbolLocation& a, const SymbolLocation& b) {
|
||||
int la = a.line < 0 ? 1'000'000 : a.line;
|
||||
int lb = b.line < 0 ? 1'000'000 : b.line;
|
||||
if (la != lb) return la < lb;
|
||||
return a.name < b.name;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::vector<SymbolLocation> collectProjectSymbols(
|
||||
const std::vector<std::pair<std::string, const Module*>>& modules) {
|
||||
std::vector<SymbolLocation> out;
|
||||
for (const auto& [filePath, module] : modules) {
|
||||
auto fileSymbols = collectFileSymbols(module, filePath);
|
||||
out.insert(out.end(), fileSymbols.begin(), fileSymbols.end());
|
||||
}
|
||||
std::sort(out.begin(), out.end(), [](const SymbolLocation& a, const SymbolLocation& b) {
|
||||
if (a.filePath != b.filePath) return a.filePath < b.filePath;
|
||||
int la = a.line < 0 ? 1'000'000 : a.line;
|
||||
int lb = b.line < 0 ? 1'000'000 : b.line;
|
||||
if (la != lb) return la < lb;
|
||||
return a.name < b.name;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::vector<SymbolLocation> filterSymbols(const std::vector<SymbolLocation>& symbols,
|
||||
const std::string& query) {
|
||||
if (query.empty()) return symbols;
|
||||
std::string q = navLower(query);
|
||||
std::vector<SymbolLocation> out;
|
||||
for (const auto& s : symbols) {
|
||||
std::string hay = navLower(s.name + " " + s.type + " " + s.filePath);
|
||||
if (hay.find(q) != std::string::npos) out.push_back(s);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::optional<SymbolLocation> goToDefinition(
|
||||
const FunctionCall* call,
|
||||
const std::string& currentFile,
|
||||
const std::vector<std::pair<std::string, const Module*>>& modules) {
|
||||
if (!call || call->functionName.empty()) return std::nullopt;
|
||||
const std::string wanted = call->functionName;
|
||||
|
||||
// Prefer current file definitions first.
|
||||
for (const auto& [filePath, module] : modules) {
|
||||
if (filePath != currentFile) continue;
|
||||
for (const auto& s : collectFileSymbols(module, filePath)) {
|
||||
if ((s.type == "function" || s.type == "methoddeclaration") && s.name == wanted) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Then project-wide.
|
||||
for (const auto& [filePath, module] : modules) {
|
||||
for (const auto& s : collectFileSymbols(module, filePath)) {
|
||||
if ((s.type == "function" || s.type == "methoddeclaration") && s.name == wanted) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
inline std::string definitionStatusMessage(const std::optional<SymbolLocation>& loc) {
|
||||
if (loc.has_value()) return "Definition found";
|
||||
return "No definition found";
|
||||
}
|
||||
179
editor/tests/step358_test.cpp
Normal file
179
editor/tests/step358_test.cpp
Normal file
@@ -0,0 +1,179 @@
|
||||
// Step 358: Go-to-Definition + Go-to-Symbol (12 tests)
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "panels/NavigationTools.h"
|
||||
#include "ast/ClassDeclaration.h"
|
||||
#include "ast/Function.h"
|
||||
#include "ast/Variable.h"
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
|
||||
// Build module A
|
||||
Module modA("mA", "modA", "python");
|
||||
auto* fnHelperA = new Function("f1", "helper");
|
||||
fnHelperA->setSpan(9, 0, 12, 0); // line 10 human-readable
|
||||
auto* fnCaller = new Function("f2", "caller");
|
||||
fnCaller->setSpan(29, 0, 33, 0);
|
||||
auto* cls = new ClassDeclaration("c1", "Engine");
|
||||
cls->setSpan(19, 0, 24, 0);
|
||||
auto* var = new Variable("v1", "count");
|
||||
var->setSpan(14, 0, 14, 5);
|
||||
modA.addChild("functions", fnHelperA);
|
||||
modA.addChild("functions", fnCaller);
|
||||
modA.addChild("classes", cls);
|
||||
modA.addChild("variables", var);
|
||||
|
||||
// Build module B with cross-file helper
|
||||
Module modB("mB", "modB", "python");
|
||||
auto* fnHelperB = new Function("f3", "external_helper");
|
||||
fnHelperB->setSpan(4, 0, 8, 0);
|
||||
modB.addChild("functions", fnHelperB);
|
||||
|
||||
std::vector<std::pair<std::string, const Module*>> modules = {
|
||||
{"a.py", &modA},
|
||||
{"b.py", &modB}
|
||||
};
|
||||
|
||||
// Test 1: F12 on function call jumps to definition
|
||||
{
|
||||
FunctionCall call;
|
||||
call.functionName = "helper";
|
||||
auto def = goToDefinition(&call, "a.py", modules);
|
||||
assert(def.has_value());
|
||||
assert(def->name == "helper");
|
||||
assert(def->filePath == "a.py");
|
||||
std::cout << "Test 1 PASSED: F12 go-to-definition\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 2: Ctrl+Shift+O lists file symbols
|
||||
{
|
||||
auto symbols = collectFileSymbols(&modA, "a.py");
|
||||
assert(symbols.size() >= 4);
|
||||
std::cout << "Test 2 PASSED: file symbol list\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 3: Ctrl+T lists project symbols
|
||||
{
|
||||
auto symbols = collectProjectSymbols(modules);
|
||||
assert(symbols.size() >= 5);
|
||||
bool hasFromB = false;
|
||||
for (const auto& s : symbols) {
|
||||
if (s.filePath == "b.py" && s.name == "external_helper") hasFromB = true;
|
||||
}
|
||||
assert(hasFromB);
|
||||
std::cout << "Test 3 PASSED: project symbol list\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 4: symbol list shows icons by type
|
||||
{
|
||||
auto symbols = collectFileSymbols(&modA, "a.py");
|
||||
bool classIcon = false;
|
||||
bool fnIcon = false;
|
||||
for (const auto& s : symbols) {
|
||||
if (s.name == "Engine" && !s.icon.empty()) classIcon = true;
|
||||
if (s.name == "helper" && !s.icon.empty()) fnIcon = true;
|
||||
}
|
||||
assert(classIcon && fnIcon);
|
||||
std::cout << "Test 4 PASSED: symbols include icons\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 5: filter works on symbol list
|
||||
{
|
||||
auto symbols = collectFileSymbols(&modA, "a.py");
|
||||
auto filtered = filterSymbols(symbols, "eng");
|
||||
assert(filtered.size() == 1);
|
||||
assert(filtered[0].name == "Engine");
|
||||
std::cout << "Test 5 PASSED: symbol filter\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 6: cross-file jump opens target buffer path
|
||||
{
|
||||
FunctionCall call;
|
||||
call.functionName = "external_helper";
|
||||
auto def = goToDefinition(&call, "a.py", modules);
|
||||
assert(def.has_value());
|
||||
assert(def->filePath == "b.py");
|
||||
std::cout << "Test 6 PASSED: cross-file jump\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 7: no match shows message
|
||||
{
|
||||
FunctionCall call;
|
||||
call.functionName = "missing_fn";
|
||||
auto def = goToDefinition(&call, "a.py", modules);
|
||||
assert(!def.has_value());
|
||||
assert(definitionStatusMessage(def) == "No definition found");
|
||||
std::cout << "Test 7 PASSED: no-match message\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 8: symbols sorted by line number
|
||||
{
|
||||
auto symbols = collectFileSymbols(&modA, "a.py");
|
||||
int prev = -1;
|
||||
for (const auto& s : symbols) {
|
||||
if (s.line < 0) continue;
|
||||
assert(s.line >= prev);
|
||||
prev = s.line;
|
||||
}
|
||||
std::cout << "Test 8 PASSED: sorted by line\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 9: empty query returns all
|
||||
{
|
||||
auto symbols = collectProjectSymbols(modules);
|
||||
auto filtered = filterSymbols(symbols, "");
|
||||
assert(filtered.size() == symbols.size());
|
||||
std::cout << "Test 9 PASSED: empty query returns all\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 10: case-insensitive filter
|
||||
{
|
||||
auto symbols = collectProjectSymbols(modules);
|
||||
auto filtered = filterSymbols(symbols, "EXTERNAL");
|
||||
assert(filtered.size() == 1);
|
||||
assert(filtered[0].name == "external_helper");
|
||||
std::cout << "Test 10 PASSED: case-insensitive filter\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 11: definition prefers current file over project
|
||||
{
|
||||
// Add same-name function in modB to validate preference.
|
||||
auto* shadow = new Function("f4", "helper");
|
||||
shadow->setSpan(1, 0, 2, 0);
|
||||
modB.addChild("functions", shadow);
|
||||
|
||||
FunctionCall call;
|
||||
call.functionName = "helper";
|
||||
auto def = goToDefinition(&call, "a.py", modules);
|
||||
assert(def.has_value());
|
||||
assert(def->filePath == "a.py");
|
||||
std::cout << "Test 11 PASSED: current-file preference\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 12: null call safely handled
|
||||
{
|
||||
auto def = goToDefinition(nullptr, "a.py", modules);
|
||||
assert(!def.has_value());
|
||||
std::cout << "Test 12 PASSED: null call safe\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
std::cout << "\nResults: " << passed << "/12\n";
|
||||
assert(passed == 12);
|
||||
return 0;
|
||||
}
|
||||
36
progress.md
36
progress.md
@@ -1935,6 +1935,42 @@ focus target for click handling.
|
||||
- `step356_test` — PASS (12/12) regression coverage
|
||||
- `step357_test` — PASS (12/12) new step coverage
|
||||
|
||||
### Step 358: Go-to-Definition + Go-to-Symbol
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implemented headless navigation tooling for symbol discovery and jump resolution:
|
||||
file-level and project-level symbol lists, symbol filtering, icon/type metadata,
|
||||
and go-to-definition lookup with current-file preference and cross-file fallback.
|
||||
|
||||
**Files created:**
|
||||
- `editor/src/panels/NavigationTools.h` — navigation model:
|
||||
- symbol collection (`collectFileSymbols`, `collectProjectSymbols`)
|
||||
- symbol filtering (`filterSymbols`)
|
||||
- F12-style definition lookup (`goToDefinition`)
|
||||
- no-match messaging (`definitionStatusMessage`)
|
||||
- line-order sorting and icon assignment
|
||||
- `editor/tests/step358_test.cpp` — 12 tests covering:
|
||||
1. definition jump from function call
|
||||
2. file symbol listing
|
||||
3. project symbol listing
|
||||
4. icon presence by symbol type
|
||||
5. symbol filtering
|
||||
6. cross-file definition jump
|
||||
7. no-match message
|
||||
8. line-number sort order
|
||||
9. empty-query behavior
|
||||
10. case-insensitive filtering
|
||||
11. current-file definition preference
|
||||
12. null-call safety
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` — `step358_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `step356_test` — PASS (12/12) regression coverage
|
||||
- `step357_test` — PASS (12/12) regression coverage
|
||||
- `step358_test` — PASS (12/12) new step coverage
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)
|
||||
|
||||
Reference in New Issue
Block a user