diff --git a/Buffer.cc b/Buffer.cc index 8c8a8a0..48e8a68 100644 --- a/Buffer.cc +++ b/Buffer.cc @@ -390,6 +390,7 @@ Buffer::operator=(const Buffer &other) filename_ = other.filename_; is_file_backed_ = other.is_file_backed_; is_virtual_ = other.is_virtual_; + id_ = NextBufferId(); // a copy is a different document dirty_ = other.dirty_; read_only_ = other.read_only_; mark_set_ = other.mark_set_; @@ -436,6 +437,7 @@ Buffer::Buffer(Buffer &&other) noexcept filename_(std::move(other.filename_)), is_file_backed_(other.is_file_backed_), is_virtual_(other.is_virtual_), + id_(other.id_), dirty_(other.dirty_), read_only_(other.read_only_), mark_set_(other.mark_set_), @@ -487,6 +489,7 @@ Buffer::operator=(Buffer &&other) noexcept filename_ = std::move(other.filename_); is_file_backed_ = other.is_file_backed_; is_virtual_ = other.is_virtual_; + id_ = other.id_; dirty_ = other.dirty_; read_only_ = other.read_only_; mark_set_ = other.mark_set_; diff --git a/Buffer.h b/Buffer.h index 17e8b46..a5b2fe1 100644 --- a/Buffer.h +++ b/Buffer.h @@ -34,6 +34,7 @@ * - Direct PieceTable access for new editing operations */ #pragma once +#include #include #include @@ -352,6 +353,15 @@ public: } + // Identity of this document, stable for its lifetime, including when the + // Buffer object is moved (e.g. by the editor's buffer vector). Copies get + // a new id. Use it, not the object's address, to key per-buffer caches. + [[nodiscard]] std::uint64_t Id() const + { + return id_; + } + + [[nodiscard]] bool Dirty() const { return dirty_; @@ -703,6 +713,13 @@ private: std::string filename_; bool is_file_backed_ = false; bool is_virtual_ = false; + std::uint64_t id_ = NextBufferId(); + + static std::uint64_t NextBufferId() + { + static std::atomic next{1}; + return next.fetch_add(1); + } bool dirty_ = false; bool read_only_ = false; bool mark_set_ = false; diff --git a/Command.cc b/Command.cc index ade8b0e..677cd46 100644 --- a/Command.cc +++ b/Command.cc @@ -5383,6 +5383,13 @@ cmd_reflow_paragraph(CommandContext &ctx) } +// Pending reload confirmation: the buffer (by Id(), since addresses are +// reused once a buffer is closed) and its version when it was armed. Any +// other command cancels it (see Execute). +static std::uint64_t reload_confirm_id = 0; +static std::uint64_t reload_confirm_version = 0; + + static bool cmd_reload_buffer(CommandContext &ctx) { @@ -5404,15 +5411,13 @@ cmd_reload_buffer(CommandContext &ctx) } // Reload discards unsaved edits (and their undo history and journal), so // a dirty buffer needs a second C-k l with no edit in between. - static const Buffer *confirm_buf = nullptr; - static std::uint64_t confirm_version = 0; - if (buf->Dirty() && !(confirm_buf == buf && confirm_version == buf->Version())) { - confirm_buf = buf; - confirm_version = buf->Version(); + if (buf->Dirty() && !(reload_confirm_id == buf->Id() && reload_confirm_version == buf->Version())) { + reload_confirm_id = buf->Id(); + reload_confirm_version = buf->Version(); ctx.editor.SetStatus("Unsaved changes will be lost. C-k l again to reload anyway"); return true; } - confirm_buf = nullptr; + reload_confirm_id = 0; std::string err; if (!buf->OpenFromFile(filename, err)) { ctx.editor.SetStatus(std::string("Reload failed: ") + err); @@ -5749,6 +5754,9 @@ Execute(Editor &ed, CommandId id, const std::string &arg, int count) if (ed.QuitConfirmPending() && id != CommandId::Quit && id != CommandId::KPrefix) { ed.SetQuitConfirmPending(false); } + // Likewise a pending reload confirmation. + if (id != CommandId::ReloadBuffer && id != CommandId::KPrefix) + reload_confirm_id = 0; // Reset kill chain unless this is a kill-like command (so consecutive kills append) if (id != CommandId::KillToEOL && id != CommandId::KillLine && id != CommandId::KillRegion && id != CommandId::CopyRegion && id != CommandId::DeleteWordPrev && id != CommandId::DeleteWordNext) { diff --git a/TerminalFrontend.cc b/TerminalFrontend.cc index d3ea354..9b0343c 100644 --- a/TerminalFrontend.cc +++ b/TerminalFrontend.cc @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -106,8 +108,18 @@ TerminalFrontend::Step(Editor &ed, bool &running) ed.SetDimensions(static_cast(r), static_cast(c)); // Allow deferred opens (including swap recovery prompts) to run. - if (ed.ProcessPendingOpens()) - changed = true; + // Its return value only reports an opened buffer; a deferred open that + // starts a prompt or sets a status must be drawn too. + { + const bool prompt_before = ed.PromptActive(); + const std::string status_before = ed.Status(); + const std::time_t status_time_before = ed.StatusTime(); + if (ed.ProcessPendingOpens()) + changed = true; + if (ed.PromptActive() != prompt_before || ed.Status() != status_before || + ed.StatusTime() != status_time_before) + changed = true; + } // Handle all pending input before drawing: one key per frame made a // pasted block take a frame (and a full redraw) per character. The first diff --git a/TerminalRenderer.cc b/TerminalRenderer.cc index c80eae8..ef9b94c 100644 --- a/TerminalRenderer.cc +++ b/TerminalRenderer.cc @@ -249,7 +249,7 @@ TerminalRenderer::Draw(Editor &ed) if (skip_cache_.size() < static_cast(content_rows)) skip_cache_.resize(static_cast(content_rows)); SkipCache &sc = skip_cache_[static_cast(r)]; - if (!(sc.valid && sc.buf == buf && sc.version == buf->Version() && sc.row == li && + if (!(sc.valid && sc.buf_id == buf->Id() && sc.version == buf->Version() && sc.row == li && sc.coloffs == coloffs)) { std::size_t s = 0, c = 0; while (s < line.size()) { @@ -275,7 +275,7 @@ TerminalRenderer::Draw(Editor &ed) } s += n; } - sc = SkipCache{buf, buf->Version(), li, coloffs, s, c, true}; + sc = SkipCache{buf->Id(), buf->Version(), li, coloffs, s, c, true}; } src_i = sc.src; render_col = sc.col; @@ -430,7 +430,7 @@ TerminalRenderer::Draw(Editor &ed) std::size_t cx = buf->Curx(); int cur_y = static_cast(cy) - static_cast(buf->Rowoffs()); std::size_t rx_recomputed = 0; - if (cy < nlines && cursor_cache_.valid && cursor_cache_.buf == buf && + if (cy < nlines && cursor_cache_.valid && cursor_cache_.buf_id == buf->Id() && cursor_cache_.version == buf->Version() && cursor_cache_.row == cy && cursor_cache_.cx == cx) { rx_recomputed = cursor_cache_.rx; } else if (cy < nlines) { @@ -462,7 +462,7 @@ TerminalRenderer::Draw(Editor &ed) src_i_cur += len; } rx_recomputed = render_col_cur; - cursor_cache_ = CursorCache{buf, buf->Version(), cy, cx, rx_recomputed, true}; + cursor_cache_ = CursorCache{buf->Id(), buf->Version(), cy, cx, rx_recomputed, true}; } int cur_x = static_cast(rx_recomputed) - static_cast(buf->Coloffs()); if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) { diff --git a/TerminalRenderer.h b/TerminalRenderer.h index c1fb3cf..170d186 100644 --- a/TerminalRenderer.h +++ b/TerminalRenderer.h @@ -21,7 +21,7 @@ private: // horizontal scroll offset), cached per screen row. Scanning a very long // line from column 0 on every frame made each redraw O(line length). struct SkipCache { - const void *buf = nullptr; + std::uint64_t buf_id = 0; // Buffer::Id(); addresses get reused std::uint64_t version = 0; std::size_t row = 0; std::size_t coloffs = 0; @@ -34,7 +34,7 @@ private: // Render column of the cursor, cached the same way. struct CursorCache { - const void *buf = nullptr; + std::uint64_t buf_id = 0; // Buffer::Id(); addresses get reused std::uint64_t version = 0; std::size_t row = 0; std::size_t cx = 0; diff --git a/main.cc b/main.cc index d5690d7..43ae724 100644 --- a/main.cc +++ b/main.cc @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -113,10 +114,33 @@ RunStressHighlighter(unsigned seconds) } +// kte edits UTF-8 text. With LANG unset (common in containers and ssh +// sessions) the C locale cannot decode or encode non-ASCII characters, and +// typed or pasted non-ASCII text was dropped (with the rest of the paste). +// Use a UTF-8 character-type locale when the environment does not give one. +static void +ensure_utf8_ctype() +{ + auto is_utf8 = [] { + std::string cs = nl_langinfo(CODESET); + for (auto &c: cs) + c = static_cast(std::tolower(static_cast(c))); + return cs == "utf-8" || cs == "utf8"; + }; + if (is_utf8()) + return; + for (const char *name: {"C.UTF-8", "C.utf8", "en_US.UTF-8", "en_US.utf8"}) { + if (std::setlocale(LC_CTYPE, name) && is_utf8()) + return; + } +} + + int main(int argc, char *argv[]) { std::setlocale(LC_ALL, ""); + ensure_utf8_ctype(); // Ensure the error handler (and its log file) is initialised early. kte::ErrorHandler::Instance(); diff --git a/tests/test_audit_regressions.cc b/tests/test_audit_regressions.cc index b3c97be..4bda202 100644 --- a/tests/test_audit_regressions.cc +++ b/tests/test_audit_regressions.cc @@ -1198,3 +1198,44 @@ TEST(Audit_InsertText_RepeatCount) ASSERT_TRUE(h.Undo()); ASSERT_EQ(h.Text(), std::string("")); } + + +// A reload confirmation is cancelled by any other command, so a later C-k l +// (after, say, moving the cursor) asks again instead of discarding edits. +TEST(Audit_Reload_ConfirmationCancelledByOtherCommand) +{ + TempDir d("reload_cancel"); + const std::string file = (d.path / "r.txt").string(); + std::ofstream(file) << "disk\n"; + TestHarness h; + Editor &ed = h.EditorRef(); + std::string err; + ASSERT_TRUE(ed.OpenFile(file, err)); + ASSERT_TRUE(h.Exec(CommandId::InsertText, "EDIT ")); + ASSERT_TRUE(h.Exec(CommandId::ReloadBuffer)); + (void) h.Exec(CommandId::MoveRight); + ASSERT_TRUE(h.Exec(CommandId::ReloadBuffer)); + ASSERT_EQ(h.Line(0), std::string("EDIT disk")); + ASSERT_TRUE(h.Exec(CommandId::ReloadBuffer)); + ASSERT_EQ(h.Line(0), std::string("disk")); +} + + +// Buffer ids: copies get a fresh id, moves keep it, so caches keyed on the +// id never mistake a new buffer at a reused address for the old one. +TEST(Audit_BufferId_StableAcrossMovesUniqueForCopies) +{ + Buffer a; + const std::uint64_t id = a.Id(); + ASSERT_TRUE(id != 0); + Buffer b(a); + ASSERT_TRUE(b.Id() != id); + Buffer c(std::move(a)); + ASSERT_EQ(c.Id(), id); + Buffer e; + e = std::move(c); + ASSERT_EQ(e.Id(), id); + Buffer f; + f = e; + ASSERT_TRUE(f.Id() != id); +}