diff --git a/CMakeLists.txt b/CMakeLists.txt index aaff17d..d4f0883 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(kte) include(GNUInstallDirs) set(CMAKE_CXX_STANDARD 20) -set(KTE_VERSION "1.8.3") +set(KTE_VERSION "1.9.0") # Default to terminal-only build to avoid SDL/OpenGL dependency by default. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. @@ -327,6 +327,7 @@ if (BUILD_TESTS) tests/test_swap_edge_cases.cc tests/test_swap_recovery_prompt.cc tests/test_swap_cleanup.cc + tests/test_swap_cleanup2.cc tests/test_swap_git_editor.cc tests/test_piece_table.cc tests/test_search.cc diff --git a/Command.cc b/Command.cc index 21d7291..f6e9be6 100644 --- a/Command.cc +++ b/Command.cc @@ -752,6 +752,8 @@ cmd_save_and_quit(CommandContext &ctx) if (buf->IsFileBacked()) { if (buf->Save(err)) { buf->SetDirty(false); + if (auto *sm = ctx.editor.Swap()) + sm->ResetJournal(*buf); } else { ctx.editor.SetStatus(err); return false; @@ -759,6 +761,8 @@ cmd_save_and_quit(CommandContext &ctx) } else if (!buf->Filename().empty()) { if (buf->SaveAs(buf->Filename(), err)) { buf->SetDirty(false); + if (auto *sm = ctx.editor.Swap()) + sm->ResetJournal(*buf); } else { ctx.editor.SetStatus(err); return false; @@ -2568,6 +2572,10 @@ cmd_newline(CommandContext &ctx) ctx.editor.SetStatus(err); } else { buf->SetDirty(false); + if (auto *sm = ctx.editor.Swap()) { + sm->NotifyFilenameChanged(*buf); + sm->ResetJournal(*buf); + } ctx.editor.SetStatus("Saved as " + value); if (auto *u = buf->Undo()) u->mark_saved(); diff --git a/Editor.cc b/Editor.cc index 33cb487..16c9572 100644 --- a/Editor.cc +++ b/Editor.cc @@ -69,20 +69,22 @@ Editor::SetStatus(const std::string &message) Buffer * Editor::CurrentBuffer() { - if (buffers_.empty() || curbuf_ >= buffers_.size()) { + auto &bufs = Buffers(); + if (bufs.empty() || curbuf_ >= bufs.size()) { return nullptr; } - return &buffers_[curbuf_]; + return &bufs[curbuf_]; } const Buffer * Editor::CurrentBuffer() const { - if (buffers_.empty() || curbuf_ >= buffers_.size()) { + const auto &bufs = Buffers(); + if (bufs.empty() || curbuf_ >= bufs.size()) { return nullptr; } - return &buffers_[curbuf_]; + return &bufs[curbuf_]; } @@ -117,8 +119,9 @@ Editor::DisplayNameFor(const Buffer &buf) const // Prepare list of other buffer paths std::vector > others; - others.reserve(buffers_.size()); - for (const auto &b: buffers_) { + const auto &bufs = Buffers(); + others.reserve(bufs.size()); + for (const auto &b: bufs) { if (&b == &buf) continue; if (b.Filename().empty()) @@ -161,41 +164,44 @@ Editor::DisplayNameFor(const Buffer &buf) const std::size_t Editor::AddBuffer(const Buffer &buf) { - buffers_.push_back(buf); + auto &bufs = Buffers(); + bufs.push_back(buf); // Attach swap recorder if (swap_) { - swap_->Attach(&buffers_.back()); - buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back())); + swap_->Attach(&bufs.back()); + bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back())); } - if (buffers_.size() == 1) { + if (bufs.size() == 1) { curbuf_ = 0; } - return buffers_.size() - 1; + return bufs.size() - 1; } std::size_t Editor::AddBuffer(Buffer &&buf) { - buffers_.push_back(std::move(buf)); + auto &bufs = Buffers(); + bufs.push_back(std::move(buf)); if (swap_) { - swap_->Attach(&buffers_.back()); - buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back())); + swap_->Attach(&bufs.back()); + bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back())); } - if (buffers_.size() == 1) { + if (bufs.size() == 1) { curbuf_ = 0; } - return buffers_.size() - 1; + return bufs.size() - 1; } bool Editor::OpenFile(const std::string &path, std::string &err) { - // If there is exactly one unnamed, empty, clean buffer, reuse it instead - // of creating a new one. - if (buffers_.size() == 1) { - Buffer &cur = buffers_[curbuf_]; + // If the current buffer is an unnamed, empty, clean scratch buffer, reuse + // it instead of creating a new one. + auto &bufs_ref = Buffers(); + if (!bufs_ref.empty() && curbuf_ < bufs_ref.size()) { + Buffer &cur = bufs_ref[curbuf_]; const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked(); const bool clean = !cur.Dirty(); const std::size_t nrows = cur.Nrows(); @@ -268,7 +274,7 @@ Editor::OpenFile(const std::string &path, std::string &err) // Add as a new buffer and switch to it std::size_t idx = AddBuffer(std::move(b)); if (swap_) { - swap_->NotifyFilenameChanged(buffers_[idx]); + swap_->NotifyFilenameChanged(Buffers()[idx]); } SwitchTo(idx); // Defensive: ensure any active prompt is closed after a successful open @@ -446,12 +452,13 @@ Editor::ProcessPendingOpens() bool Editor::SwitchTo(std::size_t index) { - if (index >= buffers_.size()) { + auto &bufs = Buffers(); + if (index >= bufs.size()) { return false; } curbuf_ = index; // Robustness: ensure a valid highlighter is installed when switching buffers - Buffer &b = buffers_[curbuf_]; + Buffer &b = bufs[curbuf_]; if (b.SyntaxEnabled()) { b.EnsureHighlighter(); if (auto *eng = b.Highlighter()) { @@ -478,21 +485,22 @@ Editor::SwitchTo(std::size_t index) bool Editor::CloseBuffer(std::size_t index) { - if (index >= buffers_.size()) { + auto &bufs = Buffers(); + if (index >= bufs.size()) { return false; } if (swap_) { // Always remove swap file when closing a buffer on normal exit. // Swap files are for crash recovery; on clean close, we don't need them. // This prevents stale swap files from accumulating (e.g., when used as git editor). - swap_->Detach(&buffers_[index], true); - buffers_[index].SetSwapRecorder(nullptr); + swap_->Detach(&bufs[index], true); + bufs[index].SetSwapRecorder(nullptr); } - buffers_.erase(buffers_.begin() + static_cast(index)); - if (buffers_.empty()) { + bufs.erase(bufs.begin() + static_cast(index)); + if (bufs.empty()) { curbuf_ = 0; - } else if (curbuf_ >= buffers_.size()) { - curbuf_ = buffers_.size() - 1; + } else if (curbuf_ >= bufs.size()) { + curbuf_ = bufs.size() - 1; } return true; } @@ -516,7 +524,12 @@ Editor::Reset() // Reset close-confirm/save state close_confirm_pending_ = false; close_after_save_ = false; - buffers_.clear(); + auto &bufs = Buffers(); + if (swap_) { + for (auto &buf : bufs) + swap_->Detach(&buf, true); + } + bufs.clear(); curbuf_ = 0; } diff --git a/Editor.h b/Editor.h index 109ce8d..f367fff 100644 --- a/Editor.h +++ b/Editor.h @@ -521,7 +521,7 @@ public: // Buffers [[nodiscard]] std::size_t BufferCount() const { - return buffers_.size(); + return Buffers().size(); } @@ -531,6 +531,19 @@ public: } + // Clamp curbuf_ to valid range. Call when the shared buffer list may + // have been modified by another editor (e.g., buffer closed in another window). + void ValidateBufferIndex() + { + const auto &bufs = Buffers(); + if (bufs.empty()) { + curbuf_ = 0; + } else if (curbuf_ >= bufs.size()) { + curbuf_ = bufs.size() - 1; + } + } + + Buffer *CurrentBuffer(); const Buffer *CurrentBuffer() const; diff --git a/ImGuiFrontend.cc b/ImGuiFrontend.cc index 0cd3f54..0abed79 100644 --- a/ImGuiFrontend.cc +++ b/ImGuiFrontend.cc @@ -30,7 +30,7 @@ static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible) // --------------------------------------------------------------------------- -// Helpers shared between Init and OpenNewWindow_ +// Helpers // --------------------------------------------------------------------------- static void @@ -96,6 +96,63 @@ update_editor_dimensions(Editor &ed, float disp_w, float disp_h) } +// --------------------------------------------------------------------------- +// SetupImGuiStyle_ — apply theme, fonts, and flags to the current ImGui context +// --------------------------------------------------------------------------- +void +GUIFrontend::SetupImGuiStyle_() +{ + ImGuiIO &io = ImGui::GetIO(); + + // Disable imgui.ini for secondary windows (primary sets its own path in Init) + io.IniFilename = nullptr; + + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + ImGui::StyleColorsDark(); + + if (config_.background == "light") + kte::SetBackgroundMode(kte::BackgroundMode::Light); + else + kte::SetBackgroundMode(kte::BackgroundMode::Dark); + kte::ApplyThemeByName(config_.theme); + + // Load fonts into this context's font atlas. + // Font registry is global and already populated by Init; just load into this atlas. + if (!kte::Fonts::FontRegistry::Instance().LoadFont(config_.font, (float) config_.font_size)) { + LoadGuiFont_(nullptr, (float) config_.font_size); + } +} + + +// --------------------------------------------------------------------------- +// Destroy a single window's ImGui context + SDL/GL resources +// --------------------------------------------------------------------------- +void +GUIFrontend::DestroyWindowResources_(WindowState &ws) +{ + if (ws.imgui_ctx) { + // Must activate this window's GL context before shutting down the + // OpenGL3 backend, otherwise it deletes another context's resources. + if (ws.window && ws.gl_ctx) + SDL_GL_MakeCurrent(ws.window, ws.gl_ctx); + ImGui::SetCurrentContext(ws.imgui_ctx); + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplSDL2_Shutdown(); + ImGui::DestroyContext(ws.imgui_ctx); + ws.imgui_ctx = nullptr; + } + if (ws.gl_ctx) { + SDL_GL_DeleteContext(ws.gl_ctx); + ws.gl_ctx = nullptr; + } + if (ws.window) { + SDL_DestroyWindow(ws.window); + ws.window = nullptr; + } +} + + bool GUIFrontend::Init(int &argc, char **argv, Editor &ed) { @@ -172,9 +229,10 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed) SDL_GL_MakeCurrent(win, gl_ctx); SDL_GL_SetSwapInterval(1); // vsync + // Create primary ImGui context IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO &io = ImGui::GetIO(); + ImGuiContext *imgui_ctx = ImGui::CreateContext(); + ImGuiIO &io = ImGui::GetIO(); // Set custom ini filename path to ~/.config/kte/imgui.ini if (const char *home = std::getenv("HOME")) { @@ -236,11 +294,12 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed) } // Build primary WindowState - auto ws = std::make_unique(); - ws->window = win; - ws->gl_ctx = gl_ctx; - ws->width = init_w; - ws->height = init_h; + auto ws = std::make_unique(); + ws->window = win; + ws->gl_ctx = gl_ctx; + ws->imgui_ctx = imgui_ctx; + ws->width = init_w; + ws->height = init_h; // The primary window's editor IS the editor passed in from main; we don't // use ws->editor for the primary — instead we keep a pointer to &ed. // We store a sentinel: window index 0 uses the external editor reference. @@ -255,8 +314,6 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed) bool GUIFrontend::OpenNewWindow_(Editor &primary) { - SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx); - Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; int w = windows_[0]->width; int h = windows_[0]->height; @@ -277,25 +334,48 @@ GUIFrontend::OpenNewWindow_(Editor &primary) SDL_GL_MakeCurrent(win, gl_ctx); SDL_GL_SetSwapInterval(1); - // Secondary windows share the ImGui context already created in Init. - // We need to init the SDL2/OpenGL backends for this new window. - // ImGui_ImplSDL2 supports multiple windows via SDL_GetWindowID checks. - ImGui_ImplOpenGL3_Init(kGlslVersion); + // Each window gets its own ImGui context — ImGui requires exactly one + // NewFrame/Render cycle per context per frame. + ImGuiContext *imgui_ctx = ImGui::CreateContext(); + ImGui::SetCurrentContext(imgui_ctx); - auto ws = std::make_unique(); - ws->window = win; - ws->gl_ctx = gl_ctx; - ws->width = w; - ws->height = h; + SetupImGuiStyle_(); + + if (!ImGui_ImplSDL2_InitForOpenGL(win, gl_ctx)) { + ImGui::DestroyContext(imgui_ctx); + SDL_GL_DeleteContext(gl_ctx); + SDL_DestroyWindow(win); + return false; + } + if (!ImGui_ImplOpenGL3_Init(kGlslVersion)) { + ImGui_ImplSDL2_Shutdown(); + ImGui::DestroyContext(imgui_ctx); + SDL_GL_DeleteContext(gl_ctx); + SDL_DestroyWindow(win); + return false; + } + + auto ws = std::make_unique(); + ws->window = win; + ws->gl_ctx = gl_ctx; + ws->imgui_ctx = imgui_ctx; + ws->width = w; + ws->height = h; // Secondary editor shares the primary's buffer list ws->editor.SetSharedBuffers(&primary.Buffers()); ws->editor.SetDimensions(primary.Rows(), primary.Cols()); + + // Open a new untitled buffer and switch to it in the new window. + ws->editor.AddBuffer(Buffer()); + ws->editor.SwitchTo(ws->editor.BufferCount() - 1); + ws->input.Attach(&ws->editor); windows_.push_back(std::move(ws)); - // Restore primary GL context as current + // Restore primary context + ImGui::SetCurrentContext(windows_[0]->imgui_ctx); SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx); return true; } @@ -305,10 +385,10 @@ void GUIFrontend::Step(Editor &ed, bool &running) { // --- Event processing --- + // SDL events carry a window ID. Route each event to the correct window's + // ImGui context (for ImGui_ImplSDL2_ProcessEvent) and input handler. SDL_Event e; while (SDL_PollEvent(&e)) { - ImGui_ImplSDL2_ProcessEvent(&e); - // Determine which window this event belongs to Uint32 event_win_id = 0; switch (e.type) { @@ -329,6 +409,9 @@ GUIFrontend::Step(Editor &ed, bool &running) case SDL_MOUSEWHEEL: event_win_id = e.wheel.windowID; break; + case SDL_MOUSEMOTION: + event_win_id = e.motion.windowID; + break; default: break; } @@ -338,59 +421,67 @@ GUIFrontend::Step(Editor &ed, bool &running) break; } + // Find the target window and route the event to its ImGui context + WindowState *target = nullptr; + std::size_t target_idx = 0; + if (event_win_id != 0) { + for (std::size_t i = 0; i < windows_.size(); ++i) { + if (SDL_GetWindowID(windows_[i]->window) == event_win_id) { + target = windows_[i].get(); + target_idx = i; + break; + } + } + } + + if (target && target->imgui_ctx) { + // Set this window's ImGui context so ImGui_ImplSDL2_ProcessEvent + // updates the correct IO state. + ImGui::SetCurrentContext(target->imgui_ctx); + ImGui_ImplSDL2_ProcessEvent(&e); + } + if (e.type == SDL_WINDOWEVENT) { if (e.window.event == SDL_WINDOWEVENT_CLOSE) { - // Mark the window as dead; primary window close = quit - for (std::size_t i = 0; i < windows_.size(); ++i) { - if (SDL_GetWindowID(windows_[i]->window) == e.window.windowID) { - if (i == 0) { - running = false; - } else { - windows_[i]->alive = false; - } - break; + if (target) { + if (target_idx == 0) { + running = false; + } else { + target->alive = false; } } } else if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { - for (auto &ws: windows_) { - if (SDL_GetWindowID(ws->window) == e.window.windowID) { - ws->width = e.window.data1; - ws->height = e.window.data2; - break; - } + if (target) { + target->width = e.window.data1; + target->height = e.window.data2; } } } // Route input events to the correct window's input handler - if (event_win_id != 0) { - // Primary window (index 0) uses the external editor &ed - if (windows_.size() > 0 && - SDL_GetWindowID(windows_[0]->window) == event_win_id) { - windows_[0]->input.ProcessSDLEvent(e); - } else { - for (std::size_t i = 1; i < windows_.size(); ++i) { - if (SDL_GetWindowID(windows_[i]->window) == event_win_id) { - windows_[i]->input.ProcessSDLEvent(e); - break; - } - } - } + if (target) { + target->input.ProcessSDLEvent(e); } } if (!running) return; - // --- Apply pending font change --- + // --- Apply pending font change (to all contexts) --- { std::string fname; float fsize = 0.0f; if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) { if (!fname.empty() && fsize > 0.0f) { - kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize); - ImGui_ImplOpenGL3_DestroyFontsTexture(); - ImGui_ImplOpenGL3_CreateFontsTexture(); + for (auto &ws : windows_) { + if (!ws->alive || !ws->imgui_ctx) + continue; + ImGui::SetCurrentContext(ws->imgui_ctx); + SDL_GL_MakeCurrent(ws->window, ws->gl_ctx); + kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize); + ImGui_ImplOpenGL3_DestroyFontsTexture(); + ImGui_ImplOpenGL3_CreateFontsTexture(); + } } } } @@ -404,7 +495,12 @@ GUIFrontend::Step(Editor &ed, bool &running) Editor &wed = (wi == 0) ? ed : ws.editor; + // Shared buffer list may have been modified by another window. + wed.ValidateBufferIndex(); + + // Activate this window's GL and ImGui contexts SDL_GL_MakeCurrent(ws.window, ws.gl_ctx); + ImGui::SetCurrentContext(ws.imgui_ctx); // Start a new ImGui frame ImGui_ImplOpenGL3_NewFrame(); @@ -442,12 +538,6 @@ GUIFrontend::Step(Editor &ed, bool &running) } } - // Handle new-window request - if (wed.NewWindowRequested()) { - wed.SetNewWindowRequested(false); - OpenNewWindow_(ed); // always share primary editor's buffers - } - if (wi == 0 && wed.QuitRequested()) { running = false; } @@ -466,52 +556,40 @@ GUIFrontend::Step(Editor &ed, bool &running) SDL_GL_SwapWindow(ws.window); } + // Handle deferred new-window requests (must happen outside the render loop + // to avoid corrupting an in-progress ImGui frame). + for (std::size_t wi = 0; wi < windows_.size(); ++wi) { + Editor &wed = (wi == 0) ? ed : windows_[wi]->editor; + if (wed.NewWindowRequested()) { + wed.SetNewWindowRequested(false); + OpenNewWindow_(ed); + } + } + // Remove dead secondary windows for (auto it = windows_.begin() + 1; it != windows_.end();) { if (!(*it)->alive) { - SDL_GL_MakeCurrent((*it)->window, (*it)->gl_ctx); - ImGui_ImplOpenGL3_Shutdown(); - SDL_GL_DeleteContext((*it)->gl_ctx); - SDL_DestroyWindow((*it)->window); + DestroyWindowResources_(**it); it = windows_.erase(it); - // Restore primary context - SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx); } else { ++it; } } + + // Restore primary context + if (!windows_.empty()) { + ImGui::SetCurrentContext(windows_[0]->imgui_ctx); + SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx); + } } void GUIFrontend::Shutdown() { - // Destroy secondary windows first - for (std::size_t i = 1; i < windows_.size(); ++i) { - SDL_GL_MakeCurrent(windows_[i]->window, windows_[i]->gl_ctx); - ImGui_ImplOpenGL3_Shutdown(); - SDL_GL_DeleteContext(windows_[i]->gl_ctx); - SDL_DestroyWindow(windows_[i]->window); - } - windows_.resize(std::min(windows_.size(), std::size_t(1))); - - // Destroy primary window - if (!windows_.empty()) { - SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx); - } - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplSDL2_Shutdown(); - ImGui::DestroyContext(); - - if (!windows_.empty()) { - if (windows_[0]->gl_ctx) { - SDL_GL_DeleteContext(windows_[0]->gl_ctx); - windows_[0]->gl_ctx = nullptr; - } - if (windows_[0]->window) { - SDL_DestroyWindow(windows_[0]->window); - windows_[0]->window = nullptr; - } + // Destroy all windows (secondary first, then primary) + for (auto it = windows_.rbegin(); it != windows_.rend(); ++it) { + DestroyWindowResources_(**it); } windows_.clear(); SDL_Quit(); @@ -549,4 +627,4 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px) io.Fonts->Build(); return true; -} \ No newline at end of file +} diff --git a/ImGuiFrontend.h b/ImGuiFrontend.h index e0a8a57..93f1278 100644 --- a/ImGuiFrontend.h +++ b/ImGuiFrontend.h @@ -13,6 +13,7 @@ struct SDL_Window; +struct ImGuiContext; typedef void *SDL_GLContext; class GUIFrontend final : public Frontend { @@ -28,10 +29,13 @@ public: void Shutdown() override; private: - // Per-window state + // Per-window state — each window owns its own ImGui context so that + // NewFrame/Render cycles are fully independent (ImGui requires exactly + // one NewFrame per Render per context). struct WindowState { - SDL_Window *window = nullptr; - SDL_GLContext gl_ctx = nullptr; + SDL_Window *window = nullptr; + SDL_GLContext gl_ctx = nullptr; + ImGuiContext *imgui_ctx = nullptr; ImGuiInputHandler input{}; ImGuiRenderer renderer{}; Editor editor{}; @@ -44,6 +48,9 @@ private: // Returns false if window creation fails. bool OpenNewWindow_(Editor &primary); + // Initialize fonts and theme for a given ImGui context (must be current). + void SetupImGuiStyle_(); + static void DestroyWindowResources_(WindowState &ws); static bool LoadGuiFont_(const char *path, float size_px); GUIConfig config_{}; diff --git a/ImGuiRenderer.cc b/ImGuiRenderer.cc index 3995f30..062a443 100644 --- a/ImGuiRenderer.cc +++ b/ImGuiRenderer.cc @@ -76,19 +76,16 @@ ImGuiRenderer::Draw(Editor &ed) // Two-way sync between Buffer::Rowoffs and ImGui scroll position: // - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it. // - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view. - static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs - static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs - const long buf_rowoffs = static_cast(buf->Rowoffs()); const long buf_coloffs = static_cast(buf->Coloffs()); // Detect programmatic change (e.g., page_down command changed rowoffs) // Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position - if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) { + if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) { float target_y = static_cast(buf_rowoffs) * row_h; ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y)); } - if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) { + if (prev_buf_coloffs_ >= 0 && buf_coloffs != prev_buf_coloffs_) { float target_x = static_cast(buf_coloffs) * space_w; float target_y = static_cast(buf_rowoffs) * row_h; ImGui::SetNextWindowScroll(ImVec2(target_x, target_y)); @@ -116,25 +113,22 @@ ImGuiRenderer::Draw(Editor &ed) // Synchronize buffer offsets from ImGui scroll if user scrolled manually bool forced_scroll = false; { - static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels - static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels - const long scroll_top = static_cast(scroll_y / row_h); const long scroll_left = static_cast(scroll_x / space_w); // Check if rowoffs was programmatically changed this frame - if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) { + if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) { forced_scroll = true; } // If user scrolled (not programmatic), update buffer offsets accordingly - if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y && !forced_scroll) { + if (prev_scroll_y_ >= 0.0f && scroll_y != prev_scroll_y_ && !forced_scroll) { if (Buffer *mbuf = const_cast(buf)) { mbuf->SetOffsets(static_cast(std::max(0L, scroll_top)), mbuf->Coloffs()); } } - if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x && !forced_scroll) { + if (prev_scroll_x_ >= 0.0f && scroll_x != prev_scroll_x_ && !forced_scroll) { if (Buffer *mbuf = const_cast(buf)) { mbuf->SetOffsets(mbuf->Rowoffs(), static_cast(std::max(0L, scroll_left))); @@ -142,11 +136,11 @@ ImGuiRenderer::Draw(Editor &ed) } // Update trackers for next frame - prev_scroll_y = scroll_y; - prev_scroll_x = scroll_x; + prev_scroll_y_ = scroll_y; + prev_scroll_x_ = scroll_x; } - prev_buf_rowoffs = buf_rowoffs; - prev_buf_coloffs = buf_coloffs; + prev_buf_rowoffs_ = buf_rowoffs; + prev_buf_coloffs_ = buf_coloffs; // Cache current horizontal offset in rendered columns for click handling const std::size_t coloffs_now = buf->Coloffs(); @@ -169,7 +163,7 @@ ImGuiRenderer::Draw(Editor &ed) const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0; const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0; - static bool mouse_selecting = false; + // (mouse_selecting__ is a member variable) auto mouse_pos_to_buf = [&]() -> std::pair { ImVec2 mp = ImGui::GetIO().MousePos; // Convert mouse pos to buffer row @@ -211,7 +205,7 @@ ImGuiRenderer::Draw(Editor &ed) // Mouse-driven selection: set mark on double-click or drag, update cursor on any press/drag if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - mouse_selecting = true; + mouse_selecting_ = true; auto [by, bx] = mouse_pos_to_buf(); char tmp[64]; std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx); @@ -225,7 +219,7 @@ ImGuiRenderer::Draw(Editor &ed) } } } - if (mouse_selecting && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + if (mouse_selecting_ && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { auto [by, bx] = mouse_pos_to_buf(); // If we are dragging (mouse moved while down), ensure mark is set to start selection if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 1.0f)) { @@ -242,8 +236,8 @@ ImGuiRenderer::Draw(Editor &ed) std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx); Execute(ed, CommandId::MoveCursorTo, std::string(tmp)); } - if (mouse_selecting && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - mouse_selecting = false; + if (mouse_selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + mouse_selecting_ = false; } for (std::size_t i = rowoffs; i < lines.size(); ++i) { // Capture the screen position before drawing the line diff --git a/ImGuiRenderer.h b/ImGuiRenderer.h index cc8e2de..000914e 100644 --- a/ImGuiRenderer.h +++ b/ImGuiRenderer.h @@ -11,4 +11,13 @@ public: ~ImGuiRenderer() override = default; void Draw(Editor &ed) override; + +private: + // Per-window scroll tracking for two-way sync between Buffer offsets and ImGui scroll. + // These must be per-instance (not static) so each window maintains independent state. + long prev_buf_rowoffs_ = -1; + long prev_buf_coloffs_ = -1; + float prev_scroll_y_ = -1.0f; + float prev_scroll_x_ = -1.0f; + bool mouse_selecting_ = false; }; diff --git a/tests/test_swap_cleanup2.cc b/tests/test_swap_cleanup2.cc new file mode 100644 index 0000000..7d695a6 --- /dev/null +++ b/tests/test_swap_cleanup2.cc @@ -0,0 +1,125 @@ +#include "Test.h" + +#include "Command.h" +#include "Editor.h" + +#include "tests/TestHarness.h" + +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + + +static void +write_file_bytes(const std::string &path, const std::string &bytes) +{ + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out.write(bytes.data(), (std::streamsize) bytes.size()); +} + + +// RAII helper to set XDG_STATE_HOME for the duration of a test and clean up. +struct XdgStateGuard { + fs::path root; + std::string old_xdg; + bool had_old; + + explicit XdgStateGuard(const std::string &suffix) + { + root = fs::temp_directory_path() / + (std::string("kte_ut_xdg_") + suffix + "_" + std::to_string((int) ::getpid())); + fs::remove_all(root); + fs::create_directories(root); + + const char *p = std::getenv("XDG_STATE_HOME"); + had_old = (p != nullptr); + if (p) + old_xdg = p; + setenv("XDG_STATE_HOME", root.string().c_str(), 1); + } + + ~XdgStateGuard() + { + if (had_old) + setenv("XDG_STATE_HOME", old_xdg.c_str(), 1); + else + unsetenv("XDG_STATE_HOME"); + fs::remove_all(root); + } +}; + + +TEST(SwapCleanup_SaveAndQuit) +{ + ktet::InstallDefaultCommandsOnce(); + XdgStateGuard xdg("save_quit"); + + const std::string path = (xdg.root / "work" / "file.txt").string(); + fs::create_directories(xdg.root / "work"); + write_file_bytes(path, "hello\n"); + + Editor ed; + ed.SetDimensions(24, 80); + ed.AddBuffer(Buffer()); + std::string err; + ASSERT_TRUE(ed.OpenFile(path, err)); + Buffer *b = ed.CurrentBuffer(); + ASSERT_TRUE(b != nullptr); + + // Edit to create swap file + ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart)); + ASSERT_TRUE(Execute(ed, CommandId::InsertText, "Z")); + ASSERT_TRUE(b->Dirty()); + + ed.Swap()->Flush(b); + const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b); + ASSERT_TRUE(fs::exists(swp)); + + // Save-and-quit should clean up the swap file + ASSERT_TRUE(Execute(ed, CommandId::SaveAndQuit)); + ed.Swap()->Flush(b); + ASSERT_TRUE(!fs::exists(swp)); + + // Cleanup + std::remove(path.c_str()); +} + + +TEST(SwapCleanup_EditorReset) +{ + ktet::InstallDefaultCommandsOnce(); + XdgStateGuard xdg("editor_reset"); + + const std::string path = (xdg.root / "work" / "file.txt").string(); + fs::create_directories(xdg.root / "work"); + write_file_bytes(path, "hello\n"); + + Editor ed; + ed.SetDimensions(24, 80); + ed.AddBuffer(Buffer()); + std::string err; + ASSERT_TRUE(ed.OpenFile(path, err)); + Buffer *b = ed.CurrentBuffer(); + ASSERT_TRUE(b != nullptr); + + // Edit to create swap file + ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart)); + ASSERT_TRUE(Execute(ed, CommandId::InsertText, "W")); + ASSERT_TRUE(b->Dirty()); + + ed.Swap()->Flush(b); + const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b); + ASSERT_TRUE(fs::exists(swp)); + + // Reset (simulates clean editor exit) should remove swap files + ed.Reset(); + ASSERT_TRUE(!fs::exists(swp)); + + // Cleanup + std::remove(path.c_str()); +}