Step 353: Command Palette (12/12 tests)
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
180
editor/tests/step353_test.cpp
Normal file
180
editor/tests/step353_test.cpp
Normal 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;
|
||||||
|
}
|
||||||
38
progress.md
38
progress.md
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user