Step 111: symbol outline panel
This commit is contained in:
@@ -214,6 +214,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1
|
||||
- [x] Step 108: **IMPLEMENTED** — Batch mutation UI with refactor actions and diff preview (3/3 tests pass)
|
||||
- [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)
|
||||
|
||||
---
|
||||
|
||||
@@ -307,6 +308,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
|
||||
**Step 108:** Compile and pass (3/3)
|
||||
**Step 109:** Compile and pass (2/2)
|
||||
**Step 110:** Compile and pass (2/2)
|
||||
**Step 111:** Compile and pass (2/2)
|
||||
|
||||
---
|
||||
|
||||
@@ -358,7 +360,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
|
||||
|
||||
## What's Next
|
||||
|
||||
Sprint 4 in progress. Step 110 (Go-to-definition) done. Next: Step 111 (symbol outline panel).
|
||||
Sprint 4 in progress. Step 111 (symbol outline panel) done. Next: Step 112 (breadcrumb navigation).
|
||||
|
||||
---
|
||||
|
||||
@@ -430,3 +432,4 @@ Sprint 4 in progress. Step 110 (Go-to-definition) done. Next: Step 111 (symbol o
|
||||
| 2026-02-09 | Codex | Step 108: Batch mutation UI with refactor actions and diff preview. 3/3 tests pass. |
|
||||
| 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. |
|
||||
|
||||
@@ -602,6 +602,10 @@ add_executable(step110_test tests/step110_test.cpp)
|
||||
target_include_directories(step110_test PRIVATE src)
|
||||
target_link_libraries(step110_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
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)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -60,6 +60,15 @@ public:
|
||||
Range range;
|
||||
};
|
||||
|
||||
struct DocumentSymbol {
|
||||
std::string name;
|
||||
std::string detail;
|
||||
int kind = 0;
|
||||
Range range;
|
||||
Range selectionRange;
|
||||
std::vector<DocumentSymbol> children;
|
||||
};
|
||||
|
||||
void initialize(const std::string& rootUri,
|
||||
const std::string& clientName,
|
||||
int processId) {
|
||||
@@ -149,6 +158,14 @@ public:
|
||||
return id;
|
||||
}
|
||||
|
||||
int requestDocumentSymbols(const std::string& uri) {
|
||||
nlohmann::json params;
|
||||
params["textDocument"] = {{"uri", uri}};
|
||||
int id = sendRequest("textDocument/documentSymbol", params);
|
||||
lastDocumentSymbolRequestId_ = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
void handleMessage(const std::string& msg) {
|
||||
nlohmann::json j;
|
||||
try {
|
||||
@@ -246,6 +263,24 @@ public:
|
||||
definitionLocations_ = std::move(parsed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (id == lastDocumentSymbolRequestId_) {
|
||||
std::vector<DocumentSymbol> parsed;
|
||||
if (result.is_array()) {
|
||||
bool isSymbolInfo = !result.empty() && result[0].contains("location");
|
||||
for (const auto& item : result) {
|
||||
DocumentSymbol sym;
|
||||
bool ok = isSymbolInfo ? parseSymbolInformation(item, sym)
|
||||
: parseDocumentSymbol(item, sym);
|
||||
if (ok) parsed.push_back(std::move(sym));
|
||||
}
|
||||
} else if (result.is_object()) {
|
||||
DocumentSymbol sym;
|
||||
if (parseDocumentSymbol(result, sym)) parsed.push_back(std::move(sym));
|
||||
}
|
||||
documentSymbols_ = std::move(parsed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +330,14 @@ public:
|
||||
definitionLocations_.clear();
|
||||
}
|
||||
|
||||
std::vector<DocumentSymbol> getDocumentSymbols() const {
|
||||
return documentSymbols_;
|
||||
}
|
||||
|
||||
void clearDocumentSymbols() {
|
||||
documentSymbols_.clear();
|
||||
}
|
||||
|
||||
void sendNotification(const std::string& method, const nlohmann::json& params) {
|
||||
nlohmann::json msg;
|
||||
msg["jsonrpc"] = "2.0";
|
||||
@@ -329,9 +372,11 @@ private:
|
||||
int lastHoverRequestId_ = -1;
|
||||
int lastSignatureRequestId_ = -1;
|
||||
int lastDefinitionRequestId_ = -1;
|
||||
int lastDocumentSymbolRequestId_ = -1;
|
||||
std::string hoverContents_;
|
||||
SignatureHelp signatureHelp_;
|
||||
std::vector<DefinitionLocation> definitionLocations_;
|
||||
std::vector<DocumentSymbol> documentSymbols_;
|
||||
|
||||
static Range parseRange(const nlohmann::json& range) {
|
||||
Range out;
|
||||
@@ -366,6 +411,39 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool parseDocumentSymbol(const nlohmann::json& item, DocumentSymbol& out) {
|
||||
if (!item.contains("name")) return false;
|
||||
out.name = item.value("name", "");
|
||||
out.detail = item.value("detail", "");
|
||||
out.kind = item.value("kind", 0);
|
||||
if (item.contains("range")) out.range = parseRange(item["range"]);
|
||||
if (item.contains("selectionRange")) {
|
||||
out.selectionRange = parseRange(item["selectionRange"]);
|
||||
} else {
|
||||
out.selectionRange = out.range;
|
||||
}
|
||||
if (item.contains("children") && item["children"].is_array()) {
|
||||
for (const auto& child : item["children"]) {
|
||||
DocumentSymbol c;
|
||||
if (parseDocumentSymbol(child, c)) out.children.push_back(std::move(c));
|
||||
}
|
||||
}
|
||||
return !out.name.empty();
|
||||
}
|
||||
|
||||
static bool parseSymbolInformation(const nlohmann::json& item, DocumentSymbol& out) {
|
||||
if (!item.contains("name") || !item.contains("location")) return false;
|
||||
out.name = item.value("name", "");
|
||||
out.detail = item.value("detail", "");
|
||||
out.kind = item.value("kind", 0);
|
||||
const auto& loc = item["location"];
|
||||
if (loc.contains("range")) {
|
||||
out.range = parseRange(loc["range"]);
|
||||
out.selectionRange = out.range;
|
||||
}
|
||||
return !out.name.empty();
|
||||
}
|
||||
|
||||
static std::string stringifyMarkup(const nlohmann::json& contents) {
|
||||
if (contents.is_string()) return contents.get<std::string>();
|
||||
if (contents.is_object()) {
|
||||
|
||||
@@ -114,6 +114,8 @@ struct EditorState {
|
||||
bool showWhitespace = false;
|
||||
bool showMinimap = false;
|
||||
bool showAnnotations = false;
|
||||
bool showOutline = true;
|
||||
char outlineFilter[128] = {};
|
||||
LayoutPreset layoutPreset = LayoutPreset::VSCode;
|
||||
WelcomeScreen welcome;
|
||||
std::string workspaceRoot;
|
||||
@@ -123,6 +125,8 @@ struct EditorState {
|
||||
FileWatcher watcher;
|
||||
std::shared_ptr<LSPTransport> lspTransport;
|
||||
std::shared_ptr<LSPClient> lsp;
|
||||
bool symbolsPending = false;
|
||||
double symbolsLastChange = 0.0;
|
||||
bool completionPending = false;
|
||||
bool completionVisible = false;
|
||||
int completionSelected = 0;
|
||||
@@ -321,6 +325,9 @@ struct EditorState {
|
||||
if (lsp && path.rfind("(untitled", 0) != 0) {
|
||||
lsp->didOpen(toFileUri(path), language, content, activeBuffer->lspVersion);
|
||||
}
|
||||
symbolsPending = true;
|
||||
symbolsLastChange = ImGui::GetTime();
|
||||
if (lsp) lsp->clearDocumentSymbols();
|
||||
}
|
||||
|
||||
bool jumpToDefinitionLocation(const LSPClient::DefinitionLocation& loc) {
|
||||
@@ -400,6 +407,9 @@ struct EditorState {
|
||||
if (!buffers.hasBuffer(path)) return;
|
||||
buffers.switchToBuffer(path);
|
||||
activeBuffer = bufferStates[path].get();
|
||||
symbolsPending = true;
|
||||
symbolsLastChange = ImGui::GetTime();
|
||||
if (lsp) lsp->clearDocumentSymbols();
|
||||
}
|
||||
|
||||
std::filesystem::path configDir() const {
|
||||
@@ -493,6 +503,8 @@ struct EditorState {
|
||||
[this]() { showMinimap = !showMinimap; });
|
||||
registerCommand("view.annotations", "View: Toggle Annotations", "",
|
||||
[this]() { showAnnotations = !showAnnotations; });
|
||||
registerCommand("view.outline", "View: Toggle Outline", "",
|
||||
[this]() { showOutline = !showOutline; });
|
||||
registerCommand("view.lsp", "View: LSP Servers...", "",
|
||||
[this]() { showLspSettings = !showLspSettings; });
|
||||
registerCommand("mode.toggle", "Mode: Toggle Text/Structured", "",
|
||||
@@ -570,6 +582,9 @@ struct EditorState {
|
||||
active()->highlightsDirty = true;
|
||||
active()->generatedHighlightsDirty = true;
|
||||
active()->modified = true;
|
||||
symbolsPending = true;
|
||||
symbolsLastChange = ImGui::GetTime();
|
||||
if (lsp) lsp->clearDocumentSymbols();
|
||||
if (lsp && active()->path.rfind("(untitled", 0) != 0) {
|
||||
active()->lspVersion += 1;
|
||||
lsp->didChange(toFileUri(active()->path), active()->editBuf, active()->lspVersion);
|
||||
@@ -1179,6 +1194,106 @@ static void collectAnnotationMarkers(const ASTNode* node, std::vector<Annotation
|
||||
}
|
||||
}
|
||||
|
||||
struct OutlineItem {
|
||||
std::string name;
|
||||
std::string kind;
|
||||
int line = -1;
|
||||
int col = -1;
|
||||
std::vector<OutlineItem> children;
|
||||
};
|
||||
|
||||
static OutlineItem makeOutlineItem(const std::string& name,
|
||||
const std::string& kind,
|
||||
const ASTNode* node) {
|
||||
OutlineItem item;
|
||||
item.name = name;
|
||||
item.kind = kind;
|
||||
if (node && node->hasSpan()) {
|
||||
item.line = node->spanStartLine;
|
||||
item.col = node->spanStartCol;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
static void collectOutlineFromFunction(const Function* fn, std::vector<OutlineItem>& out) {
|
||||
OutlineItem item = makeOutlineItem(fn->name, "Function", fn);
|
||||
for (auto* p : fn->getChildren("parameters")) {
|
||||
if (p->conceptType == "Parameter") {
|
||||
auto* param = static_cast<const Parameter*>(p);
|
||||
item.children.push_back(makeOutlineItem(param->name, "Parameter", param));
|
||||
}
|
||||
}
|
||||
for (auto* child : fn->getChildren("body")) {
|
||||
if (child->conceptType == "Variable") {
|
||||
auto* var = static_cast<const Variable*>(child);
|
||||
item.children.push_back(makeOutlineItem(var->name, "Variable", var));
|
||||
}
|
||||
}
|
||||
out.push_back(std::move(item));
|
||||
}
|
||||
|
||||
static void collectOutlineFromAST(const Module* mod, std::vector<OutlineItem>& out) {
|
||||
if (!mod) return;
|
||||
for (auto* child : mod->getChildren("variables")) {
|
||||
if (child->conceptType == "Variable") {
|
||||
auto* var = static_cast<const Variable*>(child);
|
||||
out.push_back(makeOutlineItem(var->name, "Variable", var));
|
||||
}
|
||||
}
|
||||
for (auto* child : mod->getChildren("functions")) {
|
||||
if (child->conceptType == "Function") {
|
||||
auto* fn = static_cast<const Function*>(child);
|
||||
collectOutlineFromFunction(fn, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static std::string toLowerCopy(const std::string& input) {
|
||||
std::string out = input;
|
||||
std::transform(out.begin(), out.end(), out.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool containsCaseInsensitive(const std::string& text, const std::string& filter) {
|
||||
if (filter.empty()) return true;
|
||||
std::string hay = toLowerCopy(text);
|
||||
return hay.find(filter) != std::string::npos;
|
||||
}
|
||||
|
||||
static bool outlineMatchesFilter(const OutlineItem& item, const std::string& filter) {
|
||||
if (filter.empty()) return true;
|
||||
if (containsCaseInsensitive(item.name, filter)) return true;
|
||||
for (const auto& child : item.children) {
|
||||
if (outlineMatchesFilter(child, filter)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char* lspSymbolKindLabel(int kind) {
|
||||
switch (kind) {
|
||||
case 1: return "File";
|
||||
case 2: return "Module";
|
||||
case 5: return "Class";
|
||||
case 6: return "Method";
|
||||
case 12: return "Function";
|
||||
case 13: return "Variable";
|
||||
case 14: return "Constant";
|
||||
case 19: return "String";
|
||||
case 23: return "Struct";
|
||||
default: return "Symbol";
|
||||
}
|
||||
}
|
||||
|
||||
static bool lspSymbolMatchesFilter(const LSPClient::DocumentSymbol& sym, const std::string& filter) {
|
||||
if (filter.empty()) return true;
|
||||
if (containsCaseInsensitive(sym.name, filter)) return true;
|
||||
for (const auto& child : sym.children) {
|
||||
if (lspSymbolMatchesFilter(child, filter)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Theme setup — VSCode Dark-inspired
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1609,6 +1724,7 @@ int main(int, char**) {
|
||||
ImGui::MenuItem("Show Whitespace", nullptr, &state.showWhitespace);
|
||||
ImGui::MenuItem("Show Minimap", nullptr, &state.showMinimap);
|
||||
ImGui::MenuItem("Show Annotations", nullptr, &state.showAnnotations);
|
||||
ImGui::MenuItem("Show Outline", nullptr, &state.showOutline);
|
||||
ImGui::MenuItem("LSP Servers...", nullptr, &state.showLspSettings);
|
||||
if (state.active()) {
|
||||
bool textMode = state.active()->bufferMode == BufferManager::BufferMode::Text;
|
||||
@@ -1749,6 +1865,82 @@ int main(int, char**) {
|
||||
ImGui::PopFont();
|
||||
ImGui::End();
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Symbol Outline
|
||||
// ---------------------------------------------------------------
|
||||
if (state.showOutline) {
|
||||
ImGui::Begin("Outline", &state.showOutline);
|
||||
ImGui::PushFont(uiFont);
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
ImGui::InputText("Filter", state.outlineFilter, sizeof(state.outlineFilter));
|
||||
ImGui::Separator();
|
||||
|
||||
std::string filter = toLowerCopy(state.outlineFilter);
|
||||
if (!state.active()) {
|
||||
ImGui::TextDisabled("(no file)");
|
||||
} else {
|
||||
std::vector<LSPClient::DocumentSymbol> lspSymbols =
|
||||
state.lsp ? state.lsp->getDocumentSymbols() : std::vector<LSPClient::DocumentSymbol>{};
|
||||
|
||||
if (!lspSymbols.empty()) {
|
||||
int outlineId = 0;
|
||||
std::function<void(const LSPClient::DocumentSymbol&)> renderLsp;
|
||||
renderLsp = [&](const LSPClient::DocumentSymbol& sym) {
|
||||
if (!lspSymbolMatchesFilter(sym, filter)) return;
|
||||
ImGui::PushID(outlineId++);
|
||||
std::string label = std::string(lspSymbolKindLabel(sym.kind)) + ": " + sym.name;
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanFullWidth;
|
||||
if (sym.children.empty()) flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||
bool open = ImGui::TreeNodeEx(label.c_str(), flags);
|
||||
if (ImGui::IsItemClicked()) {
|
||||
state.jumpTo(state.active(), sym.selectionRange.start.line,
|
||||
sym.selectionRange.start.character);
|
||||
}
|
||||
if (!sym.children.empty() && open) {
|
||||
for (const auto& child : sym.children) {
|
||||
renderLsp(child);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
};
|
||||
for (const auto& sym : lspSymbols) renderLsp(sym);
|
||||
} else if (state.isStructured()) {
|
||||
std::vector<OutlineItem> outline;
|
||||
collectOutlineFromAST(state.activeAST(), outline);
|
||||
if (outline.empty()) {
|
||||
ImGui::TextDisabled("(no symbols)");
|
||||
} else {
|
||||
int outlineId = 0;
|
||||
std::function<void(const OutlineItem&)> renderItem;
|
||||
renderItem = [&](const OutlineItem& item) {
|
||||
if (!outlineMatchesFilter(item, filter)) return;
|
||||
ImGui::PushID(outlineId++);
|
||||
std::string label = item.kind + ": " + item.name;
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanFullWidth;
|
||||
if (item.children.empty()) flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||
bool open = ImGui::TreeNodeEx(label.c_str(), flags);
|
||||
if (ImGui::IsItemClicked() && item.line >= 0) {
|
||||
state.jumpTo(state.active(), item.line, item.col);
|
||||
}
|
||||
if (!item.children.empty() && open) {
|
||||
for (const auto& child : item.children) {
|
||||
renderItem(child);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
};
|
||||
for (const auto& item : outline) renderItem(item);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("(no symbols)");
|
||||
}
|
||||
}
|
||||
ImGui::PopFont();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Find / Replace bar (floating at top of editor)
|
||||
// ---------------------------------------------------------------
|
||||
@@ -2179,6 +2371,13 @@ int main(int, char**) {
|
||||
state.completionPending = false;
|
||||
}
|
||||
|
||||
if (state.symbolsPending && (now - state.symbolsLastChange) > 0.3) {
|
||||
if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) {
|
||||
state.lsp->requestDocumentSymbols(EditorState::toFileUri(state.active()->path));
|
||||
}
|
||||
state.symbolsPending = false;
|
||||
}
|
||||
|
||||
if (state.analysisPending && (now - state.analysisLastChange) > 0.5) {
|
||||
if (state.isStructured()) {
|
||||
auto result = state.pipeline.run(state.active()->editBuf,
|
||||
|
||||
59
editor/tests/step111_test.cpp
Normal file
59
editor/tests/step111_test.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
// Step 111 TDD Test: Document symbol parsing
|
||||
#include "LSPClient.h"
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
struct DummyTransport : public LSPTransport {
|
||||
void send(const std::string& msg) override { (void)msg; }
|
||||
bool receive(std::string& out) override { (void)out; return false; }
|
||||
bool isOpen() const override { return true; }
|
||||
void close() override {}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
{
|
||||
auto transport = std::make_shared<DummyTransport>();
|
||||
LSPClient client(transport);
|
||||
int id = client.requestDocumentSymbols("file:///test.py");
|
||||
std::string msg = std::string("{\"jsonrpc\":\"2.0\",\"id\":") +
|
||||
std::to_string(id) +
|
||||
",\"result\":[{\"name\":\"foo\",\"kind\":12,\"range\":{\"start\":{\"line\":1,\"character\":0},\"end\":{\"line\":3,\"character\":0}},\"selectionRange\":{\"start\":{\"line\":1,\"character\":4},\"end\":{\"line\":1,\"character\":7}},\"children\":[{\"name\":\"x\",\"kind\":13,\"range\":{\"start\":{\"line\":2,\"character\":2},\"end\":{\"line\":2,\"character\":3}},\"selectionRange\":{\"start\":{\"line\":2,\"character\":2},\"end\":{\"line\":2,\"character\":3}}}]}]}";
|
||||
client.handleMessage(msg);
|
||||
auto symbols = client.getDocumentSymbols();
|
||||
bool ok = symbols.size() == 1 && symbols[0].name == "foo" && symbols[0].children.size() == 1 &&
|
||||
symbols[0].selectionRange.start.line == 1 && symbols[0].selectionRange.start.character == 4 &&
|
||||
symbols[0].children[0].name == "x" && symbols[0].children[0].range.start.line == 2;
|
||||
expect(ok, "document symbol tree", passed, failed);
|
||||
}
|
||||
|
||||
{
|
||||
auto transport = std::make_shared<DummyTransport>();
|
||||
LSPClient client(transport);
|
||||
int id = client.requestDocumentSymbols("file:///main.py");
|
||||
std::string msg = std::string("{\"jsonrpc\":\"2.0\",\"id\":") +
|
||||
std::to_string(id) +
|
||||
",\"result\":[{\"name\":\"bar\",\"kind\":12,\"location\":{\"uri\":\"file:///main.py\",\"range\":{\"start\":{\"line\":4,\"character\":0},\"end\":{\"line\":4,\"character\":3}}}}]}";
|
||||
client.handleMessage(msg);
|
||||
auto symbols = client.getDocumentSymbols();
|
||||
bool ok = symbols.size() == 1 && symbols[0].name == "bar" &&
|
||||
symbols[0].range.start.line == 4 && symbols[0].range.start.character == 0 &&
|
||||
symbols[0].selectionRange.start.line == 4;
|
||||
expect(ok, "symbol information list", passed, failed);
|
||||
}
|
||||
|
||||
std::cout << "\n=== Step 111 Results: " << passed << " passed, " << failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -321,7 +321,7 @@ Whetstone-specific AST navigation.
|
||||
Works across files when LSP supports it.
|
||||
*Wires:* `ContextAPI.h`, `LSPClient.h`
|
||||
|
||||
- [ ] **Step 111: Symbol outline panel**
|
||||
- [x] **Step 111: Symbol outline panel**
|
||||
New "Outline" panel. Primary: use LSP `textDocument/documentSymbol` for the
|
||||
symbol tree. Fallback: walk Whetstone AST for functions/variables/parameters.
|
||||
Click to navigate. Icons by symbol kind. Search/filter box.
|
||||
|
||||
Reference in New Issue
Block a user