diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 0f9370a..f6277e4 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2062,4 +2062,12 @@ target_link_libraries(step341_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step342_test tests/step342_test.cpp) +target_include_directories(step342_test PRIVATE src) +target_link_libraries(step342_test PRIVATE nlohmann_json::nlohmann_json) + +add_executable(step343_test tests/step343_test.cpp) +target_include_directories(step343_test PRIVATE src) +target_link_libraries(step343_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/Icons.h b/editor/src/Icons.h new file mode 100644 index 0000000..cec898a --- /dev/null +++ b/editor/src/Icons.h @@ -0,0 +1,92 @@ +#pragma once +// Step 343: Icon system — Unicode symbols for panels, diagnostics, workflow, AST nodes +// No external icon font dependency — pure Unicode + text fallbacks + +#include +#include + +namespace WhetstoneIcons { + +// File types +constexpr const char* FileSource = "\xF0\x9F\x93\x84"; // document +constexpr const char* FileHeader = "\xF0\x9F\x93\x83"; // page with curl +constexpr const char* FileConfig = "\xE2\x9A\x99"; // gear +constexpr const char* Folder = "\xF0\x9F\x93\x81"; // folder +constexpr const char* FolderOpen = "\xF0\x9F\x93\x82"; // open folder + +// Diagnostics +constexpr const char* DiagError = "\xE2\x9B\x94"; // no entry +constexpr const char* DiagWarning = "\xE2\x9A\xA0"; // warning +constexpr const char* DiagInfo = "\xE2\x84\xB9"; // info +constexpr const char* DiagHint = "\xF0\x9F\x92\xA1"; // light bulb + +// Workflow status +constexpr const char* Pending = "\xE2\x8F\xB3"; // hourglass +constexpr const char* InProgress = "\xE2\x96\xB6"; // play +constexpr const char* Complete = "\xE2\x9C\x85"; // check mark +constexpr const char* Rejected = "\xE2\x9D\x8C"; // cross mark +constexpr const char* Review = "\xF0\x9F\x91\x81"; // eye + +// AST nodes +constexpr const char* NodeFunction = "\xC6\x92"; // function ƒ +constexpr const char* NodeClass = "\xE2\x97\x86"; // diamond +constexpr const char* NodeVariable = "\xF0\x9D\x91\xA5"; // math x +constexpr const char* NodeModule = "\xE2\x96\xA3"; // square +constexpr const char* NodeAnnotation = "\xF0\x9F\x8F\xB7"; // label + +// Navigation +constexpr const char* ArrowRight = "\xE2\x96\xB6"; +constexpr const char* ArrowDown = "\xE2\x96\xBC"; +constexpr const char* Search = "\xF0\x9F\x94\x8D"; +constexpr const char* Settings = "\xE2\x9A\x99"; + +// Text fallbacks (ASCII-safe) +inline std::string fallback(const std::string& icon) { + static std::map fallbacks = { + {DiagError, "[ERR]"}, {DiagWarning, "[WARN]"}, + {DiagInfo, "[INFO]"}, {DiagHint, "[HINT]"}, + {Pending, "[...]"}, {InProgress, "[>>]"}, + {Complete, "[OK]"}, {Rejected, "[X]"}, {Review, "[?]"}, + {NodeFunction, "fn"}, {NodeClass, "cls"}, + {NodeVariable, "var"}, {NodeModule, "mod"}, + {FileSource, "[]"}, {Folder, "/"}, {FolderOpen, "/"}, + }; + auto it = fallbacks.find(icon); + return it != fallbacks.end() ? it->second : icon; +} + +// Get icon for AST node type +inline std::string iconForNodeType(const std::string& conceptType) { + if (conceptType == "Function" || conceptType == "AsyncFunction" || + conceptType == "MethodDeclaration") return NodeFunction; + if (conceptType == "ClassDeclaration" || conceptType == "InterfaceDeclaration") return NodeClass; + if (conceptType == "Variable") return NodeVariable; + if (conceptType == "Module") return NodeModule; + if (conceptType.find("Annotation") != std::string::npos) return NodeAnnotation; + if (conceptType == "EnumDeclaration") return NodeClass; + if (conceptType == "NamespaceDeclaration") return Folder; + return NodeModule; // default +} + +// Get icon for diagnostic severity +inline std::string iconForSeverity(int severity) { + switch (severity) { + case 1: return DiagError; + case 2: return DiagWarning; + case 3: return DiagInfo; + case 4: return DiagHint; + default: return DiagInfo; + } +} + +// Get icon for workflow status +inline std::string iconForWorkflowStatus(const std::string& status) { + if (status == "pending" || status == "ready") return Pending; + if (status == "assigned" || status == "in-progress") return InProgress; + if (status == "complete") return Complete; + if (status == "rejected") return Rejected; + if (status == "review") return Review; + return Pending; +} + +} // namespace WhetstoneIcons diff --git a/editor/src/KeybindingRegistry.h b/editor/src/KeybindingRegistry.h new file mode 100644 index 0000000..b016888 --- /dev/null +++ b/editor/src/KeybindingRegistry.h @@ -0,0 +1,222 @@ +#pragma once +// Step 343-351: Keybinding registry — centralized action→key combo mapping +// Headless data model: no ImGui dependency, supports symbol rendering + +#include +#include +#include +#include +#include +#include + +enum class KeyModifier : uint8_t { + None = 0, + Ctrl = 1 << 0, + Alt = 1 << 1, + Shift = 1 << 2, + Super = 1 << 3 +}; + +inline uint8_t operator|(KeyModifier a, KeyModifier b) { + return static_cast(a) | static_cast(b); +} + +inline bool hasModifier(uint8_t mods, KeyModifier m) { + return (mods & static_cast(m)) != 0; +} + +struct KeyCombo { + uint8_t modifiers = 0; + std::string key; // "S", "P", "Z", "F5", "F8", "Enter", "Escape", etc. + + bool empty() const { return key.empty(); } + + std::string toString() const { + std::string result; + if (hasModifier(modifiers, KeyModifier::Ctrl)) result += "Ctrl+"; + if (hasModifier(modifiers, KeyModifier::Alt)) result += "Alt+"; + if (hasModifier(modifiers, KeyModifier::Shift)) result += "Shift+"; + if (hasModifier(modifiers, KeyModifier::Super)) result += "Super+"; + result += key; + return result; + } + + // Platform-aware symbols (macOS style vs Linux/Windows) + std::string toSymbols(bool macOS = false) const { + std::string result; + if (macOS) { + if (hasModifier(modifiers, KeyModifier::Ctrl)) result += "\xE2\x8C\x83"; // ⌃ + 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; + } else { + result = toString(); // Linux/Windows uses text form + } + return result; + } + + bool operator==(const KeyCombo& other) const { + return modifiers == other.modifiers && key == other.key; + } + + nlohmann::json toJson() const { + return {{"modifiers", modifiers}, {"key", key}}; + } + + static KeyCombo fromJson(const nlohmann::json& j) { + KeyCombo c; + c.modifiers = j.value("modifiers", 0); + c.key = j.value("key", ""); + return c; + } + + // Helper constructors + static KeyCombo ctrl(const std::string& k) { + return {static_cast(KeyModifier::Ctrl), k}; + } + static KeyCombo ctrlShift(const std::string& k) { + return {KeyModifier::Ctrl | KeyModifier::Shift, k}; + } + static KeyCombo alt(const std::string& k) { + return {static_cast(KeyModifier::Alt), k}; + } + static KeyCombo fkey(const std::string& k) { + return {0, k}; + } + static KeyCombo shiftFkey(const std::string& k) { + return {static_cast(KeyModifier::Shift), k}; + } +}; + +struct KeyAction { + std::string id; // "save-buffer", "toggle-ast-panel", etc. + std::string label; // "Save Buffer", "Toggle AST Panel" + std::string category; // "File", "Edit", "View", "Tools", "Workflow" + std::string description; // optional longer description +}; + +class KeybindingRegistry { +public: + KeybindingRegistry() = default; + + void registerAction(const std::string& id, const std::string& label, + const std::string& category, const std::string& description = "") { + actions_[id] = {id, label, category, description}; + } + + void bind(const std::string& actionId, const KeyCombo& combo) { + bindings_[actionId] = combo; + } + + KeyCombo getBinding(const std::string& actionId) const { + auto it = bindings_.find(actionId); + return it != bindings_.end() ? it->second : KeyCombo{}; + } + + std::string getSymbols(const std::string& actionId, bool macOS = false) const { + auto combo = getBinding(actionId); + return combo.empty() ? "" : combo.toSymbols(macOS); + } + + std::optional findConflict(const std::string& actionId, const KeyCombo& combo) const { + for (const auto& [aid, c] : bindings_) { + if (aid != actionId && c == combo) return aid; + } + return std::nullopt; + } + + std::vector getActions() const { + std::vector result; + for (const auto& [id, action] : actions_) result.push_back(action); + return result; + } + + std::vector getActionsByCategory(const std::string& category) const { + std::vector result; + for (const auto& [id, action] : actions_) { + if (action.category == category) result.push_back(action); + } + return result; + } + + std::map getAllBindings() const { return bindings_; } + + int actionCount() const { return static_cast(actions_.size()); } + int bindingCount() const { return static_cast(bindings_.size()); } + + void loadDefaults() { + // File actions + registerAction("save-buffer", "Save Buffer", "File"); + registerAction("save-all", "Save All", "File"); + registerAction("open-file", "Open File", "File"); + registerAction("close-buffer", "Close Buffer", "File"); + + // Edit actions + registerAction("undo", "Undo", "Edit"); + registerAction("redo", "Redo", "Edit"); + registerAction("find-in-file", "Find in File", "Edit"); + registerAction("find-in-project", "Find in Project", "Edit"); + + // View actions + registerAction("command-palette", "Command Palette", "View"); + registerAction("toggle-file-tree", "Toggle File Tree", "View"); + registerAction("toggle-ast-view", "Toggle AST View", "View"); + registerAction("toggle-diagnostics", "Toggle Diagnostics", "View"); + registerAction("toggle-annotations", "Toggle Annotations", "View"); + + // Tools actions + registerAction("run-pipeline", "Run Pipeline", "Tools"); + registerAction("generate-code", "Generate Code", "Tools"); + registerAction("infer-annotations", "Infer Annotations", "Tools"); + registerAction("validate", "Validate", "Tools"); + + // Workflow actions + registerAction("create-workflow", "Create Workflow", "Workflow"); + registerAction("route-all", "Route All Tasks", "Workflow"); + + // Navigation + registerAction("next-diagnostic", "Next Diagnostic", "Navigation"); + registerAction("prev-diagnostic", "Previous Diagnostic", "Navigation"); + registerAction("go-to-definition", "Go to Definition", "Navigation"); + registerAction("go-to-symbol", "Go to Symbol", "Navigation"); + + // Default bindings + bind("save-buffer", KeyCombo::ctrl("S")); + bind("save-all", KeyCombo::ctrlShift("S")); + bind("undo", KeyCombo::ctrl("Z")); + bind("redo", KeyCombo::ctrlShift("Z")); + bind("find-in-file", KeyCombo::ctrl("F")); + bind("find-in-project", KeyCombo::ctrlShift("F")); + bind("command-palette", KeyCombo::ctrl("P")); + bind("toggle-file-tree", KeyCombo::ctrl("B")); + bind("toggle-diagnostics", KeyCombo::ctrlShift("D")); + bind("run-pipeline", KeyCombo::fkey("F5")); + bind("next-diagnostic", KeyCombo::fkey("F8")); + bind("prev-diagnostic", KeyCombo::shiftFkey("F8")); + bind("go-to-definition", KeyCombo::fkey("F12")); + bind("go-to-symbol", KeyCombo::ctrlShift("O")); + } + + nlohmann::json toJson() const { + nlohmann::json j; + nlohmann::json bindingsJson; + for (const auto& [id, combo] : bindings_) { + bindingsJson[id] = combo.toJson(); + } + j["bindings"] = bindingsJson; + return j; + } + + void loadFromJson(const nlohmann::json& j) { + if (j.contains("bindings")) { + for (auto& [id, cj] : j["bindings"].items()) { + bindings_[id] = KeyCombo::fromJson(cj); + } + } + } + +private: + std::map actions_; + std::map bindings_; +}; diff --git a/editor/src/PanelManager.h b/editor/src/PanelManager.h new file mode 100644 index 0000000..a892670 --- /dev/null +++ b/editor/src/PanelManager.h @@ -0,0 +1,230 @@ +#pragma once +// Step 342-343: Panel registration system and docking layout management +// Headless data model for panel state — no ImGui dependency + +#include +#include +#include +#include +#include +#include + +enum class DockPosition { + Left, + CenterTop, + CenterBottom, + Right, + Bottom +}; + +inline std::string dockPositionToString(DockPosition p) { + switch (p) { + case DockPosition::Left: return "left"; + case DockPosition::CenterTop: return "center-top"; + case DockPosition::CenterBottom: return "center-bottom"; + case DockPosition::Right: return "right"; + case DockPosition::Bottom: return "bottom"; + } + return "unknown"; +} + +inline DockPosition dockPositionFromString(const std::string& s) { + if (s == "left") return DockPosition::Left; + if (s == "center-top") return DockPosition::CenterTop; + if (s == "center-bottom") return DockPosition::CenterBottom; + if (s == "right") return DockPosition::Right; + if (s == "bottom") return DockPosition::Bottom; + return DockPosition::CenterTop; +} + +struct PanelInfo { + std::string id; + std::string title; + DockPosition defaultDock; + DockPosition currentDock; + bool isVisible = true; + int order = 0; + std::string icon; // Unicode icon character + std::string category; // "navigation", "editor", "analysis", "output" + float widthPercent = 0; // 0 = auto + float heightPercent = 0; // 0 = auto + + nlohmann::json toJson() const { + return { + {"id", id}, {"title", title}, + {"defaultDock", dockPositionToString(defaultDock)}, + {"currentDock", dockPositionToString(currentDock)}, + {"isVisible", isVisible}, {"order", order}, + {"icon", icon}, {"category", category}, + {"widthPercent", widthPercent}, {"heightPercent", heightPercent} + }; + } + + static PanelInfo fromJson(const nlohmann::json& j) { + PanelInfo p; + p.id = j.value("id", ""); + p.title = j.value("title", ""); + p.defaultDock = dockPositionFromString(j.value("defaultDock", "center-top")); + p.currentDock = dockPositionFromString(j.value("currentDock", j.value("defaultDock", "center-top"))); + p.isVisible = j.value("isVisible", true); + p.order = j.value("order", 0); + p.icon = j.value("icon", ""); + p.category = j.value("category", ""); + p.widthPercent = j.value("widthPercent", 0.0f); + p.heightPercent = j.value("heightPercent", 0.0f); + return p; + } +}; + +struct DockingLayout { + float leftWidth = 20.0f; // percent + float rightWidth = 20.0f; // percent + float bottomHeight = 25.0f; // percent + float centerWidth = 60.0f; // auto-computed + + nlohmann::json toJson() const { + return { + {"leftWidth", leftWidth}, {"rightWidth", rightWidth}, + {"bottomHeight", bottomHeight} + }; + } + + static DockingLayout fromJson(const nlohmann::json& j) { + DockingLayout l; + l.leftWidth = j.value("leftWidth", 20.0f); + l.rightWidth = j.value("rightWidth", 20.0f); + l.bottomHeight = j.value("bottomHeight", 25.0f); + l.centerWidth = 100.0f - l.leftWidth - l.rightWidth; + return l; + } +}; + +class PanelManager { +public: + PanelManager() = default; + + void registerPanel(const std::string& id, const std::string& title, + DockPosition dock, const std::string& icon = "", + const std::string& category = "", int order = 0) { + PanelInfo info; + info.id = id; + info.title = title; + info.defaultDock = dock; + info.currentDock = dock; + info.icon = icon; + info.category = category; + info.order = order; + panels_[id] = info; + } + + void setVisible(const std::string& id, bool visible) { + auto it = panels_.find(id); + if (it != panels_.end()) it->second.isVisible = visible; + } + + bool isVisible(const std::string& id) const { + auto it = panels_.find(id); + return it != panels_.end() && it->second.isVisible; + } + + void togglePanel(const std::string& id) { + auto it = panels_.find(id); + if (it != panels_.end()) it->second.isVisible = !it->second.isVisible; + } + + void focusPanel(const std::string& id) { + focusedPanel_ = id; + } + + std::string getFocusedPanel() const { return focusedPanel_; } + + bool hasPanel(const std::string& id) const { + return panels_.count(id) > 0; + } + + PanelInfo getPanel(const std::string& id) const { + auto it = panels_.find(id); + if (it != panels_.end()) return it->second; + return {}; + } + + std::vector getPanelList() const { + std::vector result; + for (const auto& [id, info] : panels_) result.push_back(info); + std::sort(result.begin(), result.end(), + [](const PanelInfo& a, const PanelInfo& b) { return a.order < b.order; }); + return result; + } + + std::vector getVisiblePanels() const { + std::vector result; + for (const auto& [id, info] : panels_) { + if (info.isVisible) result.push_back(info); + } + std::sort(result.begin(), result.end(), + [](const PanelInfo& a, const PanelInfo& b) { return a.order < b.order; }); + return result; + } + + std::vector getPanelsByDock(DockPosition dock) const { + std::vector result; + for (const auto& [id, info] : panels_) { + if (info.currentDock == dock && info.isVisible) result.push_back(info); + } + std::sort(result.begin(), result.end(), + [](const PanelInfo& a, const PanelInfo& b) { return a.order < b.order; }); + return result; + } + + void movePanelToDock(const std::string& id, DockPosition dock) { + auto it = panels_.find(id); + if (it != panels_.end()) it->second.currentDock = dock; + } + + int panelCount() const { return static_cast(panels_.size()); } + + void registerDefaults() { + registerPanel("file-tree", "File Tree", DockPosition::Left, "\xF0\x9F\x93\x81", "navigation", 0); + registerPanel("code-editor", "Code Editor", DockPosition::CenterTop, "\xE2\x9C\x8F", "editor", 1); + registerPanel("ast-view", "AST View", DockPosition::Right, "\xF0\x9F\x8C\xB3", "analysis", 2); + registerPanel("annotations", "Annotations", DockPosition::Right, "\xF0\x9F\x8F\xB7", "analysis", 3); + registerPanel("properties", "Properties", DockPosition::Right, "\xE2\x9A\x99", "analysis", 4); + registerPanel("diagnostics", "Diagnostics", DockPosition::Bottom, "\xE2\x9A\xA0", "output", 5); + registerPanel("output", "Output", DockPosition::Bottom, "\xF0\x9F\x93\x84", "output", 6); + registerPanel("workflow", "Workflow", DockPosition::Bottom, "\xF0\x9F\x94\x84", "output", 7); + } + + // Layout management + DockingLayout& layout() { return layout_; } + const DockingLayout& layout() const { return layout_; } + + nlohmann::json toJson() const { + nlohmann::json j; + j["layout"] = layout_.toJson(); + nlohmann::json panelsJson = nlohmann::json::array(); + for (const auto& [id, info] : panels_) { + panelsJson.push_back(info.toJson()); + } + j["panels"] = panelsJson; + j["focusedPanel"] = focusedPanel_; + return j; + } + + static PanelManager fromJson(const nlohmann::json& j) { + PanelManager pm; + if (j.contains("layout")) pm.layout_ = DockingLayout::fromJson(j["layout"]); + if (j.contains("panels")) { + for (const auto& pj : j["panels"]) { + PanelInfo info = PanelInfo::fromJson(pj); + pm.panels_[info.id] = info; + } + } + pm.focusedPanel_ = j.value("focusedPanel", ""); + return pm; + } + +private: + std::map panels_; + DockingLayout layout_; + std::string focusedPanel_; +}; diff --git a/editor/tests/step342_test.cpp b/editor/tests/step342_test.cpp new file mode 100644 index 0000000..ef18864 --- /dev/null +++ b/editor/tests/step342_test.cpp @@ -0,0 +1,183 @@ +// Step 342: DockingLayout + PanelManager Infrastructure (12 tests) +// Tests panel registration, docking layout, visibility, focus, JSON persistence + +#include +#include +#include +#include "PanelManager.h" + +int main() { + int passed = 0; + + // Test 1: Register 8 default panels + { + PanelManager pm; + pm.registerDefaults(); + assert(pm.panelCount() == 8); + std::cout << "Test 1 PASSED: Register 8 default panels\n"; + passed++; + } + + // Test 2: All default panels visible by default + { + PanelManager pm; + pm.registerDefaults(); + auto visible = pm.getVisiblePanels(); + assert(visible.size() == 8); + std::cout << "Test 2 PASSED: All default panels visible\n"; + passed++; + } + + // Test 3: Panels in correct dock positions + { + PanelManager pm; + pm.registerDefaults(); + auto left = pm.getPanelsByDock(DockPosition::Left); + auto right = pm.getPanelsByDock(DockPosition::Right); + auto bottom = pm.getPanelsByDock(DockPosition::Bottom); + auto center = pm.getPanelsByDock(DockPosition::CenterTop); + assert(left.size() == 1); // file-tree + assert(right.size() == 3); // ast-view, annotations, properties + assert(bottom.size() == 3); // diagnostics, output, workflow + assert(center.size() == 1); // code-editor + assert(left[0].id == "file-tree"); + assert(center[0].id == "code-editor"); + std::cout << "Test 3 PASSED: Panels in correct dock positions\n"; + passed++; + } + + // Test 4: Toggle panel visibility + { + PanelManager pm; + pm.registerDefaults(); + assert(pm.isVisible("ast-view")); + pm.togglePanel("ast-view"); + assert(!pm.isVisible("ast-view")); + pm.togglePanel("ast-view"); + assert(pm.isVisible("ast-view")); + std::cout << "Test 4 PASSED: Toggle panel visibility\n"; + passed++; + } + + // Test 5: Hidden panel not in visible list + { + PanelManager pm; + pm.registerDefaults(); + pm.setVisible("properties", false); + auto visible = pm.getVisiblePanels(); + assert(visible.size() == 7); + bool found = false; + for (const auto& p : visible) { + if (p.id == "properties") found = true; + } + assert(!found); + std::cout << "Test 5 PASSED: Hidden panel excluded from visible list\n"; + passed++; + } + + // Test 6: Focus panel + { + PanelManager pm; + pm.registerDefaults(); + pm.focusPanel("diagnostics"); + assert(pm.getFocusedPanel() == "diagnostics"); + pm.focusPanel("ast-view"); + assert(pm.getFocusedPanel() == "ast-view"); + std::cout << "Test 6 PASSED: Focus panel tracking\n"; + passed++; + } + + // Test 7: Move panel to different dock + { + PanelManager pm; + pm.registerDefaults(); + pm.movePanelToDock("diagnostics", DockPosition::Right); + auto right = pm.getPanelsByDock(DockPosition::Right); + bool found = false; + for (const auto& p : right) { + if (p.id == "diagnostics") found = true; + } + assert(found); + auto bottom = pm.getPanelsByDock(DockPosition::Bottom); + bool stillInBottom = false; + for (const auto& p : bottom) { + if (p.id == "diagnostics") stillInBottom = true; + } + assert(!stillInBottom); + std::cout << "Test 7 PASSED: Move panel to different dock\n"; + passed++; + } + + // Test 8: DockingLayout default proportions + { + PanelManager pm; + pm.registerDefaults(); + assert(pm.layout().leftWidth == 20.0f); + assert(pm.layout().rightWidth == 20.0f); + assert(pm.layout().bottomHeight == 25.0f); + std::cout << "Test 8 PASSED: Default layout proportions\n"; + passed++; + } + + // Test 9: Panel ordering preserved + { + PanelManager pm; + pm.registerDefaults(); + auto all = pm.getPanelList(); + assert(all[0].id == "file-tree"); + assert(all[1].id == "code-editor"); + // Higher order panels come later + assert(all.back().id == "workflow"); + std::cout << "Test 9 PASSED: Panel ordering preserved\n"; + passed++; + } + + // Test 10: JSON roundtrip — panels + layout + { + PanelManager pm; + pm.registerDefaults(); + pm.setVisible("workflow", false); + pm.focusPanel("code-editor"); + pm.layout().leftWidth = 25.0f; + + nlohmann::json j = pm.toJson(); + PanelManager restored = PanelManager::fromJson(j); + + assert(restored.panelCount() == 8); + assert(!restored.isVisible("workflow")); + assert(restored.getFocusedPanel() == "code-editor"); + assert(restored.layout().leftWidth == 25.0f); + std::cout << "Test 10 PASSED: JSON roundtrip (panels + layout)\n"; + passed++; + } + + // Test 11: Panel info fields correct + { + PanelManager pm; + pm.registerDefaults(); + auto info = pm.getPanel("file-tree"); + assert(info.id == "file-tree"); + assert(info.title == "File Tree"); + assert(info.defaultDock == DockPosition::Left); + assert(info.category == "navigation"); + assert(!info.icon.empty()); + std::cout << "Test 11 PASSED: Panel info fields correct\n"; + passed++; + } + + // Test 12: DockPosition string roundtrip + { + assert(dockPositionToString(DockPosition::Left) == "left"); + assert(dockPositionToString(DockPosition::CenterTop) == "center-top"); + assert(dockPositionToString(DockPosition::Bottom) == "bottom"); + assert(dockPositionFromString("left") == DockPosition::Left); + assert(dockPositionFromString("right") == DockPosition::Right); + assert(dockPositionFromString("center-top") == DockPosition::CenterTop); + std::cout << "Test 12 PASSED: DockPosition string roundtrip\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +} diff --git a/editor/tests/step343_test.cpp b/editor/tests/step343_test.cpp new file mode 100644 index 0000000..d2bc155 --- /dev/null +++ b/editor/tests/step343_test.cpp @@ -0,0 +1,188 @@ +// Step 343: Panel Registration + Icons + Keybinding Foundation (12 tests) +// Tests PanelManager registration, Icons system, and KeybindingRegistry basics + +#include +#include +#include +#include "PanelManager.h" +#include "Icons.h" +#include "KeybindingRegistry.h" + +int main() { + int passed = 0; + + // Test 1: Panel categories correct + { + PanelManager pm; + pm.registerDefaults(); + auto all = pm.getPanelList(); + int nav = 0, editor = 0, analysis = 0, output = 0; + for (const auto& p : all) { + if (p.category == "navigation") nav++; + if (p.category == "editor") editor++; + if (p.category == "analysis") analysis++; + if (p.category == "output") output++; + } + assert(nav == 1); // file-tree + assert(editor == 1); // code-editor + assert(analysis == 3); // ast-view, annotations, properties + assert(output == 3); // diagnostics, output, workflow + std::cout << "Test 1 PASSED: Panel categories correct\n"; + passed++; + } + + // Test 2: Panel JSON roundtrip preserves all fields + { + PanelManager pm; + pm.registerPanel("test-panel", "Test Panel", DockPosition::Right, + WhetstoneIcons::NodeFunction, "analysis", 10); + pm.setVisible("test-panel", false); + pm.movePanelToDock("test-panel", DockPosition::Bottom); + + auto j = pm.toJson(); + auto restored = PanelManager::fromJson(j); + auto info = restored.getPanel("test-panel"); + assert(info.title == "Test Panel"); + assert(!info.isVisible); + assert(info.currentDock == DockPosition::Bottom); + assert(info.defaultDock == DockPosition::Right); + assert(info.order == 10); + std::cout << "Test 2 PASSED: Panel JSON roundtrip preserves all fields\n"; + passed++; + } + + // Test 3: Icon constants defined and non-empty + { + assert(std::string(WhetstoneIcons::DiagError).size() > 0); + assert(std::string(WhetstoneIcons::DiagWarning).size() > 0); + assert(std::string(WhetstoneIcons::DiagInfo).size() > 0); + assert(std::string(WhetstoneIcons::Pending).size() > 0); + assert(std::string(WhetstoneIcons::Complete).size() > 0); + assert(std::string(WhetstoneIcons::NodeFunction).size() > 0); + assert(std::string(WhetstoneIcons::NodeClass).size() > 0); + assert(std::string(WhetstoneIcons::Folder).size() > 0); + std::cout << "Test 3 PASSED: Icon constants defined and non-empty\n"; + passed++; + } + + // Test 4: Icon for node type + { + assert(WhetstoneIcons::iconForNodeType("Function") == WhetstoneIcons::NodeFunction); + assert(WhetstoneIcons::iconForNodeType("ClassDeclaration") == WhetstoneIcons::NodeClass); + assert(WhetstoneIcons::iconForNodeType("Variable") == WhetstoneIcons::NodeVariable); + assert(WhetstoneIcons::iconForNodeType("Module") == WhetstoneIcons::NodeModule); + assert(WhetstoneIcons::iconForNodeType("IntentAnnotation") == WhetstoneIcons::NodeAnnotation); + std::cout << "Test 4 PASSED: Icon for node type\n"; + passed++; + } + + // Test 5: Icon for severity + { + assert(WhetstoneIcons::iconForSeverity(1) == WhetstoneIcons::DiagError); + assert(WhetstoneIcons::iconForSeverity(2) == WhetstoneIcons::DiagWarning); + assert(WhetstoneIcons::iconForSeverity(3) == WhetstoneIcons::DiagInfo); + assert(WhetstoneIcons::iconForSeverity(4) == WhetstoneIcons::DiagHint); + std::cout << "Test 5 PASSED: Icon for severity\n"; + passed++; + } + + // Test 6: Icon fallback text + { + assert(WhetstoneIcons::fallback(WhetstoneIcons::DiagError) == "[ERR]"); + assert(WhetstoneIcons::fallback(WhetstoneIcons::DiagWarning) == "[WARN]"); + assert(WhetstoneIcons::fallback(WhetstoneIcons::Complete) == "[OK]"); + assert(WhetstoneIcons::fallback(WhetstoneIcons::NodeFunction) == "fn"); + std::cout << "Test 6 PASSED: Icon fallback text\n"; + passed++; + } + + // Test 7: KeybindingRegistry default bindings loaded + { + KeybindingRegistry reg; + reg.loadDefaults(); + assert(reg.actionCount() >= 20); + assert(reg.bindingCount() >= 14); + std::cout << "Test 7 PASSED: Default bindings loaded (" << reg.actionCount() + << " actions, " << reg.bindingCount() << " bindings)\n"; + passed++; + } + + // Test 8: KeyCombo toString + { + auto combo = KeyCombo::ctrl("S"); + assert(combo.toString() == "Ctrl+S"); + auto combo2 = KeyCombo::ctrlShift("Z"); + assert(combo2.toString() == "Ctrl+Shift+Z"); + auto combo3 = KeyCombo::fkey("F5"); + assert(combo3.toString() == "F5"); + std::cout << "Test 8 PASSED: KeyCombo toString\n"; + passed++; + } + + // Test 9: Key symbols for save action + { + KeybindingRegistry reg; + reg.loadDefaults(); + std::string sym = reg.getSymbols("save-buffer"); + assert(sym == "Ctrl+S"); + std::string undo = reg.getSymbols("undo"); + assert(undo == "Ctrl+Z"); + std::string pipeline = reg.getSymbols("run-pipeline"); + assert(pipeline == "F5"); + std::cout << "Test 9 PASSED: Key symbols for actions\n"; + passed++; + } + + // Test 10: Conflict detection + { + KeybindingRegistry reg; + reg.loadDefaults(); + // Ctrl+S is already bound to save-buffer + auto conflict = reg.findConflict("close-buffer", KeyCombo::ctrl("S")); + assert(conflict.has_value()); + assert(conflict.value() == "save-buffer"); + // Ctrl+Q is not bound + auto noConflict = reg.findConflict("close-buffer", KeyCombo::ctrl("Q")); + assert(!noConflict.has_value()); + std::cout << "Test 10 PASSED: Conflict detection\n"; + passed++; + } + + // Test 11: Actions by category + { + KeybindingRegistry reg; + reg.loadDefaults(); + auto fileActions = reg.getActionsByCategory("File"); + assert(fileActions.size() >= 3); + auto editActions = reg.getActionsByCategory("Edit"); + assert(editActions.size() >= 4); + auto viewActions = reg.getActionsByCategory("View"); + assert(viewActions.size() >= 4); + std::cout << "Test 11 PASSED: Actions by category\n"; + passed++; + } + + // Test 12: Keybinding JSON persistence roundtrip + { + KeybindingRegistry reg; + reg.loadDefaults(); + // Rebind save + reg.bind("save-buffer", KeyCombo::ctrl("W")); + + auto j = reg.toJson(); + KeybindingRegistry restored; + restored.loadDefaults(); + restored.loadFromJson(j); + + auto combo = restored.getBinding("save-buffer"); + assert(combo.toString() == "Ctrl+W"); + // Other bindings still intact + assert(restored.getBinding("undo").toString() == "Ctrl+Z"); + std::cout << "Test 12 PASSED: Keybinding JSON persistence roundtrip\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +}