Step 118: settings panel

This commit is contained in:
Bill
2026-02-09 13:02:43 -07:00
parent fb9d24eee5
commit da3fec9f15
8 changed files with 374 additions and 10 deletions

View File

@@ -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. |

View File

@@ -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)

View File

@@ -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<int>* 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);

View File

@@ -96,6 +96,7 @@ public:
}
int getTabSize() const { return indent_.tabSize; }
void setTabSize(int size) { indent_.tabSize = size; }
// --- Comments ---

View File

@@ -5,6 +5,8 @@
#include <vector>
#include <filesystem>
#include <cstdlib>
#include <fstream>
#include <nlohmann/json.hpp>
#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<LSPServerConfig> 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", {}));

View File

@@ -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<MemoryStrategyInference::Suggestion> 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);

View File

@@ -0,0 +1,52 @@
// Step 118 TDD Test: Settings persistence
#include "SettingsManager.h"
#include <iostream>
#include <filesystem>
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;
}

View File

@@ -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 (1224),
tab size (2/4/8), theme variant (Dark/Light), auto-save interval,
show minimap, show line numbers, LSP server paths, layout preset,