Step 353: Command Palette (12/12 tests)

This commit is contained in:
Bill
2026-02-16 09:03:01 -07:00
parent 0ba936da2b
commit e79e42bab1
4 changed files with 347 additions and 3 deletions

View File

@@ -2106,4 +2106,8 @@ add_executable(step352_test tests/step352_test.cpp)
target_include_directories(step352_test PRIVATE src) target_include_directories(step352_test PRIVATE src)
target_link_libraries(step352_test PRIVATE nlohmann_json::nlohmann_json) target_link_libraries(step352_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step353_test tests/step353_test.cpp)
target_include_directories(step353_test PRIVATE src)
target_link_libraries(step353_test PRIVATE nlohmann_json::nlohmann_json)
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)

View File

@@ -2,14 +2,20 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <unordered_map> #include <unordered_map>
#include <optional>
#include <algorithm> #include <algorithm>
#include "KeybindingRegistry.h"
#include "PanelManager.h"
#include "Icons.h"
struct CommandEntry { struct CommandEntry {
std::string id; std::string id;
std::string label; std::string label;
std::string shortcut; std::string shortcut;
std::string icon;
std::string category; std::string category;
std::string inputHint; std::string inputHint;
std::vector<std::string> aliases;
int contextMask = 0; int contextMask = 0;
bool inlineInput = false; bool inlineInput = false;
int lastUsed = 0; int lastUsed = 0;
@@ -32,13 +38,33 @@ enum CommandContext : int {
class CommandPalette { class CommandPalette {
public: public:
void open() {
isOpen_ = true;
selectedIndex_ = 0;
}
void close() {
isOpen_ = false;
}
bool isOpen() const { return isOpen_; }
void setQuery(const std::string& q) {
query_ = q;
selectedIndex_ = 0;
}
const std::string& query() const { return query_; }
void registerCommand(const std::string& id, void registerCommand(const std::string& id,
const std::string& label, const std::string& label,
const std::string& shortcut, const std::string& shortcut,
const std::string& category = "", const std::string& category = "",
int contextMask = CommandContext_Any, int contextMask = CommandContext_Any,
const std::string& inputHint = "", const std::string& inputHint = "",
bool inlineInput = false) { bool inlineInput = false,
const std::vector<std::string>& aliases = {},
const std::string& icon = "") {
auto& entry = commands_[id]; auto& entry = commands_[id];
entry.id = id; entry.id = id;
entry.label = label; entry.label = label;
@@ -47,6 +73,43 @@ public:
entry.contextMask = contextMask; entry.contextMask = contextMask;
entry.inputHint = inputHint; entry.inputHint = inputHint;
entry.inlineInput = inlineInput; entry.inlineInput = inlineInput;
entry.aliases = aliases;
entry.icon = icon;
}
void registerFromKeybindings(const KeybindingRegistry& registry) {
for (const auto& action : registry.getActions()) {
const auto binding = registry.getBinding(action.id);
registerCommand(
action.id,
action.label,
binding.toString(),
action.category,
CommandContext_Any,
"",
false,
{action.id},
WhetstoneIcons::Search
);
}
}
void registerPanelToggles(const PanelManager& panels) {
for (const auto& panel : panels.getPanelList()) {
std::string id = "toggle-" + panel.id;
std::string label = "Toggle " + panel.title;
registerCommand(
id,
label,
"",
"View",
CommandContext_Any,
"",
false,
{panel.id, panel.title},
panel.icon.empty() ? WhetstoneIcons::Search : panel.icon
);
}
} }
void markUsed(const std::string& id) { void markUsed(const std::string& id) {
@@ -67,9 +130,44 @@ public:
if (query.empty()) { if (query.empty()) {
match.score = cmd.lastUsed > 0 ? 50 + cmd.lastUsed : 1; match.score = cmd.lastUsed > 0 ? 50 + cmd.lastUsed : 1;
} else { } else {
int bestScore = 0;
std::vector<int> bestIndices;
bool found = false;
int score = 0; int score = 0;
if (!fuzzyMatch(cmd.label, query, score, match.matchIndices)) continue; std::vector<int> indices;
match.score = score; if (fuzzyMatch(cmd.label, query, score, indices)) {
found = true;
bestScore = score + 20; // label match bias
bestIndices = indices;
}
if (fuzzyMatch(cmd.id, query, score, indices)) {
found = true;
if (score + 10 > bestScore) {
bestScore = score + 10;
bestIndices = indices;
}
}
if (fuzzyMatch(cmd.category, query, score, indices)) {
found = true;
if (score + 5 > bestScore) {
bestScore = score + 5;
bestIndices = indices;
}
}
for (const auto& alias : cmd.aliases) {
if (fuzzyMatch(alias, query, score, indices)) {
found = true;
if (score + 15 > bestScore) {
bestScore = score + 15;
bestIndices = indices;
}
}
}
if (!found) continue;
match.score = bestScore;
match.matchIndices = bestIndices;
if (contextOk) match.score += 25; if (contextOk) match.score += 25;
else match.score -= 10; else match.score -= 10;
} }
@@ -105,6 +203,27 @@ public:
return result; return result;
} }
void moveSelection(int delta, int resultCount) {
if (resultCount <= 0) {
selectedIndex_ = 0;
return;
}
selectedIndex_ += delta;
if (selectedIndex_ < 0) selectedIndex_ = resultCount - 1;
if (selectedIndex_ >= resultCount) selectedIndex_ = 0;
}
int selectedIndex() const { return selectedIndex_; }
std::optional<std::string> executeSelected(const std::vector<CommandMatch>& matches) {
if (matches.empty() || selectedIndex_ < 0 || selectedIndex_ >= (int)matches.size()) {
return std::nullopt;
}
const std::string id = matches[selectedIndex_].entry.id;
markUsed(id);
return id;
}
static bool fuzzyMatch(const std::string& text, static bool fuzzyMatch(const std::string& text,
const std::string& query, const std::string& query,
int& outScore, int& outScore,
@@ -150,4 +269,7 @@ private:
std::unordered_map<std::string, CommandEntry> commands_; std::unordered_map<std::string, CommandEntry> commands_;
int useCounter_ = 0; int useCounter_ = 0;
bool isOpen_ = false;
std::string query_;
int selectedIndex_ = 0;
}; };

View File

@@ -0,0 +1,180 @@
// Step 353: Command Palette (12 tests)
#include <cassert>
#include <iostream>
#include <string>
#include <vector>
#include "CommandPalette.h"
#include "KeybindingRegistry.h"
#include "PanelManager.h"
int main() {
int passed = 0;
// Test 1: open/close state
{
CommandPalette cp;
assert(!cp.isOpen());
cp.open();
assert(cp.isOpen());
cp.close();
assert(!cp.isOpen());
std::cout << "Test 1 PASSED: open/close state\n";
passed++;
}
// Test 2: fuzzy search by action label
{
CommandPalette cp;
cp.registerCommand("save-buffer", "Save Buffer", "Ctrl+S", "File");
auto r = cp.search("svb");
assert(!r.empty());
assert(r[0].entry.id == "save-buffer");
std::cout << "Test 2 PASSED: fuzzy search by label\n";
passed++;
}
// Test 3: fuzzy search by alias
{
CommandPalette cp;
cp.registerCommand("run-pipeline", "Run Pipeline", "F5", "Tools",
CommandContext_Any, "", false, {"build", "compile"});
auto r = cp.search("bld");
assert(!r.empty());
assert(r[0].entry.id == "run-pipeline");
std::cout << "Test 3 PASSED: fuzzy search by alias\n";
passed++;
}
// Test 4: register from keybindings
{
KeybindingRegistry reg;
reg.loadDefaults();
CommandPalette cp;
cp.registerFromKeybindings(reg);
auto all = cp.all();
assert(all.size() >= 20);
bool hasSave = false;
for (const auto& c : all) {
if (c.id == "save-buffer") {
hasSave = true;
assert(c.shortcut == "Ctrl+S");
}
}
assert(hasSave);
std::cout << "Test 4 PASSED: register from keybindings\n";
passed++;
}
// Test 5: register panel toggles
{
PanelManager pm;
pm.registerDefaults();
CommandPalette cp;
cp.registerPanelToggles(pm);
auto all = cp.all();
bool hasFileTree = false;
for (const auto& c : all) {
if (c.id == "toggle-file-tree") {
hasFileTree = true;
assert(c.label.find("File Tree") != std::string::npos);
}
}
assert(hasFileTree);
std::cout << "Test 5 PASSED: register panel toggles\n";
passed++;
}
// Test 6: enter executes selected action
{
CommandPalette cp;
cp.registerCommand("open-file", "Open File", "Ctrl+O", "File");
auto r = cp.search("");
auto id = cp.executeSelected(r);
assert(id.has_value());
assert(id.value() == "open-file");
std::cout << "Test 6 PASSED: execute selected action\n";
passed++;
}
// Test 7: arrow navigation wraps
{
CommandPalette cp;
cp.registerCommand("a", "Action A", "", "");
cp.registerCommand("b", "Action B", "", "");
auto r = cp.search("");
assert(!r.empty());
cp.moveSelection(-1, (int)r.size());
assert(cp.selectedIndex() == (int)r.size() - 1);
cp.moveSelection(1, (int)r.size());
assert(cp.selectedIndex() == 0);
std::cout << "Test 7 PASSED: arrow navigation wraps\n";
passed++;
}
// Test 8: query reset resets selection
{
CommandPalette cp;
cp.registerCommand("x", "Action X", "", "");
cp.registerCommand("y", "Action Y", "", "");
auto r = cp.search("");
cp.moveSelection(1, (int)r.size());
assert(cp.selectedIndex() == 1);
cp.setQuery("a");
assert(cp.selectedIndex() == 0);
std::cout << "Test 8 PASSED: query reset resets selection\n";
passed++;
}
// Test 9: recently used actions appear first on empty query
{
CommandPalette cp;
cp.registerCommand("open", "Open", "", "");
cp.registerCommand("save", "Save", "", "");
cp.markUsed("save");
auto r = cp.search("");
assert(!r.empty());
assert(r[0].entry.id == "save");
std::cout << "Test 9 PASSED: recent actions first\n";
passed++;
}
// Test 10: strict context filter
{
CommandPalette cp;
cp.registerCommand("editor-only", "Editor Only", "", "",
CommandContext_Editor);
cp.registerCommand("terminal-only", "Terminal Only", "", "",
CommandContext_Terminal);
auto editor = cp.search("", CommandContext_Editor, true);
assert(editor.size() == 1);
assert(editor[0].entry.id == "editor-only");
std::cout << "Test 10 PASSED: strict context filter\n";
passed++;
}
// Test 11: search can match category text
{
CommandPalette cp;
cp.registerCommand("run", "Run Pipeline", "F5", "Tools");
auto r = cp.search("tls");
assert(!r.empty());
assert(r[0].entry.id == "run");
std::cout << "Test 11 PASSED: category text search\n";
passed++;
}
// Test 12: no-match query returns empty results
{
CommandPalette cp;
cp.registerCommand("save", "Save Buffer", "Ctrl+S", "File");
auto r = cp.search("zzzz");
assert(r.empty());
std::cout << "Test 12 PASSED: no-match query returns empty\n";
passed++;
}
std::cout << "\nResults: " << passed << "/12\n";
assert(passed == 12);
return 0;
}

View File

@@ -1752,6 +1752,44 @@ with shortcut columns, and multi-key chord strings in both text and symbol forms
- `step351_test` — PASS (12/12) regression coverage - `step351_test` — PASS (12/12) regression coverage
- `step352_test` — PASS (12/12) new step coverage - `step352_test` — PASS (12/12) new step coverage
### Step 353: Command Palette
**Status:** PASS (12/12 tests)
Extended the command palette into a richer step-353 foundation: stateful open/close,
selection movement, execute-selected behavior, and broadened fuzzy search over labels,
ids, categories, and aliases. Added population helpers that pull actions from
`KeybindingRegistry` and panel toggles from `PanelManager`.
**Files modified:**
- `editor/src/CommandPalette.h` — added:
- palette state APIs: `open()`, `close()`, `isOpen()`, `setQuery()`, `selectedIndex()`
- command metadata: `icon`, `aliases`
- registration helpers: `registerFromKeybindings(...)`, `registerPanelToggles(...)`
- expanded fuzzy search scoring across label/id/category/aliases
- selection navigation: `moveSelection(...)`
- execution helper: `executeSelected(...)` with recent-use tracking
- `editor/CMakeLists.txt``step353_test` target
**Files created:**
- `editor/tests/step353_test.cpp` — 12 tests covering:
1. open/close state
2. fuzzy search by label
3. fuzzy search by alias
4. keybinding registry population
5. panel toggle population
6. execute selected action
7. selection wrap behavior
8. query reset behavior
9. recent action ranking
10. strict context filtering
11. category-based matching
12. no-match behavior
**Verification run:**
- `step344_test` — PASS (12/12) command palette integration regression
- `step346_test` — PASS (8/8) layout + palette integration regression
- `step353_test` — PASS (12/12) new step coverage
# Roadmap Planning — Sprints 12-25+ # Roadmap Planning — Sprints 12-25+
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md) ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)