- Introduced comprehensive tests: - `test_buffer_open_nonexistent_save.cc`: Save after opening a non-existent file. - `test_buffer_save.cc`: Save buffer contents to disk. - `test_buffer_save_existing.cc`: Save after opening existing files. - Implemented `PieceTable::WriteToStream()` to directly stream content without full materialization. - Updated `Buffer::Save` and `Buffer::SaveAs` to use efficient streaming via `PieceTable`. - Enhanced editing commands (`Insert`, `Delete`, `Replace`, etc.) to use PieceTable APIs, ensuring proper undo and save functionality.
372 lines
8.2 KiB
C++
372 lines
8.2 KiB
C++
#include <algorithm>
|
|
#include <utility>
|
|
#include <filesystem>
|
|
|
|
#include "Editor.h"
|
|
#include "syntax/HighlighterRegistry.h"
|
|
#include "syntax/CppHighlighter.h"
|
|
#include "syntax/NullHighlighter.h"
|
|
|
|
|
|
Editor::Editor()
|
|
{
|
|
swap_ = std::make_unique<kte::SwapManager>();
|
|
}
|
|
|
|
|
|
void
|
|
Editor::SetDimensions(std::size_t rows, std::size_t cols)
|
|
{
|
|
rows_ = rows;
|
|
cols_ = cols;
|
|
}
|
|
|
|
|
|
void
|
|
Editor::SetStatus(const std::string &message)
|
|
{
|
|
msg_ = message;
|
|
msgtm_ = std::time(nullptr);
|
|
}
|
|
|
|
|
|
Buffer *
|
|
Editor::CurrentBuffer()
|
|
{
|
|
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
|
return nullptr;
|
|
}
|
|
return &buffers_[curbuf_];
|
|
}
|
|
|
|
|
|
const Buffer *
|
|
Editor::CurrentBuffer() const
|
|
{
|
|
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
|
return nullptr;
|
|
}
|
|
return &buffers_[curbuf_];
|
|
}
|
|
|
|
|
|
static std::vector<std::filesystem::path>
|
|
split_reverse(const std::filesystem::path &p)
|
|
{
|
|
std::vector<std::filesystem::path> parts;
|
|
for (auto it = p; !it.empty(); it = it.parent_path()) {
|
|
if (it == it.parent_path()) {
|
|
// root or single element
|
|
if (!it.empty())
|
|
parts.push_back(it);
|
|
break;
|
|
}
|
|
parts.push_back(it.filename());
|
|
}
|
|
return parts; // from leaf toward root
|
|
}
|
|
|
|
|
|
std::string
|
|
Editor::DisplayNameFor(const Buffer &buf) const
|
|
{
|
|
std::string full = buf.Filename();
|
|
if (full.empty())
|
|
return std::string("[no name]");
|
|
|
|
std::filesystem::path target(full);
|
|
auto target_parts = split_reverse(target);
|
|
if (target_parts.empty())
|
|
return target.filename().string();
|
|
|
|
// Prepare list of other buffer paths
|
|
std::vector<std::vector<std::filesystem::path> > others;
|
|
others.reserve(buffers_.size());
|
|
for (const auto &b: buffers_) {
|
|
if (&b == &buf)
|
|
continue;
|
|
if (b.Filename().empty())
|
|
continue;
|
|
others.push_back(split_reverse(std::filesystem::path(b.Filename())));
|
|
}
|
|
|
|
// Increase suffix length until unique among others
|
|
std::size_t need = 1; // at least basename
|
|
for (;;) {
|
|
// Build candidate suffix for target
|
|
std::filesystem::path cand;
|
|
for (std::size_t i = 0; i < need && i < target_parts.size(); ++i) {
|
|
cand = std::filesystem::path(target_parts[i]) / cand;
|
|
}
|
|
// Compare against others
|
|
bool clash = false;
|
|
for (const auto &o_parts: others) {
|
|
std::filesystem::path ocand;
|
|
for (std::size_t i = 0; i < need && i < o_parts.size(); ++i) {
|
|
ocand = std::filesystem::path(o_parts[i]) / ocand;
|
|
}
|
|
if (ocand == cand) {
|
|
clash = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!clash || need >= target_parts.size()) {
|
|
std::string s = cand.string();
|
|
// Remove any trailing slash that may appear from root joining
|
|
if (!s.empty() && (s.back() == '/' || s.back() == '\\'))
|
|
s.pop_back();
|
|
return s;
|
|
}
|
|
++need;
|
|
}
|
|
}
|
|
|
|
|
|
std::size_t
|
|
Editor::AddBuffer(const Buffer &buf)
|
|
{
|
|
buffers_.push_back(buf);
|
|
// Attach swap recorder
|
|
if (swap_) {
|
|
buffers_.back().SetSwapRecorder(swap_.get());
|
|
swap_->Attach(&buffers_.back());
|
|
}
|
|
if (buffers_.size() == 1) {
|
|
curbuf_ = 0;
|
|
}
|
|
return buffers_.size() - 1;
|
|
}
|
|
|
|
|
|
std::size_t
|
|
Editor::AddBuffer(Buffer &&buf)
|
|
{
|
|
buffers_.push_back(std::move(buf));
|
|
if (swap_) {
|
|
buffers_.back().SetSwapRecorder(swap_.get());
|
|
swap_->Attach(&buffers_.back());
|
|
}
|
|
if (buffers_.size() == 1) {
|
|
curbuf_ = 0;
|
|
}
|
|
return buffers_.size() - 1;
|
|
}
|
|
|
|
|
|
bool
|
|
Editor::OpenFile(const std::string &path, std::string &err)
|
|
{
|
|
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
|
|
// of creating a new one.
|
|
if (buffers_.size() == 1) {
|
|
Buffer &cur = buffers_[curbuf_];
|
|
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
|
const bool clean = !cur.Dirty();
|
|
const auto &rows = cur.Rows();
|
|
const bool rows_empty = rows.empty();
|
|
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
|
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
|
bool ok = cur.OpenFromFile(path, err);
|
|
if (!ok)
|
|
return false;
|
|
// Ensure swap recorder is attached for this buffer
|
|
if (swap_) {
|
|
cur.SetSwapRecorder(swap_.get());
|
|
swap_->Attach(&cur);
|
|
swap_->NotifyFilenameChanged(cur);
|
|
}
|
|
// Setup highlighting using registry (extension + shebang)
|
|
cur.EnsureHighlighter();
|
|
std::string first = "";
|
|
const auto &rows = cur.Rows();
|
|
if (!rows.empty())
|
|
first = static_cast<std::string>(rows[0]);
|
|
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
|
if (!ft.empty()) {
|
|
cur.SetFiletype(ft);
|
|
cur.SetSyntaxEnabled(true);
|
|
if (auto *eng = cur.Highlighter()) {
|
|
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
|
eng->InvalidateFrom(0);
|
|
}
|
|
} else {
|
|
cur.SetFiletype("");
|
|
cur.SetSyntaxEnabled(true);
|
|
if (auto *eng = cur.Highlighter()) {
|
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
|
eng->InvalidateFrom(0);
|
|
}
|
|
}
|
|
// Defensive: ensure any active prompt is closed after a successful open
|
|
CancelPrompt();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Buffer b;
|
|
if (!b.OpenFromFile(path, err)) {
|
|
return false;
|
|
}
|
|
if (swap_) {
|
|
b.SetSwapRecorder(swap_.get());
|
|
// path is known, notify
|
|
swap_->Attach(&b);
|
|
swap_->NotifyFilenameChanged(b);
|
|
}
|
|
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
|
b.EnsureHighlighter();
|
|
std::string first = "";
|
|
{
|
|
const auto &rows = b.Rows();
|
|
if (!rows.empty())
|
|
first = static_cast<std::string>(rows[0]);
|
|
}
|
|
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
|
if (!ft.empty()) {
|
|
b.SetFiletype(ft);
|
|
b.SetSyntaxEnabled(true);
|
|
if (auto *eng = b.Highlighter()) {
|
|
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
|
eng->InvalidateFrom(0);
|
|
}
|
|
} else {
|
|
b.SetFiletype("");
|
|
b.SetSyntaxEnabled(true);
|
|
if (auto *eng = b.Highlighter()) {
|
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
|
eng->InvalidateFrom(0);
|
|
}
|
|
}
|
|
// Add as a new buffer and switch to it
|
|
std::size_t idx = AddBuffer(std::move(b));
|
|
SwitchTo(idx);
|
|
// Defensive: ensure any active prompt is closed after a successful open
|
|
CancelPrompt();
|
|
return true;
|
|
}
|
|
|
|
|
|
bool
|
|
Editor::SwitchTo(std::size_t index)
|
|
{
|
|
if (index >= buffers_.size()) {
|
|
return false;
|
|
}
|
|
curbuf_ = index;
|
|
// Robustness: ensure a valid highlighter is installed when switching buffers
|
|
Buffer &b = buffers_[curbuf_];
|
|
if (b.SyntaxEnabled()) {
|
|
b.EnsureHighlighter();
|
|
if (auto *eng = b.Highlighter()) {
|
|
if (!eng->HasHighlighter()) {
|
|
// Try to set based on existing filetype; fall back to NullHighlighter
|
|
if (!b.Filetype().empty()) {
|
|
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
|
|
if (hl) {
|
|
eng->SetHighlighter(std::move(hl));
|
|
} else {
|
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
|
}
|
|
} else {
|
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
|
}
|
|
eng->InvalidateFrom(0);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool
|
|
Editor::CloseBuffer(std::size_t index)
|
|
{
|
|
if (index >= buffers_.size()) {
|
|
return false;
|
|
}
|
|
buffers_.erase(buffers_.begin() + static_cast<std::ptrdiff_t>(index));
|
|
if (buffers_.empty()) {
|
|
curbuf_ = 0;
|
|
} else if (curbuf_ >= buffers_.size()) {
|
|
curbuf_ = buffers_.size() - 1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
void
|
|
Editor::Reset()
|
|
{
|
|
rows_ = cols_ = 0;
|
|
mode_ = 0;
|
|
kill_ = 0;
|
|
no_kill_ = 0;
|
|
dirtyex_ = 0;
|
|
msg_.clear();
|
|
msgtm_ = 0;
|
|
uarg_ = 0;
|
|
ucount_ = 0;
|
|
repeatable_ = false;
|
|
quit_requested_ = false;
|
|
quit_confirm_pending_ = false;
|
|
// Reset close-confirm/save state
|
|
close_confirm_pending_ = false;
|
|
close_after_save_ = false;
|
|
buffers_.clear();
|
|
curbuf_ = 0;
|
|
}
|
|
|
|
|
|
// --- Universal argument helpers ---
|
|
void
|
|
Editor::UArgStart()
|
|
{
|
|
// If not active, start fresh; else multiply by 4 per ke semantics
|
|
if (uarg_ == 0) {
|
|
ucount_ = 0;
|
|
} else {
|
|
if (ucount_ == 0) {
|
|
ucount_ = 1;
|
|
}
|
|
ucount_ *= 4;
|
|
}
|
|
uarg_ = 1;
|
|
char buf[64];
|
|
std::snprintf(buf, sizeof(buf), "C-u %d", ucount_);
|
|
SetStatus(buf);
|
|
}
|
|
|
|
|
|
void
|
|
Editor::UArgDigit(int d)
|
|
{
|
|
if (d < 0)
|
|
d = 0;
|
|
if (d > 9)
|
|
d = 9;
|
|
if (uarg_ == 0) {
|
|
uarg_ = 1;
|
|
ucount_ = 0;
|
|
}
|
|
ucount_ = ucount_ * 10 + d;
|
|
char buf[64];
|
|
std::snprintf(buf, sizeof(buf), "C-u %d", ucount_);
|
|
SetStatus(buf);
|
|
}
|
|
|
|
|
|
void
|
|
Editor::UArgClear()
|
|
{
|
|
uarg_ = 0;
|
|
ucount_ = 0;
|
|
}
|
|
|
|
|
|
int
|
|
Editor::UArgGet()
|
|
{
|
|
int n = (ucount_ > 0) ? ucount_ : 1;
|
|
UArgClear();
|
|
return n;
|
|
} |