diff --git a/PROGRESS.md b/PROGRESS.md index 3df8d3d..4ea5e89 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 05664a8..2c8e66e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/FileWatcher.h b/editor/src/FileWatcher.h new file mode 100644 index 0000000..a4e83ba --- /dev/null +++ b/editor/src/FileWatcher.h @@ -0,0 +1,45 @@ +#pragma once +// Step 89: File watcher (polling) + +#include +#include +#include +#include +#include + +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 poll() { + std::vector 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 files_; + std::chrono::steady_clock::time_point lastPoll_ = std::chrono::steady_clock::now() - std::chrono::seconds(10); +}; diff --git a/editor/src/main.cpp b/editor/src/main.cpp index d221299..74b0152 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -23,6 +23,7 @@ #include "FileTree.h" #include "WelcomeScreen.h" #include "DragDropHandler.h" +#include "FileWatcher.h" #include "ast/Generator.h" #include @@ -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 { diff --git a/editor/tests/step89_test.cpp b/editor/tests/step89_test.cpp new file mode 100644 index 0000000..d9b607c --- /dev/null +++ b/editor/tests/step89_test.cpp @@ -0,0 +1,44 @@ +// Step 89 TDD Test: FileWatcher +// +// Tests: +// 1. Detects change on file modification + +#include +#include +#include +#include +#include +#include +#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; +}