Step 89: file watching and auto-reload

This commit is contained in:
Bill
2026-02-09 09:40:47 -07:00
parent 23e3758cca
commit 94a77b6890
5 changed files with 135 additions and 3 deletions

View File

@@ -186,6 +186,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1
- [x] Step 86: **IMPLEMENTED** — Multi-tab editing with BufferManager and per-buffer editor state (3/3 tests pass)
- [x] Step 87: **IMPLEMENTED** — Welcome screen wired with recent files persistence and quick actions (2/2 tests pass)
- [x] Step 88: **IMPLEMENTED** — Drag-and-drop file/folder open via SDL_DROPFILE (2/2 tests pass)
- [x] Step 89: **IMPLEMENTED** — File watcher polling with auto-reload for clean buffers (1/1 tests pass)
---
@@ -253,6 +254,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
**Step 86:** Compile and pass (3/3)
**Step 87:** Compile and pass (2/2)
**Step 88:** Compile and pass (2/2)
**Step 89:** Compile and pass (1/1)
---
@@ -349,3 +351,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e
| 2026-02-09 | Codex | Step 86: Multi-tab editing wired via BufferManager with per-buffer state. 3/3 tests pass. |
| 2026-02-09 | Codex | Step 87: Welcome screen wired with recent files persistence and quick actions. 2/2 tests pass. |
| 2026-02-09 | Codex | Step 88: Drag-and-drop open for files and folders. 2/2 tests pass. |
| 2026-02-09 | Codex | Step 89: File watcher polling with auto-reload for clean buffers. 1/1 tests pass. |

View File

@@ -499,6 +499,9 @@ target_include_directories(step87_test PRIVATE src)
add_executable(step88_test tests/step88_test.cpp)
target_include_directories(step88_test PRIVATE src)
add_executable(step89_test tests/step89_test.cpp)
target_include_directories(step89_test PRIVATE src)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

45
editor/src/FileWatcher.h Normal file
View File

@@ -0,0 +1,45 @@
#pragma once
// Step 89: File watcher (polling)
#include <string>
#include <vector>
#include <unordered_map>
#include <filesystem>
#include <chrono>
class FileWatcher {
public:
void watch(const std::string& path) {
if (path.empty()) return;
auto p = std::filesystem::path(path);
if (!std::filesystem::exists(p)) return;
files_[path] = std::filesystem::last_write_time(p);
}
void unwatch(const std::string& path) {
files_.erase(path);
}
std::vector<std::string> poll() {
std::vector<std::string> changed;
auto now = std::chrono::steady_clock::now();
if (now - lastPoll_ < std::chrono::seconds(2)) return changed;
lastPoll_ = now;
for (auto& kv : files_) {
const std::string& path = kv.first;
auto p = std::filesystem::path(path);
if (!std::filesystem::exists(p)) continue;
auto ts = std::filesystem::last_write_time(p);
if (ts != kv.second) {
kv.second = ts;
changed.push_back(path);
}
}
return changed;
}
private:
std::unordered_map<std::string, std::filesystem::file_time_type> files_;
std::chrono::steady_clock::time_point lastPoll_ = std::chrono::steady_clock::now() - std::chrono::seconds(10);
};

View File

@@ -23,6 +23,7 @@
#include "FileTree.h"
#include "WelcomeScreen.h"
#include "DragDropHandler.h"
#include "FileWatcher.h"
#include "ast/Generator.h"
#include <cstdio>
@@ -80,6 +81,7 @@ struct EditorState {
FileTree fileTree;
FileNode fileTreeRoot;
bool fileTreeDirty = true;
FileWatcher watcher;
BufferState* active() { return activeBuffer; }
@@ -112,6 +114,7 @@ struct EditorState {
buffers.openBuffer(path, content, language);
activeBuffer = state.get();
bufferStates[path] = std::move(state);
if (path.rfind("(untitled", 0) != 0) watcher.watch(path);
}
void switchToBuffer(const std::string& path) {
@@ -285,6 +288,36 @@ struct EditorState {
}
}
void reloadBuffer(const std::string& path) {
auto it = bufferStates.find(path);
if (it == bufferStates.end()) return;
std::ifstream in(path);
if (!in.is_open()) return;
std::ostringstream ss;
ss << in.rdbuf();
auto* buf = it->second.get();
buf->editBuf = ss.str();
buf->editor.setContent(buf->editBuf, buf->language);
buf->sync.setText(buf->editBuf, buf->language);
buf->sync.syncNow();
buf->highlightsDirty = true;
buf->modified = false;
outputLog += "Reloaded: " + path + "\n";
}
void handleFileChanges() {
auto changed = watcher.poll();
for (const auto& path : changed) {
auto it = bufferStates.find(path);
if (it == bufferStates.end()) continue;
if (it->second->modified) {
outputLog += "File changed on disk (dirty): " + path + "\n";
} else {
reloadBuffer(path);
}
}
}
void updateHighlights() {
if (!active()) return;
if (!active()->highlightsDirty) return;
@@ -615,9 +648,9 @@ int main(int, char**) {
std::string lastDialogPath;
bool done = false;
while (!done) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
while (!done) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT)
done = true;
@@ -672,6 +705,9 @@ int main(int, char**) {
}
}
// File watcher polling
state.handleFileChanges();
// Start frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame();
@@ -888,6 +924,7 @@ int main(int, char**) {
if (!open) {
state.buffers.closeBuffer(path);
state.bufferStates.erase(path);
if (path.rfind("(untitled", 0) != 0) state.watcher.unwatch(path);
if (state.buffers.bufferCount() > 0) {
state.switchToBuffer(state.buffers.getOpenBuffers().front());
} else {

View File

@@ -0,0 +1,44 @@
// Step 89 TDD Test: FileWatcher
//
// Tests:
// 1. Detects change on file modification
#include <cassert>
#include <iostream>
#include <filesystem>
#include <fstream>
#include <thread>
#include <chrono>
#include "FileWatcher.h"
int main() {
int passed = 0;
int failed = 0;
std::filesystem::path root = std::filesystem::current_path() / "tmp_step89";
std::filesystem::create_directories(root);
std::filesystem::path file = root / "file.txt";
{
std::ofstream f(file.string());
f << "a";
}
FileWatcher watcher;
watcher.watch(file.string());
std::this_thread::sleep_for(std::chrono::milliseconds(10));
{
std::ofstream f(file.string());
f << "b";
}
auto changed = watcher.poll();
assert(!changed.empty());
std::cout << "Test 1 PASS: Change detected" << std::endl;
++passed;
std::filesystem::remove_all(root);
std::cout << "\n=== Step 89 Results: " << passed << " passed, " << failed << " failed ===" << std::endl;
return failed > 0 ? 1 : 0;
}