diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 7414c9f..e133240 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -33,31 +33,11 @@
-
-
-
-
-
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -164,8 +144,17 @@
1764457173148
-
+
+
+
+ 1764485311566
+
+
+
+ 1764485311566
+
+
@@ -176,6 +165,11 @@
+
+
+
+
+
diff --git a/Buffer.cc b/Buffer.cc
index b2fec81..16c96b3 100644
--- a/Buffer.cc
+++ b/Buffer.cc
@@ -20,21 +20,21 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
{
// If the file doesn't exist, initialize an empty, non-file-backed buffer
// with the provided filename. Do not touch the filesystem until Save/SaveAs.
- if (!std::filesystem::exists(path)) {
- rows_.clear();
- nrows_ = 0;
- filename_ = path;
- is_file_backed_ = false;
- dirty_ = false;
+ if (!std::filesystem::exists(path)) {
+ rows_.clear();
+ nrows_ = 0;
+ filename_ = path;
+ is_file_backed_ = false;
+ dirty_ = false;
- // Reset cursor/viewport state
- curx_ = cury_ = rx_ = 0;
- rowoffs_ = coloffs_ = 0;
- mark_set_ = false;
- mark_curx_ = mark_cury_ = 0;
+ // Reset cursor/viewport state
+ curx_ = cury_ = rx_ = 0;
+ rowoffs_ = coloffs_ = 0;
+ mark_set_ = false;
+ mark_curx_ = mark_cury_ = 0;
- return true;
- }
+ return true;
+ }
std::ifstream in(path, std::ios::in | std::ios::binary);
if (!in) {
@@ -42,19 +42,19 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
return false;
}
- 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_.push_back(line);
- }
+ 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);
+ }
// 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.
@@ -86,12 +86,14 @@ Buffer::Save(std::string &err) const
err = "Failed to open for write: " + filename_;
return false;
}
- for (std::size_t i = 0; i < rows_.size(); ++i) {
- out.write(rows_[i].data(), static_cast(rows_[i].size()));
- if (i + 1 < rows_.size()) {
- out.put('\n');
- }
- }
+ for (std::size_t i = 0; i < rows_.size(); ++i) {
+ const char *d = rows_[i].Data();
+ std::size_t n = rows_[i].Size();
+ if (d && n) out.write(d, static_cast(n));
+ if (i + 1 < rows_.size()) {
+ out.put('\n');
+ }
+ }
if (!out.good()) {
err = "Write error";
return false;
@@ -111,12 +113,14 @@ Buffer::SaveAs(const std::string &path, std::string &err)
err = "Failed to open for write: " + path;
return false;
}
- for (std::size_t i = 0; i < rows_.size(); ++i) {
- out.write(rows_[i].data(), static_cast(rows_[i].size()));
- if (i + 1 < rows_.size()) {
- out.put('\n');
- }
- }
+ for (std::size_t i = 0; i < rows_.size(); ++i) {
+ const char *d = rows_[i].Data();
+ std::size_t n = rows_[i].Size();
+ if (d && n) out.write(d, static_cast(n));
+ if (i + 1 < rows_.size()) {
+ out.put('\n');
+ }
+ }
if (!out.good()) {
err = "Write error";
return false;
diff --git a/Buffer.h b/Buffer.h
index 4d43efb..d8b917b 100644
--- a/Buffer.h
+++ b/Buffer.h
@@ -8,6 +8,8 @@
#include
#include
+#include "AppendBuffer.h"
+
class Buffer {
public:
Buffer();
@@ -57,13 +59,129 @@ public:
}
- [[nodiscard]] const std::vector &Rows() const
+ // Line wrapper backed by AppendBuffer (GapBuffer/PieceTable)
+ class Line {
+ public:
+ Line() = default;
+
+ Line(const char *s)
+ {
+ assign_from(s ? std::string(s) : std::string());
+ }
+
+ Line(const std::string &s)
+ {
+ assign_from(s);
+ }
+
+ Line(const Line &other) = default;
+ Line &operator=(const Line &other) = default;
+ Line(Line &&other) noexcept = default;
+ Line &operator=(Line &&other) noexcept = default;
+
+ // capacity helpers
+ void Clear() { buf_.Clear(); }
+
+ // size/access
+ [[nodiscard]] std::size_t size() const { return buf_.Size(); }
+ [[nodiscard]] bool empty() const { return size() == 0; }
+
+ // read-only raw view
+ [[nodiscard]] const char *Data() const { return buf_.Data(); }
+ [[nodiscard]] std::size_t Size() const { return buf_.Size(); }
+
+ // element access (read-only)
+ [[nodiscard]] char operator[](std::size_t i) const
+ {
+ const char *d = buf_.Data();
+ return (i < buf_.Size() && d) ? d[i] : '\0';
+ }
+
+ // conversions
+ operator std::string() const { return std::string(buf_.Data() ? buf_.Data() : "", buf_.Size()); }
+
+ // string-like API used by command/renderer layers (implemented via materialization for now)
+ std::string substr(std::size_t pos) const
+ {
+ const std::size_t n = buf_.Size();
+ if (pos >= n) return std::string();
+ return std::string(buf_.Data() + pos, n - pos);
+ }
+
+ std::string substr(std::size_t pos, std::size_t len) const
+ {
+ const std::size_t n = buf_.Size();
+ if (pos >= n) return std::string();
+ const std::size_t take = (pos + len > n) ? (n - pos) : len;
+ return std::string(buf_.Data() + pos, take);
+ }
+
+ void erase(std::size_t pos)
+ {
+ // erase to end
+ material_edit([&](std::string &s) {
+ if (pos < s.size()) s.erase(pos);
+ });
+ }
+
+ void erase(std::size_t pos, std::size_t len)
+ {
+ material_edit([&](std::string &s) {
+ if (pos < s.size()) s.erase(pos, len);
+ });
+ }
+
+ void insert(std::size_t pos, const std::string &seg)
+ {
+ material_edit([&](std::string &s) {
+ if (pos > s.size()) pos = s.size();
+ s.insert(pos, seg);
+ });
+ }
+
+ Line &operator+=(const Line &other)
+ {
+ buf_.Append(other.buf_.Data(), other.buf_.Size());
+ return *this;
+ }
+
+ Line &operator+=(const std::string &s)
+ {
+ buf_.Append(s.data(), s.size());
+ return *this;
+ }
+
+ Line &operator=(const std::string &s)
+ {
+ assign_from(s);
+ return *this;
+ }
+
+ private:
+ void assign_from(const std::string &s)
+ {
+ buf_.Clear();
+ if (!s.empty()) buf_.Append(s.data(), s.size());
+ }
+
+ template
+ void material_edit(F fn)
+ {
+ std::string tmp = static_cast(*this);
+ fn(tmp);
+ assign_from(tmp);
+ }
+
+ AppendBuffer buf_;
+ };
+
+ [[nodiscard]] const std::vector &Rows() const
{
return rows_;
}
- [[nodiscard]] std::vector &Rows()
+ [[nodiscard]] std::vector &Rows()
{
return rows_;
}
@@ -154,7 +272,7 @@ private:
std::size_t rx_ = 0; // render x (tabs expanded)
std::size_t nrows_ = 0; // number of rows
std::size_t rowoffs_ = 0, coloffs_ = 0; // viewport offsets
- std::vector rows_; // buffer rows (without trailing newlines)
+ std::vector rows_; // buffer rows (without trailing newlines)
std::string filename_;
bool is_file_backed_ = false;
bool dirty_ = false;
diff --git a/Command.cc b/Command.cc
index 1868c00..91e860c 100644
--- a/Command.cc
+++ b/Command.cc
@@ -922,9 +922,9 @@ cmd_newline(CommandContext &ctx)
std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1;
for (int i = 0; i < repeat; ++i) {
- if (y >= rows.size())
- rows.resize(y + 1);
- std::string &line = rows[y];
+ if (y >= rows.size())
+ rows.resize(y + 1);
+ auto &line = rows[y];
std::string tail;
if (x < line.size()) {
tail = line.substr(x);
@@ -1103,7 +1103,7 @@ cmd_kill_line(CommandContext &ctx)
if (rows.size() == 1) {
// last remaining line: clear its contents
killed_total += rows[0];
- rows[0].clear();
+ rows[0].Clear();
y = 0;
} else if (y < rows.size()) {
// erase current line; keep y pointing at the next line