Step 351: Keybinding Registry (12/12 tests)
This commit is contained in:
@@ -2098,4 +2098,8 @@ add_executable(step350_test tests/step350_test.cpp)
|
||||
target_include_directories(step350_test PRIVATE src)
|
||||
target_link_libraries(step350_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step351_test tests/step351_test.cpp)
|
||||
target_include_directories(step351_test PRIVATE src)
|
||||
target_link_libraries(step351_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
enum class KeyModifier : uint8_t {
|
||||
@@ -49,13 +50,39 @@ struct KeyCombo {
|
||||
if (hasModifier(modifiers, KeyModifier::Alt)) result += "\xE2\x8C\xA5"; // ⌥
|
||||
if (hasModifier(modifiers, KeyModifier::Shift)) result += "\xE2\x87\xA7"; // ⇧
|
||||
if (hasModifier(modifiers, KeyModifier::Super)) result += "\xE2\x8C\x98"; // ⌘
|
||||
result += key;
|
||||
result += keyToSymbol(key);
|
||||
} else {
|
||||
result = toString(); // Linux/Windows uses text form
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string normalizeKey(const std::string& in) {
|
||||
std::string k = in;
|
||||
std::transform(k.begin(), k.end(), k.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::toupper(c)); });
|
||||
return k;
|
||||
}
|
||||
|
||||
static std::string keyToSymbol(const std::string& keyName) {
|
||||
std::string k = normalizeKey(keyName);
|
||||
if (k == "ENTER" || k == "RETURN") return "\xE2\x86\xB5"; // ↵
|
||||
if (k == "ESC" || k == "ESCAPE") return "\xE2\x8E\x8B"; // ⎋
|
||||
if (k == "TAB") return "\xE2\x87\xA5"; // ⇥
|
||||
if (k == "SPACE") return "\xE2\x90\xA0"; // ␠
|
||||
if (k == "BACKSPACE") return "\xE2\x8C\xAB"; // ⌫
|
||||
if (k == "DELETE") return "\xE2\x8C\xA6"; // ⌦
|
||||
return keyName;
|
||||
}
|
||||
|
||||
std::string toSymbolsAuto() const {
|
||||
#ifdef __APPLE__
|
||||
return toSymbols(true);
|
||||
#else
|
||||
return toSymbols(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool operator==(const KeyCombo& other) const {
|
||||
return modifiers == other.modifiers && key == other.key;
|
||||
}
|
||||
@@ -87,6 +114,23 @@ struct KeyCombo {
|
||||
static KeyCombo shiftFkey(const std::string& k) {
|
||||
return {static_cast<uint8_t>(KeyModifier::Shift), k};
|
||||
}
|
||||
|
||||
struct InputState {
|
||||
bool ctrl = false;
|
||||
bool alt = false;
|
||||
bool shift = false;
|
||||
bool super = false;
|
||||
std::string key;
|
||||
};
|
||||
|
||||
bool matches(const InputState& input) const {
|
||||
if (normalizeKey(key) != normalizeKey(input.key)) return false;
|
||||
if (hasModifier(modifiers, KeyModifier::Ctrl) != input.ctrl) return false;
|
||||
if (hasModifier(modifiers, KeyModifier::Alt) != input.alt) return false;
|
||||
if (hasModifier(modifiers, KeyModifier::Shift) != input.shift) return false;
|
||||
if (hasModifier(modifiers, KeyModifier::Super) != input.super) return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct KeyAction {
|
||||
@@ -141,6 +185,7 @@ public:
|
||||
}
|
||||
|
||||
std::map<std::string, KeyCombo> getAllBindings() const { return bindings_; }
|
||||
std::map<std::string, KeyCombo> getAll() const { return bindings_; }
|
||||
|
||||
int actionCount() const { return static_cast<int>(actions_.size()); }
|
||||
int bindingCount() const { return static_cast<int>(bindings_.size()); }
|
||||
@@ -216,6 +261,33 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
bool saveToJson(const std::string& path) const {
|
||||
std::ofstream out(path);
|
||||
if (!out.good()) return false;
|
||||
out << toJson().dump(2);
|
||||
return out.good();
|
||||
}
|
||||
|
||||
bool loadFromJsonFile(const std::string& path) {
|
||||
std::ifstream in(path);
|
||||
if (!in.good()) return false;
|
||||
nlohmann::json j;
|
||||
try {
|
||||
in >> j;
|
||||
loadFromJson(j);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> processInput(const KeyCombo::InputState& input) const {
|
||||
for (const auto& [actionId, combo] : bindings_) {
|
||||
if (combo.matches(input)) return actionId;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, KeyAction> actions_;
|
||||
std::map<std::string, KeyCombo> bindings_;
|
||||
|
||||
171
editor/tests/step351_test.cpp
Normal file
171
editor/tests/step351_test.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
// Step 351: Keybinding Registry (12 tests)
|
||||
|
||||
#include <cassert>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "KeybindingRegistry.h"
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
|
||||
// Test 1: bind and retrieve
|
||||
{
|
||||
KeybindingRegistry reg;
|
||||
reg.registerAction("save-buffer", "Save Buffer", "File");
|
||||
reg.bind("save-buffer", KeyCombo::ctrl("S"));
|
||||
assert(reg.getBinding("save-buffer").toString() == "Ctrl+S");
|
||||
std::cout << "Test 1 PASSED: bind and retrieve\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 2: default bindings loaded
|
||||
{
|
||||
KeybindingRegistry reg;
|
||||
reg.loadDefaults();
|
||||
assert(reg.getBinding("save-buffer").toString() == "Ctrl+S");
|
||||
assert(reg.getBinding("save-all").toString() == "Ctrl+Shift+S");
|
||||
assert(reg.getBinding("run-pipeline").toString() == "F5");
|
||||
std::cout << "Test 2 PASSED: default bindings loaded\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 3: key combo toString
|
||||
{
|
||||
KeyCombo c1 = KeyCombo::ctrl("P");
|
||||
KeyCombo c2 = KeyCombo::ctrlShift("F");
|
||||
KeyCombo c3 = {KeyModifier::Alt | KeyModifier::Shift, "F4"};
|
||||
assert(c1.toString() == "Ctrl+P");
|
||||
assert(c2.toString() == "Ctrl+Shift+F");
|
||||
assert(c3.toString() == "Alt+Shift+F4");
|
||||
std::cout << "Test 3 PASSED: key combo toString\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 4: key combo toSymbols (platform-aware string mode)
|
||||
{
|
||||
uint8_t mods = static_cast<uint8_t>(KeyModifier::Ctrl) |
|
||||
static_cast<uint8_t>(KeyModifier::Shift) |
|
||||
static_cast<uint8_t>(KeyModifier::Super);
|
||||
KeyCombo mac = {mods, "P"};
|
||||
std::string symbols = mac.toSymbols(true);
|
||||
assert(symbols.find("P") != std::string::npos);
|
||||
assert(symbols.find("\xE2\x8C\x83") != std::string::npos); // ⌃
|
||||
assert(symbols.find("\xE2\x87\xA7") != std::string::npos); // ⇧
|
||||
assert(symbols.find("\xE2\x8C\x98") != std::string::npos); // ⌘
|
||||
std::cout << "Test 4 PASSED: key combo toSymbols mac-style\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 5: processInput detects active combo
|
||||
{
|
||||
KeybindingRegistry reg;
|
||||
reg.loadDefaults();
|
||||
KeyCombo::InputState in;
|
||||
in.ctrl = true;
|
||||
in.key = "S";
|
||||
auto action = reg.processInput(in);
|
||||
assert(action.has_value());
|
||||
assert(action.value() == "save-buffer");
|
||||
std::cout << "Test 5 PASSED: processInput detects combo\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 6: processInput no match
|
||||
{
|
||||
KeybindingRegistry reg;
|
||||
reg.loadDefaults();
|
||||
KeyCombo::InputState in;
|
||||
in.ctrl = true;
|
||||
in.alt = true;
|
||||
in.key = "S";
|
||||
auto action = reg.processInput(in);
|
||||
assert(!action.has_value());
|
||||
std::cout << "Test 6 PASSED: processInput no match\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 7: save/load JSON roundtrip via file
|
||||
{
|
||||
KeybindingRegistry reg;
|
||||
reg.loadDefaults();
|
||||
reg.bind("save-buffer", KeyCombo::ctrl("W"));
|
||||
|
||||
const std::string path = "/tmp/whetstone_step351_keybindings.json";
|
||||
assert(reg.saveToJson(path));
|
||||
|
||||
KeybindingRegistry loaded;
|
||||
loaded.loadDefaults();
|
||||
assert(loaded.loadFromJsonFile(path));
|
||||
assert(loaded.getBinding("save-buffer").toString() == "Ctrl+W");
|
||||
std::filesystem::remove(path);
|
||||
std::cout << "Test 7 PASSED: save/load JSON roundtrip\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 8: rebind action
|
||||
{
|
||||
KeybindingRegistry reg;
|
||||
reg.loadDefaults();
|
||||
reg.bind("undo", KeyCombo::ctrl("U"));
|
||||
assert(reg.getBinding("undo").toString() == "Ctrl+U");
|
||||
std::cout << "Test 8 PASSED: rebind action\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 9: conflict detection
|
||||
{
|
||||
KeybindingRegistry reg;
|
||||
reg.loadDefaults();
|
||||
auto conflict = reg.findConflict("close-buffer", KeyCombo::ctrl("S"));
|
||||
assert(conflict.has_value());
|
||||
assert(conflict.value() == "save-buffer");
|
||||
auto none = reg.findConflict("close-buffer", KeyCombo::ctrl("Q"));
|
||||
assert(!none.has_value());
|
||||
std::cout << "Test 9 PASSED: conflict detection\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 10: getSymbols returns displayable string
|
||||
{
|
||||
KeybindingRegistry reg;
|
||||
reg.loadDefaults();
|
||||
std::string saveText = reg.getSymbols("save-buffer", false);
|
||||
std::string saveMac = reg.getSymbols("save-buffer", true);
|
||||
assert(saveText == "Ctrl+S");
|
||||
assert(!saveMac.empty());
|
||||
std::cout << "Test 10 PASSED: getSymbols returns displayable string\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 11: modifier bitmask correctness
|
||||
{
|
||||
uint8_t mods = KeyModifier::Ctrl | KeyModifier::Shift;
|
||||
assert(hasModifier(mods, KeyModifier::Ctrl));
|
||||
assert(hasModifier(mods, KeyModifier::Shift));
|
||||
assert(!hasModifier(mods, KeyModifier::Alt));
|
||||
std::cout << "Test 11 PASSED: modifier bitmask correctness\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 12: matches() exactness + getAll map
|
||||
{
|
||||
KeyCombo combo = KeyCombo::ctrlShift("Z");
|
||||
KeyCombo::InputState ok{true, false, true, false, "z"};
|
||||
KeyCombo::InputState wrongShift{true, false, false, false, "z"};
|
||||
assert(combo.matches(ok));
|
||||
assert(!combo.matches(wrongShift));
|
||||
|
||||
KeybindingRegistry reg;
|
||||
reg.loadDefaults();
|
||||
auto all = reg.getAll();
|
||||
assert(!all.empty());
|
||||
assert(all.find("save-buffer") != all.end());
|
||||
std::cout << "Test 12 PASSED: matches exactness + getAll\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
std::cout << "\nResults: " << passed << "/12\n";
|
||||
assert(passed == 12);
|
||||
return 0;
|
||||
}
|
||||
88
progress.md
88
progress.md
@@ -1626,8 +1626,96 @@ alias_declaration, and type_definition node types.
|
||||
object-like/function-like macros, scoped/plain enums, namespace with body, using alias,
|
||||
typedef, mixed file, backward compat
|
||||
|
||||
### Step 340: C++ Generator — Preprocessor, Enum, Namespace
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
All 9 generators (C++, Python, Java, Rust, Go, JavaScript, Elisp, Kotlin, C#)
|
||||
now produce language-appropriate output for all 6 new AST node types:
|
||||
IncludeDirective, PragmaDirective, MacroDefinition, EnumDeclaration,
|
||||
NamespaceDeclaration, TypeAlias. ProjectionGenerator.h dispatch extended
|
||||
with 6 new branches + default visitor methods.
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/ast/ProjectionGenerator.h` — 6 new virtual visitors + dispatch branches,
|
||||
added PreprocessorNodes.h + EnumNamespaceNodes.h includes
|
||||
- `editor/src/ast/CppGeneratorTypes.h` — 6 visitor implementations (native C++ output)
|
||||
- `editor/src/ast/PythonGenerator.h` — import, class(Enum), def macro, type alias
|
||||
- `editor/src/ast/JavaGenerator.h` — import, enum, package, static final
|
||||
- `editor/src/ast/RustGenerator.h` — use, enum, mod, macro_rules!, type alias
|
||||
- `editor/src/ast/GoGenerator.h` — import, type+iota, package, const
|
||||
- `editor/src/ast/JavaScriptGenerator.h` — import, Object.freeze enum, const, JSDoc typedef
|
||||
- `editor/src/ast/ElispGenerator.h` — require, defconst enum, defmacro, defalias
|
||||
- `editor/src/ast/KotlinGenerator.h` — import, enum class, package, typealias
|
||||
- `editor/src/ast/CSharpGenerator.h` — using, enum, namespace, #pragma, type alias
|
||||
|
||||
**Files created:**
|
||||
- `editor/tests/step340_test.cpp` — 12 tests: C++ include/enum/namespace output,
|
||||
Python/Java/Rust/Go enum adaptation, cross-language includes (8 generators),
|
||||
using/typedef, Kotlin+C# enum, pragma+define, all 9 generators non-empty for 6 types
|
||||
|
||||
### Step 341: Phase 12d Integration + Sprint 12 Summary
|
||||
**Status:** PASS (8/8 tests)
|
||||
|
||||
End-to-end integration tests for full C++ depth: realistic header parsing (includes
|
||||
+ pragma + namespace + enum + class + function), JSON roundtrip, enum-inside-namespace
|
||||
scoping, cross-language enum output (6 languages), combined namespace+class+enum pipeline,
|
||||
batch serialization verification, CompactAST names, self-hosting progress (simplified
|
||||
Whetstone header fragment parsed successfully).
|
||||
|
||||
**Files created:**
|
||||
- `editor/tests/step341_test.cpp` — 8 integration tests
|
||||
|
||||
**Key results:**
|
||||
- Phase 12d complete: all 5 steps pass (56/56 tests across steps 337–341)
|
||||
- All 10 generators produce language-appropriate output for 6 new C++ node types
|
||||
- C++ parser handles: preprocessor, enum class, namespace, type alias
|
||||
- Self-hosting milestone: simplified Whetstone header fragment parses correctly
|
||||
- Sprint 12 complete: all 4 phases pass (248/248 tests across steps 320–341)
|
||||
- 56+ MCP tools, workflow engine, routing engine, review gates, C++ depth
|
||||
|
||||
---
|
||||
|
||||
# Sprint 13 Progress — GUI Overhaul Phase 1
|
||||
|
||||
---
|
||||
|
||||
### Step 351: Keybinding Registry
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Expanded the centralized keybinding registry so actions have stable bindings
|
||||
that can be displayed with symbols, matched from input state, and persisted
|
||||
to/from disk. This extends the earlier keybinding foundation with stronger
|
||||
runtime and persistence primitives for the upcoming command palette/menu work.
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/KeybindingRegistry.h` — added:
|
||||
- `KeyCombo::InputState` and `KeyCombo::matches(...)` for key state matching
|
||||
- `KeyCombo::keyToSymbol(...)`, `normalizeKey(...)`, `toSymbolsAuto(...)`
|
||||
- `KeybindingRegistry::getAll()` alias for settings/UI consumption
|
||||
- `KeybindingRegistry::processInput(...) -> optional<actionId>`
|
||||
- `KeybindingRegistry::saveToJson(path)` and `loadFromJsonFile(path)`
|
||||
- `editor/CMakeLists.txt` — `step351_test` target
|
||||
|
||||
**Files created:**
|
||||
- `editor/tests/step351_test.cpp` — 12 tests covering:
|
||||
1. bind + retrieve
|
||||
2. default bindings
|
||||
3. `KeyCombo::toString`
|
||||
4. mac-style symbol rendering
|
||||
5. input processing match
|
||||
6. input processing no-match
|
||||
7. file save/load roundtrip
|
||||
8. rebinding
|
||||
9. conflict detection
|
||||
10. display symbol retrieval
|
||||
11. modifier bitmask helpers
|
||||
12. exact match behavior + `getAll`
|
||||
|
||||
**Verification run:**
|
||||
- `step343_test` — PASS (12/12) regression coverage
|
||||
- `step350_test` — PASS (8/8) latest completed sprint checkpoint
|
||||
- `step351_test` — PASS (12/12) new step coverage
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)
|
||||
|
||||
Reference in New Issue
Block a user