Step 135: library-aware completion
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
49
editor/src/CompletionUtils.h
Normal file
49
editor/src/CompletionUtils.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
#include "LSPClient.h"
|
||||
#include "PrimitivesRegistry.h"
|
||||
#include <unordered_set>
|
||||
#include <algorithm>
|
||||
|
||||
struct CompletionBuildResult {
|
||||
std::vector<LSPClient::CompletionItem> items;
|
||||
std::unordered_set<std::string> 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<LSPClient::CompletionItem>& lspItems,
|
||||
const std::vector<PrimitiveSymbol>& primitives,
|
||||
const std::string& prefix) {
|
||||
CompletionBuildResult out;
|
||||
out.items = lspItems;
|
||||
std::unordered_set<std::string> 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;
|
||||
}
|
||||
@@ -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<PrimitiveSymbol> 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<LSPClient::CompletionItem> 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();
|
||||
|
||||
44
editor/tests/step135_test.cpp
Normal file
44
editor/tests/step135_test.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
// Step 135 TDD Test: Library-aware completion ranking
|
||||
#include "CompletionUtils.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;
|
||||
|
||||
std::vector<LSPClient::CompletionItem> lsp = {
|
||||
{"foo", 0, "", "foo", ""},
|
||||
{"bar", 0, "", "bar", ""}
|
||||
};
|
||||
std::vector<PrimitiveSymbol> 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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user