diff --git a/PROGRESS.md b/PROGRESS.md index f82a565..f900234 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -382,7 +382,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi ## What's Next -Sprint 5 in progress. Step 131 (library API indexing via type stubs) done. Next: Step 132 (library symbol browser). +Sprint 5 in progress. Step 132 (library symbol browser) done. Next: Step 133 (import statement generation). --- @@ -477,3 +477,4 @@ Sprint 5 in progress. Step 131 (library API indexing via type stubs) done. Next: | 2026-02-09 | Codex | Step 129: Dependency management UI panel with add/remove/update, source targeting, and writeback support for requirements.txt/package.json/Cargo.toml/go.mod/vcpkg.json. 8/8 tests pass. | | 2026-02-09 | Codex | Step 130: LSP workspace symbol + completion indexing for dependencies with ExternalModule population and library index polling. 7/7 tests pass. | | 2026-02-09 | Codex | Step 131: Stub-based library indexing (pyi/d.ts/headers/lib.rs) with workspace scan fallback. 8/8 tests pass. | +| 2026-02-09 | Codex | Step 132: Library symbol browser panel with filtering, doc detail display, and insert template helpers. 5/5 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index aa325b1..1d2ea3d 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -740,6 +740,12 @@ add_executable(step131_test tests/step131_test.cpp) target_include_directories(step131_test PRIVATE src) target_link_libraries(step131_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step132_test tests/step132_test.cpp) +target_include_directories(step132_test PRIVATE src) +target_link_libraries(step132_test PRIVATE + imgui::imgui + nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index b0dfc87..a680ca7 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -45,6 +45,7 @@ #include "BuildSystem.h" #include "DependencyPanel.h" #include "LibraryIndexer.h" +#include "LibraryBrowserPanel.h" #include "IncrementalOptimizer.h" #include "ast/Serialization.h" #include "ast/Generator.h" @@ -167,6 +168,8 @@ struct EditorState { std::string lastBuildCommand; bool showDependencyPanel = true; DependencyPanelState dependencyPanel; + bool showLibraryBrowserPanel = true; + LibraryBrowserState libraryBrowser; struct LibraryIndexRequest { std::string name; std::string version; @@ -355,6 +358,15 @@ struct EditorState { onTextChanged(); } + void insertTextAtCursor(const std::string& text) { + if (!active() || text.empty()) return; + bool changed = false; + active()->widget.insertText(active()->editBuf, text, changed); + if (changed) { + onTextChanged(); + } + } + std::string makeUntitledName() const { if (!buffers.hasBuffer("(untitled)")) return "(untitled)"; int i = 1; diff --git a/editor/src/LibraryBrowserPanel.h b/editor/src/LibraryBrowserPanel.h new file mode 100644 index 0000000..79d2d89 --- /dev/null +++ b/editor/src/LibraryBrowserPanel.h @@ -0,0 +1,120 @@ +#pragma once +#include "imgui.h" +#include "LibraryIndexer.h" +#include "ast/ExternalModule.h" +#include +#include +#include +#include + +struct LibraryBrowserState { + char filterBuf[128] = {}; + std::string selectedLibrary; + std::string selectedSymbol; + std::string docText; +}; + +static std::string formatUsageTemplate(const std::string& symbol) { + if (symbol.empty()) return symbol; + if (std::isupper((unsigned char)symbol[0])) return symbol; + if (symbol.find('(') != std::string::npos) return symbol; + return symbol + "()"; +} + +static void updateDocText(LibraryBrowserState& state, + const LibraryIndexData& index, + const std::string& library, + const std::string& symbol) { + state.docText.clear(); + auto it = index.symbolsByLibrary.find(library); + if (it != index.symbolsByLibrary.end()) { + for (const auto& sym : it->second) { + if (sym.name == symbol) { + if (!sym.detail.empty()) { + state.docText = sym.detail; + } + break; + } + } + } + if (state.docText.empty()) { + state.docText = "No documentation available."; + } +} + +static bool symbolMatchesFilter(const std::string& symbol, const std::string& filter) { + if (filter.empty()) return true; + std::string a = symbol; + std::string b = filter; + std::transform(a.begin(), a.end(), a.begin(), + [](unsigned char c) { return (char)std::tolower(c); }); + std::transform(b.begin(), b.end(), b.begin(), + [](unsigned char c) { return (char)std::tolower(c); }); + return a.find(b) != std::string::npos; +} + +static bool renderLibraryBrowser(LibraryBrowserState& state, + Module* ast, + const LibraryIndexData& index, + std::string& outInsert, + std::string& outputLog) { + outInsert.clear(); + if (!ast) { + ImGui::TextDisabled("(no structured AST)"); + return false; + } + + ImGui::InputText("Filter", state.filterBuf, sizeof(state.filterBuf)); + std::string filter = state.filterBuf; + + ImGui::Separator(); + bool inserted = false; + auto modules = ast->getChildren("externalModules"); + if (modules.empty()) { + ImGui::TextDisabled("(no libraries indexed)"); + return false; + } + + for (auto* node : modules) { + auto* ext = static_cast(node); + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanFullWidth; + bool open = ImGui::TreeNodeEx(ext->name.c_str(), flags); + if (open) { + auto sigs = ext->getChildren("signatures"); + for (auto* sigNode : sigs) { + auto* sig = static_cast(sigNode); + if (!symbolMatchesFilter(sig->name, filter)) continue; + std::string label = sig->name; + bool selected = (state.selectedLibrary == ext->name && state.selectedSymbol == sig->name); + if (ImGui::Selectable(label.c_str(), selected)) { + state.selectedLibrary = ext->name; + state.selectedSymbol = sig->name; + updateDocText(state, index, ext->name, sig->name); + } + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { + std::string usage = formatUsageTemplate(sig->name); + ImGui::SetDragDropPayload("WhetstoneSymbol", usage.c_str(), usage.size() + 1); + ImGui::TextUnformatted(usage.c_str()); + ImGui::EndDragDropSource(); + } + } + ImGui::TreePop(); + } + } + + if (!state.selectedSymbol.empty()) { + ImGui::Separator(); + ImGui::Text("Selected: %s", state.selectedSymbol.c_str()); + ImGui::TextWrapped("%s", state.docText.c_str()); + std::string usage = formatUsageTemplate(state.selectedSymbol); + if (ImGui::Button("Insert")) { + outInsert = usage; + outputLog += "[libs] Inserted " + usage + "\n"; + inserted = true; + } + ImGui::SameLine(); + ImGui::TextDisabled("%s", usage.c_str()); + } + + return inserted; +} diff --git a/editor/src/main.cpp b/editor/src/main.cpp index f7e91d6..4a38302 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -305,6 +305,7 @@ int main(int, char**) { ImGui::MenuItem("Terminal", state.keys.getBinding("view.toggleTerminal").toString().c_str(), &state.showTerminalPanel); ImGui::MenuItem("Dependencies", nullptr, &state.showDependencyPanel); + ImGui::MenuItem("Libraries", nullptr, &state.showLibraryBrowserPanel); ImGui::MenuItem("Settings", nullptr, &state.showSettingsPanel); ImGui::MenuItem("LSP Servers...", nullptr, &state.showLspSettings); if (state.active()) { @@ -568,6 +569,21 @@ int main(int, char**) { } } + if (state.showLibraryBrowserPanel) { + ImGui::Begin("Libraries", &state.showLibraryBrowserPanel); + ImGui::PushFont(uiFont); + std::string insertText; + if (renderLibraryBrowser(state.libraryBrowser, + state.activeAST(), + state.libraryIndex, + insertText, + state.outputLog)) { + state.insertTextAtCursor(insertText); + } + ImGui::PopFont(); + ImGui::End(); + } + // --------------------------------------------------------------- // Find / Replace bar (floating at top of editor) // --------------------------------------------------------------- diff --git a/editor/tests/step132_test.cpp b/editor/tests/step132_test.cpp new file mode 100644 index 0000000..9d1f8f4 --- /dev/null +++ b/editor/tests/step132_test.cpp @@ -0,0 +1,37 @@ +// Step 132 TDD Test: Library symbol browser panel helpers +#include "LibraryBrowserPanel.h" +#include "ast/ExternalModule.h" +#include "ast/TypeSignature.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; + + expect(formatUsageTemplate("foo") == "foo()", "usage template adds parens", passed, failed); + expect(formatUsageTemplate("Widget") == "Widget", "usage template keeps class", passed, failed); + expect(symbolMatchesFilter("numpy.array", "ARRAY"), "filter is case-insensitive", passed, failed); + + LibraryIndexData index; + index.symbolsByLibrary["numpy"].push_back({"numpy.array", "numpy", "Create array", 0}); + LibraryBrowserState state; + updateDocText(state, index, "numpy", "numpy.array"); + expect(state.docText == "Create array", "doc text from LSP detail", passed, failed); + + LibraryBrowserState fallbackState; + updateDocText(fallbackState, index, "missing", "missing"); + expect(fallbackState.docText == "No documentation available.", "fallback doc text", passed, failed); + + std::cout << "\n=== Step 132 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint5_plan.md b/sprint5_plan.md index fb47320..a8dd054 100644 --- a/sprint5_plan.md +++ b/sprint5_plan.md @@ -71,7 +71,7 @@ The foundation: import libraries, parse their APIs, index their symbols. `TypeSignature` AST nodes attached to `ExternalModule`. *New:* `StubParser.h`, modifies `ExternalModule` AST concept -- [ ] **Step 132: Library symbol browser** +- [x] **Step 132: Library symbol browser** New panel or Explorer sub-section: "Libraries". Tree view of imported libraries → modules → classes/functions → parameters. Click to see documentation (from LSP hover or stub docstrings). Search/filter box.