Force a UTF-8 ctype, stable buffer ids, redraw after deferred opens
- main: when the environment's locale is not UTF-8 (LANG unset, C/POSIX), switch LC_CTYPE to a UTF-8 locale so ncurses delivers and draws multibyte characters instead of dropping them. - Buffer::Id(): a process-unique id that survives moves and is fresh for copies. The terminal renderer's skip and cursor caches and the reload confirmation now key on it rather than the buffer's address, which is reused after a buffer is closed. - The reload confirmation is cancelled by any other command, like the quit confirmation. - TerminalFrontend redraws when a deferred open starts a prompt or sets a status (e.g. the swap recovery question), not only when a buffer opens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
@@ -390,6 +390,7 @@ Buffer::operator=(const Buffer &other)
|
|||||||
filename_ = other.filename_;
|
filename_ = other.filename_;
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
is_virtual_ = other.is_virtual_;
|
is_virtual_ = other.is_virtual_;
|
||||||
|
id_ = NextBufferId(); // a copy is a different document
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
read_only_ = other.read_only_;
|
read_only_ = other.read_only_;
|
||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
@@ -436,6 +437,7 @@ Buffer::Buffer(Buffer &&other) noexcept
|
|||||||
filename_(std::move(other.filename_)),
|
filename_(std::move(other.filename_)),
|
||||||
is_file_backed_(other.is_file_backed_),
|
is_file_backed_(other.is_file_backed_),
|
||||||
is_virtual_(other.is_virtual_),
|
is_virtual_(other.is_virtual_),
|
||||||
|
id_(other.id_),
|
||||||
dirty_(other.dirty_),
|
dirty_(other.dirty_),
|
||||||
read_only_(other.read_only_),
|
read_only_(other.read_only_),
|
||||||
mark_set_(other.mark_set_),
|
mark_set_(other.mark_set_),
|
||||||
@@ -487,6 +489,7 @@ Buffer::operator=(Buffer &&other) noexcept
|
|||||||
filename_ = std::move(other.filename_);
|
filename_ = std::move(other.filename_);
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
is_virtual_ = other.is_virtual_;
|
is_virtual_ = other.is_virtual_;
|
||||||
|
id_ = other.id_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
read_only_ = other.read_only_;
|
read_only_ = other.read_only_;
|
||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
* - Direct PieceTable access for new editing operations
|
* - Direct PieceTable access for new editing operations
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
@@ -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
|
[[nodiscard]] bool Dirty() const
|
||||||
{
|
{
|
||||||
return dirty_;
|
return dirty_;
|
||||||
@@ -703,6 +713,13 @@ private:
|
|||||||
std::string filename_;
|
std::string filename_;
|
||||||
bool is_file_backed_ = false;
|
bool is_file_backed_ = false;
|
||||||
bool is_virtual_ = false;
|
bool is_virtual_ = false;
|
||||||
|
std::uint64_t id_ = NextBufferId();
|
||||||
|
|
||||||
|
static std::uint64_t NextBufferId()
|
||||||
|
{
|
||||||
|
static std::atomic<std::uint64_t> next{1};
|
||||||
|
return next.fetch_add(1);
|
||||||
|
}
|
||||||
bool dirty_ = false;
|
bool dirty_ = false;
|
||||||
bool read_only_ = false;
|
bool read_only_ = false;
|
||||||
bool mark_set_ = false;
|
bool mark_set_ = false;
|
||||||
|
|||||||
+14
-6
@@ -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
|
static bool
|
||||||
cmd_reload_buffer(CommandContext &ctx)
|
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
|
// 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.
|
// a dirty buffer needs a second C-k l with no edit in between.
|
||||||
static const Buffer *confirm_buf = nullptr;
|
if (buf->Dirty() && !(reload_confirm_id == buf->Id() && reload_confirm_version == buf->Version())) {
|
||||||
static std::uint64_t confirm_version = 0;
|
reload_confirm_id = buf->Id();
|
||||||
if (buf->Dirty() && !(confirm_buf == buf && confirm_version == buf->Version())) {
|
reload_confirm_version = buf->Version();
|
||||||
confirm_buf = buf;
|
|
||||||
confirm_version = buf->Version();
|
|
||||||
ctx.editor.SetStatus("Unsaved changes will be lost. C-k l again to reload anyway");
|
ctx.editor.SetStatus("Unsaved changes will be lost. C-k l again to reload anyway");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
confirm_buf = nullptr;
|
reload_confirm_id = 0;
|
||||||
std::string err;
|
std::string err;
|
||||||
if (!buf->OpenFromFile(filename, err)) {
|
if (!buf->OpenFromFile(filename, err)) {
|
||||||
ctx.editor.SetStatus(std::string("Reload failed: ") + 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) {
|
if (ed.QuitConfirmPending() && id != CommandId::Quit && id != CommandId::KPrefix) {
|
||||||
ed.SetQuitConfirmPending(false);
|
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)
|
// 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 !=
|
if (id != CommandId::KillToEOL && id != CommandId::KillLine && id != CommandId::KillRegion && id !=
|
||||||
CommandId::CopyRegion && id != CommandId::DeleteWordPrev && id != CommandId::DeleteWordNext) {
|
CommandId::CopyRegion && id != CommandId::DeleteWordPrev && id != CommandId::DeleteWordNext) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
#include <termios.h>
|
#include <termios.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -106,8 +108,18 @@ TerminalFrontend::Step(Editor &ed, bool &running)
|
|||||||
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
|
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
|
||||||
|
|
||||||
// Allow deferred opens (including swap recovery prompts) to run.
|
// Allow deferred opens (including swap recovery prompts) to run.
|
||||||
|
// 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())
|
if (ed.ProcessPendingOpens())
|
||||||
changed = true;
|
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
|
// 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
|
// pasted block take a frame (and a full redraw) per character. The first
|
||||||
|
|||||||
+4
-4
@@ -249,7 +249,7 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
if (skip_cache_.size() < static_cast<std::size_t>(content_rows))
|
if (skip_cache_.size() < static_cast<std::size_t>(content_rows))
|
||||||
skip_cache_.resize(static_cast<std::size_t>(content_rows));
|
skip_cache_.resize(static_cast<std::size_t>(content_rows));
|
||||||
SkipCache &sc = skip_cache_[static_cast<std::size_t>(r)];
|
SkipCache &sc = skip_cache_[static_cast<std::size_t>(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)) {
|
sc.coloffs == coloffs)) {
|
||||||
std::size_t s = 0, c = 0;
|
std::size_t s = 0, c = 0;
|
||||||
while (s < line.size()) {
|
while (s < line.size()) {
|
||||||
@@ -275,7 +275,7 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
s += n;
|
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;
|
src_i = sc.src;
|
||||||
render_col = sc.col;
|
render_col = sc.col;
|
||||||
@@ -430,7 +430,7 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
std::size_t cx = buf->Curx();
|
std::size_t cx = buf->Curx();
|
||||||
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
|
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
|
||||||
std::size_t rx_recomputed = 0;
|
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) {
|
cursor_cache_.version == buf->Version() && cursor_cache_.row == cy && cursor_cache_.cx == cx) {
|
||||||
rx_recomputed = cursor_cache_.rx;
|
rx_recomputed = cursor_cache_.rx;
|
||||||
} else if (cy < nlines) {
|
} else if (cy < nlines) {
|
||||||
@@ -462,7 +462,7 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
src_i_cur += len;
|
src_i_cur += len;
|
||||||
}
|
}
|
||||||
rx_recomputed = render_col_cur;
|
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<int>(rx_recomputed) - static_cast<int>(buf->Coloffs());
|
int cur_x = static_cast<int>(rx_recomputed) - static_cast<int>(buf->Coloffs());
|
||||||
if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) {
|
if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) {
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ private:
|
|||||||
// horizontal scroll offset), cached per screen row. Scanning a very long
|
// horizontal scroll offset), cached per screen row. Scanning a very long
|
||||||
// line from column 0 on every frame made each redraw O(line length).
|
// line from column 0 on every frame made each redraw O(line length).
|
||||||
struct SkipCache {
|
struct SkipCache {
|
||||||
const void *buf = nullptr;
|
std::uint64_t buf_id = 0; // Buffer::Id(); addresses get reused
|
||||||
std::uint64_t version = 0;
|
std::uint64_t version = 0;
|
||||||
std::size_t row = 0;
|
std::size_t row = 0;
|
||||||
std::size_t coloffs = 0;
|
std::size_t coloffs = 0;
|
||||||
@@ -34,7 +34,7 @@ private:
|
|||||||
|
|
||||||
// Render column of the cursor, cached the same way.
|
// Render column of the cursor, cached the same way.
|
||||||
struct CursorCache {
|
struct CursorCache {
|
||||||
const void *buf = nullptr;
|
std::uint64_t buf_id = 0; // Buffer::Id(); addresses get reused
|
||||||
std::uint64_t version = 0;
|
std::uint64_t version = 0;
|
||||||
std::size_t row = 0;
|
std::size_t row = 0;
|
||||||
std::size_t cx = 0;
|
std::size_t cx = 0;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <clocale>
|
#include <clocale>
|
||||||
|
#include <langinfo.h>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -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<char>(std::tolower(static_cast<unsigned char>(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
|
int
|
||||||
main(int argc, char *argv[])
|
main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
std::setlocale(LC_ALL, "");
|
std::setlocale(LC_ALL, "");
|
||||||
|
ensure_utf8_ctype();
|
||||||
|
|
||||||
// Ensure the error handler (and its log file) is initialised early.
|
// Ensure the error handler (and its log file) is initialised early.
|
||||||
kte::ErrorHandler::Instance();
|
kte::ErrorHandler::Instance();
|
||||||
|
|||||||
@@ -1198,3 +1198,44 @@ TEST(Audit_InsertText_RepeatCount)
|
|||||||
ASSERT_TRUE(h.Undo());
|
ASSERT_TRUE(h.Undo());
|
||||||
ASSERT_EQ(h.Text(), std::string(""));
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user