- Introduced `SwapManager` for buffering and writing incremental edits to sidecar `.kte.swp` files. - Implemented basic operations: insertion, deletion, split, join, and checkpointing. - Added recovery design doc (`docs/plans/swap-files.md`). - Updated editor initialization to integrate `SwapManager` instance for crash recovery across buffers.
628 lines
11 KiB
C++
628 lines
11 KiB
C++
/*
|
|
* Editor.h - top-level editor state and buffer management
|
|
*/
|
|
#pragma once
|
|
#include <cstddef>
|
|
#include <ctime>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Buffer.h"
|
|
#include "Swap.h"
|
|
|
|
|
|
class Editor {
|
|
public:
|
|
Editor();
|
|
|
|
// Dimensions (terminal or viewport)
|
|
void SetDimensions(std::size_t rows, std::size_t cols);
|
|
|
|
|
|
[[nodiscard]] std::size_t Rows() const
|
|
{
|
|
return rows_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t Cols() const
|
|
{
|
|
return cols_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t ContentRows() const
|
|
{
|
|
// Always compute from current rows_ to avoid stale values.
|
|
// Reserve 1 row for status line.
|
|
if (rows_ == 0)
|
|
return 1;
|
|
return std::max<std::size_t>(1, rows_ - 1);
|
|
}
|
|
|
|
|
|
// Mode and flags (mirroring legacy fields)
|
|
void SetMode(int m)
|
|
{
|
|
mode_ = m;
|
|
}
|
|
|
|
|
|
[[nodiscard]] int Mode() const
|
|
{
|
|
return mode_;
|
|
}
|
|
|
|
|
|
void SetKillChain(bool on)
|
|
{
|
|
kill_ = on ? 1 : 0;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool KillChain() const
|
|
{
|
|
return kill_ != 0;
|
|
}
|
|
|
|
|
|
void SetNoKill(bool on)
|
|
{
|
|
no_kill_ = on ? 1 : 0;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool NoKill() const
|
|
{
|
|
return no_kill_ != 0;
|
|
}
|
|
|
|
|
|
// Kill ring API
|
|
void KillRingClear()
|
|
{
|
|
kill_ring_.clear();
|
|
}
|
|
|
|
|
|
void KillRingPush(const std::string &text)
|
|
{
|
|
if (text.empty())
|
|
return;
|
|
// push to front
|
|
kill_ring_.insert(kill_ring_.begin(), text);
|
|
if (kill_ring_.size() > kill_ring_max_)
|
|
kill_ring_.resize(kill_ring_max_);
|
|
}
|
|
|
|
|
|
void KillRingAppend(const std::string &text)
|
|
{
|
|
if (text.empty())
|
|
return;
|
|
if (kill_ring_.empty()) {
|
|
KillRingPush(text);
|
|
} else {
|
|
kill_ring_.front() += text;
|
|
}
|
|
}
|
|
|
|
|
|
void KillRingPrepend(const std::string &text)
|
|
{
|
|
if (text.empty())
|
|
return;
|
|
if (kill_ring_.empty()) {
|
|
KillRingPush(text);
|
|
} else {
|
|
kill_ring_.front() = text + kill_ring_.front();
|
|
}
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::string KillRingHead() const
|
|
{
|
|
return kill_ring_.empty() ? std::string() : kill_ring_.front();
|
|
}
|
|
|
|
|
|
void SetDirtyEx(int d)
|
|
{
|
|
dirtyex_ = d;
|
|
}
|
|
|
|
|
|
[[nodiscard]] int DirtyEx() const
|
|
{
|
|
return dirtyex_;
|
|
}
|
|
|
|
|
|
void SetUniversalArg(int uarg, int ucount)
|
|
{
|
|
uarg_ = uarg;
|
|
ucount_ = ucount;
|
|
}
|
|
|
|
|
|
[[nodiscard]] int UArg() const
|
|
{
|
|
return uarg_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] int UCount() const
|
|
{
|
|
return ucount_;
|
|
}
|
|
|
|
|
|
// --- Universal argument control (C-u) ---
|
|
// Begin or extend a universal argument (like ke's uarg_start)
|
|
void UArgStart();
|
|
|
|
// Add a digit 0..9 to the current universal argument (like ke's uarg_digit)
|
|
void UArgDigit(int d);
|
|
|
|
// Clear universal-argument state (like ke's uarg_clear)
|
|
void UArgClear();
|
|
|
|
// Consume the current universal argument, returning count >= 1.
|
|
// If no universal argument active, returns 1.
|
|
int UArgGet();
|
|
|
|
// Repeatable command flag: input layer can mark the next command as repeatable
|
|
void SetRepeatable(bool on)
|
|
{
|
|
repeatable_ = on;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool Repeatable() const
|
|
{
|
|
return repeatable_;
|
|
}
|
|
|
|
|
|
// Status message storage. Rendering is renderer-dependent; the editor
|
|
// merely stores the current message and its timestamp.
|
|
void SetStatus(const std::string &message);
|
|
|
|
|
|
[[nodiscard]] const std::string &Status() const
|
|
{
|
|
return msg_;
|
|
}
|
|
|
|
|
|
// --- Quit/Exit state ---
|
|
void SetQuitRequested(bool on)
|
|
{
|
|
quit_requested_ = on;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool QuitRequested() const
|
|
{
|
|
return quit_requested_;
|
|
}
|
|
|
|
|
|
void SetQuitConfirmPending(bool on)
|
|
{
|
|
quit_confirm_pending_ = on;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool QuitConfirmPending() const
|
|
{
|
|
return quit_confirm_pending_;
|
|
}
|
|
|
|
|
|
// --- Buffer close/save confirmation state ---
|
|
void SetCloseConfirmPending(bool on)
|
|
{
|
|
close_confirm_pending_ = on;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool CloseConfirmPending() const
|
|
{
|
|
return close_confirm_pending_;
|
|
}
|
|
|
|
|
|
void SetCloseAfterSave(bool on)
|
|
{
|
|
close_after_save_ = on;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool CloseAfterSave() const
|
|
{
|
|
return close_after_save_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::time_t StatusTime() const
|
|
{
|
|
return msgtm_;
|
|
}
|
|
|
|
|
|
// --- Minimal search state for incremental search (milestone 6) ---
|
|
void SetSearchActive(bool on)
|
|
{
|
|
search_active_ = on;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool SearchActive() const
|
|
{
|
|
return search_active_;
|
|
}
|
|
|
|
|
|
void SetSearchQuery(const std::string &q)
|
|
{
|
|
search_query_ = q;
|
|
}
|
|
|
|
|
|
[[nodiscard]] const std::string &SearchQuery() const
|
|
{
|
|
return search_query_;
|
|
}
|
|
|
|
|
|
void SetSearchMatch(std::size_t y, std::size_t x, std::size_t len)
|
|
{
|
|
search_y_ = y;
|
|
search_x_ = x;
|
|
search_len_ = len;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t SearchMatchY() const
|
|
{
|
|
return search_y_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t SearchMatchX() const
|
|
{
|
|
return search_x_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t SearchMatchLen() const
|
|
{
|
|
return search_len_;
|
|
}
|
|
|
|
|
|
// Additional helpers for search session bookkeeping
|
|
void SetSearchOrigin(std::size_t x, std::size_t y, std::size_t rowoffs, std::size_t coloffs)
|
|
{
|
|
search_origin_set_ = true;
|
|
search_orig_x_ = x;
|
|
search_orig_y_ = y;
|
|
search_orig_rowoffs_ = rowoffs;
|
|
search_orig_coloffs_ = coloffs;
|
|
}
|
|
|
|
|
|
void ClearSearchOrigin()
|
|
{
|
|
search_origin_set_ = false;
|
|
search_orig_x_ = search_orig_y_ = search_orig_rowoffs_ = search_orig_coloffs_ = 0;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool SearchOriginSet() const
|
|
{
|
|
return search_origin_set_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t SearchOrigX() const
|
|
{
|
|
return search_orig_x_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t SearchOrigY() const
|
|
{
|
|
return search_orig_y_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t SearchOrigRowoffs() const
|
|
{
|
|
return search_orig_rowoffs_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t SearchOrigColoffs() const
|
|
{
|
|
return search_orig_coloffs_;
|
|
}
|
|
|
|
|
|
void SetSearchIndex(int i)
|
|
{
|
|
search_index_ = i;
|
|
}
|
|
|
|
|
|
[[nodiscard]] int SearchIndex() const
|
|
{
|
|
return search_index_;
|
|
}
|
|
|
|
|
|
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
|
enum class PromptKind {
|
|
None = 0,
|
|
Search,
|
|
RegexSearch,
|
|
RegexReplaceFind, // step 1 of Regex Search & Replace: find pattern
|
|
RegexReplaceWith, // step 2 of Regex Search & Replace: replacement text
|
|
OpenFile,
|
|
SaveAs,
|
|
Confirm,
|
|
BufferSwitch,
|
|
GotoLine,
|
|
Chdir,
|
|
ReplaceFind, // step 1 of Search & Replace: find what
|
|
ReplaceWith, // step 2 of Search & Replace: replace with
|
|
Command // generic command prompt (": ")
|
|
};
|
|
|
|
|
|
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
|
|
{
|
|
prompt_active_ = true;
|
|
prompt_kind_ = kind;
|
|
prompt_label_ = label;
|
|
prompt_text_ = initial;
|
|
}
|
|
|
|
|
|
void CancelPrompt()
|
|
{
|
|
prompt_active_ = false;
|
|
prompt_kind_ = PromptKind::None;
|
|
prompt_label_.clear();
|
|
prompt_text_.clear();
|
|
}
|
|
|
|
|
|
void AcceptPrompt()
|
|
{
|
|
// Editor-level accept only ends prompt; commands act on value.
|
|
prompt_active_ = false;
|
|
}
|
|
|
|
|
|
void SetPromptText(const std::string &t)
|
|
{
|
|
prompt_text_ = t;
|
|
}
|
|
|
|
|
|
void AppendPromptText(const std::string &t)
|
|
{
|
|
prompt_text_ += t;
|
|
}
|
|
|
|
|
|
void BackspacePromptText()
|
|
{
|
|
if (!prompt_text_.empty())
|
|
prompt_text_.pop_back();
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool PromptActive() const
|
|
{
|
|
return prompt_active_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] PromptKind CurrentPromptKind() const
|
|
{
|
|
return prompt_kind_;
|
|
}
|
|
|
|
|
|
// --- Overwrite confirmation (save-as on existing file) ---
|
|
void SetPendingOverwritePath(const std::string &path)
|
|
{
|
|
pending_overwrite_path_ = path;
|
|
}
|
|
|
|
|
|
void ClearPendingOverwritePath()
|
|
{
|
|
pending_overwrite_path_.clear();
|
|
}
|
|
|
|
|
|
[[nodiscard]] const std::string &PendingOverwritePath() const
|
|
{
|
|
return pending_overwrite_path_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] const std::string &PromptLabel() const
|
|
{
|
|
return prompt_label_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] const std::string &PromptText() const
|
|
{
|
|
return prompt_text_;
|
|
}
|
|
|
|
|
|
// Buffers
|
|
[[nodiscard]] std::size_t BufferCount() const
|
|
{
|
|
return buffers_.size();
|
|
}
|
|
|
|
|
|
[[nodiscard]] std::size_t CurrentBufferIndex() const
|
|
{
|
|
return curbuf_;
|
|
}
|
|
|
|
|
|
Buffer *CurrentBuffer();
|
|
|
|
const Buffer *CurrentBuffer() const;
|
|
|
|
// Compute a display-friendly short name for a buffer path that is the
|
|
// shortest unique suffix among all open buffers. If buffer has no name,
|
|
// returns "[no name]".
|
|
[[nodiscard]] std::string DisplayNameFor(const Buffer &buf) const;
|
|
|
|
// Add an existing buffer (copy/move) or open from file path
|
|
std::size_t AddBuffer(const Buffer &buf);
|
|
|
|
std::size_t AddBuffer(Buffer &&buf);
|
|
|
|
bool OpenFile(const std::string &path, std::string &err);
|
|
|
|
// Buffer switching/closing
|
|
bool SwitchTo(std::size_t index);
|
|
|
|
bool CloseBuffer(std::size_t index);
|
|
|
|
// Reset to initial state
|
|
void Reset();
|
|
|
|
// Direct access when needed (try to prefer methods above)
|
|
[[nodiscard]] const std::vector<Buffer> &Buffers() const
|
|
{
|
|
return buffers_;
|
|
}
|
|
|
|
|
|
std::vector<Buffer> &Buffers()
|
|
{
|
|
return buffers_;
|
|
}
|
|
|
|
|
|
// Swap manager access (for advanced integrations/tests)
|
|
[[nodiscard]] kte::SwapManager *Swap()
|
|
{
|
|
return swap_.get();
|
|
}
|
|
|
|
|
|
// --- GUI: Visual File Picker state ---
|
|
void SetFilePickerVisible(bool on)
|
|
{
|
|
file_picker_visible_ = on;
|
|
}
|
|
|
|
|
|
[[nodiscard]] bool FilePickerVisible() const
|
|
{
|
|
return file_picker_visible_;
|
|
}
|
|
|
|
|
|
void SetFilePickerDir(const std::string &path)
|
|
{
|
|
file_picker_dir_ = path;
|
|
}
|
|
|
|
|
|
[[nodiscard]] const std::string &FilePickerDir() const
|
|
{
|
|
return file_picker_dir_;
|
|
}
|
|
|
|
private:
|
|
std::size_t rows_ = 0, cols_ = 0;
|
|
int mode_ = 0;
|
|
int kill_ = 0; // KILL CHAIN
|
|
int no_kill_ = 0; // don't kill in delete_row
|
|
int dirtyex_ = 0;
|
|
std::string msg_;
|
|
std::time_t msgtm_ = 0;
|
|
int uarg_ = 0, ucount_ = 0; // C-u support
|
|
bool repeatable_ = false; // whether the next command is repeatable
|
|
|
|
std::vector<Buffer> buffers_;
|
|
std::size_t curbuf_ = 0; // index into buffers_
|
|
|
|
// Swap journaling manager (lifetime = editor)
|
|
std::unique_ptr<kte::SwapManager> swap_;
|
|
|
|
// Kill ring (Emacs-like)
|
|
std::vector<std::string> kill_ring_;
|
|
std::size_t kill_ring_max_ = 60;
|
|
|
|
// Quit state
|
|
bool quit_requested_ = false;
|
|
bool quit_confirm_pending_ = false;
|
|
bool close_confirm_pending_ = false; // awaiting y/N to save-before-close
|
|
bool close_after_save_ = false; // if true, close buffer after successful Save/SaveAs
|
|
|
|
// Search state
|
|
bool search_active_ = false;
|
|
std::string search_query_;
|
|
std::size_t search_y_ = 0, search_x_ = 0, search_len_ = 0;
|
|
// Search session bookkeeping
|
|
bool search_origin_set_ = false;
|
|
std::size_t search_orig_x_ = 0, search_orig_y_ = 0;
|
|
std::size_t search_orig_rowoffs_ = 0, search_orig_coloffs_ = 0;
|
|
int search_index_ = -1;
|
|
|
|
// Prompt state
|
|
bool prompt_active_ = false;
|
|
PromptKind prompt_kind_ = PromptKind::None;
|
|
std::string prompt_label_;
|
|
std::string prompt_text_;
|
|
std::string pending_overwrite_path_;
|
|
|
|
// GUI-only state (safe no-op in terminal builds)
|
|
bool file_picker_visible_ = false;
|
|
std::string file_picker_dir_;
|
|
|
|
// Temporary state for Search & Replace flow
|
|
public:
|
|
void SetReplaceFindTmp(const std::string &s)
|
|
{
|
|
replace_find_tmp_ = s;
|
|
}
|
|
|
|
|
|
void SetReplaceWithTmp(const std::string &s)
|
|
{
|
|
replace_with_tmp_ = s;
|
|
}
|
|
|
|
|
|
[[nodiscard]] const std::string &ReplaceFindTmp() const
|
|
{
|
|
return replace_find_tmp_;
|
|
}
|
|
|
|
|
|
[[nodiscard]] const std::string &ReplaceWithTmp() const
|
|
{
|
|
return replace_with_tmp_;
|
|
}
|
|
|
|
private:
|
|
std::string replace_find_tmp_;
|
|
std::string replace_with_tmp_;
|
|
}; |