From fc5483e0b60bcce62fc5411a932f754ba21cafdc Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 08:56:45 -0700 Subject: [PATCH] Step 351: Keybinding Registry (12/12 tests) --- editor/CMakeLists.txt | 4 + editor/src/KeybindingRegistry.h | 74 +++++++++++++- editor/tests/step351_test.cpp | 171 ++++++++++++++++++++++++++++++++ progress.md | 88 ++++++++++++++++ 4 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 editor/tests/step351_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index e4b9992..2d7f2e4 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/KeybindingRegistry.h b/editor/src/KeybindingRegistry.h index b016888..74d35bf 100644 --- a/editor/src/KeybindingRegistry.h +++ b/editor/src/KeybindingRegistry.h @@ -7,6 +7,7 @@ #include #include #include +#include #include 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(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(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 getAllBindings() const { return bindings_; } + std::map getAll() const { return bindings_; } int actionCount() const { return static_cast(actions_.size()); } int bindingCount() const { return static_cast(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 processInput(const KeyCombo::InputState& input) const { + for (const auto& [actionId, combo] : bindings_) { + if (combo.matches(input)) return actionId; + } + return std::nullopt; + } + private: std::map actions_; std::map bindings_; diff --git a/editor/tests/step351_test.cpp b/editor/tests/step351_test.cpp new file mode 100644 index 0000000..b600971 --- /dev/null +++ b/editor/tests/step351_test.cpp @@ -0,0 +1,171 @@ +// Step 351: Keybinding Registry (12 tests) + +#include +#include +#include +#include +#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(KeyModifier::Ctrl) | + static_cast(KeyModifier::Shift) | + static_cast(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; +} diff --git a/progress.md b/progress.md index 89ec6c1..55f0c51 100644 --- a/progress.md +++ b/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` + - `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)