From da3fec9f1515764d4c1c57bce0cda7bab2c73b46 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 9 Feb 2026 13:02:43 -0700 Subject: [PATCH] Step 118: settings panel --- PROGRESS.md | 5 +- editor/CMakeLists.txt | 4 + editor/src/CodeEditorWidget.h | 16 ++- editor/src/EditorMode.h | 1 + editor/src/SettingsManager.h | 89 ++++++++++++++ editor/src/main.cpp | 215 +++++++++++++++++++++++++++++++++- editor/tests/step118_test.cpp | 52 ++++++++ sprint4_plan.md | 2 +- 8 files changed, 374 insertions(+), 10 deletions(-) create mode 100644 editor/tests/step118_test.cpp diff --git a/PROGRESS.md b/PROGRESS.md index 7f411d1..6dbbe1d 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -221,6 +221,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 115: **IMPLEMENTED** — Orchestrator wired for structured mutations; Emacs config path setting (3/3 tests pass) - [x] Step 116: **IMPLEMENTED** — Project save/load (.whetstone) with AST serialization (4/4 tests pass) - [x] Step 117: **IMPLEMENTED** — Session persistence (layout, buffers, cursors, folds) (5/5 tests pass) +- [x] Step 118: **IMPLEMENTED** — Settings panel + persistence (11/11 tests pass) --- @@ -321,6 +322,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 115:** Compile and pass (3/3) **Step 116:** Compile and pass (4/4) **Step 117:** Compile and pass (5/5) +**Step 118:** Compile and pass (11/11) --- @@ -372,7 +374,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi ## What's Next -Sprint 4 in progress. Step 117 (session persistence) done. Next: Step 118 (settings panel). +Sprint 4 in progress. Step 118 (settings panel) done. Next: Step 119 (zoom). --- @@ -451,3 +453,4 @@ Sprint 4 in progress. Step 117 (session persistence) done. Next: Step 118 (setti | 2026-02-09 | Codex | Step 115: Orchestrator wired for structured mutations; Emacs config path setting. 3/3 tests pass. | | 2026-02-09 | Codex | Step 116: Project save/load (.whetstone) with AST serialization. 4/4 tests pass. | | 2026-02-09 | Codex | Step 117: Session persistence (layout, buffers, cursors, folds). 5/5 tests pass. | +| 2026-02-09 | Codex | Step 118: Settings panel + persistence. 11/11 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 651949f..ce16488 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -630,6 +630,10 @@ add_executable(step117_test tests/step117_test.cpp) target_include_directories(step117_test PRIVATE src) target_link_libraries(step117_test PRIVATE nlohmann_json::nlohmann_json) +add_executable(step118_test tests/step118_test.cpp) +target_include_directories(step118_test PRIVATE src) +target_link_libraries(step118_test PRIVATE nlohmann_json::nlohmann_json) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/CodeEditorWidget.h b/editor/src/CodeEditorWidget.h index 47e0aa9..487218a 100644 --- a/editor/src/CodeEditorWidget.h +++ b/editor/src/CodeEditorWidget.h @@ -22,6 +22,7 @@ struct CodeEditorOptions { bool enableFolding = false; bool showMinimap = false; bool showAnnotations = false; + bool showLineNumbers = true; bool showCurrentLine = true; int annotationLayout = 0; // 0=above, 1=margin, 2=beside const std::vector* errorLines = nullptr; @@ -147,7 +148,8 @@ public: // Measure const float lineHeight = ImGui::GetTextLineHeightWithSpacing(); const float charAdvance = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, "M").x; - const float gutterWidth = calcGutterWidth(lineCount, font, charAdvance); + const float gutterWidth = options.showLineNumbers ? + calcGutterWidth(lineCount, font, charAdvance) : 12.0f; const float minimapWidth = options.showMinimap ? 80.0f : 0.0f; // Estimate content size for scrollbars @@ -307,11 +309,13 @@ public: drawList->AddRectFilled(gutterA, gutterB, IM_COL32(20, 20, 20, 255)); // Line number (right-aligned) - char numBuf[16]; - snprintf(numBuf, sizeof(numBuf), "%d", ln + 1); - ImVec2 numSize = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, numBuf); - ImVec2 numPos(origin.x + gutterWidth - 4.0f - numSize.x, y); - drawList->AddText(font, font->FontSize, numPos, IM_COL32(120, 120, 120, 255), numBuf); + if (options.showLineNumbers) { + char numBuf[16]; + snprintf(numBuf, sizeof(numBuf), "%d", ln + 1); + ImVec2 numSize = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, numBuf); + ImVec2 numPos(origin.x + gutterWidth - 4.0f - numSize.x, y); + drawList->AddText(font, font->FontSize, numPos, IM_COL32(120, 120, 120, 255), numBuf); + } // Diagnostics marker bool hasError = lineIn(options.errorLines, ln); diff --git a/editor/src/EditorMode.h b/editor/src/EditorMode.h index 75f469b..093f1cc 100644 --- a/editor/src/EditorMode.h +++ b/editor/src/EditorMode.h @@ -96,6 +96,7 @@ public: } int getTabSize() const { return indent_.tabSize; } + void setTabSize(int size) { indent_.tabSize = size; } // --- Comments --- diff --git a/editor/src/SettingsManager.h b/editor/src/SettingsManager.h index e6ec431..f4c6c63 100644 --- a/editor/src/SettingsManager.h +++ b/editor/src/SettingsManager.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "ast/Schema.h" #include "ast/ASTNode.h" @@ -26,6 +28,85 @@ public: const std::string& getEmacsConfigPath() const { return emacsConfigPath_; } void setEmacsConfigPath(const std::string& path) { emacsConfigPath_ = path; } std::string& getEmacsConfigPathMutable() { return emacsConfigPath_; } + int getFontSize() const { return fontSize_; } + void setFontSize(int size) { fontSize_ = size; } + int getTabSize() const { return tabSize_; } + void setTabSize(int size) { tabSize_ = size; } + const std::string& getTheme() const { return theme_; } + void setTheme(const std::string& theme) { theme_ = theme; } + int getAutoSaveSeconds() const { return autoSaveSeconds_; } + void setAutoSaveSeconds(int seconds) { autoSaveSeconds_ = seconds; } + bool getShowMinimap() const { return showMinimap_; } + void setShowMinimap(bool value) { showMinimap_ = value; } + bool getShowLineNumbers() const { return showLineNumbers_; } + void setShowLineNumbers(bool value) { showLineNumbers_ = value; } + const std::string& getLayoutPreset() const { return layoutPreset_; } + void setLayoutPreset(const std::string& name) { layoutPreset_ = name; } + const std::string& getKeybindingProfile() const { return keybindingProfile_; } + void setKeybindingProfile(const std::string& name) { keybindingProfile_ = name; } + + bool loadFromFile(const std::string& path) { + try { + std::ifstream in(path, std::ios::binary); + if (!in.is_open()) return false; + nlohmann::json j; + in >> j; + fontSize_ = j.value("fontSize", fontSize_); + tabSize_ = j.value("tabSize", tabSize_); + theme_ = j.value("theme", theme_); + autoSaveSeconds_ = j.value("autoSaveSeconds", autoSaveSeconds_); + showMinimap_ = j.value("showMinimap", showMinimap_); + showLineNumbers_ = j.value("showLineNumbers", showLineNumbers_); + layoutPreset_ = j.value("layoutPreset", layoutPreset_); + keybindingProfile_ = j.value("keybindingProfile", keybindingProfile_); + emacsConfigPath_ = j.value("emacsConfigPath", emacsConfigPath_); + if (j.contains("lspServers") && j["lspServers"].is_array()) { + for (const auto& item : j["lspServers"]) { + std::string lang = item.value("language", ""); + if (lang.empty()) continue; + auto* cfg = getServer(lang); + if (!cfg) continue; + cfg->path = item.value("path", cfg->path); + cfg->argsLine = item.value("args", cfg->argsLine); + cfg->enabled = item.value("enabled", cfg->enabled); + syncArgs(*cfg); + } + } + return true; + } catch (...) { + return false; + } + } + + bool saveToFile(const std::string& path) const { + try { + nlohmann::json j; + j["fontSize"] = fontSize_; + j["tabSize"] = tabSize_; + j["theme"] = theme_; + j["autoSaveSeconds"] = autoSaveSeconds_; + j["showMinimap"] = showMinimap_; + j["showLineNumbers"] = showLineNumbers_; + j["layoutPreset"] = layoutPreset_; + j["keybindingProfile"] = keybindingProfile_; + j["emacsConfigPath"] = emacsConfigPath_; + j["lspServers"] = nlohmann::json::array(); + for (const auto& cfg : lspServers_) { + j["lspServers"].push_back({ + {"language", cfg.language}, + {"path", cfg.path}, + {"args", cfg.argsLine}, + {"enabled", cfg.enabled} + }); + } + std::ofstream out(path, std::ios::binary); + if (!out.is_open()) return false; + out << j.dump(2); + return true; + } catch (...) { + return false; + } + } LSPServerConfig* getServer(const std::string& language) { for (auto& s : lspServers_) { @@ -67,6 +148,14 @@ public: private: std::vector lspServers_; std::string emacsConfigPath_; + int fontSize_ = 15; + int tabSize_ = 4; + std::string theme_ = "Dark"; + int autoSaveSeconds_ = 0; + bool showMinimap_ = false; + bool showLineNumbers_ = true; + std::string layoutPreset_ = "VSCode"; + std::string keybindingProfile_ = "VSCode"; void loadDefaults() { lspServers_.push_back(makeConfig("python", "pylsp", {})); diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 1fbcd71..e9756c1 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -130,6 +130,7 @@ struct EditorState { bool showMinimap = false; bool showAnnotations = false; bool showOutline = true; + bool showLineNumbers = true; char outlineFilter[128] = {}; LayoutPreset layoutPreset = LayoutPreset::VSCode; WelcomeScreen welcome; @@ -165,6 +166,8 @@ struct EditorState { bool showLspSettings = false; Orchestrator orchestrator; bool orchestratorDirty = true; + bool showSettingsPanel = false; + double lastAutoSave = 0.0; ASTMutationAPI mutator; std::vector suggestions; bool showSuggestionPopup = false; @@ -562,6 +565,10 @@ struct EditorState { return configDir() / "session.json"; } + std::filesystem::path settingsFilePath() const { + return configDir() / "settings.json"; + } + bool saveSession(const std::string& imguiIni) { SessionData session; session.workspaceRoot = workspaceRoot; @@ -628,12 +635,43 @@ struct EditorState { } } + void applyTabSizeToBuffers(int size) { + for (auto& kv : bufferStates) { + kv.second->mode.setTabSize(size); + kv.second->generatedMode.setTabSize(size); + } + } + + void applySettingsToState() { + showMinimap = settings.getShowMinimap(); + showLineNumbers = settings.getShowLineNumbers(); + layoutPreset = LayoutManager::presetFromName(settings.getLayoutPreset()); + keys.setProfile(KeybindingManager::profileFromName(settings.getKeybindingProfile())); + registerCommands(); + applyTabSizeToBuffers(settings.getTabSize()); + } + + void loadSettingsFromDisk() { + settings.loadFromFile(settingsFilePath().string()); + applySettingsToState(); + } + + void saveSettingsToDisk() { + settings.setShowMinimap(showMinimap); + settings.setShowLineNumbers(showLineNumbers); + settings.setLayoutPreset(LayoutManager::presetName(layoutPreset)); + settings.setKeybindingProfile(KeybindingManager::profileName(keys.getProfile())); + std::filesystem::create_directories(settingsFilePath().parent_path()); + settings.saveToFile(settingsFilePath().string()); + } + void init() { outputLog = "Whetstone Editor ready.\n"; workspaceRoot = std::filesystem::current_path().string(); projectSearch.setRoot(workspaceRoot); fileTreeDirty = true; loadRecentFiles(); + loadSettingsFromDisk(); registerCommands(); } @@ -1618,6 +1656,48 @@ static void SetupVSCodeDarkTheme() { style.ItemSpacing = ImVec2(6, 4); } +static void SetupVSCodeLightTheme() { + ImGuiStyle& style = ImGui::GetStyle(); + ImVec4* colors = style.Colors; + + colors[ImGuiCol_WindowBg] = ImVec4(0.95f, 0.95f, 0.95f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.95f, 0.95f, 0.95f, 1.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.98f, 0.98f, 0.98f, 1.00f); + + colors[ImGuiCol_Border] = ImVec4(0.75f, 0.75f, 0.75f, 1.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.85f, 0.85f, 0.85f, 1.00f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.80f, 0.80f, 0.80f, 1.00f); + + colors[ImGuiCol_TitleBg] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.86f, 0.86f, 0.86f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + + colors[ImGuiCol_MenuBarBg] = ImVec4(0.92f, 0.92f, 0.92f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.85f, 0.85f, 0.85f, 1.00f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.78f, 0.78f, 0.78f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.70f, 0.70f, 0.70f, 1.00f); + + colors[ImGuiCol_Header] = ImVec4(0.82f, 0.82f, 0.82f, 1.00f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.75f, 0.75f, 0.75f, 1.00f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.70f, 0.70f, 0.70f, 1.00f); + + colors[ImGuiCol_Text] = ImVec4(0.10f, 0.10f, 0.10f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.45f, 0.45f, 0.45f, 1.00f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.40f, 0.60f, 0.90f, 0.45f); + + style.WindowRounding = 0.0f; + style.FrameRounding = 2.0f; + style.ScrollbarRounding = 2.0f; + style.GrabRounding = 2.0f; + style.TabRounding = 0.0f; + style.WindowBorderSize = 1.0f; + style.FrameBorderSize = 0.0f; + style.WindowPadding = ImVec2(8, 8); + style.FramePadding = ImVec2(6, 3); + style.ItemSpacing = ImVec2(6, 4); +} + // --------------------------------------------------------------------------- // Syntax highlight color map // --------------------------------------------------------------------------- @@ -1792,13 +1872,14 @@ int main(int, char**) { io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Load a monospace font (Consolas on Windows, fallback to default) + const float baseFontSize = 15.0f; ImFont* monoFont = nullptr; - monoFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\consola.ttf", 15.0f); + monoFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\consola.ttf", baseFontSize); if (!monoFont) { monoFont = io.Fonts->AddFontDefault(); } // Also keep default font for UI elements - ImFont* uiFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", 15.0f); + ImFont* uiFont = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", baseFontSize); if (!uiFont) { uiFont = io.Fonts->AddFontDefault(); } @@ -1811,6 +1892,12 @@ int main(int, char**) { // Editor state EditorState state; state.init(); + if (state.settings.getTheme() == "Light") { + SetupVSCodeLightTheme(); + } else { + SetupVSCodeDarkTheme(); + } + io.FontGlobalScale = state.settings.getFontSize() / baseFontSize; SessionData session; if (state.loadSession(session)) { if (!session.imguiIni.empty()) { @@ -1898,6 +1985,13 @@ int main(int, char**) { // File watcher polling state.handleFileChanges(); + if (state.settings.getAutoSaveSeconds() > 0 && state.active() && state.active()->modified) { + double now = ImGui::GetTime(); + if ((now - state.lastAutoSave) >= state.settings.getAutoSaveSeconds()) { + state.doSave(); + state.lastAutoSave = now; + } + } // Start frame ImGui_ImplOpenGL3_NewFrame(); @@ -2007,6 +2101,7 @@ int main(int, char**) { ImGui::MenuItem("Show Minimap", nullptr, &state.showMinimap); ImGui::MenuItem("Show Annotations", nullptr, &state.showAnnotations); ImGui::MenuItem("Show Outline", nullptr, &state.showOutline); + ImGui::MenuItem("Settings", nullptr, &state.showSettingsPanel); ImGui::MenuItem("LSP Servers...", nullptr, &state.showLspSettings); if (state.active()) { bool textMode = state.active()->bufferMode == BufferManager::BufferMode::Text; @@ -2312,6 +2407,118 @@ int main(int, char**) { ImGui::End(); } + // --------------------------------------------------------------- + // Settings Panel + // --------------------------------------------------------------- + if (state.showSettingsPanel) { + ImGui::Begin("Settings", &state.showSettingsPanel); + ImGui::PushFont(uiFont); + bool settingsChanged = false; + ImGuiIO& io = ImGui::GetIO(); + + int fontSize = state.settings.getFontSize(); + if (ImGui::SliderInt("Font Size", &fontSize, 12, 24)) { + state.settings.setFontSize(fontSize); + io.FontGlobalScale = fontSize / baseFontSize; + settingsChanged = true; + } + + int tabSize = state.settings.getTabSize(); + const int tabSizes[] = {2, 4, 8}; + int tabIndex = 1; + for (int i = 0; i < 3; ++i) { + if (tabSize == tabSizes[i]) tabIndex = i; + } + const char* tabLabels[] = {"2", "4", "8"}; + if (ImGui::Combo("Tab Size", &tabIndex, tabLabels, 3)) { + state.settings.setTabSize(tabSizes[tabIndex]); + state.applyTabSizeToBuffers(tabSizes[tabIndex]); + settingsChanged = true; + } + + const char* themeLabels[] = {"Dark", "Light"}; + int themeIndex = state.settings.getTheme() == "Light" ? 1 : 0; + if (ImGui::Combo("Theme", &themeIndex, themeLabels, 2)) { + state.settings.setTheme(themeLabels[themeIndex]); + if (themeIndex == 1) SetupVSCodeLightTheme(); + else SetupVSCodeDarkTheme(); + settingsChanged = true; + } + + int autoSave = state.settings.getAutoSaveSeconds(); + if (ImGui::InputInt("Auto-save (sec)", &autoSave)) { + autoSave = std::max(0, autoSave); + state.settings.setAutoSaveSeconds(autoSave); + settingsChanged = true; + } + + bool showMinimap = state.showMinimap; + if (ImGui::Checkbox("Show Minimap", &showMinimap)) { + state.showMinimap = showMinimap; + settingsChanged = true; + } + bool showLineNumbers = state.showLineNumbers; + if (ImGui::Checkbox("Show Line Numbers", &showLineNumbers)) { + state.showLineNumbers = showLineNumbers; + settingsChanged = true; + } + + LayoutPreset preset = state.layoutPreset; + int presetIndex = 0; + if (preset == LayoutPreset::Emacs) presetIndex = 1; + else if (preset == LayoutPreset::JetBrains) presetIndex = 2; + const char* presetLabels[] = {"VSCode", "Emacs", "JetBrains"}; + if (ImGui::Combo("Layout Preset", &presetIndex, presetLabels, 3)) { + state.layoutPreset = (presetIndex == 1) ? LayoutPreset::Emacs : + (presetIndex == 2) ? LayoutPreset::JetBrains : LayoutPreset::VSCode; + settingsChanged = true; + } + + int keyProfileIndex = 0; + if (state.keys.getProfile() == KeybindingProfile::JetBrains) keyProfileIndex = 1; + else if (state.keys.getProfile() == KeybindingProfile::Emacs) keyProfileIndex = 2; + const char* keyProfiles[] = {"VSCode", "JetBrains", "Emacs"}; + if (ImGui::Combo("Keybindings", &keyProfileIndex, keyProfiles, 3)) { + KeybindingProfile profile = KeybindingProfile::VSCode; + if (keyProfileIndex == 1) profile = KeybindingProfile::JetBrains; + else if (keyProfileIndex == 2) profile = KeybindingProfile::Emacs; + state.keys.setProfile(profile); + state.registerCommands(); + settingsChanged = true; + } + + ImGui::Separator(); + ImGui::TextUnformatted("Emacs Config"); + ImGui::SetNextItemWidth(320); + if (InputTextStr("Config Path", &state.settings.getEmacsConfigPathMutable())) { + settingsChanged = true; + } + + if (ImGui::CollapsingHeader("LSP Servers", ImGuiTreeNodeFlags_DefaultOpen)) { + for (auto& cfg : state.settings.getLSPServersMutable()) { + ImGui::PushID(cfg.language.c_str()); + ImGui::Checkbox("Enabled", &cfg.enabled); + ImGui::SameLine(); + ImGui::Text("%s", cfg.language.c_str()); + ImGui::SetNextItemWidth(320); + if (InputTextStr("Path", &cfg.path)) settingsChanged = true; + ImGui::SetNextItemWidth(320); + if (InputTextStr("Args", &cfg.argsLine)) { + state.settings.syncArgs(cfg); + settingsChanged = true; + } + ImGui::Separator(); + ImGui::PopID(); + } + } + + if (settingsChanged) { + state.saveSettingsToDisk(); + } + ImGui::PopFont(); + ImGui::End(); + } + // --------------------------------------------------------------- // Go To Line (Ctrl+G) // --------------------------------------------------------------- @@ -2680,6 +2887,7 @@ int main(int, char**) { opts.enableFolding = true; opts.showMinimap = state.showMinimap; opts.showAnnotations = state.showAnnotations; + opts.showLineNumbers = state.showLineNumbers; if (state.layoutPreset == LayoutPreset::Emacs) opts.annotationLayout = 1; else if (state.layoutPreset == LayoutPreset::JetBrains) opts.annotationLayout = 2; else opts.annotationLayout = 0; @@ -2732,6 +2940,7 @@ int main(int, char**) { genOpts.readOnly = true; genOpts.showWhitespace = state.showWhitespace; genOpts.mode = &buf->generatedMode; + genOpts.showLineNumbers = state.showLineNumbers; genOpts.showCurrentLine = false; genOpts.highlightLine = buf->generatedHighlightLine; genOpts.syncScrollX = &buf->splitScrollX; @@ -3561,6 +3770,7 @@ int main(int, char**) { CodeEditorOptions leftOpts; leftOpts.readOnly = true; leftOpts.showWhitespace = state.showWhitespace; + leftOpts.showLineNumbers = state.showLineNumbers; leftOpts.showCurrentLine = false; leftOpts.highlightLines = &state.diff.beforeLines; leftOpts.highlightLineColor = IM_COL32(160, 80, 80, 120); @@ -3575,6 +3785,7 @@ int main(int, char**) { CodeEditorOptions rightOpts; rightOpts.readOnly = true; rightOpts.showWhitespace = state.showWhitespace; + rightOpts.showLineNumbers = state.showLineNumbers; rightOpts.showCurrentLine = false; rightOpts.highlightLines = &state.diff.afterLines; rightOpts.highlightLineColor = IM_COL32(80, 160, 80, 120); diff --git a/editor/tests/step118_test.cpp b/editor/tests/step118_test.cpp new file mode 100644 index 0000000..7a483d7 --- /dev/null +++ b/editor/tests/step118_test.cpp @@ -0,0 +1,52 @@ +// Step 118 TDD Test: Settings persistence +#include "SettingsManager.h" +#include +#include + +static void expect(bool cond, const std::string& name, int& passed, int& failed) { + if (cond) { + std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n"; + ++passed; + } else { + std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n"; + ++failed; + } +} + +int main() { + int passed = 0; + int failed = 0; + + std::filesystem::path path = std::filesystem::temp_directory_path() / "whetstone_settings_test.json"; + + SettingsManager settings; + settings.setFontSize(18); + settings.setTabSize(2); + settings.setTheme("Light"); + settings.setAutoSaveSeconds(15); + settings.setShowMinimap(true); + settings.setShowLineNumbers(false); + settings.setLayoutPreset("Emacs"); + settings.setKeybindingProfile("JetBrains"); + settings.setEmacsConfigPath("C:/emacs"); + bool saved = settings.saveToFile(path.string()); + expect(saved, "save settings", passed, failed); + + SettingsManager loaded; + bool loadedOk = loaded.loadFromFile(path.string()); + expect(loadedOk, "load settings", passed, failed); + expect(loaded.getFontSize() == 18, "font size", passed, failed); + expect(loaded.getTabSize() == 2, "tab size", passed, failed); + expect(loaded.getTheme() == "Light", "theme", passed, failed); + expect(loaded.getAutoSaveSeconds() == 15, "auto-save", passed, failed); + expect(loaded.getShowMinimap() == true, "minimap", passed, failed); + expect(loaded.getShowLineNumbers() == false, "line numbers", passed, failed); + expect(loaded.getLayoutPreset() == "Emacs", "layout preset", passed, failed); + expect(loaded.getKeybindingProfile() == "JetBrains", "keybindings", passed, failed); + expect(loaded.getEmacsConfigPath() == "C:/emacs", "emacs path", passed, failed); + + std::filesystem::remove(path); + + std::cout << "\n=== Step 118 Results: " << passed << " passed, " << failed << " failed ===\n"; + return failed == 0 ? 0 : 1; +} diff --git a/sprint4_plan.md b/sprint4_plan.md index dd637d2..3fa1b3d 100644 --- a/sprint4_plan.md +++ b/sprint4_plan.md @@ -373,7 +373,7 @@ AST extensions for Sprint 5. Saved to `~/.whetstone/session.json`. Restore on next launch. *New:* `SessionManager.h` -- [ ] **Step 118: Settings panel** +- [x] **Step 118: Settings panel** View > Settings opens a dockable panel. Options: font size (12–24), tab size (2/4/8), theme variant (Dark/Light), auto-save interval, show minimap, show line numbers, LSP server paths, layout preset,