Step 107c: persist editor mode in recent files

This commit is contained in:
Bill
2026-02-09 11:57:18 -07:00
parent a411d23247
commit de70ac74c0
5 changed files with 75 additions and 14 deletions

View File

@@ -210,7 +210,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1
- [x] Step 107: **IMPLEMENTED** — Before/after diff view with preview and accept/reject (1/1 tests pass)
- [x] Step 107a: **IMPLEMENTED** — Text-Editor Mode toggle and per-buffer mode tracking (2/2 tests pass)
- [x] Step 107b: **IMPLEMENTED** — Mode-specific UI behavior and feature gating (2/2 tests pass)
- [ ] Step 107c: **PLANNED** — Per-buffer persistence for Text/Structured modes (not started)
- [x] Step 107c: **IMPLEMENTED** — Per-buffer mode persistence in recent files (2/2 tests pass)
---
@@ -300,6 +300,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
**Step 107:** Compile and pass (1/1)
**Step 107a:** Compile and pass (2/2)
**Step 107b:** Compile and pass (2/2)
**Step 107c:** Compile and pass (2/2)
---
@@ -419,3 +420,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e
| 2026-02-09 | Codex | Step 107: Before/after diff view with preview and accept/reject. 1/1 tests pass. |
| 2026-02-09 | Codex | Step 107a: Text-Editor Mode toggle and per-buffer mode tracking. 2/2 tests pass. |
| 2026-02-09 | Codex | Step 107b: Mode-specific UI behavior and feature gating. 2/2 tests pass. |
| 2026-02-09 | Codex | Step 107c: Per-buffer mode persistence in recent files. 2/2 tests pass. |

View File

@@ -586,6 +586,10 @@ add_executable(step107b_test tests/step107b_test.cpp)
target_include_directories(step107b_test PRIVATE src)
target_link_libraries(step107b_test PRIVATE nlohmann_json::nlohmann_json)
add_executable(step107c_test tests/step107c_test.cpp)
target_include_directories(step107c_test PRIVATE src)
target_link_libraries(step107c_test PRIVATE nlohmann_json::nlohmann_json)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

View File

@@ -18,6 +18,7 @@ struct WelcomeAction {
struct RecentFile {
std::string path;
std::string language;
std::string mode;
std::string displayName; // just the filename
};
@@ -54,18 +55,21 @@ public:
void setActions(const std::vector<WelcomeAction>& a) { actions_ = a; }
const std::vector<RecentFile>& getRecentFiles() const { return recentFiles_; }
void addRecentFile(const std::string& path, const std::string& lang) {
void addRecentFile(const std::string& path, const std::string& lang, const std::string& mode = "structured") {
// Extract display name from path
std::string name = path;
auto sep = path.find_last_of("/\\");
if (sep != std::string::npos) name = path.substr(sep + 1);
// Don't add duplicates
for (const auto& f : recentFiles_) {
if (f.path == path) return;
for (auto it = recentFiles_.begin(); it != recentFiles_.end(); ++it) {
if (it->path == path) {
recentFiles_.erase(it);
break;
}
}
// Keep max 10 recent files
if (recentFiles_.size() >= 10) recentFiles_.pop_back();
recentFiles_.insert(recentFiles_.begin(), {path, lang, name});
recentFiles_.insert(recentFiles_.begin(), {path, lang, mode, name});
}
void clearRecentFiles() { recentFiles_.clear(); }

View File

@@ -241,7 +241,9 @@ struct EditorState {
}
}
void createBuffer(const std::string& path, const std::string& content, const std::string& language) {
void createBuffer(const std::string& path, const std::string& content,
const std::string& language,
BufferManager::BufferMode mode = BufferManager::BufferMode::Structured) {
if (buffers.hasBuffer(path)) {
buffers.switchToBuffer(path);
activeBuffer = bufferStates[path].get();
@@ -262,8 +264,8 @@ struct EditorState {
state->generatedHighlightsDirty = true;
state->modified = false;
state->lspVersion = 1;
buffers.openBuffer(path, content, language, BufferManager::BufferMode::Structured);
state->bufferMode = BufferManager::BufferMode::Structured;
buffers.openBuffer(path, content, language, mode);
state->bufferMode = mode;
activeBuffer = state.get();
bufferStates[path] = std::move(state);
if (path.rfind("(untitled", 0) != 0) watcher.watch(path);
@@ -301,7 +303,8 @@ struct EditorState {
if (!item.contains("path")) continue;
std::string path = item.value("path", "");
std::string lang = item.value("language", "");
if (!path.empty()) welcome.addRecentFile(path, lang);
std::string mode = item.value("mode", "structured");
if (!path.empty()) welcome.addRecentFile(path, lang, mode);
}
}
@@ -310,7 +313,7 @@ struct EditorState {
std::filesystem::create_directories(p.parent_path());
nlohmann::json j = nlohmann::json::array();
for (const auto& rf : welcome.getRecentFiles()) {
j.push_back({{"path", rf.path}, {"language", rf.language}});
j.push_back({{"path", rf.path}, {"language", rf.language}, {"mode", rf.mode}});
}
std::ofstream out(p.string(), std::ios::binary);
out << j.dump(2);
@@ -439,7 +442,8 @@ struct EditorState {
}
}
void doOpen(const std::string& path) {
void doOpen(const std::string& path,
BufferManager::BufferMode modeOverride = BufferManager::BufferMode::Structured) {
std::ifstream in(path);
if (in.is_open()) {
std::ostringstream ss;
@@ -464,8 +468,8 @@ struct EditorState {
language = "rust";
else if (path.size() > 3 && path.substr(path.size() - 3) == ".go")
language = "go";
createBuffer(path, content, language);
welcome.addRecentFile(path, language);
createBuffer(path, content, language, modeOverride);
welcome.addRecentFile(path, language, bufferModeToString(modeOverride));
saveRecentFiles();
outputLog += "Opened: " + path + "\n";
} else {
@@ -642,6 +646,15 @@ static int countLines(const std::string& text) {
return lines;
}
static std::string bufferModeToString(BufferManager::BufferMode mode) {
return mode == BufferManager::BufferMode::Text ? "text" : "structured";
}
static BufferManager::BufferMode bufferModeFromString(const std::string& mode) {
if (mode == "text") return BufferManager::BufferMode::Text;
return BufferManager::BufferMode::Structured;
}
static std::string generateForLanguage(const Module* ast, const std::string& language) {
if (!ast) return "";
if (language == "python") {
@@ -1138,7 +1151,7 @@ static void RenderWelcome(WelcomeScreen& welcome, EditorState& state, std::strin
ImGui::Text("Recent Files");
for (const auto& rf : welcome.getRecentFiles()) {
if (ImGui::Selectable(rf.displayName.c_str())) {
state.doOpen(rf.path);
state.doOpen(rf.path, bufferModeFromString(rf.mode));
}
}
@@ -1368,6 +1381,12 @@ int main(int, char**) {
} else {
state.onTextChanged();
}
if (state.active()->path.rfind("(untitled", 0) != 0) {
state.welcome.addRecentFile(state.active()->path,
state.active()->language,
bufferModeToString(state.active()->bufferMode));
state.saveRecentFiles();
}
}
}
if (ImGui::BeginMenu("Layout")) {

View File

@@ -0,0 +1,32 @@
// Step 107c TDD Test: Recent file mode persistence
//
// Tests:
// 1. Recent files store mode string
// 2. Re-adding updates mode
#include <cassert>
#include <iostream>
#include "WelcomeScreen.h"
int main() {
int passed = 0;
int failed = 0;
WelcomeScreen welcome;
welcome.addRecentFile("/tmp/a.py", "python", "text");
auto recents = welcome.getRecentFiles();
assert(recents.size() == 1);
assert(recents[0].mode == "text");
std::cout << "Test 1 PASS: mode stored" << std::endl;
++passed;
welcome.addRecentFile("/tmp/a.py", "python", "structured");
recents = welcome.getRecentFiles();
assert(recents.size() == 1);
assert(recents[0].mode == "structured");
std::cout << "Test 2 PASS: mode updated" << std::endl;
++passed;
std::cout << "\n=== Step 107c Results: " << passed << " passed, " << failed << " failed ===" << std::endl;
return failed > 0 ? 1 : 0;
}