diff --git a/PROGRESS.md b/PROGRESS.md index 48c38cb..49ecf87 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 134 (available primitives registry) done. Next: Step 135 (library-aware completion). +Sprint 5 in progress. Step 135 (library-aware completion) done. Next: Step 136 (agent library-aware mode). --- @@ -480,3 +480,4 @@ Sprint 5 in progress. Step 134 (available primitives registry) done. Next: Step | 2026-02-09 | Codex | Step 132: Library symbol browser panel with filtering, doc detail display, and insert template helpers. 5/5 tests pass. | | 2026-02-09 | Codex | Step 133: Import statement generation + unused import warnings across Python/JS/Rust/Go/Elisp with auto-insert on library symbol use. 7/7 tests pass. | | 2026-02-09 | Codex | Step 134: PrimitivesRegistry aggregating builtins, imports, and in-scope symbols. 6/6 tests pass. | +| 2026-02-09 | Codex | Step 135: Library-aware completion ordering with auto-import hints for non-primitive symbols. 5/5 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index da717a5..172345e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -754,6 +754,10 @@ add_executable(step134_test tests/step134_test.cpp) target_include_directories(step134_test PRIVATE src) target_link_libraries(step134_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step135_test tests/step135_test.cpp) +target_include_directories(step135_test PRIVATE src) +target_link_libraries(step135_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/CompletionUtils.h b/editor/src/CompletionUtils.h new file mode 100644 index 0000000..b7f9593 --- /dev/null +++ b/editor/src/CompletionUtils.h @@ -0,0 +1,49 @@ +#pragma once +#include "LSPClient.h" +#include "PrimitivesRegistry.h" +#include +#include + +struct CompletionBuildResult { + std::vector items; + std::unordered_set preferredNames; +}; + +static inline bool matchesPrefix(const std::string& text, const std::string& prefix) { + if (prefix.empty()) return true; + return text.rfind(prefix, 0) == 0; +} + +static inline CompletionBuildResult buildLibraryAwareCompletions( + const std::vector& lspItems, + const std::vector& primitives, + const std::string& prefix) { + CompletionBuildResult out; + out.items = lspItems; + std::unordered_set existing; + for (const auto& item : lspItems) { + existing.insert(item.label); + } + + for (const auto& prim : primitives) { + out.preferredNames.insert(prim.name); + if (!matchesPrefix(prim.name, prefix)) continue; + if (existing.insert(prim.name).second) { + LSPClient::CompletionItem ci; + ci.label = prim.name; + ci.insertText = prim.name; + ci.detail = prim.source; + out.items.push_back(std::move(ci)); + } + } + + std::stable_sort(out.items.begin(), out.items.end(), + [&](const LSPClient::CompletionItem& a, const LSPClient::CompletionItem& b) { + bool aPref = out.preferredNames.count(a.label) > 0; + bool bPref = out.preferredNames.count(b.label) > 0; + if (aPref != bPref) return aPref > bPref; + return a.label < b.label; + }); + + return out; +} diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 0fb2fc3..96c41a5 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -14,6 +14,7 @@ #include "EditorState.h" #include "EditorUtils.h" +#include "CompletionUtils.h" // --------------------------------------------------------------------------- // Main @@ -1315,9 +1316,25 @@ int main(int, char**) { auto items = state.lsp->getCompletionItems(); int cursor = state.active()->widget.getCursor(); std::string prefix = EditorState::wordPrefixAt(state.active()->editBuf, cursor); + std::string nodeId; + if (state.activeAST()) { + ASTNode* scopeNode = findNodeAtPosition(state.activeAST(), + std::max(0, state.active()->cursorLine - 1), + std::max(0, state.active()->cursorCol - 1)); + if (scopeNode) nodeId = scopeNode->id; + } + std::vector primitives; + auto funcs = state.primitives.getAvailableFunctions(nodeId); + auto types = state.primitives.getAvailableTypes(nodeId); + auto consts = state.primitives.getAvailableConstants(nodeId); + primitives.insert(primitives.end(), funcs.begin(), funcs.end()); + primitives.insert(primitives.end(), types.begin(), types.end()); + primitives.insert(primitives.end(), consts.begin(), consts.end()); + + auto built = buildLibraryAwareCompletions(items, primitives, prefix); std::vector filtered; - filtered.reserve(items.size()); - for (const auto& item : items) { + filtered.reserve(built.items.size()); + for (const auto& item : built.items) { const std::string& key = item.filterText.empty() ? item.label : item.filterText; if (prefix.empty() || key.rfind(prefix, 0) == 0) filtered.push_back(item); } @@ -1342,20 +1359,44 @@ int main(int, char**) { state.completionVisible = false; } + auto deriveLibrary = [](const std::string& name) { + auto pos = name.find("::"); + if (pos != std::string::npos) return name.substr(0, pos); + pos = name.find('.'); + if (pos != std::string::npos) return name.substr(0, pos); + return std::string(); + }; + for (int i = 0; i < (int)filtered.size(); ++i) { std::string itemLabel = filtered[i].label; if (filtered[i].kind != 0) { itemLabel = "[" + std::to_string(filtered[i].kind) + "] " + itemLabel; } bool selected = (i == state.completionSelected); + bool preferred = built.preferredNames.count(filtered[i].label) > 0; + std::string libHint = deriveLibrary(filtered[i].label); + if (!preferred && !libHint.empty()) itemLabel += " (auto-import)"; + if (!preferred) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + } if (ImGui::Selectable(itemLabel.c_str(), selected)) { state.completionSelected = i; accept = true; } + if (!preferred) { + ImGui::PopStyleColor(); + } } if (accept && !filtered.empty()) { const auto& item = filtered[state.completionSelected]; + bool preferred = built.preferredNames.count(item.label) > 0; + if (!preferred) { + std::string libHint = deriveLibrary(item.label); + if (!libHint.empty()) { + state.ensureImportForSymbol(libHint, item.label); + } + } int start = EditorState::wordStartAt(state.active()->editBuf, cursor); state.applyCompletion(state.active(), item.insertText, start, cursor); state.lsp->clearCompletionItems(); diff --git a/editor/tests/step135_test.cpp b/editor/tests/step135_test.cpp new file mode 100644 index 0000000..6cb59c8 --- /dev/null +++ b/editor/tests/step135_test.cpp @@ -0,0 +1,44 @@ +// Step 135 TDD Test: Library-aware completion ranking +#include "CompletionUtils.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; + + std::vector lsp = { + {"foo", 0, "", "foo", ""}, + {"bar", 0, "", "bar", ""} + }; + std::vector prims = { + {"bar", "function", "import", "lib"}, + {"baz", "function", "import", "lib"} + }; + + auto built = buildLibraryAwareCompletions(lsp, prims, ""); + expect(built.preferredNames.count("bar") == 1, "preferred contains bar", passed, failed); + expect(built.preferredNames.count("baz") == 1, "preferred contains baz", passed, failed); + expect(built.items.size() == 3, "completion list includes primitive", passed, failed); + expect(built.items[0].label == "bar" || built.items[0].label == "baz", + "preferred items sorted first", passed, failed); + + auto builtPref = buildLibraryAwareCompletions(lsp, prims, "ba"); + bool hasBaz = false; + for (const auto& item : builtPref.items) { + if (item.label == "baz") hasBaz = true; + } + expect(hasBaz, "prefix adds matching primitive", passed, failed); + + std::cout << "\n=== Step 135 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint5_plan.md b/sprint5_plan.md index c90beb0..5815347 100644 --- a/sprint5_plan.md +++ b/sprint5_plan.md @@ -110,7 +110,7 @@ what's already available. `getAvailableConstants()`, filtered by scope and import state. *New:* `PrimitivesRegistry.h` -- [ ] **Step 135: Library-aware completion** +- [x] **Step 135: Library-aware completion** Modify the completion system (LSP + Whetstone) to **prioritize** symbols from `PrimitivesRegistry` without excluding others. When an agent or user types, completion candidates are ranked: imported library functions first,