From 0002f0f56bbee2f4785617a0bb0aaefbff93320b Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 29 Nov 2025 23:14:35 -0800 Subject: [PATCH] Handle end-of-file newline semantics and improve scroll alignment logic. --- .idea/workspace.xml | 19 +++++++++++----- Buffer.cc | 55 ++++++++++++++++++++++++++++++++------------- GUIRenderer.cc | 15 +++++++------ 3 files changed, 60 insertions(+), 29 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index e133240..a1142d2 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -33,11 +33,9 @@ - + - - @@ -168,7 +174,8 @@ - diff --git a/Buffer.cc b/Buffer.cc index 16c96b3..38eb25b 100644 --- a/Buffer.cc +++ b/Buffer.cc @@ -41,25 +41,48 @@ Buffer::OpenFromFile(const std::string &path, std::string &err) err = "Failed to open file: " + path; return false; } + + // Detect if file ends with a newline so we can preserve a final empty line + // in our in-memory representation (mg-style semantics). + bool ends_with_nl = false; + { + in.seekg(0, std::ios::end); + std::streamoff sz = in.tellg(); + if (sz > 0) { + in.seekg(-1, std::ios::end); + char last = 0; + in.read(&last, 1); + ends_with_nl = (last == '\n'); + } else { + in.clear(); + } + // Rewind to start for line-by-line read + in.clear(); + in.seekg(0, std::ios::beg); + } - rows_.clear(); - std::string line; - while (std::getline(in, line)) { - // std::getline strips the '\n', keep raw line - if (!line.empty() && !in.good()) { - // fallthrough - } - // Handle potential Windows CRLF: strip trailing '\r' - if (!line.empty() && line.back() == '\r') { - line.pop_back(); - } - rows_.emplace_back(line); - } + rows_.clear(); + std::string line; + while (std::getline(in, line)) { + // std::getline strips the '\n', keep raw line content only + // Handle potential Windows CRLF: strip trailing '\r' + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + rows_.emplace_back(line); + } - // If file ends with a trailing newline, getline will have produced an empty - // last line already. If the file is empty and no lines were read, keep rows_ empty. + // If the file ended with a newline and we didn't already get an + // empty final row from getline (e.g., when the last textual line + // had content followed by '\n'), append an empty row to represent + // the cursor position past the last newline. + if (ends_with_nl) { + if (rows_.empty() || !rows_.back().empty()) { + rows_.emplace_back(std::string()); + } + } - nrows_ = rows_.size(); + nrows_ = rows_.size(); filename_ = path; is_file_backed_ = true; dirty_ = false; diff --git a/GUIRenderer.cc b/GUIRenderer.cc index 7034538..ec9d16a 100644 --- a/GUIRenderer.cc +++ b/GUIRenderer.cc @@ -82,13 +82,14 @@ GUIRenderer::Draw(Editor &ed) vis_rows = 1; long last_row = first_row + vis_rows - 1; - // A) If user scrolled (scroll_y changed), and cursor outside, move cursor to nearest visible row - if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) { - long cyr = static_cast(cy); - if (cyr < first_row || cyr > last_row) { - long new_row = (cyr < first_row) ? first_row : last_row; - if (new_row < 0) - new_row = 0; + // A) If user scrolled (scroll_y changed), and cursor outside, move cursor to nearest visible row + // Skip this when we just forced a scroll alignment this frame (programmatic change). + if (!forced_scroll && prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) { + long cyr = static_cast(cy); + if (cyr < first_row || cyr > last_row) { + long new_row = (cyr < first_row) ? first_row : last_row; + if (new_row < 0) + new_row = 0; if (new_row >= static_cast(lines.size())) new_row = static_cast(lines.empty() ? 0 : (lines.size() - 1)); // Clamp column to line length