#include #include #include #include #include #include "Buffer.h" #include "UndoSystem.h" #include "UndoTree.h" // For reconstructing highlighter state on copies #include "syntax/HighlighterRegistry.h" #include "syntax/NullHighlighter.h" Buffer::Buffer() { // Initialize undo system per buffer undo_tree_ = std::make_unique(); undo_sys_ = std::make_unique(*this, *undo_tree_); } Buffer::Buffer(const std::string &path) { std::string err; OpenFromFile(path, err); } // Copy constructor/assignment: perform a deep copy of core fields; reinitialize undo for the new buffer. Buffer::Buffer(const Buffer &other) { curx_ = other.curx_; cury_ = other.cury_; rx_ = other.rx_; nrows_ = other.nrows_; rowoffs_ = other.rowoffs_; coloffs_ = other.coloffs_; rows_ = other.rows_; #ifdef KTE_USE_BUFFER_PIECE_TABLE content_ = other.content_; rows_cache_dirty_ = other.rows_cache_dirty_; #endif filename_ = other.filename_; is_file_backed_ = other.is_file_backed_; dirty_ = other.dirty_; read_only_ = other.read_only_; mark_set_ = other.mark_set_; mark_curx_ = other.mark_curx_; mark_cury_ = other.mark_cury_; // Copy syntax/highlighting flags version_ = other.version_; syntax_enabled_ = other.syntax_enabled_; filetype_ = other.filetype_; // Fresh undo system for the copy undo_tree_ = std::make_unique(); undo_sys_ = std::make_unique(*this, *undo_tree_); // Recreate a highlighter engine for this copy based on filetype/syntax state if (syntax_enabled_) { // Allocate engine and install an appropriate highlighter highlighter_ = std::make_unique(); if (!filetype_.empty()) { auto hl = kte::HighlighterRegistry::CreateFor(filetype_); if (hl) { highlighter_->SetHighlighter(std::move(hl)); } else { // Unsupported filetype -> NullHighlighter keeps syntax pipeline active highlighter_->SetHighlighter(std::make_unique()); } } else { // No filetype -> keep syntax enabled but use NullHighlighter highlighter_->SetHighlighter(std::make_unique()); } // Fresh engine has empty caches; nothing to invalidate } } Buffer & Buffer::operator=(const Buffer &other) { if (this == &other) return *this; curx_ = other.curx_; cury_ = other.cury_; rx_ = other.rx_; nrows_ = other.nrows_; rowoffs_ = other.rowoffs_; coloffs_ = other.coloffs_; rows_ = other.rows_; #ifdef KTE_USE_BUFFER_PIECE_TABLE content_ = other.content_; rows_cache_dirty_ = other.rows_cache_dirty_; #endif filename_ = other.filename_; is_file_backed_ = other.is_file_backed_; dirty_ = other.dirty_; read_only_ = other.read_only_; mark_set_ = other.mark_set_; mark_curx_ = other.mark_curx_; mark_cury_ = other.mark_cury_; version_ = other.version_; syntax_enabled_ = other.syntax_enabled_; filetype_ = other.filetype_; // Recreate undo system for this instance undo_tree_ = std::make_unique(); undo_sys_ = std::make_unique(*this, *undo_tree_); // Recreate highlighter engine consistent with syntax settings highlighter_.reset(); if (syntax_enabled_) { highlighter_ = std::make_unique(); if (!filetype_.empty()) { auto hl = kte::HighlighterRegistry::CreateFor(filetype_); if (hl) { highlighter_->SetHighlighter(std::move(hl)); } else { highlighter_->SetHighlighter(std::make_unique()); } } else { highlighter_->SetHighlighter(std::make_unique()); } } return *this; } // Move constructor: move all fields and update UndoSystem's buffer reference Buffer::Buffer(Buffer &&other) noexcept : curx_(other.curx_), cury_(other.cury_), rx_(other.rx_), nrows_(other.nrows_), rowoffs_(other.rowoffs_), coloffs_(other.coloffs_), rows_(std::move(other.rows_)), filename_(std::move(other.filename_)), is_file_backed_(other.is_file_backed_), dirty_(other.dirty_), read_only_(other.read_only_), mark_set_(other.mark_set_), mark_curx_(other.mark_curx_), mark_cury_(other.mark_cury_), undo_tree_(std::move(other.undo_tree_)), undo_sys_(std::move(other.undo_sys_)) { // Move syntax/highlighting state version_ = other.version_; syntax_enabled_ = other.syntax_enabled_; filetype_ = std::move(other.filetype_); highlighter_ = std::move(other.highlighter_); #ifdef KTE_USE_BUFFER_PIECE_TABLE content_ = std::move(other.content_); rows_cache_dirty_ = other.rows_cache_dirty_; #endif // Update UndoSystem's buffer reference to point to this object if (undo_sys_) { undo_sys_->UpdateBufferReference(*this); } } // Move assignment: move all fields and update UndoSystem's buffer reference Buffer & Buffer::operator=(Buffer &&other) noexcept { if (this == &other) return *this; curx_ = other.curx_; cury_ = other.cury_; rx_ = other.rx_; nrows_ = other.nrows_; rowoffs_ = other.rowoffs_; coloffs_ = other.coloffs_; rows_ = std::move(other.rows_); filename_ = std::move(other.filename_); is_file_backed_ = other.is_file_backed_; dirty_ = other.dirty_; read_only_ = other.read_only_; mark_set_ = other.mark_set_; mark_curx_ = other.mark_curx_; mark_cury_ = other.mark_cury_; undo_tree_ = std::move(other.undo_tree_); undo_sys_ = std::move(other.undo_sys_); // Move syntax/highlighting state version_ = other.version_; syntax_enabled_ = other.syntax_enabled_; filetype_ = std::move(other.filetype_); highlighter_ = std::move(other.highlighter_); #ifdef KTE_USE_BUFFER_PIECE_TABLE content_ = std::move(other.content_); rows_cache_dirty_ = other.rows_cache_dirty_; #endif // Update UndoSystem's buffer reference to point to this object if (undo_sys_) { undo_sys_->UpdateBufferReference(*this); } return *this; } bool Buffer::OpenFromFile(const std::string &path, std::string &err) { auto normalize_path = [](const std::string &in) -> std::string { std::string expanded = in; // Expand leading '~' to HOME if (!expanded.empty() && expanded[0] == '~') { const char *home = std::getenv("HOME"); if (home && expanded.size() >= 2 && (expanded[1] == '/' || expanded[1] == '\\')) { expanded = std::string(home) + expanded.substr(1); } else if (home && expanded.size() == 1) { expanded = std::string(home); } } try { std::filesystem::path p(expanded); if (std::filesystem::exists(p)) { return std::filesystem::canonical(p).string(); } return std::filesystem::absolute(p).string(); } catch (...) { // On any error, fall back to input return expanded; } }; const std::string norm = normalize_path(path); // 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(norm)) { rows_.clear(); nrows_ = 0; filename_ = norm; 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; #ifdef KTE_USE_BUFFER_PIECE_TABLE // Empty PieceTable content_.Clear(); rows_cache_dirty_ = true; #endif return true; } std::ifstream in(norm, std::ios::in | std::ios::binary); if (!in) { err = "Failed to open file: " + norm; return false; } #ifdef KTE_USE_BUFFER_PIECE_TABLE // Read entire file into PieceTable as-is std::string data; in.seekg(0, std::ios::end); auto sz = in.tellg(); if (sz > 0) { data.resize(static_cast(sz)); in.seekg(0, std::ios::beg); in.read(data.data(), static_cast(data.size())); } content_.Clear(); if (!data.empty()) content_.Append(data.data(), data.size()); rows_cache_dirty_ = true; nrows_ = 0; // not used under adapter #else // 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 content only // Handle potential Windows CRLF: strip trailing '\r' if (!line.empty() && line.back() == '\r') { line.pop_back(); } rows_.emplace_back(line); } // 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(); #endif filename_ = norm; is_file_backed_ = true; dirty_ = false; // Reset/initialize undo system for this loaded file if (!undo_tree_) undo_tree_ = std::make_unique(); if (!undo_sys_) undo_sys_ = std::make_unique(*this, *undo_tree_); // Clear any existing history for a fresh load undo_sys_->clear(); // Reset cursor/viewport state curx_ = cury_ = rx_ = 0; rowoffs_ = coloffs_ = 0; mark_set_ = false; mark_curx_ = mark_cury_ = 0; return true; } bool Buffer::Save(std::string &err) const { if (!is_file_backed_ || filename_.empty()) { err = "Buffer is not file-backed; use SaveAs()"; return false; } std::ofstream out(filename_, std::ios::out | std::ios::binary | std::ios::trunc); if (!out) { err = "Failed to open for write: " + filename_; return false; } #ifdef KTE_USE_BUFFER_PIECE_TABLE const char *d = content_.Data(); std::size_t n = content_.Size(); if (d && n) out.write(d, static_cast(n)); #else 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'); } } #endif if (!out.good()) { err = "Write error"; return false; } // Note: const method cannot change dirty_. Intentionally const to allow UI code // to decide when to flip dirty flag after successful save. return true; } bool Buffer::SaveAs(const std::string &path, std::string &err) { // Normalize output path first std::string out_path; try { std::filesystem::path p(path); // Do a light expansion of '~' std::string expanded = path; if (!expanded.empty() && expanded[0] == '~') { const char *home = std::getenv("HOME"); if (home && expanded.size() >= 2 && (expanded[1] == '/' || expanded[1] == '\\')) expanded = std::string(home) + expanded.substr(1); else if (home && expanded.size() == 1) expanded = std::string(home); } std::filesystem::path ep(expanded); out_path = std::filesystem::absolute(ep).string(); } catch (...) { out_path = path; } // Write to the given path std::ofstream out(out_path, std::ios::out | std::ios::binary | std::ios::trunc); if (!out) { err = "Failed to open for write: " + out_path; return false; } #ifdef KTE_USE_BUFFER_PIECE_TABLE { const char *d = content_.Data(); std::size_t n = content_.Size(); if (d && n) out.write(d, static_cast(n)); } #else 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'); } } #endif if (!out.good()) { err = "Write error"; return false; } filename_ = out_path; is_file_backed_ = true; dirty_ = false; return true; } std::string Buffer::AsString() const { std::stringstream ss; ss << "Buffer<" << this->filename_; if (this->Dirty()) { ss << "*"; } #ifdef KTE_USE_BUFFER_PIECE_TABLE ss << ">: " << content_.LineCount() << " lines"; #else ss << ">: " << rows_.size() << " lines"; #endif return ss.str(); } // --- Raw editing APIs (no undo recording, cursor untouched) --- void Buffer::insert_text(int row, int col, std::string_view text) { #ifdef KTE_USE_BUFFER_PIECE_TABLE if (row < 0) row = 0; if (col < 0) col = 0; const std::size_t off = content_.LineColToByteOffset(static_cast(row), static_cast(col)); if (!text.empty()) { content_.Insert(off, text.data(), text.size()); rows_cache_dirty_ = true; } return; #else if (row < 0) row = 0; if (static_cast(row) > rows_.size()) row = static_cast(rows_.size()); if (rows_.empty()) rows_.emplace_back(""); if (static_cast(row) >= rows_.size()) rows_.emplace_back(""); auto y = static_cast(row); auto x = static_cast(col); if (x > rows_[y].size()) { x = rows_[y].size(); } std::string remain(text); while (true) { auto pos = remain.find('\n'); if (pos == std::string::npos) { rows_[y].insert(x, remain); break; } // Insert up to newline std::string seg = remain.substr(0, pos); rows_[y].insert(x, seg); x += seg.size(); // Split line at x std::string tail = rows_[y].substr(x); rows_[y].erase(x); rows_.insert(rows_.begin() + static_cast(y + 1), Line(tail)); y += 1; x = 0; remain.erase(0, pos + 1); } // Do not set dirty here; UndoSystem will manage state/dirty externally #endif } #ifdef KTE_USE_BUFFER_PIECE_TABLE // ===== Adapter helpers for PieceTable-backed Buffer ===== void Buffer::ensure_rows_cache() const { if (!rows_cache_dirty_) return; rows_.clear(); const std::size_t lc = content_.LineCount(); rows_.reserve(lc); for (std::size_t i = 0; i < lc; ++i) { rows_.emplace_back(content_.GetLine(i)); } // Keep nrows_ in sync for any legacy code that still reads it const_cast(this)->nrows_ = rows_.size(); rows_cache_dirty_ = false; } std::size_t Buffer::content_LineCount_() const { return content_.LineCount(); } #endif void Buffer::delete_text(int row, int col, std::size_t len) { #ifdef KTE_USE_BUFFER_PIECE_TABLE if (len == 0) return; if (row < 0) row = 0; if (col < 0) col = 0; std::size_t start = content_.LineColToByteOffset(static_cast(row), static_cast(col)); // Walk len logical characters across lines to compute end offset std::size_t r = static_cast(row); std::size_t c = static_cast(col); std::size_t remaining = len; const std::size_t line_count = content_.LineCount(); while (remaining > 0 && r < line_count) { auto range = content_.GetLineRange(r); // [start,end) // Compute end of line excluding trailing '\n' std::size_t line_end = range.second; if (line_end > range.first) { // If last char is '\n', don't count in-column span std::string last = content_.GetRange(line_end - 1, 1); if (!last.empty() && last[0] == '\n') { line_end -= 1; } } std::size_t cur_off = content_.LineColToByteOffset(r, c); std::size_t in_line = (cur_off < line_end) ? (line_end - cur_off) : 0; if (remaining <= in_line) { // All within current line std::size_t end = cur_off + remaining; content_.Delete(start, end - start); rows_cache_dirty_ = true; return; } // Consume rest of line remaining -= in_line; std::size_t end = cur_off + in_line; // If there is a next line and remaining > 0, consider consuming the newline as 1 if (r + 1 < line_count) { if (remaining > 0) { // newline end += 1; remaining -= 1; } // Move to next line r += 1; c = 0; // Update start deletion length so far by postponing until we know final end; we keep start fixed if (remaining == 0) { content_.Delete(start, end - start); rows_cache_dirty_ = true; return; } // Continue loop with updated r/c; but also keep track of 'end' as current consumed position // Rather than tracking incrementally, we will recompute cur_off at top of loop. // However, we need to carry forward the consumed part; we can temporarily store 'end' in start_of_next // To simplify, after loop finishes we will compute final end using current r/c using remaining. } else { // No next line; delete to file end std::size_t total = content_.Size(); content_.Delete(start, total - start); rows_cache_dirty_ = true; return; } } // If loop ended because remaining==0 at a line boundary if (remaining == 0) { std::size_t end = content_.LineColToByteOffset(r, c); content_.Delete(start, end - start); rows_cache_dirty_ = true; } return; #else if (rows_.empty() || len == 0) return; if (row < 0) row = 0; if (static_cast(row) >= rows_.size()) return; const auto y = static_cast(row); const auto x = std::min(static_cast(col), rows_[y].size()); std::size_t remaining = len; while (remaining > 0 && y < rows_.size()) { auto &line = rows_[y]; const std::size_t in_line = std::min(remaining, line.size() - std::min(x, line.size())); if (x < line.size() && in_line > 0) { line.erase(x, in_line); remaining -= in_line; } if (remaining == 0) break; // If at or beyond end of line and there is a next line, join it (deleting the implied '\n') if (y + 1 < rows_.size()) { line += rows_[y + 1]; rows_.erase(rows_.begin() + static_cast(y + 1)); // deleting the newline consumes one virtual character if (remaining > 0) { // Treat the newline as one deletion unit if len spans it // We already joined, so nothing else to do here. } } else { break; } } #endif } void Buffer::split_line(int row, const int col) { #ifdef KTE_USE_BUFFER_PIECE_TABLE if (row < 0) row = 0; if (col < 0) row = 0; const std::size_t off = content_.LineColToByteOffset(static_cast(row), static_cast(col)); const char nl = '\n'; content_.Insert(off, &nl, 1); rows_cache_dirty_ = true; return; #else if (row < 0) { row = 0; } if (static_cast(row) >= rows_.size()) { rows_.resize(static_cast(row) + 1); } const auto y = static_cast(row); const auto x = std::min(static_cast(col), rows_[y].size()); const auto tail = rows_[y].substr(x); rows_[y].erase(x); rows_.insert(rows_.begin() + static_cast(y + 1), Line(tail)); #endif } void Buffer::join_lines(int row) { #ifdef KTE_USE_BUFFER_PIECE_TABLE if (row < 0) row = 0; std::size_t r = static_cast(row); if (r + 1 >= content_.LineCount()) return; // Delete the newline between line r and r+1 std::size_t end_of_line = content_.LineColToByteOffset(r, std::numeric_limits::max()); // end_of_line now equals line end (clamped before newline). The newline should be exactly at this position. content_.Delete(end_of_line, 1); rows_cache_dirty_ = true; return; #else if (row < 0) { row = 0; } const auto y = static_cast(row); if (y + 1 >= rows_.size()) { return; } rows_[y] += rows_[y + 1]; rows_.erase(rows_.begin() + static_cast(y + 1)); #endif } void Buffer::insert_row(int row, const std::string_view text) { #ifdef KTE_USE_BUFFER_PIECE_TABLE if (row < 0) row = 0; std::size_t off = content_.LineColToByteOffset(static_cast(row), 0); if (!text.empty()) content_.Insert(off, text.data(), text.size()); const char nl = '\n'; content_.Insert(off + text.size(), &nl, 1); rows_cache_dirty_ = true; return; #else if (row < 0) row = 0; if (static_cast(row) > rows_.size()) row = static_cast(rows_.size()); rows_.insert(rows_.begin() + row, Line(std::string(text))); #endif } void Buffer::delete_row(int row) { #ifdef KTE_USE_BUFFER_PIECE_TABLE if (row < 0) row = 0; std::size_t r = static_cast(row); if (r >= content_.LineCount()) return; auto range = content_.GetLineRange(r); // [start,end) // If not last line, ensure we include the separating newline by using end as-is (which points to next line start) // If last line, end may equal total_size_. We still delete [start,end) which removes the last line content. std::size_t start = range.first; std::size_t end = range.second; content_.Delete(start, end - start); rows_cache_dirty_ = true; return; #else if (row < 0) row = 0; if (static_cast(row) >= rows_.size()) return; rows_.erase(rows_.begin() + row); #endif } // Undo system accessors UndoSystem * Buffer::Undo() { return undo_sys_.get(); } const UndoSystem * Buffer::Undo() const { return undo_sys_.get(); }