Step 89: file watching and auto-reload
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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
45
editor/src/FileWatcher.h
Normal 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);
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
44
editor/tests/step89_test.cpp
Normal file
44
editor/tests/step89_test.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user