Step 342-343: PanelManager + Icons + KeybindingRegistry (24/24 tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
92
editor/src/Icons.h
Normal file
92
editor/src/Icons.h
Normal file
@@ -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 <string>
|
||||
#include <map>
|
||||
|
||||
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<std::string, std::string> 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
|
||||
222
editor/src/KeybindingRegistry.h
Normal file
222
editor/src/KeybindingRegistry.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <algorithm>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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<uint8_t>(a) | static_cast<uint8_t>(b);
|
||||
}
|
||||
|
||||
inline bool hasModifier(uint8_t mods, KeyModifier m) {
|
||||
return (mods & static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(KeyModifier::Alt), k};
|
||||
}
|
||||
static KeyCombo fkey(const std::string& k) {
|
||||
return {0, k};
|
||||
}
|
||||
static KeyCombo shiftFkey(const std::string& k) {
|
||||
return {static_cast<uint8_t>(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<std::string> 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<KeyAction> getActions() const {
|
||||
std::vector<KeyAction> result;
|
||||
for (const auto& [id, action] : actions_) result.push_back(action);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<KeyAction> getActionsByCategory(const std::string& category) const {
|
||||
std::vector<KeyAction> result;
|
||||
for (const auto& [id, action] : actions_) {
|
||||
if (action.category == category) result.push_back(action);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::map<std::string, KeyCombo> getAllBindings() const { return bindings_; }
|
||||
|
||||
int actionCount() const { return static_cast<int>(actions_.size()); }
|
||||
int bindingCount() const { return static_cast<int>(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<std::string, KeyAction> actions_;
|
||||
std::map<std::string, KeyCombo> bindings_;
|
||||
};
|
||||
230
editor/src/PanelManager.h
Normal file
230
editor/src/PanelManager.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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<PanelInfo> getPanelList() const {
|
||||
std::vector<PanelInfo> 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<PanelInfo> getVisiblePanels() const {
|
||||
std::vector<PanelInfo> 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<PanelInfo> getPanelsByDock(DockPosition dock) const {
|
||||
std::vector<PanelInfo> 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<int>(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<std::string, PanelInfo> panels_;
|
||||
DockingLayout layout_;
|
||||
std::string focusedPanel_;
|
||||
};
|
||||
183
editor/tests/step342_test.cpp
Normal file
183
editor/tests/step342_test.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
// Step 342: DockingLayout + PanelManager Infrastructure (12 tests)
|
||||
// Tests panel registration, docking layout, visibility, focus, JSON persistence
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#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;
|
||||
}
|
||||
188
editor/tests/step343_test.cpp
Normal file
188
editor/tests/step343_test.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
// Step 343: Panel Registration + Icons + Keybinding Foundation (12 tests)
|
||||
// Tests PanelManager registration, Icons system, and KeybindingRegistry basics
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user