diff --git a/CMakeLists.txt b/CMakeLists.txt index 51e01c6..52a7ce3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(kte) include(GNUInstallDirs) set(CMAKE_CXX_STANDARD 20) -set(KTE_VERSION "1.6.4") +set(KTE_VERSION "1.6.5") # Default to terminal-only build to avoid SDL/OpenGL dependency by default. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. @@ -310,6 +310,7 @@ if (BUILD_TESTS) tests/test_swap_replay.cc tests/test_swap_recovery_prompt.cc tests/test_swap_cleanup.cc + tests/test_swap_git_editor.cc tests/test_piece_table.cc tests/test_search.cc tests/test_search_replace_flow.cc diff --git a/Editor.cc b/Editor.cc index f05dbbc..11b5d58 100644 --- a/Editor.cc +++ b/Editor.cc @@ -486,9 +486,10 @@ Editor::CloseBuffer(std::size_t index) return false; } if (swap_) { - // If the buffer is clean, remove its swap file when closing. - // (Crash recovery is unaffected: on crash, close paths are not executed.) - swap_->Detach(&buffers_[index], !buffers_[index].Dirty()); + // 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); } buffers_.erase(buffers_.begin() + static_cast(index)); diff --git a/tests/test_swap_git_editor.cc b/tests/test_swap_git_editor.cc new file mode 100644 index 0000000..9a58a9a --- /dev/null +++ b/tests/test_swap_git_editor.cc @@ -0,0 +1,94 @@ +#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()); +} + + +// Simulate git editor workflow: open file, edit, save, edit more, close. +// The swap file should be deleted on close, not left behind. +TEST (SwapCleanup_GitEditorWorkflow) +{ + ktet::InstallDefaultCommandsOnce(); + + const fs::path xdg_root = fs::temp_directory_path() / + (std::string("kte_ut_xdg_state_git_editor_") + std::to_string((int) ::getpid())); + fs::remove_all(xdg_root); + fs::create_directories(xdg_root); + + const char *old_xdg_p = std::getenv("XDG_STATE_HOME"); + const std::string old_xdg = old_xdg_p ? std::string(old_xdg_p) : std::string(); + const std::string xdg_s = xdg_root.string(); + setenv("XDG_STATE_HOME", xdg_s.c_str(), 1); + + // Simulate git's COMMIT_EDITMSG path + const std::string path = (xdg_root / ".git" / "COMMIT_EDITMSG").string(); + fs::create_directories((xdg_root / ".git")); + std::remove(path.c_str()); + write_file_bytes(path, "# Enter commit message\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); + + // User edits the file + ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart)); + ASSERT_TRUE(Execute(ed, CommandId::InsertText, "X")); + ASSERT_TRUE(b->Dirty()); + + // User saves (git will read this) + ASSERT_TRUE(Execute(ed, CommandId::Save)); + ASSERT_TRUE(!b->Dirty()); + ed.Swap()->Flush(b); + + const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b); + // After save, swap should be deleted + ASSERT_TRUE(!fs::exists(swp)); + + // User makes more edits (common in git editor workflow - refining message) + ASSERT_TRUE(Execute(ed, CommandId::InsertText, "Y")); + ASSERT_TRUE(b->Dirty()); + ed.Swap()->Flush(b); + + // Now there's a new swap file for the unsaved edits + ASSERT_TRUE(fs::exists(swp)); + + // User closes the buffer (or kte exits) + // This simulates what happens when git is done and kte closes + const std::size_t idx = ed.CurrentBufferIndex(); + ed.CloseBuffer(idx); + + // The swap file should be deleted on close, even though buffer was dirty + // This prevents stale swap files when used as git editor + ASSERT_TRUE(!fs::exists(swp)); + + // Cleanup + std::remove(path.c_str()); + if (!old_xdg.empty()) + setenv("XDG_STATE_HOME", old_xdg.c_str(), 1); + else + unsetenv("XDG_STATE_HOME"); + fs::remove_all(xdg_root); +} \ No newline at end of file