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 @@
-
+
-
-
@@ -144,7 +142,7 @@
1764457173148
-
+
@@ -154,7 +152,15 @@
1764485311566
-
+
+
+ 1764486011231
+
+
+
+ 1764486011231
+
+
@@ -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