#include #include #include #include #include #include #include #include #include #include "Command.h" #include "syntax/HighlighterRegistry.h" #include "syntax/NullHighlighter.h" #include "Editor.h" #include "Buffer.h" #include "UndoSystem.h" #include "HelpText.h" #include "syntax/LanguageHighlighter.h" #include "syntax/HighlighterEngine.h" #include "syntax/CppHighlighter.h" #ifdef KTE_BUILD_GUI # include "GUITheme.h" # if !defined(KTE_USE_QT) # include "fonts/FontRegistry.h" # include "imgui.h" # endif # if defined(KTE_USE_QT) # include # include # endif #endif // Define cross-frontend theme change flags declared in GUITheme.h namespace kte { bool gThemeChangePending = false; std::string gThemeChangeRequest; // Qt font change globals bool gFontChangePending = false; std::string gFontFamilyRequest; float gFontSizeRequest = 0.0f; std::string gCurrentFontFamily; float gCurrentFontSize = 0.0f; // Request Qt visual font dialog bool gFontDialogRequested = false; } // Keep buffer viewport offsets so that the cursor stays within the visible // window based on the editor's current dimensions. The bottom row is reserved // for the status line. static std::size_t compute_render_x(std::string_view line, const std::size_t curx, const std::size_t tabw) { std::size_t rx = 0; for (std::size_t i = 0; i < curx && i < line.size(); ++i) { if (line[i] == '\t') { rx += tabw - (rx % tabw); } else { rx += 1; } } return rx; } static void ensure_cursor_visible(const Editor &ed, Buffer &buf) { const std::size_t cols = ed.Cols(); if (cols == 0) return; const std::size_t content_rows = ed.ContentRows(); const std::size_t cury = buf.Cury(); const std::size_t curx = buf.Curx(); std::size_t rowoffs = buf.Rowoffs(); std::size_t coloffs = buf.Coloffs(); // Vertical scrolling if (cury < rowoffs) { rowoffs = cury; } else if (content_rows > 0 && cury >= rowoffs + content_rows) { rowoffs = cury - content_rows + 1; } // Clamp vertical offset to available content. Use the materialized rows cache // because some legacy editing commands still modify Buffer::Rows() directly. // TerminalRenderer also renders from Buffer::Rows(), so keeping viewport math // consistent with that avoids desync where the cursor goes off-screen when // inserting newlines at EOF. const auto total_rows = buf.Rows().size(); if (content_rows < total_rows) { std::size_t max_rowoffs = total_rows - content_rows; if (rowoffs > max_rowoffs) rowoffs = max_rowoffs; } else { rowoffs = 0; } // Horizontal scrolling (use rendered columns with tabs expanded) std::size_t rx = 0; const auto total = buf.Nrows(); if (cury < total) { // Avoid materializing all rows and copying strings; get a zero-copy view rx = compute_render_x(buf.GetLineView(cury), curx, 8); } if (rx < coloffs) { coloffs = rx; } else if (rx >= coloffs + cols) { coloffs = rx - cols + 1; } buf.SetOffsets(rowoffs, coloffs); buf.SetRenderX(rx); } static bool cmd_center_on_cursor(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; std::size_t total = buf->Nrows(); std::size_t content = ctx.editor.ContentRows(); if (content == 0) content = 1; std::size_t cy = buf->Cury(); std::size_t half = content / 2; std::size_t new_rowoffs = (cy > half) ? (cy - half) : 0; // Clamp to valid range if (total > content) { std::size_t max_rowoffs = total - content; if (new_rowoffs > max_rowoffs) new_rowoffs = max_rowoffs; } else { new_rowoffs = 0; } buf->SetOffsets(new_rowoffs, buf->Coloffs()); return true; } static void ensure_at_least_one_line(Buffer &buf) { if (buf.Nrows() == 0) { buf.insert_row(0, ""); buf.SetDirty(true); } } // Determine if a command mutates the buffer contents (text edits) static bool is_mutating_command(CommandId id) { switch (id) { case CommandId::InsertText: case CommandId::Newline: case CommandId::Backspace: case CommandId::DeleteChar: case CommandId::KillToEOL: case CommandId::KillLine: case CommandId::Yank: case CommandId::DeleteWordPrev: case CommandId::DeleteWordNext: case CommandId::IndentRegion: case CommandId::UnindentRegion: case CommandId::ReflowParagraph: case CommandId::KillRegion: case CommandId::Undo: case CommandId::Redo: return true; default: return false; } } // --- UI/status helpers --- static bool cmd_uarg_status(CommandContext &ctx) { // ctx.arg should contain the digits/minus entered so far (may be empty) ctx.editor.SetStatus(std::string("C-u ") + ctx.arg); return true; } // Helper: compute ordered region between mark and cursor. Returns false if no mark set or zero-length. static bool compute_mark_region(Buffer &buf, std::size_t &sx, std::size_t &sy, std::size_t &ex, std::size_t &ey) { if (!buf.MarkSet()) return false; std::size_t cx = buf.Curx(); std::size_t cy = buf.Cury(); std::size_t mx = buf.MarkCurx(); std::size_t my = buf.MarkCury(); if (cy < my || (cy == my && cx < mx)) { sy = cy; sx = cx; ey = my; ex = mx; } else { sy = my; sx = mx; ey = cy; ex = cx; } if (sy == ey && sx == ex) return false; // empty region return true; } // Helper: extract text from [sx,sy) to [ex,ey) without modifying buffer. Newlines inserted between lines. static std::string extract_region_text(const Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::size_t ey) { const auto &rows = buf.Rows(); if (sy >= rows.size()) return std::string(); if (ey >= rows.size()) ey = rows.size() - 1; if (sy == ey) { const auto &line = rows[sy]; std::size_t xs = std::min(sx, line.size()); std::size_t xe = std::min(ex, line.size()); if (xe < xs) std::swap(xs, xe); return line.substr(xs, xe - xs); } std::string out; // first line tail { const auto &line = rows[sy]; std::size_t xs = std::min(sx, line.size()); out += line.substr(xs); out += '\n'; } // middle lines full for (std::size_t y = sy + 1; y < ey; ++y) { out += static_cast(rows[y]); out += '\n'; } // last line head { const auto &line = rows[ey]; std::size_t xe = std::min(ex, line.size()); out += line.substr(0, xe); } return out; } // Helper: delete region and leave cursor at start (sx,sy). Adjust lines appropriately. static void delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::size_t ey) { std::size_t nrows = buf.Nrows(); if (nrows == 0) return; if (sy >= nrows) return; if (ey >= nrows) ey = nrows - 1; if (sy == ey) { // Single line: delete text from xs to xe const auto &rows = buf.Rows(); const auto &line = rows[sy]; std::size_t xs = std::min(sx, line.size()); std::size_t xe = std::min(ex, line.size()); if (xe < xs) std::swap(xs, xe); buf.delete_text(static_cast(sy), static_cast(xs), xe - xs); } else { // Multi-line: delete from (sx,sy) to (ex,ey) // Strategy: // 1. Save suffix of last line (from ex to end) // 2. Delete tail of first line (from sx to end) // 3. Delete all lines from sy+1 to ey (inclusive) // 4. Insert saved suffix at end of first line // 5. Join if needed (no, suffix is appended directly) const auto &rows = buf.Rows(); std::size_t first_line_len = rows[sy].size(); std::size_t last_line_len = rows[ey].size(); std::size_t xs = std::min(sx, first_line_len); std::size_t xe = std::min(ex, last_line_len); // Save suffix of last line before any modifications std::string suffix = rows[ey].substr(xe); // Delete tail of first line (from xs to end) if (xs < first_line_len) { buf.delete_text(static_cast(sy), static_cast(xs), first_line_len - xs); } // Delete lines from ey down to sy+1 (reverse order to preserve indices) for (std::size_t i = ey; i > sy; --i) { buf.delete_row(static_cast(i)); } // Append saved suffix to first line if (!suffix.empty()) { // Get current length of line sy after deletions const auto &rows_after = buf.Rows(); std::size_t line_len = rows_after[sy].size(); buf.insert_text(static_cast(sy), static_cast(line_len), suffix); } } buf.SetCursor(sx, sy); buf.SetDirty(true); } // Insert arbitrary text at cursor, supporting newlines. Updates cursor position. static void insert_text_at_cursor(Buffer &buf, const std::string &text) { std::size_t nrows = buf.Nrows(); std::size_t y = buf.Cury(); std::size_t x = buf.Curx(); if (y > nrows) y = nrows; if (nrows == 0) { buf.insert_row(0, ""); nrows = 1; } if (y >= nrows) { buf.insert_row(static_cast(nrows), ""); nrows = buf.Nrows(); } std::size_t cur_y = y; std::size_t cur_x = x; std::string remain = text; while (true) { auto pos = remain.find('\n'); if (pos == std::string::npos) { // insert remaining into current line nrows = buf.Nrows(); if (cur_y >= nrows) { buf.insert_row(static_cast(nrows), ""); } const auto &rows = buf.Rows(); if (cur_x > rows[cur_y].size()) cur_x = rows[cur_y].size(); buf.insert_text(static_cast(cur_y), static_cast(cur_x), remain); cur_x += remain.size(); break; } // insert segment before newline std::string seg = remain.substr(0, pos); { const auto &rows = buf.Rows(); if (cur_x > rows[cur_y].size()) cur_x = rows[cur_y].size(); } buf.insert_text(static_cast(cur_y), static_cast(cur_x), seg); // split line at cur_x + seg.size() cur_x += seg.size(); buf.split_line(static_cast(cur_y), static_cast(cur_x)); // move to start of next line cur_y += 1; cur_x = 0; // advance remain after newline remain.erase(0, pos + 1); } buf.SetCursor(cur_x, cur_y); buf.SetDirty(true); } static std::size_t inverse_render_to_source_col(const std::string &line, std::size_t rx_target, std::size_t tabw) { // Find source column that best matches given rendered x (with tabs expanded) std::size_t rx = 0; if (rx_target == 0) return 0; std::size_t best_col = 0; std::size_t best_dist = rx_target; // max for (std::size_t i = 0; i <= line.size(); ++i) { std::size_t dist = (rx > rx_target) ? (rx - rx_target) : (rx_target - rx); if (dist <= best_dist) { best_dist = dist; best_col = i; } if (i == line.size()) break; if (line[i] == '\t') { rx += tabw - (rx % tabw); } else { rx += 1; } if (rx >= rx_target && i + 1 <= line.size()) { // next iteration will evaluate i+1 with updated rx } } if (best_col > line.size()) best_col = line.size(); return best_col; } // --- Direct cursor placement --- static bool cmd_move_cursor_to(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; ensure_at_least_one_line(*buf); // Accept either: // - "row:col" (buffer coordinates) // - "@row:col" (screen coordinates within viewport; translated using offsets) std::size_t row = buf->Cury(); std::size_t col = buf->Curx(); const std::string &a = ctx.arg; if (!a.empty()) { bool screen = false; std::size_t start = 0; if (!a.empty() && a[0] == '@') { screen = true; start = 1; } std::size_t p = a.find(':', start); if (p != std::string::npos) { try { long ay = std::stol(a.substr(start, p - start)); long ax = std::stol(a.substr(p + 1)); if (ay < 0) ay = 0; if (ax < 0) ax = 0; if (screen) { // Translate screen to buffer coordinates using offsets and tab expansion for x std::size_t vy = static_cast(ay); std::size_t vx = static_cast(ax); std::size_t bro = buf->Rowoffs(); std::size_t bco = buf->Coloffs(); std::size_t by = bro + vy; // Clamp by to existing lines later ensure_at_least_one_line(*buf); const auto &lines2 = buf->Rows(); if (by >= lines2.size()) by = lines2.size() - 1; std::string line2 = static_cast(lines2[by]); std::size_t rx_target = bco + vx; std::size_t sx = inverse_render_to_source_col(line2, rx_target, 8); row = by; col = sx; } else { row = static_cast(ay); col = static_cast(ax); } } catch (...) { // ignore parse errors } } } ensure_at_least_one_line(*buf); const auto &lines = buf->Rows(); if (row >= lines.size()) row = lines.size() - 1; std::string line = static_cast(lines[row]); if (col > line.size()) col = line.size(); buf->SetCursor(col, row); ensure_cursor_visible(ctx.editor, *buf); return true; } // --- Search helpers (UI-agnostic) --- static std::vector > search_compute_matches(const Buffer &buf, const std::string &q) { std::vector > out; if (q.empty()) return out; const auto &rows = buf.Rows(); for (std::size_t y = 0; y < rows.size(); ++y) { std::string line = static_cast(rows[y]); std::size_t pos = 0; while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) { out.emplace_back(y, pos); pos += q.size(); } } return out; } // Regex-based matches (per-line), capturing match length for highlighting struct RegexMatch { std::size_t y; std::size_t x; std::size_t len; }; static std::vector search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std::string &err_out) { std::vector out; err_out.clear(); if (pattern.empty()) return out; try { const std::regex rx(pattern); const auto &rows = buf.Rows(); for (std::size_t y = 0; y < rows.size(); ++y) { std::string line = static_cast(rows[y]); for (auto it = std::sregex_iterator(line.begin(), line.end(), rx); it != std::sregex_iterator(); ++it) { const auto &m = *it; out.push_back(RegexMatch{ y, static_cast(m.position()), static_cast(m.length()) }); } } } catch (const std::regex_error &e) { err_out = e.what(); // Return empty results on error } return out; } static void search_apply_match_regex(Editor &ed, Buffer &buf, const std::vector &matches) { const std::string &q = ed.SearchQuery(); if (matches.empty()) { ed.SetSearchMatch(0, 0, 0); // Restore cursor to origin if present if (ed.SearchOriginSet()) { buf.SetCursor(ed.SearchOrigX(), ed.SearchOrigY()); buf.SetOffsets(ed.SearchOrigRowoffs(), ed.SearchOrigColoffs()); } ed.SetSearchIndex(-1); ed.SetStatus("Regex: " + q); return; } int idx = ed.SearchIndex(); if (idx < 0 || idx >= static_cast(matches.size())) idx = 0; const auto &m = matches[static_cast(idx)]; ed.SetSearchMatch(m.y, m.x, m.len); buf.SetCursor(m.x, m.y); ensure_cursor_visible(ed, buf); char tmp[64]; snprintf(tmp, sizeof(tmp), "%d/%zu", idx + 1, matches.size()); ed.SetStatus(std::string("Regex: ") + q + " " + tmp); ed.SetSearchIndex(idx); } static void search_apply_match(Editor &ed, Buffer &buf, const std::vector > &matches) { const std::string &q = ed.SearchQuery(); if (matches.empty()) { ed.SetSearchMatch(0, 0, 0); // Restore cursor to origin if present if (ed.SearchOriginSet()) { buf.SetCursor(ed.SearchOrigX(), ed.SearchOrigY()); buf.SetOffsets(ed.SearchOrigRowoffs(), ed.SearchOrigColoffs()); } ed.SetSearchIndex(-1); ed.SetStatus("Find: " + q); return; } int idx = ed.SearchIndex(); if (idx < 0 || idx >= static_cast(matches.size())) idx = 0; auto [y, x] = matches[static_cast(idx)]; ed.SetSearchMatch(y, x, q.size()); buf.SetCursor(x, y); ensure_cursor_visible(ed, buf); char tmp[64]; snprintf(tmp, sizeof(tmp), "%d/%zu", idx + 1, matches.size()); ed.SetStatus(std::string("Find: ") + q + " " + tmp); ed.SetSearchIndex(idx); } // --- File/Session commands --- static bool cmd_save(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to save"); return false; } std::string err; // Allow saving directly to a filename if buffer was opened with a // non-existent path (not yet file-backed but has a filename). if (!buf->IsFileBacked()) { if (!buf->Filename().empty()) { // If first-time save to an existing path, confirm overwrite if (std::filesystem::exists(buf->Filename())) { ctx.editor.StartPrompt(Editor::PromptKind::Confirm, "Overwrite", ""); ctx.editor.SetPendingOverwritePath(buf->Filename()); ctx.editor.SetStatus( std::string("Overwrite existing file '") + buf->Filename() + "'? (y/N)"); return true; } else { if (!buf->SaveAs(buf->Filename(), err)) { ctx.editor.SetStatus(err); return false; } buf->SetDirty(false); ctx.editor.SetStatus("Saved " + buf->Filename()); return true; } } // If buffer has no name, prompt for a filename ctx.editor.StartPrompt(Editor::PromptKind::SaveAs, "Save as", ""); ctx.editor.SetStatus("Save as: "); return true; } if (!buf->Save(err)) { ctx.editor.SetStatus(err); return false; } buf->SetDirty(false); ctx.editor.SetStatus("Saved " + buf->Filename()); if (auto *u = buf->Undo()) u->mark_saved(); return true; } // --- Working directory commands --- static bool cmd_show_working_directory(CommandContext &ctx) { try { std::filesystem::path cwd = std::filesystem::current_path(); ctx.editor.SetStatus(std::string("cwd: ") + cwd.string()); return true; } catch (const std::exception &e) { ctx.editor.SetStatus(std::string("cwd: ") + e.what()); return false; } } static bool cmd_change_working_directory_start(CommandContext &ctx) { std::string initial; try { initial = std::filesystem::current_path().string() + "/"; } catch (...) { initial.clear(); } ctx.editor.StartPrompt(Editor::PromptKind::Chdir, "chdir", initial); ctx.editor.SetStatus(std::string("chdir: ") + ctx.editor.PromptText()); return true; } static bool cmd_save_as(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to save"); return false; } if (ctx.arg.empty()) { ctx.editor.SetStatus("save-as requires a filename"); return false; } std::string err; if (!buf->SaveAs(ctx.arg, err)) { ctx.editor.SetStatus(err); return false; } ctx.editor.SetStatus("Saved as " + ctx.arg); if (auto *u = buf->Undo()) u->mark_saved(); return true; } static bool cmd_quit(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); // If a confirmation is already pending, quit now without saving if (ctx.editor.QuitConfirmPending()) { ctx.editor.SetQuitConfirmPending(false); ctx.editor.SetQuitRequested(true); ctx.editor.SetStatus("Quit requested"); return true; } // If current buffer exists and is dirty, warn and arm confirmation if (buf && buf->Dirty()) { ctx.editor.SetStatus("Unsaved changes. C-k q to quit without saving"); ctx.editor.SetQuitConfirmPending(true); return true; } // Otherwise quit immediately ctx.editor.SetQuitRequested(true); ctx.editor.SetStatus("Quit requested"); return true; } static bool cmd_save_and_quit(CommandContext &ctx) { // Try save current buffer (if any), then mark quit requested. Buffer *buf = ctx.editor.CurrentBuffer(); if (buf && buf->Dirty()) { std::string err; if (buf->IsFileBacked()) { if (buf->Save(err)) { buf->SetDirty(false); } else { ctx.editor.SetStatus(err); return false; } } else if (!buf->Filename().empty()) { if (buf->SaveAs(buf->Filename(), err)) { buf->SetDirty(false); } else { ctx.editor.SetStatus(err); return false; } } else { ctx.editor.SetStatus("Buffer not file-backed; use save-as before quitting"); return false; } } ctx.editor.SetStatus("Save and quit requested"); ctx.editor.SetQuitRequested(true); return true; } static bool cmd_quit_now(CommandContext &ctx) { ctx.editor.SetQuitRequested(true); ctx.editor.SetStatus("Quit requested"); return true; } static bool cmd_refresh(CommandContext &ctx) { // If a generic prompt is active, cancel it if (ctx.editor.PromptActive()) { // If also in search mode, restore state if (ctx.editor.SearchActive()) { Buffer *buf = ctx.editor.CurrentBuffer(); if (buf && ctx.editor.SearchOriginSet()) { buf->SetCursor(ctx.editor.SearchOrigX(), ctx.editor.SearchOrigY()); buf->SetOffsets(ctx.editor.SearchOrigRowoffs(), ctx.editor.SearchOrigColoffs()); } ctx.editor.SetSearchActive(false); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetSearchIndex(-1); } // Clear any pending close/overwrite state associated with prompts ctx.editor.SetCloseConfirmPending(false); ctx.editor.SetCloseAfterSave(false); ctx.editor.ClearPendingOverwritePath(); ctx.editor.CancelPrompt(); ctx.editor.SetStatus("Canceled"); return true; } // If in search mode (legacy flag), treat refresh/ESC/C-g as cancel if (ctx.editor.SearchActive()) { Buffer *buf = ctx.editor.CurrentBuffer(); if (buf && ctx.editor.SearchOriginSet()) { buf->SetCursor(ctx.editor.SearchOrigX(), ctx.editor.SearchOrigY()); buf->SetOffsets(ctx.editor.SearchOrigRowoffs(), ctx.editor.SearchOrigColoffs()); } ctx.editor.SetSearchActive(false); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetSearchIndex(-1); ctx.editor.SetStatus("Find canceled"); return true; } // Otherwise just a hint; renderer will redraw ctx.editor.SetStatus(""); return true; } static bool cmd_kprefix(CommandContext &ctx) { // Close any pending edit batch before entering k-prefix if (Buffer *b = ctx.editor.CurrentBuffer()) { if (auto *u = b->Undo()) u->commit(); } // Show k-command mode hint in status ctx.editor.SetStatus("C-k _"); return true; } // Start generic command prompt (": ") static bool cmd_command_prompt_start(const CommandContext &ctx) { // Close any pending edit batch before entering prompt if (Buffer *b = ctx.editor.CurrentBuffer()) { if (auto *u = b->Undo()) u->commit(); } ctx.editor.StartPrompt(Editor::PromptKind::Command, "", ""); ctx.editor.SetStatus(": "); return true; } static bool cmd_unknown_kcommand(CommandContext &ctx) { char ch = '?'; if (!ctx.arg.empty()) { ch = ctx.arg[0]; } char buf[64]; std::snprintf(buf, sizeof(buf), "unknown k-command %c", ch); ctx.editor.SetStatus(buf); return true; } static bool cmd_unknown_esc_command(CommandContext &ctx) { (void) ctx; ctx.editor.SetStatus("invalid escape command"); return true; } // --- Syntax highlighting commands --- static void apply_filetype(Buffer &buf, const std::string &ft) { buf.EnsureHighlighter(); auto *eng = buf.Highlighter(); if (!eng) return; std::string val = ft; // trim + lower auto trim = [](const std::string &s) { std::string r = s; auto notsp = [](int ch) { return !std::isspace(ch); }; r.erase(r.begin(), std::find_if(r.begin(), r.end(), notsp)); r.erase(std::find_if(r.rbegin(), r.rend(), notsp).base(), r.end()); return r; }; val = trim(val); for (auto &ch: val) ch = static_cast(std::tolower(static_cast(ch))); if (val == "off") { eng->SetHighlighter(nullptr); buf.SetFiletype(""); buf.SetSyntaxEnabled(false); return; } if (val.empty()) { // Empty means unknown/unspecified -> use NullHighlighter but keep syntax enabled buf.SetFiletype(""); buf.SetSyntaxEnabled(true); eng->SetHighlighter(std::make_unique()); eng->InvalidateFrom(0); return; } // Normalize and create via registry std::string norm = kte::HighlighterRegistry::Normalize(val); auto hl = kte::HighlighterRegistry::CreateFor(norm); if (hl) { eng->SetHighlighter(std::move(hl)); buf.SetFiletype(norm); buf.SetSyntaxEnabled(true); eng->InvalidateFrom(0); } else { // Unknown -> install NullHighlighter and keep syntax enabled eng->SetHighlighter(std::make_unique()); buf.SetFiletype(val); // record what user asked even if unsupported buf.SetSyntaxEnabled(true); eng->InvalidateFrom(0); } } static bool cmd_syntax(CommandContext &ctx) { Buffer *b = ctx.editor.CurrentBuffer(); if (!b) { ctx.editor.SetStatus("No buffer"); return true; } std::string arg = ctx.arg; // trim auto trim = [](std::string &s) { auto notsp = [](int ch) { return !std::isspace(ch); }; s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp)); s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end()); }; trim(arg); if (arg == "on") { b->SetSyntaxEnabled(true); // If no highlighter but filetype is cpp by extension, set it if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) { apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype()); } ctx.editor.SetStatus("syntax: on"); } else if (arg == "off") { b->SetSyntaxEnabled(false); ctx.editor.SetStatus("syntax: off"); } else if (arg == "reload") { if (auto *eng = b->Highlighter()) eng->InvalidateFrom(0); ctx.editor.SetStatus("syntax: reloaded"); } else { ctx.editor.SetStatus("usage: :syntax on|off|reload"); } return true; } static bool cmd_set_option(CommandContext &ctx) { Buffer *b = ctx.editor.CurrentBuffer(); if (!b) { ctx.editor.SetStatus("No buffer"); return true; } // Expect key=value auto eq = ctx.arg.find('='); if (eq == std::string::npos) { ctx.editor.SetStatus("usage: :set key=value"); return true; } std::string key = ctx.arg.substr(0, eq); std::string val = ctx.arg.substr(eq + 1); // trim auto trim = [](std::string &s) { auto notsp = [](int ch) { return !std::isspace(ch); }; s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp)); s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end()); }; trim(key); trim(val); // lower-case value for filetype for (auto &ch: val) ch = static_cast(std::tolower(static_cast(ch))); if (key == "filetype") { apply_filetype(*b, val); if (b->SyntaxEnabled()) ctx.editor.SetStatus( std::string("filetype: ") + (b->Filetype().empty() ? "off" : b->Filetype())); else ctx.editor.SetStatus("filetype: off"); return true; } ctx.editor.SetStatus("unknown option: " + key); return true; } // GUI theme cycling commands (available in GUI build; ImGui-only for now) #if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) static bool cmd_theme_next(CommandContext &ctx) { auto id = kte::NextTheme(); ctx.editor.SetStatus(std::string("Theme: ") + kte::ThemeName(id)); return true; } static bool cmd_theme_prev(CommandContext &ctx) { auto id = kte::PrevTheme(); ctx.editor.SetStatus(std::string("Theme: ") + kte::ThemeName(id)); return true; } #else static bool cmd_theme_next(CommandContext &ctx) { ctx.editor.SetStatus("Theme switching only available in GUI build"); return true; } static bool cmd_theme_prev(CommandContext &ctx) { ctx.editor.SetStatus("Theme switching only available in GUI build"); return true; } #endif // Theme set by name command #if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) static bool cmd_theme_set_by_name(const CommandContext &ctx) { std::string name = ctx.arg; // trim spaces auto ltrim = [](std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); }; auto rtrim = [](std::string &s) { s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); }; ltrim(name); rtrim(name); if (name.empty()) { // Show current theme when no argument provided ctx.editor.SetStatus( std::string("Current theme: ") + kte::CurrentThemeName()); return true; } if (kte::ApplyThemeByName(name)) { ctx.editor.SetStatus( std::string("Theme: ") + name + std::string(" (bg: ") + kte::BackgroundModeName() + ")"); } else { // Build list of available themes const auto ® = kte::ThemeRegistry(); std::string avail; for (size_t i = 0; i < reg.size(); ++i) { if (i) avail += ", "; avail += reg[i]->Name(); } ctx.editor.SetStatus(std::string("Unknown theme; available: ") + avail); } return true; } #else static bool cmd_theme_set_by_name(CommandContext &ctx) { # if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT) // Qt GUI build: schedule theme change for frontend std::string name = ctx.arg; // trim spaces auto ltrim = [](std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); }; auto rtrim = [](std::string &s) { s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); }; ltrim(name); rtrim(name); if (name.empty()) { ctx.editor.SetStatus("theme: provide a name (e.g., nord, solarized-dark, gruvbox-light, eink)"); return true; } kte::gThemeChangeRequest = name; kte::gThemeChangePending = true; ctx.editor.SetStatus(std::string("Theme requested: ") + name); return true; # else (void) ctx; // No-op in terminal build return true; # endif } #endif // Font set by name (GUI) #if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) static bool cmd_font_set_by_name(const CommandContext &ctx) { using namespace kte::Fonts; std::string name = ctx.arg; // trim auto ltrim = [](std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); }; auto rtrim = [](std::string &s) { s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); }; ltrim(name); rtrim(name); std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return (char) std::tolower(c); }); if (name.empty()) { // Show current font when no argument provided auto ® = FontRegistry::Instance(); std::string current_font = reg.CurrentFontName(); if (current_font.empty()) current_font = "default"; ctx.editor.SetStatus(std::string("Current font: ") + current_font); return true; } auto ® = FontRegistry::Instance(); if (!reg.HasFont(name)) { ctx.editor.SetStatus("font: unknown name"); return true; } float size = reg.CurrentFontSize(); if (size <= 0.0f) { // Fallback to current ImGui font size if available size = ImGui::GetFontSize(); if (size <= 0.0f) size = 16.0f; } reg.RequestLoadFont(name, size); ctx.editor.SetStatus(std::string("Font: ") + name + " (" + std::to_string((int) std::round(size)) + ")"); return true; } #else static bool cmd_font_set_by_name(CommandContext &ctx) { // Qt build: queue font family change std::string name = ctx.arg; // trim auto ltrim = [](std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); }; auto rtrim = [](std::string &s) { s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); }; ltrim(name); rtrim(name); if (name.empty()) { // Show current font when no argument provided std::string cur = kte::gCurrentFontFamily.empty() ? std::string("default") : kte::gCurrentFontFamily; ctx.editor.SetStatus(std::string("Current font: ") + cur); return true; } kte::gFontFamilyRequest = name; // Keep size if not specified by user; signal change kte::gFontChangePending = true; ctx.editor.SetStatus(std::string("Font requested: ") + name); return true; } #endif // Font size set (GUI, ImGui-only for now) #if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) static bool cmd_font_set_size(const CommandContext &ctx) { using namespace kte::Fonts; std::string a = ctx.arg; auto ltrim = [](std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); }; auto rtrim = [](std::string &s) { s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); }; ltrim(a); rtrim(a); if (a.empty()) { // Show current font size when no argument provided auto ® = FontRegistry::Instance(); float current_size = reg.CurrentFontSize(); if (current_size <= 0.0f) { // Fallback to current ImGui font size if available current_size = ImGui::GetFontSize(); if (current_size <= 0.0f) current_size = 16.0f; } ctx.editor.SetStatus( std::string("Current font size: ") + std::to_string((int) std::round(current_size))); return true; } char *endp = nullptr; float size = strtof(a.c_str(), &endp); if (endp == a.c_str() || !std::isfinite(size)) { ctx.editor.SetStatus("font-size: expected number"); return true; } // Clamp to a reasonable range if (size < 6.0f) size = 6.0f; if (size > 96.0f) size = 96.0f; auto ® = FontRegistry::Instance(); std::string name = reg.CurrentFontName(); if (name.empty()) name = "default"; if (!reg.HasFont(name)) name = "default"; reg.RequestLoadFont(name, size); ctx.editor.SetStatus(std::string("Font size: ") + std::to_string((int) std::round(size))); return true; } #else static bool cmd_font_set_size(CommandContext &ctx) { // Qt build: parse size and queue change std::string a = ctx.arg; auto ltrim = [](std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); }; auto rtrim = [](std::string &s) { s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); }; ltrim(a); rtrim(a); if (a.empty()) { float cur = (kte::gCurrentFontSize > 0.0f) ? kte::gCurrentFontSize : 18.0f; ctx.editor.SetStatus(std::string("Current font size: ") + std::to_string((int) std::round(cur))); return true; } char *endp = nullptr; float size = strtof(a.c_str(), &endp); if (endp == a.c_str() || !std::isfinite(size)) { ctx.editor.SetStatus("font-size: expected number"); return true; } if (size < 6.0f) size = 6.0f; if (size > 96.0f) size = 96.0f; kte::gFontSizeRequest = size; kte::gFontChangePending = true; ctx.editor.SetStatus(std::string("Font size requested: ") + std::to_string((int) std::round(size))); return true; } #endif // Background set command (GUI, ImGui-only for now) #if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) static bool cmd_background_set(const CommandContext &ctx) { std::string mode = ctx.arg; // trim auto ltrim = [](std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); }; auto rtrim = [](std::string &s) { s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); }; ltrim(mode); rtrim(mode); std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return (char) std::tolower(c); }); if (mode != "light" && mode != "dark") { ctx.editor.SetStatus("background: expected 'light' or 'dark'"); return true; } kte::SetBackgroundMode(mode == "light" ? kte::BackgroundMode::Light : kte::BackgroundMode::Dark); // Re-apply current theme to reflect background change kte::ApplyThemeByName(kte::CurrentThemeName()); ctx.editor.SetStatus(std::string("Background: ") + mode + std::string("; Theme: ") + kte::CurrentThemeName()); return true; } #else static bool cmd_background_set(CommandContext &ctx) { (void) ctx; return true; } #endif static bool cmd_find_start(const CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to search"); return false; } // Save original cursor/viewport to restore on cancel ctx.editor.SetSearchOrigin(buf->Curx(), buf->Cury(), buf->Rowoffs(), buf->Coloffs()); // Enter search mode; start a generic prompt for search ctx.editor.SetSearchActive(true); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.SetSearchIndex(-1); ctx.editor.StartPrompt(Editor::PromptKind::Search, "Find", ""); ctx.editor.SetStatus("Find: "); return true; } static bool cmd_regex_find_start(const CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to search"); return false; } // Save origin for cancel ctx.editor.SetSearchOrigin(buf->Curx(), buf->Cury(), buf->Rowoffs(), buf->Coloffs()); // Enter regex search mode using the generic prompt system ctx.editor.SetSearchActive(true); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.SetSearchIndex(-1); ctx.editor.StartPrompt(Editor::PromptKind::RegexSearch, "Regex", ""); ctx.editor.SetStatus("Regex: "); return true; } static bool cmd_search_replace_start(const CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to search"); return false; } // Save original cursor/viewport to restore on cancel ctx.editor.SetSearchOrigin(buf->Curx(), buf->Cury(), buf->Rowoffs(), buf->Coloffs()); // Enter search-highlighting mode for the find step ctx.editor.SetSearchActive(true); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.SetSearchIndex(-1); // Two-step prompt: first collect find string, then replacement ctx.editor.SetReplaceFindTmp(""); ctx.editor.SetReplaceWithTmp(""); ctx.editor.StartPrompt(Editor::PromptKind::ReplaceFind, "Replace: find", ""); ctx.editor.SetStatus("Replace: find: "); return true; } static bool cmd_regex_replace_start(const CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to search"); return false; } // Save original cursor/viewport to restore on cancel ctx.editor.SetSearchOrigin(buf->Curx(), buf->Cury(), buf->Rowoffs(), buf->Coloffs()); // Enter search-highlighting mode for the find step (regex) ctx.editor.SetSearchActive(true); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.SetSearchIndex(-1); // Two-step prompt: first collect regex find pattern, then replacement ctx.editor.SetReplaceFindTmp(""); ctx.editor.SetReplaceWithTmp(""); ctx.editor.StartPrompt(Editor::PromptKind::RegexReplaceFind, "Regex replace: find", ""); ctx.editor.SetStatus("Regex replace: find: "); return true; } static bool cmd_open_file_start(const CommandContext &ctx) { // Start a generic prompt to read a path ctx.editor.StartPrompt(Editor::PromptKind::OpenFile, "Open", ""); ctx.editor.SetStatus("Open: "); return true; } // GUI: toggle visual file picker (no-op in terminal; renderer will consume flag) static bool cmd_visual_file_picker_toggle(const CommandContext &ctx) { // Toggle visibility bool show = !ctx.editor.FilePickerVisible(); ctx.editor.SetFilePickerVisible(show); if (show) { // Initialize directory to current working directory if empty if (ctx.editor.FilePickerDir().empty()) { try { ctx.editor.SetFilePickerDir(std::filesystem::current_path().string()); } catch (...) { ctx.editor.SetFilePickerDir("."); } } ctx.editor.SetStatus("Open File (visual)"); } else { ctx.editor.SetStatus("Closed file picker"); } return true; } // GUI: request visual font picker (Qt frontend will consume flag) static bool cmd_visual_font_picker_toggle(const CommandContext &ctx) { #ifdef KTE_BUILD_GUI kte::gFontDialogRequested = true; ctx.editor.SetStatus("Font chooser"); #else ctx.editor.SetStatus("Font chooser not available in terminal"); #endif return true; } static bool cmd_jump_to_line_start(const CommandContext &ctx) { // Start a prompt to read a 1-based line number and jump there (clamped) ctx.editor.StartPrompt(Editor::PromptKind::GotoLine, "Goto", ""); ctx.editor.SetStatus("Goto line: "); return true; } // --- Buffers: switch/next/prev/close --- static bool cmd_buffer_switch_start(const CommandContext &ctx) { // If only one (or zero) buffer is open, do nothing per spec if (ctx.editor.BufferCount() <= 1) { ctx.editor.SetStatus("No other buffers open."); return true; } ctx.editor.StartPrompt(Editor::PromptKind::BufferSwitch, "Buffer", ""); ctx.editor.SetStatus("Buffer: "); return true; } static std::string buffer_display_name(const Buffer &b) { if (!b.Filename().empty()) return b.Filename(); return {""}; } static std::string buffer_basename(const Buffer &b) { const std::string &p = b.Filename(); if (p.empty()) return {""}; auto pos = p.find_last_of("/\\"); if (pos == std::string::npos) return p; return p.substr(pos + 1); } static bool cmd_buffer_next(const CommandContext &ctx) { const auto cnt = ctx.editor.BufferCount(); if (cnt <= 1) { ctx.editor.SetStatus("No other buffers open."); return true; } std::size_t idx = ctx.editor.CurrentBufferIndex(); idx = (idx + 1) % cnt; ctx.editor.SwitchTo(idx); const Buffer *b = ctx.editor.CurrentBuffer(); ctx.editor.SetStatus(std::string("Switched: ") + (b ? buffer_display_name(*b) : std::string(""))); return true; } static bool cmd_buffer_prev(const CommandContext &ctx) { const auto cnt = ctx.editor.BufferCount(); if (cnt <= 1) { ctx.editor.SetStatus("No other buffers open."); return true; } std::size_t idx = ctx.editor.CurrentBufferIndex(); idx = (idx + cnt - 1) % cnt; ctx.editor.SwitchTo(idx); const Buffer *b = ctx.editor.CurrentBuffer(); ctx.editor.SetStatus(std::string("Switched: ") + (b ? buffer_display_name(*b) : std::string(""))); return true; } static bool cmd_buffer_close(const CommandContext &ctx) { if (ctx.editor.BufferCount() == 0) return true; std::size_t idx = ctx.editor.CurrentBufferIndex(); Buffer *b = ctx.editor.CurrentBuffer(); std::string name = b ? buffer_display_name(*b) : std::string(""); // If buffer is dirty, prompt to save first (for both named and unnamed buffers) if (b && b->Dirty()) { ctx.editor.StartPrompt(Editor::PromptKind::Confirm, "Save", ""); ctx.editor.SetCloseConfirmPending(true); ctx.editor.SetStatus(std::string("Save changes to ") + name + "? (y/N)"); return true; } // Otherwise close immediately if (b && b->Undo()) b->Undo()->discard_pending(); ctx.editor.CloseBuffer(idx); if (ctx.editor.BufferCount() == 0) { // Open a fresh empty buffer Buffer empty; ctx.editor.AddBuffer(std::move(empty)); ctx.editor.SwitchTo(0); } const Buffer *cur = ctx.editor.CurrentBuffer(); ctx.editor.SetStatus(std::string("Closed: ") + name + std::string(" Now: ") + (cur ? buffer_display_name(*cur) : std::string(""))); return true; } // Create a new empty, unnamed buffer and switch to it static bool cmd_buffer_new(const CommandContext &ctx) { // Create an empty buffer and add it to the editor Buffer empty; std::size_t idx = ctx.editor.AddBuffer(std::move(empty)); ctx.editor.SwitchTo(idx); ctx.editor.SetStatus("New buffer"); return true; } // --- Editing --- static bool cmd_insert_text(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to edit"); return false; } // If a prompt is active, edit prompt text if (ctx.editor.PromptActive()) { // Special-case: Tab-completion for prompts if (ctx.arg == "\t") { auto kind = ctx.editor.CurrentPromptKind(); // Buffer switch prompt supports Tab-completion on buffer names if (kind == Editor::PromptKind::BufferSwitch) { // Complete against buffer names (path and basename) const std::string prefix = ctx.editor.PromptText(); std::vector > cands; // name, index const auto &bs = ctx.editor.Buffers(); for (std::size_t i = 0; i < bs.size(); ++i) { std::string full = buffer_display_name(bs[i]); std::string base = buffer_basename(bs[i]); if (full.rfind(prefix, 0) == 0) { cands.emplace_back(full, i); } if (base.rfind(prefix, 0) == 0 && base != full) { cands.emplace_back(base, i); } } if (cands.empty()) { // no change } else if (cands.size() == 1) { ctx.editor.SetPromptText(cands[0].first); } else { // extend to longest common prefix std::string lcp = cands[0].first; for (std::size_t i = 1; i < cands.size(); ++i) { const std::string &s = cands[i].first; std::size_t j = 0; while (j < lcp.size() && j < s.size() && lcp[j] == s[j]) ++j; lcp.resize(j); if (lcp.empty()) break; } if (!lcp.empty() && lcp != ctx.editor.PromptText()) ctx.editor.SetPromptText(lcp); } ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText()); return true; } // File path completion for OpenFile/SaveAs/Chdir if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) { auto expand_user_path = [](const std::string &in) -> std::string { if (!in.empty() && in[0] == '~') { const char *home = std::getenv("HOME"); if (home && in.size() == 1) return std::string(home); if (home && (in.size() > 1) && (in[1] == '/' || in[1] == '\\')) { std::string rest = in.substr(1); // keep leading slash return std::string(home) + rest; } } return in; }; std::string text = ctx.editor.PromptText(); // Build a path and split dir + base prefix std::string expanded = expand_user_path(text); std::filesystem::path p(expanded); std::filesystem::path dir; std::string base; if (expanded.empty()) { dir = std::filesystem::current_path(); base.clear(); } else if (std::filesystem::is_directory(p)) { dir = p; base.clear(); } else { dir = p.parent_path(); base = p.filename().string(); if (dir.empty()) dir = std::filesystem::current_path(); } std::error_code ec; std::vector entries; std::filesystem::directory_iterator it(dir, ec), end; for (; !ec && it != end; it.increment(ec)) { entries.push_back(*it); } // Filter by base prefix std::vector cands; for (const auto &de: entries) { std::string name = de.path().filename().string(); if (base.empty() || name.rfind(base, 0) == 0) { std::string candidate = (dir / name).string(); // For dirs, add trailing slash hint if (de.is_directory(ec)) candidate += "/"; cands.push_back(candidate); } } // If no candidates, keep as-is if (cands.empty()) { // no-op } else if (cands.size() == 1) { ctx.editor.SetPromptText(cands[0]); } else { // Longest common prefix of display strings auto lcp = cands[0]; for (size_t i = 1; i < cands.size(); ++i) { const auto &s = cands[i]; size_t j = 0; while (j < lcp.size() && j < s.size() && lcp[j] == s[j]) ++j; lcp.resize(j); if (lcp.empty()) break; } if (!lcp.empty() && lcp != ctx.editor.PromptText()) { ctx.editor.SetPromptText(lcp); } else { // Show some choices in status (trim to avoid spam) std::string msg = ctx.editor.PromptLabel() + ": "; size_t shown = 0; for (const auto &s: cands) { if (shown >= 10) { msg += " …"; break; } if (shown > 0) msg += ' '; msg += std::filesystem::path(s).filename().string(); ++shown; } ctx.editor.SetStatus(msg); return true; } } ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText()); return true; } // Generic command prompt completion if (kind == Editor::PromptKind::Command) { std::string text = ctx.editor.PromptText(); // Split into command and arg prefix auto sp = text.find(' '); if (sp == std::string::npos) { // complete command name from public commands std::string prefix = text; std::vector names; for (const auto &c: CommandRegistry::All()) { if (c.isPublic) { if (prefix.empty() || c.name.rfind(prefix, 0) == 0) names.push_back(c.name); } } if (names.empty()) { // no change } else if (names.size() == 1) { ctx.editor.SetPromptText(names[0]); } else { // compute LCP std::string lcp = names[0]; for (size_t i = 1; i < names.size(); ++i) { const std::string &s = names[i]; size_t j = 0; while (j < lcp.size() && j < s.size() && lcp[j] == s[j]) ++j; lcp.resize(j); if (lcp.empty()) break; } if (!lcp.empty() && lcp != text) ctx.editor.SetPromptText(lcp); } ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText()); return true; } else { std::string cmd = text.substr(0, sp); std::string argprefix = text.substr(sp + 1); // Only special-case argument completion for certain commands if (cmd == "theme") { #if defined(KTE_BUILD_GUI) # if !defined(KTE_USE_QT) std::vector cands; const auto ® = kte::ThemeRegistry(); for (const auto &t: reg) { std::string n = t->Name(); if (argprefix.empty() || n.rfind(argprefix, 0) == 0) cands.push_back(n); } # else // Qt: offer known theme names handled by ApplyQtThemeByName static const char *qt_themes[] = { "nord", "solarized-dark", "solarized-light", "gruvbox-dark", "gruvbox-light", "eink" }; std::vector cands; for (const char *t: qt_themes) { std::string n(t); if (argprefix.empty() || n.rfind(argprefix, 0) == 0) cands.push_back(n); } # endif if (cands.empty()) { // no change } else if (cands.size() == 1) { ctx.editor.SetPromptText(cmd + std::string(" ") + cands[0]); } else { std::string lcp = cands[0]; for (size_t i = 1; i < cands.size(); ++i) { const std::string &s = cands[i]; size_t j = 0; while (j < lcp.size() && j < s.size() && lcp[j] == s[j]) ++j; lcp.resize(j); if (lcp.empty()) break; } if (!lcp.empty() && lcp != argprefix) ctx.editor.SetPromptText(cmd + std::string(" ") + lcp); } ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText()); return true; #else (void) argprefix; // no completion in non-GUI build #endif } if (cmd == "font") { #if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT) // Complete against installed font families (case-insensitive prefix) std::vector cands; QStringList fams = QFontDatabase::families(); std::string apfx_lower = argprefix; std::transform(apfx_lower.begin(), apfx_lower.end(), apfx_lower.begin(), [](unsigned char c) { return (char) std::tolower(c); }); for (const auto &fam: fams) { std::string n = fam.toStdString(); std::string nlower = n; std::transform(nlower.begin(), nlower.end(), nlower.begin(), [](unsigned char c) { return (char) std::tolower(c); }); if (apfx_lower.empty() || nlower.rfind(apfx_lower, 0) == 0) cands.push_back(n); } if (cands.empty()) { // no change } else if (cands.size() == 1) { ctx.editor.SetPromptText(cmd + std::string(" ") + cands[0]); } else { std::string lcp = cands[0]; for (size_t i = 1; i < cands.size(); ++i) { const std::string &s = cands[i]; size_t j = 0; while (j < lcp.size() && j < s.size() && lcp[j] == s[j]) ++j; lcp.resize(j); if (lcp.empty()) break; } if (!lcp.empty() && lcp != argprefix) ctx.editor.SetPromptText(cmd + std::string(" ") + lcp); } ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText()); return true; #else (void) argprefix; #endif } // default: no special arg completion ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText()); return true; } } } ctx.editor.AppendPromptText(ctx.arg); // If it's a search prompt, mirror text to search state if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search || ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind || ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) { ctx.editor.SetSearchQuery(ctx.editor.PromptText()); if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) { std::string err; auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err); if (!err.empty()) { ctx.editor.SetStatus( std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]"); } if (ctx.editor.SearchIndex() >= static_cast(rmatches.size())) ctx.editor.SetSearchIndex(rmatches.empty() ? -1 : 0); search_apply_match_regex(ctx.editor, *buf, rmatches); } else { auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); // Keep index stable unless out of range if (ctx.editor.SearchIndex() >= static_cast(matches.size())) ctx.editor.SetSearchIndex(matches.empty() ? -1 : 0); search_apply_match(ctx.editor, *buf, matches); } } else { // For other prompts, just echo label:text in status ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText()); } return true; } // If in search mode, treat printable input as query update if (ctx.editor.SearchActive()) { std::string q = ctx.editor.SearchQuery(); q += ctx.arg; // arg already printable text ctx.editor.SetSearchQuery(q); // Recompute matches and move cursor to current index const auto &rows = buf->Rows(); std::vector > matches; if (!q.empty()) { for (std::size_t y = 0; y < rows.size(); ++y) { std::string line = static_cast(rows[y]); std::size_t pos = 0; while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) { matches.emplace_back(y, pos); pos += q.size(); } } } if (matches.empty()) { ctx.editor.SetSearchMatch(0, 0, 0); // Restore to origin if available if (ctx.editor.SearchOriginSet()) { buf->SetCursor(ctx.editor.SearchOrigX(), ctx.editor.SearchOrigY()); buf->SetOffsets(ctx.editor.SearchOrigRowoffs(), ctx.editor.SearchOrigColoffs()); } ctx.editor.SetSearchIndex(-1); ctx.editor.SetStatus("Find: " + q); } else { int idx = ctx.editor.SearchIndex(); if (idx < 0 || idx >= static_cast(matches.size())) idx = 0; auto [y, x] = matches[static_cast(idx)]; ctx.editor.SetSearchMatch(y, x, q.size()); buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); char tmp[64]; snprintf(tmp, sizeof(tmp), "%d/%zu", idx + 1, matches.size()); ctx.editor.SetStatus(std::string("Find: ") + q + " " + tmp); ctx.editor.SetSearchIndex(idx); } return true; } // Disallow newlines in InsertText; they should come via Newline if (ctx.arg.find('\n') != std::string::npos || ctx.arg.find('\r') != std::string::npos) { ctx.editor.SetStatus("InsertText arg must not contain newlines"); return false; } ensure_at_least_one_line(*buf); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); std::size_t ins_y = y; std::size_t ins_x = x; // remember insertion start for undo positioning int repeat = ctx.count > 0 ? ctx.count : 1; // Apply edits to the underlying PieceTable through Buffer::insert_text, // not directly to the legacy rows_ cache. This ensures Save() persists text. for (int i = 0; i < repeat; ++i) { buf->insert_text(static_cast(y), static_cast(x), std::string_view(ctx.arg)); x += ctx.arg.size(); } buf->SetDirty(true); // Record undo for this contiguous insert at the original insertion point if (auto *u = buf->Undo()) { // Position cursor at insertion start for the undo record buf->SetCursor(ins_x, ins_y); u->Begin(UndoType::Insert); for (int i = 0; i < repeat; ++i) { u->Append(std::string_view(ctx.arg)); } // Finalize this contiguous insert as a single undoable action u->commit(); } buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); return true; } // Toggle read-only state of the current buffer static bool cmd_toggle_read_only(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer"); return false; } buf->ToggleReadOnly(); ctx.editor.SetStatus(std::string("Read-only: ") + (buf->IsReadOnly() ? "ON" : "OFF")); return true; } // Open or refresh the +HELP+ buffer with content from docs/kte.1 static bool cmd_show_help(CommandContext &ctx) { const std::string help_name = "+HELP+"; // Try to locate existing +HELP+ buffer std::vector &bufs = ctx.editor.Buffers(); std::size_t help_index = static_cast(-1); for (std::size_t i = 0; i < bufs.size(); ++i) { if (bufs[i].Filename() == help_name && !bufs[i].IsFileBacked()) { help_index = i; break; } } auto roff_to_text = [](const std::string &in) -> std::string { std::istringstream iss(in); std::ostringstream out; std::string line; auto unquote = [](std::string s) { if (!s.empty() && (s.front() == '"' || s.front() == '\'')) s.erase(s.begin()); if (!s.empty() && (s.back() == '"' || s.back() == '\'')) s.pop_back(); return s; }; while (std::getline(iss, line)) { if (line.rfind("'", 0) == 0) { continue; // comment line } if (line.rfind(".", 0) == 0) { // Macro line std::istringstream ls(line); std::string dot, macro; ls >> dot >> macro; if (macro == "TH" || macro == "SH") { std::string title; std::getline(ls, title); // trim leading spaces while (!title.empty() && (title.front() == ' ' || title.front() == '\t')) title.erase(title.begin()); title = unquote(title); out << "\n\n"; for (auto &c: title) c = static_cast(std::toupper(static_cast(c))); out << title << "\n"; } else if (macro == "PP" || macro == "P" || macro == "TP") { out << "\n"; } else if (macro == "B" || macro == "I" || macro == "BR" || macro == "IR") { std::string rest; std::getline(ls, rest); while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t')) rest.erase(rest.begin()); out << unquote(rest) << "\n"; } else if (macro == "nf" || macro == "fi") { // ignore fill mode toggles for now } else { // Unhandled macro: ignore } continue; } // Regular text; apply minimal escape replacements for (std::size_t i = 0; i < line.size(); ++i) { if (line[i] == '\\') { if (i + 1 < line.size() && line[i + 1] == '-') { out << '-'; ++i; continue; } if (i + 3 < line.size() && line[i + 1] == '(') { std::string esc = line.substr(i + 2, 2); if (esc == "em") { out << "—"; i += 3; continue; } if (esc == "en") { out << "-"; i += 3; continue; } } } out << line[i]; } out << "\n"; } return out.str(); }; auto load_help_text = [&](bool &used_man) -> std::string { // 1) Prefer embedded/customizable help content { std::string embedded = HelpText::Text(); if (!embedded.empty()) { used_man = false; return embedded; } } // 2) Fall back to the manpage and convert roff to plain text const char *man_candidates[] = { "docs/kte.1", "./docs/kte.1", "/usr/local/share/man/man1/kte.1", "/usr/share/man/man1/kte.1" }; for (const char *p: man_candidates) { std::ifstream in(p); if (in.good()) { std::string s((std::istreambuf_iterator(in)), std::istreambuf_iterator()); if (!s.empty()) { used_man = true; return roff_to_text(s); } } } // Fallback minimal help text used_man = false; return std::string( "KTE - Kyle's Text Editor\n\n" "About:\n" " kte is Kyle's Text Editor and is probably ill-suited to everyone else. It was\n" " inspired by Antirez' kilo text editor by way of someone's writeup of the\n" " process of writing a text editor from scratch. It has keybindings inspired by\n" " VDE (and the Wordstar family) and emacs; its spiritual parent is mg(1).\n\n" "Core keybindings:\n" " C-k h Show this help\n" " C-k s Save buffer\n" " C-k x Save and quit\n" " C-k q Quit (confirm if dirty)\n" " C-k C-q Quit now (no confirm)\n" " C-k c Close current buffer\n" " C-k b Switch buffer\n" " C-k p Next buffer\n" " C-k n Previous buffer\n" " C-k e Open file (prompt)\n" " C-k g Jump to line\n" " C-k u Undo\n" " C-k r Redo\n" " C-k d Kill to end of line\n" " C-k C-d Kill entire line\n" " C-k = Indent region\n" " C-k - Unindent region\n" " C-k ' Toggle read-only\n" " C-k l Reload buffer from disk\n" " C-k a Mark all and jump to end\n" " C-k v Toggle visual file picker (GUI)\n" " C-k w Show working directory\n" " C-k o Change working directory (prompt)\n\n" "ESC/Alt commands:\n" " ESC q Reflow paragraph\n" " ESC BACKSPACE Delete previous word\n" " ESC d Delete next word\n" " Alt-w Copy region to kill ring\n\n" "Buffers:\n +HELP+ is read-only. Press C-k ' to toggle if you need to edit; C-k h restores it.\n"); }; auto populate_from_text = [](Buffer &b, const std::string &text) { // Clear existing rows while (b.Nrows() > 0) { b.delete_row(0); } // Parse text and insert rows std::string line; line.reserve(128); int row_idx = 0; for (char ch: text) { if (ch == '\n') { b.insert_row(row_idx++, line); line.clear(); } else if (ch != '\r') { line.push_back(ch); } } // Add last line (even if empty) b.insert_row(row_idx, line); b.SetDirty(false); b.SetCursor(0, 0); b.SetOffsets(0, 0); b.SetRenderX(0); }; if (help_index != static_cast(-1)) { Buffer &hb = bufs[help_index]; // If dirty, overwrite with original contents if (hb.Dirty()) { bool used_man = false; std::string text = load_help_text(used_man); populate_from_text(hb, text); } hb.SetReadOnly(true); ctx.editor.SwitchTo(help_index); ctx.editor.SetStatus("Help opened"); return true; } // Create a new help buffer Buffer help; help.SetVirtualName(help_name); bool used_man = false; std::string text = load_help_text(used_man); populate_from_text(help, text); help.SetReadOnly(true); std::size_t idx = ctx.editor.AddBuffer(std::move(help)); ctx.editor.SwitchTo(idx); ctx.editor.SetStatus("Help opened"); return true; } static bool cmd_newline(CommandContext &ctx) { // If a prompt is active, accept it and perform the associated action if (ctx.editor.PromptActive()) { Editor::PromptKind kind = ctx.editor.CurrentPromptKind(); std::string value = ctx.editor.PromptText(); ctx.editor.AcceptPrompt(); if (kind == Editor::PromptKind::Command) { // Parse COMMAND ARG and dispatch only public commands // Trim leading/trailing spaces auto ltrim = [](std::string &s) { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); }; auto rtrim = [](std::string &s) { s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); }; ltrim(value); rtrim(value); if (value.empty()) { ctx.editor.SetStatus("Canceled"); return true; } // Split first token std::string cmdname; std::string arg; auto sp = value.find(' '); if (sp == std::string::npos) { cmdname = value; } else { cmdname = value.substr(0, sp); arg = value.substr(sp + 1); } const Command *cmd = CommandRegistry::FindByName(cmdname); if (!cmd || !cmd->isPublic) { ctx.editor.SetStatus(std::string("Unknown command: ") + cmdname); return true; } bool ok = Execute(ctx.editor, cmdname, arg); if (!ok) { ctx.editor.SetStatus(std::string("Command failed: ") + cmdname); } return true; } if (kind == Editor::PromptKind::Search || kind == Editor::PromptKind::RegexSearch) { // Finish search: keep cursor where it is, clear search UI prompt ctx.editor.SetSearchActive(false); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetStatus(kind == Editor::PromptKind::RegexSearch ? "Regex find done" : "Find done"); Buffer *b = ctx.editor.CurrentBuffer(); if (b) ensure_cursor_visible(ctx.editor, *b); } else if (kind == Editor::PromptKind::ReplaceFind) { // Proceed to replacement text prompt ctx.editor.SetReplaceFindTmp(value); // Keep search highlights active using the collected find string ctx.editor.SetSearchActive(true); ctx.editor.SetSearchQuery(value); if (Buffer *b = ctx.editor.CurrentBuffer()) { auto matches = search_compute_matches(*b, ctx.editor.SearchQuery()); search_apply_match(ctx.editor, *b, matches); } ctx.editor.StartPrompt(Editor::PromptKind::ReplaceWith, "Replace: with", ""); ctx.editor.SetStatus("Replace: with: "); return true; } else if (kind == Editor::PromptKind::ReplaceWith) { // Execute replace-all Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (buf->IsReadOnly()) { ctx.editor.SetStatus("Read-only buffer"); // Clear search UI state ctx.editor.SetSearchActive(false); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetSearchIndex(-1); return true; } const std::string find = ctx.editor.ReplaceFindTmp(); const std::string with = value; ctx.editor.SetReplaceWithTmp(with); if (find.empty()) { ctx.editor.SetStatus("Replace canceled (empty find)"); // Clear search UI state ctx.editor.SetSearchActive(false); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetSearchIndex(-1); return true; } // Save original cursor to restore after operations std::size_t orig_x = buf->Curx(); std::size_t orig_y = buf->Cury(); std::size_t total = 0; UndoSystem *u = buf->Undo(); if (u) u->commit(); // end any pending batch for (std::size_t y = 0; y < buf->Rows().size(); ++y) { std::size_t pos = 0; while (true) { const auto &rows_view = buf->Rows(); if (y >= rows_view.size()) break; std::string line = static_cast(rows_view[y]); if (find.empty()) break; std::size_t p = line.find(find, pos); if (p == std::string::npos) break; // Delete matched segment from the piece table buf->delete_text(static_cast(y), static_cast(p), find.size()); if (u) { buf->SetCursor(p, y); u->Begin(UndoType::Delete); u->Append(std::string_view(find)); } // Insert replacement if provided if (!with.empty()) { buf->insert_text(static_cast(y), static_cast(p), std::string_view(with)); if (u) { buf->SetCursor(p, y); u->Begin(UndoType::Insert); u->Append(std::string_view(with)); } pos = p + with.size(); } else { // When replacing with empty, continue after the deletion point to avoid re-matching pos = p; if (pos < static_cast(buf->Rows()[y].size())) ++pos; else break; } ++total; } } buf->SetDirty(true); // Restore original cursor if (orig_y < buf->Rows().size()) buf->SetCursor(orig_x, orig_y); ensure_cursor_visible(ctx.editor, *buf); char msg[128]; std::snprintf(msg, sizeof(msg), "Replaced %zu occurrence%s", total, (total == 1 ? "" : "s")); ctx.editor.SetStatus(msg); // Clear search-highlighting state after replace completes ctx.editor.SetSearchActive(false); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetSearchIndex(-1); return true; } else if (kind == Editor::PromptKind::OpenFile) { std::string err; // Expand "~" to the user's home directory auto expand_user_path = [](const std::string &in) -> std::string { if (!in.empty() && in[0] == '~') { const char *home = std::getenv("HOME"); if (home && in.size() == 1) return std::string(home); if (home && (in.size() > 1) && (in[1] == '/' || in[1] == '\\')) { std::string rest = in.substr(1); return std::string(home) + rest; } } return in; }; value = expand_user_path(value); if (value.empty()) { ctx.editor.SetStatus("Open canceled (empty)"); } else if (!ctx.editor.OpenFile(value, err)) { ctx.editor.SetStatus(err.empty() ? std::string("Failed to open ") + value : err); } else { ctx.editor.SetStatus(std::string("Opened ") + value); // Close the prompt so subsequent typing edits the buffer, not the prompt ctx.editor.CancelPrompt(); } } else if (kind == Editor::PromptKind::BufferSwitch) { // Resolve to a buffer index by exact match against path or basename; // if multiple partial matches, prefer exact; if none, keep status. const auto &bs = ctx.editor.Buffers(); std::vector matches; for (std::size_t i = 0; i < bs.size(); ++i) { if (value == buffer_display_name(bs[i]) || value == buffer_basename(bs[i])) { matches.push_back(i); } } if (matches.empty()) { // Try prefix match if no exact for (std::size_t i = 0; i < bs.size(); ++i) { const std::string full = buffer_display_name(bs[i]); const std::string base = buffer_basename(bs[i]); if ((!value.empty() && full.rfind(value, 0) == 0) || ( !value.empty() && base.rfind(value, 0) == 0)) { matches.push_back(i); } } } if (matches.empty()) { ctx.editor.SetStatus("No such buffer: " + value); } else { ctx.editor.SwitchTo(matches[0]); const Buffer *cur = ctx.editor.CurrentBuffer(); ctx.editor.SetStatus(std::string("Switched: ") + (cur ? buffer_display_name(*cur) : std::string(""))); } } else if (kind == Editor::PromptKind::SaveAs) { if (value.empty()) { ctx.editor.SetStatus("Save canceled (empty filename)"); } else { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to save"); } else { // Expand "~" for save path auto expand_user_path = [](const std::string &in) -> std::string { if (!in.empty() && in[0] == '~') { const char *home = std::getenv("HOME"); if (home && in.size() == 1) return std::string(home); if (home && (in.size() > 1) && ( in[1] == '/' || in[1] == '\\')) { std::string rest = in.substr(1); return std::string(home) + rest; } } return in; }; value = expand_user_path(value); // If this is a first-time save (unnamed/non-file-backed) and the // target exists, ask for confirmation before overwriting. if (!buf->IsFileBacked() && std::filesystem::exists(value)) { ctx.editor.StartPrompt(Editor::PromptKind::Confirm, "Overwrite", ""); ctx.editor.SetPendingOverwritePath(value); ctx.editor.SetStatus( std::string("Overwrite existing file '") + value + "'? (y/N)"); } else { std::string err; if (!buf->SaveAs(value, err)) { ctx.editor.SetStatus(err); } else { buf->SetDirty(false); ctx.editor.SetStatus("Saved as " + value); if (auto *u = buf->Undo()) u->mark_saved(); // Close the prompt on successful save-as ctx.editor.CancelPrompt(); // If a close-after-save was requested (from closing a dirty, unnamed buffer), // close the buffer now. if (ctx.editor.CloseAfterSave()) { ctx.editor.SetCloseAfterSave(false); std::size_t idx_close = ctx.editor.CurrentBufferIndex(); std::string name_close = buffer_display_name(*buf); if (buf->Undo()) buf->Undo()->discard_pending(); ctx.editor.CloseBuffer(idx_close); if (ctx.editor.BufferCount() == 0) { Buffer empty; ctx.editor.AddBuffer(std::move(empty)); ctx.editor.SwitchTo(0); } const Buffer *cur = ctx.editor.CurrentBuffer(); ctx.editor.SetStatus( std::string("Closed: ") + name_close + std::string(" Now: ") + (cur ? buffer_display_name(*cur) : std::string(""))); } } } } } } else if (kind == Editor::PromptKind::Confirm) { // Confirmation for potentially destructive operations (e.g., overwrite on save-as) Buffer *buf = ctx.editor.CurrentBuffer(); const std::string target = ctx.editor.PendingOverwritePath(); if (!target.empty() && buf) { bool yes = false; if (!value.empty()) { char c = value[0]; yes = (c == 'y' || c == 'Y'); } if (yes) { std::string err; if (!buf->SaveAs(target, err)) { ctx.editor.SetStatus(err); } else { buf->SetDirty(false); ctx.editor.SetStatus("Saved as " + target); if (auto *u = buf->Undo()) u->mark_saved(); // If this overwrite confirm was part of a close-after-save flow, close now. if (ctx.editor.CloseAfterSave()) { ctx.editor.SetCloseAfterSave(false); std::size_t idx_close = ctx.editor.CurrentBufferIndex(); std::string name_close = buffer_display_name(*buf); if (buf->Undo()) buf->Undo()->discard_pending(); ctx.editor.CloseBuffer(idx_close); if (ctx.editor.BufferCount() == 0) { Buffer empty; ctx.editor.AddBuffer(std::move(empty)); ctx.editor.SwitchTo(0); } const Buffer *cur = ctx.editor.CurrentBuffer(); ctx.editor.SetStatus( std::string("Closed: ") + name_close + std::string( " Now: ") + (cur ? buffer_display_name(*cur) : std::string(""))); } // Close the prompt after successful confirmation ctx.editor.CancelPrompt(); } } else { ctx.editor.SetStatus("Save canceled"); // Close the prompt after negative confirmation ctx.editor.CancelPrompt(); } ctx.editor.ClearPendingOverwritePath(); // Regardless of answer, end any close-after-save pending state for safety. ctx.editor.SetCloseAfterSave(false); } else if (ctx.editor.CloseConfirmPending() && buf) { bool yes = false; if (!value.empty()) { char c = value[0]; yes = (c == 'y' || c == 'Y'); } // Prepare close details std::size_t idx_close = ctx.editor.CurrentBufferIndex(); std::string name_close = buffer_display_name(*buf); bool proceed_to_close = true; if (yes) { std::string err; if (buf->IsFileBacked()) { if (!buf->Save(err)) { ctx.editor.SetStatus(err); proceed_to_close = false; } else { buf->SetDirty(false); if (auto *u = buf->Undo()) u->mark_saved(); } } else if (!buf->Filename().empty()) { if (!buf->SaveAs(buf->Filename(), err)) { ctx.editor.SetStatus(err); proceed_to_close = false; } else { buf->SetDirty(false); if (auto *u = buf->Undo()) u->mark_saved(); } } else { // No filename; fall back to Save As flow and set close-after-save ctx.editor.StartPrompt(Editor::PromptKind::SaveAs, "Save as", ""); ctx.editor.SetCloseAfterSave(true); ctx.editor.SetStatus("Save as: "); ctx.editor.SetCloseConfirmPending(false); return true; } } if (proceed_to_close) { if (buf->Undo()) buf->Undo()->discard_pending(); ctx.editor.CloseBuffer(idx_close); if (ctx.editor.BufferCount() == 0) { Buffer empty; ctx.editor.AddBuffer(std::move(empty)); ctx.editor.SwitchTo(0); } const Buffer *cur = ctx.editor.CurrentBuffer(); ctx.editor.SetStatus( std::string("Closed: ") + name_close + std::string(" Now: ") + (cur ? buffer_display_name(*cur) : std::string(""))); } ctx.editor.SetCloseConfirmPending(false); } else { ctx.editor.SetStatus("Nothing to confirm"); } } else if (kind == Editor::PromptKind::GotoLine) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer"); return true; } std::size_t nrows = buf->Nrows(); if (nrows == 0) { buf->SetCursor(0, 0); ensure_cursor_visible(ctx.editor, *buf); ctx.editor.SetStatus("Empty buffer"); return true; } // Parse 1-based line number; on failure, keep cursor and show status std::size_t line1 = 0; try { if (!value.empty()) line1 = static_cast(std::stoull(value)); } catch (...) { line1 = 0; } if (line1 == 0) { ctx.editor.SetStatus("Goto canceled (invalid line)"); return true; } std::size_t y = line1 - 1; // convert to 0-based if (y >= nrows) y = nrows - 1; // clamp to last line buf->SetCursor(0, y); ensure_cursor_visible(ctx.editor, *buf); ctx.editor.SetStatus("Goto line " + std::to_string(line1)); } else if (kind == Editor::PromptKind::Chdir) { // Attempt to change the current working directory if (value.empty()) { ctx.editor.SetStatus("chdir canceled (empty)"); return true; } try { // Expand "~" for chdir auto expand_user_path = [](const std::string &in) -> std::string { if (!in.empty() && in[0] == '~') { const char *home = std::getenv("HOME"); if (home && in.size() == 1) return std::string(home); if (home && (in.size() > 1) && (in[1] == '/' || in[1] == '\\')) { std::string rest = in.substr(1); return std::string(home) + rest; } } return in; }; value = expand_user_path(value); std::filesystem::path p(value); std::error_code ec; // Expand if value is relative: resolve against current_path implicitly if (!std::filesystem::exists(p, ec)) { ctx.editor.SetStatus(std::string("chdir: no such path: ") + value); return true; } if (!std::filesystem::is_directory(p, ec)) { ctx.editor.SetStatus(std::string("chdir: not a directory: ") + value); return true; } std::filesystem::current_path(p); ctx.editor.SetStatus(std::string("cwd: ") + std::filesystem::current_path().string()); } catch (const std::exception &e) { ctx.editor.SetStatus(std::string("chdir failed: ") + e.what()); } } else if (kind == Editor::PromptKind::RegexReplaceFind) { // Proceed to regex replacement text prompt ctx.editor.SetReplaceFindTmp(value); // Keep search highlights active using the collected regex pattern ctx.editor.SetSearchActive(true); ctx.editor.SetSearchQuery(value); if (Buffer *b = ctx.editor.CurrentBuffer()) { std::string err; auto rm = search_compute_matches_regex(*b, ctx.editor.SearchQuery(), err); if (!err.empty()) { ctx.editor.SetStatus(std::string("Regex: ") + value + " [error: " + err + "]"); } search_apply_match_regex(ctx.editor, *b, rm); } ctx.editor.StartPrompt(Editor::PromptKind::RegexReplaceWith, "Regex replace: with", ""); ctx.editor.SetStatus("Regex replace: with: "); return true; } else if (kind == Editor::PromptKind::RegexReplaceWith) { // Execute regex replace-all Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (buf->IsReadOnly()) { ctx.editor.SetStatus("Read-only buffer"); // Clear search UI state ctx.editor.SetSearchActive(false); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetSearchIndex(-1); return true; } const std::string patt = ctx.editor.ReplaceFindTmp(); const std::string repl = value; ctx.editor.SetReplaceWithTmp(repl); if (patt.empty()) { ctx.editor.SetStatus("Regex replace canceled (empty pattern)"); ctx.editor.SetSearchActive(false); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetSearchIndex(-1); return true; } std::regex rx; try { rx = std::regex(patt); } catch (const std::regex_error &e) { ctx.editor.SetStatus(std::string("Regex error: ") + e.what()); // Clear search UI state ctx.editor.SetSearchActive(false); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetSearchIndex(-1); return true; } std::size_t changed = 0; // Iterate by index to allow modifications via PieceTable helpers for (std::size_t y = 0; y < buf->Rows().size(); ++y) { std::string before = static_cast(buf->Rows()[y]); std::string after = std::regex_replace(before, rx, repl); if (after != before) { // Replace entire line y with 'after' using PieceTable ops buf->delete_row(static_cast(y)); buf->insert_row(static_cast(y), std::string_view(after)); ++changed; } } buf->SetDirty(true); ctx.editor.SetStatus("Regex replaced in " + std::to_string(changed) + " line(s)"); // Clear search UI state ctx.editor.SetSearchActive(false); ctx.editor.SetSearchQuery(""); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetSearchIndex(-1); if (auto *b = ctx.editor.CurrentBuffer()) ensure_cursor_visible(ctx.editor, *b); return true; } return true; } // In search mode, Enter accepts the current match and exits search if (ctx.editor.SearchActive()) { ctx.editor.SetSearchActive(false); ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.ClearSearchOrigin(); ctx.editor.SetStatus("Find done"); Buffer *buf = ctx.editor.CurrentBuffer(); if (buf) ensure_cursor_visible(ctx.editor, *buf); return true; } Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to edit"); return false; } ensure_at_least_one_line(*buf); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); int repeat = ctx.count > 0 ? ctx.count : 1; for (int i = 0; i < repeat; ++i) { buf->split_line(static_cast(y), static_cast(x)); // Move to start of next line y += 1; x = 0; } buf->SetCursor(x, y); buf->SetDirty(true); if (auto *u = buf->Undo()) { u->Begin(UndoType::Newline); u->commit(); } ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_backspace(CommandContext &ctx) { // If a prompt is active, backspace edits the prompt text if (ctx.editor.PromptActive()) { ctx.editor.BackspacePromptText(); if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search || ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind || ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) { Buffer *buf2 = ctx.editor.CurrentBuffer(); if (buf2) { ctx.editor.SetSearchQuery(ctx.editor.PromptText()); if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) { std::string err; auto rm = search_compute_matches_regex(*buf2, ctx.editor.SearchQuery(), err); if (!err.empty()) { ctx.editor.SetStatus( std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]"); } search_apply_match_regex(ctx.editor, *buf2, rm); } else { auto matches = search_compute_matches(*buf2, ctx.editor.SearchQuery()); search_apply_match(ctx.editor, *buf2, matches); } } } else { ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText()); } return true; } // In search mode, backspace edits the query if (ctx.editor.SearchActive()) { if (!ctx.editor.SearchQuery().empty()) { std::string q = ctx.editor.SearchQuery(); q.pop_back(); ctx.editor.SetSearchQuery(q); } Buffer *buf2 = ctx.editor.CurrentBuffer(); if (buf2) { auto matches = search_compute_matches(*buf2, ctx.editor.SearchQuery()); search_apply_match(ctx.editor, *buf2, matches); } return true; } Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to edit"); return false; } ensure_at_least_one_line(*buf); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); UndoSystem *u = buf->Undo(); int repeat = ctx.count > 0 ? ctx.count : 1; for (int i = 0; i < repeat; ++i) { // Refresh a read-only view of lines for char capture/lengths const auto &rows_view = buf->Rows(); if (x > 0) { char deleted = '\0'; if (y < rows_view.size() && x - 1 < rows_view[y].size()) deleted = rows_view[y][x - 1]; buf->delete_text(static_cast(y), static_cast(x - 1), 1); x -= 1; buf->SetCursor(x, y); if (u) { u->Begin(UndoType::Delete); if (deleted != '\0') u->Append(deleted); } } else if (y > 0) { // Compute previous line length before join std::size_t prev_len = 0; if (y - 1 < rows_view.size()) prev_len = rows_view[y - 1].size(); buf->join_lines(static_cast(y - 1)); y = y - 1; x = prev_len; buf->SetCursor(x, y); if (u) { u->Begin(UndoType::Newline); u->commit(); } } else { break; } } buf->SetCursor(x, y); buf->SetDirty(true); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_delete_char(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to edit"); return false; } ensure_at_least_one_line(*buf); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); UndoSystem *u = buf->Undo(); int repeat = ctx.count > 0 ? ctx.count : 1; for (int i = 0; i < repeat; ++i) { const auto &rows_view = buf->Rows(); if (y >= rows_view.size()) break; if (x < rows_view[y].size()) { char deleted = rows_view[y][x]; buf->delete_text(static_cast(y), static_cast(x), 1); if (u) { u->Begin(UndoType::Delete); u->Append(deleted); } } else if (y + 1 < rows_view.size()) { buf->join_lines(static_cast(y)); if (u) { u->Begin(UndoType::Newline); u->commit(); } } else { break; } } buf->SetDirty(true); ensure_cursor_visible(ctx.editor, *buf); return true; } // --- Undo/Redo --- static bool cmd_undo(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) { // Ensure pending batch is finalized so it can be undone u->commit(); u->undo(); // Keep cursor within buffer bounds ensure_cursor_visible(ctx.editor, *buf); ctx.editor.SetStatus("Undone"); return true; } return false; } static bool cmd_redo(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) { // Finalize any pending batch before redoing u->commit(); u->redo(); ensure_cursor_visible(ctx.editor, *buf); ctx.editor.SetStatus("Redone"); return true; } return false; } static bool cmd_kill_to_eol(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to edit"); return false; } ensure_at_least_one_line(*buf); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); int repeat = ctx.count > 0 ? ctx.count : 1; std::string killed_total; for (int i = 0; i < repeat; ++i) { const auto &rows_view = buf->Rows(); if (y >= rows_view.size()) break; if (x < rows_view[y].size()) { // delete from cursor to end of line killed_total += rows_view[y].substr(x); std::size_t len = rows_view[y].size() - x; buf->delete_text(static_cast(y), static_cast(x), len); } else if (y + 1 < rows_view.size()) { // at EOL: delete the newline (join with next line) killed_total += "\n"; buf->join_lines(static_cast(y)); } else { // nothing to delete break; } } buf->SetDirty(true); ensure_cursor_visible(ctx.editor, *buf); if (!killed_total.empty()) { if (ctx.editor.KillChain()) ctx.editor.KillRingAppend(killed_total); else ctx.editor.KillRingPush(killed_total); ctx.editor.SetKillChain(true); } return true; } static bool cmd_kill_line(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to edit"); return false; } ensure_at_least_one_line(*buf); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); (void) x; // cursor x will be reset to 0 int repeat = ctx.count > 0 ? ctx.count : 1; std::string killed_total; for (int i = 0; i < repeat; ++i) { const auto &rows_view = buf->Rows(); if (rows_view.empty()) break; if (rows_view.size() == 1) { // last remaining line: clear its contents killed_total += static_cast(rows_view[0]); if (!rows_view[0].empty()) buf->delete_text(0, 0, rows_view[0].size()); y = 0; } else if (y < rows_view.size()) { // erase current line; keep y pointing at the next line killed_total += static_cast(rows_view[y]); killed_total += "\n"; buf->delete_row(static_cast(y)); const auto &rows_after = buf->Rows(); if (y >= rows_after.size()) { // deleted last line; move to previous y = rows_after.empty() ? 0 : rows_after.size() - 1; } } else { // out of range const auto &rows2 = buf->Rows(); y = rows2.empty() ? 0 : rows2.size() - 1; } } buf->SetCursor(0, y); buf->SetDirty(true); ensure_cursor_visible(ctx.editor, *buf); if (!killed_total.empty()) { if (ctx.editor.KillChain()) ctx.editor.KillRingAppend(killed_total); else ctx.editor.KillRingPush(killed_total); ctx.editor.SetKillChain(true); } return true; } static bool cmd_yank(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) { ctx.editor.SetStatus("No buffer to edit"); return false; } std::string text = ctx.editor.KillRingHead(); if (text.empty()) { ctx.editor.SetStatus("Kill ring is empty"); return false; } ensure_at_least_one_line(*buf); int repeat = ctx.count > 0 ? ctx.count : 1; for (int i = 0; i < repeat; ++i) { insert_text_at_cursor(*buf, text); } ensure_cursor_visible(ctx.editor, *buf); // Start a new kill chain only from kill commands; yanking should break it ctx.editor.SetKillChain(false); return true; } // --- Marks/Regions and File boundaries --- static bool cmd_move_file_start(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; ensure_at_least_one_line(*buf); buf->SetCursor(0, 0); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_move_file_end(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; ensure_at_least_one_line(*buf); const auto &rows = buf->Rows(); std::size_t y = rows.empty() ? 0 : rows.size() - 1; std::size_t x = rows.empty() ? 0 : rows[y].size(); buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_toggle_mark(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (buf->MarkSet()) { buf->ClearMark(); ctx.editor.SetStatus("Mark cleared"); } else { buf->SetMark(buf->Curx(), buf->Cury()); ctx.editor.SetStatus("Mark set"); } return true; } static bool cmd_jump_to_mark(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (!buf->MarkSet()) { ctx.editor.SetStatus("Mark not set"); return false; } std::size_t cx = buf->Curx(); std::size_t cy = buf->Cury(); std::size_t mx = buf->MarkCurx(); std::size_t my = buf->MarkCury(); buf->SetCursor(mx, my); buf->SetMark(cx, cy); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_kill_region(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; ensure_at_least_one_line(*buf); std::size_t sx, sy, ex, ey; if (!compute_mark_region(*buf, sx, sy, ex, ey)) { ctx.editor.SetStatus("No region to kill"); return false; } std::string text = extract_region_text(*buf, sx, sy, ex, ey); delete_region(*buf, sx, sy, ex, ey); ensure_cursor_visible(ctx.editor, *buf); if (!text.empty()) { if (ctx.editor.KillChain()) ctx.editor.KillRingAppend(text); else ctx.editor.KillRingPush(text); ctx.editor.SetKillChain(true); } buf->ClearMark(); return true; } static bool cmd_copy_region(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; ensure_at_least_one_line(*buf); std::size_t sx, sy, ex, ey; if (!compute_mark_region(*buf, sx, sy, ex, ey)) { ctx.editor.SetStatus("No region to copy"); return false; } std::string text = extract_region_text(*buf, sx, sy, ex, ey); if (!text.empty()) { if (ctx.editor.KillChain()) ctx.editor.KillRingAppend(text); else ctx.editor.KillRingPush(text); ctx.editor.SetKillChain(true); } buf->ClearMark(); return true; } static bool cmd_flush_kill_ring(CommandContext &ctx) { ctx.editor.KillRingClear(); ctx.editor.SetKillChain(false); ctx.editor.SetStatus("Kill ring cleared"); return true; } // --- Navigation --- // (helper removed) static bool cmd_move_left(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); // If a prompt is active and it's search, go to previous match if (ctx.editor.PromptActive() && (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search || ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind)) { if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) { std::string err; auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err); if (!err.empty()) { ctx.editor.SetStatus( std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]"); } if (!rmatches.empty()) { int idx = ctx.editor.SearchIndex(); if (idx < 0) idx = 0; idx = (idx - 1 + static_cast(rmatches.size())) % static_cast(rmatches.size()); ctx.editor.SetSearchIndex(idx); search_apply_match_regex(ctx.editor, *buf, rmatches); } else { search_apply_match_regex(ctx.editor, *buf, rmatches); } } else { auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); if (!matches.empty()) { int idx = ctx.editor.SearchIndex(); if (idx < 0) idx = 0; idx = (idx - 1 + static_cast(matches.size())) % static_cast(matches.size()); ctx.editor.SetSearchIndex(idx); search_apply_match(ctx.editor, *buf, matches); } else { search_apply_match(ctx.editor, *buf, matches); } } return true; } if (ctx.editor.SearchActive()) { auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); if (!matches.empty()) { int idx = ctx.editor.SearchIndex(); if (idx < 0) idx = 0; idx = (idx - 1 + static_cast(matches.size())) % static_cast(matches.size()); ctx.editor.SetSearchIndex(idx); search_apply_match(ctx.editor, *buf, matches); } else { search_apply_match(ctx.editor, *buf, matches); } return true; } ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); int repeat = ctx.count > 0 ? ctx.count : 1; while (repeat-- > 0) { if (x > 0) { --x; } else if (y > 0) { --y; x = rows[y].size(); } } buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_move_right(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); if (ctx.editor.PromptActive() && (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search || ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind)) { if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) { std::string err; auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err); if (!err.empty()) { ctx.editor.SetStatus( std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]"); } if (!rmatches.empty()) { int idx = ctx.editor.SearchIndex(); if (idx < 0) idx = 0; idx = (idx + 1) % static_cast(rmatches.size()); ctx.editor.SetSearchIndex(idx); search_apply_match_regex(ctx.editor, *buf, rmatches); } else { search_apply_match_regex(ctx.editor, *buf, rmatches); } } else { auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); if (!matches.empty()) { int idx = ctx.editor.SearchIndex(); if (idx < 0) idx = 0; idx = (idx + 1) % static_cast(matches.size()); ctx.editor.SetSearchIndex(idx); search_apply_match(ctx.editor, *buf, matches); } else { search_apply_match(ctx.editor, *buf, matches); } } return true; } if (ctx.editor.SearchActive()) { auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); if (!matches.empty()) { int idx = ctx.editor.SearchIndex(); if (idx < 0) idx = 0; idx = (idx + 1) % static_cast(matches.size()); ctx.editor.SetSearchIndex(idx); search_apply_match(ctx.editor, *buf, matches); } else { search_apply_match(ctx.editor, *buf, matches); } return true; } ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); int repeat = ctx.count > 0 ? ctx.count : 1; while (repeat-- > 0) { if (y < rows.size() && x < rows[y].size()) { ++x; } else if (y + 1 < rows.size()) { ++y; x = 0; } } buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_move_up(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor. SearchActive()) { // Up == previous match auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); if (!matches.empty()) { int idx = ctx.editor.SearchIndex(); if (idx < 0) idx = 0; idx = (idx - 1 + static_cast(matches.size())) % static_cast(matches.size()); ctx.editor.SetSearchIndex(idx); search_apply_match(ctx.editor, *buf, matches); } else { search_apply_match(ctx.editor, *buf, matches); } return true; } ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); int repeat = ctx.count > 0 ? ctx.count : 1; if (repeat > static_cast(y)) repeat = static_cast(y); y -= static_cast(repeat); if (x > rows[y].size()) x = rows[y].size(); buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_move_down(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor. SearchActive()) { // Down == next match auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); if (!matches.empty()) { int idx = ctx.editor.SearchIndex(); if (idx < 0) idx = 0; idx = (idx + 1) % static_cast(matches.size()); ctx.editor.SetSearchIndex(idx); search_apply_match(ctx.editor, *buf, matches); } else { search_apply_match(ctx.editor, *buf, matches); } return true; } ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); int repeat = ctx.count > 0 ? ctx.count : 1; std::size_t max_down = rows.size() - 1 - y; if (repeat > static_cast(max_down)) repeat = static_cast(max_down); y += static_cast(repeat); if (x > rows[y].size()) x = rows[y].size(); buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_move_home(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); ensure_at_least_one_line(*buf); std::size_t y = buf->Cury(); buf->SetCursor(0, y); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_move_end(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); std::size_t y = buf->Cury(); std::size_t x = (y < rows.size()) ? rows[y].size() : 0; buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_page_up(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); int repeat = ctx.count > 0 ? ctx.count : 1; std::size_t content_rows = std::max(1, ctx.editor.ContentRows()); // Base on current top-of-screen (row offset) std::size_t rowoffs = buf->Rowoffs(); while (repeat-- > 0) { if (rowoffs >= content_rows) rowoffs -= content_rows; else rowoffs = 0; } // Clamp to valid range if (rows.size() > content_rows) { std::size_t max_top = rows.size() - content_rows; if (rowoffs > max_top) rowoffs = max_top; } else { rowoffs = 0; } // Move cursor to first visible line, column 0 std::size_t y = rowoffs; if (y >= rows.size()) y = rows.empty() ? 0 : rows.size() - 1; buf->SetOffsets(rowoffs, 0); buf->SetCursor(0, y); return true; } static bool cmd_page_down(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); int repeat = ctx.count > 0 ? ctx.count : 1; std::size_t content_rows = std::max(1, ctx.editor.ContentRows()); std::size_t rowoffs = buf->Rowoffs(); // Compute maximum top offset std::size_t max_top = 0; if (!rows.empty()) { if (rows.size() > content_rows) max_top = rows.size() - content_rows; else max_top = 0; } while (repeat-- > 0) { if (rowoffs + content_rows <= max_top) rowoffs += content_rows; else rowoffs = max_top; } // Move cursor to first visible line, column 0 std::size_t y = std::min(rowoffs, rows.empty() ? 0 : rows.size() - 1); buf->SetOffsets(rowoffs, 0); buf->SetCursor(0, y); return true; } static bool cmd_scroll_up(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; ensure_at_least_one_line(*buf); const auto &rows = buf->Rows(); std::size_t content_rows = std::max(1, ctx.editor.ContentRows()); std::size_t rowoffs = buf->Rowoffs(); // Scroll up by 3 lines (or count if specified), without moving cursor int scroll_amount = ctx.count > 0 ? ctx.count : 3; if (rowoffs >= static_cast(scroll_amount)) rowoffs -= static_cast(scroll_amount); else rowoffs = 0; buf->SetOffsets(rowoffs, buf->Coloffs()); // If cursor is now below the visible area, move it to the last visible line std::size_t cury = buf->Cury(); if (cury >= rowoffs + content_rows) { std::size_t new_y = rowoffs + content_rows - 1; if (new_y >= rows.size() && !rows.empty()) new_y = rows.size() - 1; buf->SetCursor(buf->Curx(), new_y); } return true; } static bool cmd_scroll_down(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; ensure_at_least_one_line(*buf); const auto &rows = buf->Rows(); std::size_t content_rows = std::max(1, ctx.editor.ContentRows()); std::size_t rowoffs = buf->Rowoffs(); // Scroll down by 3 lines (or count if specified), without moving cursor int scroll_amount = ctx.count > 0 ? ctx.count : 3; // Compute maximum top offset std::size_t max_top = 0; if (!rows.empty() && rows.size() > content_rows) max_top = rows.size() - content_rows; rowoffs += static_cast(scroll_amount); if (rowoffs > max_top) rowoffs = max_top; buf->SetOffsets(rowoffs, buf->Coloffs()); // If cursor is now above the visible area, move it to the first visible line std::size_t cury = buf->Cury(); if (cury < rowoffs) { buf->SetCursor(buf->Curx(), rowoffs); } return true; } static inline bool is_word_char(unsigned char c) { return std::isalnum(c) || c == '_'; } static bool cmd_word_prev(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); int repeat = ctx.count > 0 ? ctx.count : 1; while (repeat-- > 0) { if (y >= rows.size()) { y = rows.empty() ? 0 : rows.size() - 1; x = rows[y].size(); } // If at start of line and not first line, move to end of previous line if (x == 0) { if (y == 0) break; --y; x = rows[y].size(); } // Move left one first if (x > 0) --x; // Skip any whitespace leftwards while (y < rows.size() && (x > 0 || (x == 0 && y > 0))) { if (x == 0) { --y; x = rows[y].size(); if (x == 0) continue; } unsigned char c = x > 0 ? static_cast(rows[y][x - 1]) : 0; if (!std::isspace(c)) break; --x; } // Skip word characters leftwards while (y < rows.size() && (x > 0 || (x == 0 && y > 0))) { if (x == 0) break; unsigned char c = static_cast(rows[y][x - 1]); if (!is_word_char(c)) break; --x; } } buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_word_next(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); int repeat = ctx.count > 0 ? ctx.count : 1; while (repeat-- > 0) { if (y >= rows.size()) break; // First, if currently on a word, skip to its end while (y < rows.size()) { if (x < rows[y].size() && is_word_char(static_cast(rows[y][x]))) { ++x; continue; } if (x >= rows[y].size()) { if (y + 1 >= rows.size()) break; ++y; x = 0; continue; } break; } // Then, skip any non-word characters (including punctuation and whitespace) while (y < rows.size()) { if (x < rows[y].size()) { unsigned char c = static_cast(rows[y][x]); if (is_word_char(c)) break; ++x; continue; } if (x >= rows[y].size()) { if (y + 1 >= rows.size()) break; ++y; x = 0; continue; } } } buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_delete_word_prev(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); int repeat = ctx.count > 0 ? ctx.count : 1; std::string killed_total; for (int i = 0; i < repeat; ++i) { if (y >= rows.size()) { y = rows.empty() ? 0 : rows.size() - 1; x = rows[y].size(); } std::size_t start_y = y; std::size_t start_x = x; // If at start of line and not first line, move to end of previous line if (x == 0) { if (y == 0) break; --y; x = rows[y].size(); } // Move left one first if (x > 0) --x; // Skip any whitespace leftwards while (y < rows.size() && (x > 0 || (x == 0 && y > 0))) { if (x == 0) { --y; x = rows[y].size(); if (x == 0) continue; } unsigned char c = x > 0 ? static_cast(rows[y][x - 1]) : 0; if (!std::isspace(c)) break; --x; } // Skip word characters leftwards while (y < rows.size() && (x > 0 || (x == 0 && y > 0))) { if (x == 0) break; unsigned char c = static_cast(rows[y][x - 1]); if (!is_word_char(c)) break; --x; } // Now delete from (x, y) to (start_x, start_y) using PieceTable std::string deleted = extract_region_text(*buf, x, y, start_x, start_y); delete_region(*buf, x, y, start_x, start_y); // Prepend to killed_total (since we're deleting backwards) killed_total = deleted + killed_total; } buf->SetCursor(x, y); buf->SetDirty(true); ensure_cursor_visible(ctx.editor, *buf); if (!killed_total.empty()) { if (ctx.editor.KillChain()) ctx.editor.KillRingPrepend(killed_total); else ctx.editor.KillRingPush(killed_total); ctx.editor.SetKillChain(true); } return true; } static bool cmd_delete_word_next(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (auto *u = buf->Undo()) u->commit(); ensure_at_least_one_line(*buf); const auto &rows = buf->Rows(); std::size_t y = buf->Cury(); std::size_t x = buf->Curx(); int repeat = ctx.count > 0 ? ctx.count : 1; std::string killed_total; for (int i = 0; i < repeat; ++i) { if (y >= rows.size()) break; std::size_t start_y = y; std::size_t start_x = x; // First, if currently on a word, skip to its end while (y < rows.size()) { if (x < rows[y].size() && is_word_char(static_cast(rows[y][x]))) { ++x; continue; } if (x >= rows[y].size()) { if (y + 1 >= rows.size()) break; ++y; x = 0; continue; } break; } // Then, skip any non-word characters (including punctuation and whitespace) while (y < rows.size()) { if (x < rows[y].size()) { unsigned char c = static_cast(rows[y][x]); if (is_word_char(c)) break; ++x; continue; } if (x >= rows[y].size()) { if (y + 1 >= rows.size()) break; ++y; x = 0; continue; } } // Now delete from (start_x, start_y) to (x, y) using PieceTable std::string deleted = extract_region_text(*buf, start_x, start_y, x, y); delete_region(*buf, start_x, start_y, x, y); y = start_y; x = start_x; killed_total += deleted; } buf->SetCursor(x, y); buf->SetDirty(true); ensure_cursor_visible(ctx.editor, *buf); if (!killed_total.empty()) { if (ctx.editor.KillChain()) ctx.editor.KillRingAppend(killed_total); else ctx.editor.KillRingPush(killed_total); ctx.editor.SetKillChain(true); } return true; } static bool cmd_indent_region(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (!buf->MarkSet()) { ctx.editor.SetStatus("No mark set"); return false; } std::size_t sx, sy, ex, ey; if (!compute_mark_region(*buf, sx, sy, ex, ey)) { ctx.editor.SetStatus("No region to indent"); return false; } for (std::size_t y = sy; y <= ey && y < buf->Rows().size(); ++y) { buf->insert_text(static_cast(y), 0, std::string_view("\t")); } buf->SetDirty(true); buf->ClearMark(); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_unindent_region(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; if (!buf->MarkSet()) { ctx.editor.SetStatus("No mark set"); return false; } std::size_t sx, sy, ex, ey; if (!compute_mark_region(*buf, sx, sy, ex, ey)) { ctx.editor.SetStatus("No region to unindent"); return false; } for (std::size_t y = sy; y <= ey && y < buf->Rows().size(); ++y) { const auto &rows_view = buf->Rows(); if (y >= rows_view.size()) break; const std::string line = static_cast(rows_view[y]); if (!line.empty()) { if (line[0] == '\t') { buf->delete_text(static_cast(y), 0, 1); } else if (line[0] == ' ') { std::size_t spaces = 0; while (spaces < line.size() && spaces < 8 && line[spaces] == ' ') { ++spaces; } if (spaces > 0) buf->delete_text(static_cast(y), 0, spaces); } } } buf->SetDirty(true); buf->ClearMark(); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_reflow_paragraph(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; ensure_at_least_one_line(*buf); auto &rows = buf->Rows(); std::size_t y = buf->Cury(); // Treat a universal-argument count of 1 as "no width specified". // Editor::UArgGet() returns 1 when no explicit count was provided. int width = ctx.count > 1 ? ctx.count : 72; std::size_t para_start = y; while (para_start > 0 && !rows[para_start - 1].empty()) --para_start; std::size_t para_end = y; while (para_end + 1 < rows.size() && !rows[para_end + 1].empty()) ++para_end; if (para_start > para_end) return false; auto is_space = [](char c) { return c == ' ' || c == '\t'; }; auto leading_ws = [&](const std::string &s) { std::size_t i = 0; while (i < s.size() && is_space(s[i])) ++i; return s.substr(0, i); }; auto starts_with = [](const std::string &s, const std::string &pfx) { return s.size() >= pfx.size() && std::equal(pfx.begin(), pfx.end(), s.begin()); }; auto is_bullet_line = [&](const std::string &s, std::string &indent_out, char &marker_out, std::size_t &after_prefix_idx) -> bool { indent_out = leading_ws(s); std::size_t i = indent_out.size(); if (i + 1 < s.size()) { char m = s[i]; if ((m == '-' || m == '+' || m == '*') && s[i + 1] == ' ') { marker_out = m; after_prefix_idx = i + 2; // after marker + space return true; } } return false; }; auto normalize_spaces = [](const std::string &in) { std::string out; out.reserve(in.size()); bool in_space = false; for (char c: in) { char cc = (c == '\t') ? ' ' : c; if (cc == ' ') { if (!in_space) { out.push_back(' '); in_space = true; } } else { out.push_back(cc); in_space = false; } } // trim leading/trailing spaces // leading std::size_t start = 0; while (start < out.size() && out[start] == ' ') ++start; // trailing std::size_t end = out.size(); while (end > start && out[end - 1] == ' ') --end; return out.substr(start, end - start); }; auto wrap_with_prefixes = [&](const std::string &content, const std::string &first_prefix, const std::string &cont_prefix, int w, std::vector &dst) { // Tokenize by spaces std::vector words; std::size_t pos = 0; while (pos < content.size()) { while (pos < content.size() && content[pos] == ' ') ++pos; if (pos >= content.size()) break; std::size_t ws = pos; while (pos < content.size() && content[pos] != ' ') ++pos; words.emplace_back(content.substr(ws, pos - ws)); } std::string line = first_prefix; std::size_t cur_len = line.size(); bool first_word_on_line = true; auto flush_line = [&]() { dst.emplace_back(line); line = cont_prefix; cur_len = line.size(); first_word_on_line = true; }; if (words.empty()) { // Still emit a line with just the prefix (e.g., empty bullet) dst.emplace_back(line); return; } for (std::size_t i = 0; i < words.size(); ++i) { const std::string &wrd = words[i]; std::size_t needed = wrd.size() + (first_word_on_line ? 0 : 1); if (static_cast(cur_len + needed) > w) { // wrap flush_line(); } if (!first_word_on_line) { line.push_back(' '); ++cur_len; } line += wrd; cur_len += wrd.size(); first_word_on_line = false; } if (!line.empty()) dst.emplace_back(line); }; std::vector new_lines; // Determine if this region looks like a list: any line starting with bullet bool region_has_bullet = false; for (std::size_t i = para_start; i <= para_end; ++i) { std::string s = static_cast(rows[i]); std::string indent; char marker; std::size_t idx; if (is_bullet_line(s, indent, marker, idx)) { region_has_bullet = true; break; } } if (region_has_bullet) { // Parse as list items; support hanging indent continuations for (std::size_t i = para_start; i <= para_end; ++i) { std::string s = static_cast(rows[i]); std::string indent; char marker = 0; std::size_t after_idx = 0; if (is_bullet_line(s, indent, marker, after_idx)) { std::string first_prefix = indent + std::string(1, marker) + " "; std::string cont_prefix = indent + " "; std::string content = s.substr(after_idx); // consume continuation lines that are part of this bullet item std::size_t j = i + 1; while (j <= para_end) { std::string ns = static_cast(rows[j]); if (starts_with(ns, indent + " ")) { content += ' '; content += ns.substr(indent.size() + 2); ++j; continue; } // stop if next bullet at same indentation or different structure std::string nindent; char nmarker; std::size_t nidx; if (is_bullet_line(ns, nindent, nmarker, nidx)) { break; // next item } // Not a continuation and not a bullet: stop (treat as separate paragraph chunk) break; } content = normalize_spaces(content); wrap_with_prefixes(content, first_prefix, cont_prefix, width, new_lines); i = j - 1; // advance } else { // A non-bullet line within a list region; treat as its own wrapped paragraph preserving its indent std::string base_indent = leading_ws(s); std::string content = s.substr(base_indent.size()); std::size_t j = i + 1; while (j <= para_end) { std::string ns = static_cast(rows[j]); std::string nindent = leading_ws(ns); std::string tmp_indent; char tmp_marker; std::size_t tmp_idx; if (is_bullet_line(ns, tmp_indent, tmp_marker, tmp_idx)) { break; // next bullet starts } if (nindent.size() >= base_indent.size()) { content += ' '; content += ns.substr(base_indent.size()); ++j; } else { break; } } content = normalize_spaces(content); wrap_with_prefixes(content, base_indent, base_indent, width, new_lines); i = j - 1; } } } else { // Normal paragraph: preserve indentation of first line std::string s0 = static_cast(rows[para_start]); std::string pfx = leading_ws(s0); std::string content; for (std::size_t i = para_start; i <= para_end; ++i) { std::string si = static_cast(rows[i]); // strip the same prefix length if present if (si.size() >= pfx.size() && starts_with(si, pfx)) si.erase(0, pfx.size()); if (!content.empty()) content.push_back(' '); content += si; } content = normalize_spaces(content); wrap_with_prefixes(content, pfx, pfx, width, new_lines); } if (new_lines.empty()) new_lines.push_back(""); // Replace paragraph lines via PieceTable-backed operations for (std::size_t i = para_end; i + 1 > para_start; --i) { buf->delete_row(static_cast(i)); if (i == 0) break; // prevent wrap on size_t } // Insert new lines starting at para_start std::size_t insert_y = para_start; for (const auto &ln : new_lines) { buf->insert_row(static_cast(insert_y), std::string_view(ln)); insert_y += 1; } // Place cursor at the end of the paragraph std::size_t new_last_y = para_start + (new_lines.empty() ? 0 : new_lines.size() - 1); std::size_t new_last_x = new_lines.empty() ? 0 : new_lines.back().size(); buf->SetCursor(new_last_x, new_last_y); buf->SetDirty(true); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_reload_buffer(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; // Remember the current cursor position so we can attempt to restore it const std::size_t old_x = buf->Curx(); const std::size_t old_y = buf->Cury(); const std::string &filename = buf->Filename(); if (filename.empty()) { ctx.editor.SetStatus("Cannot reload unnamed buffer"); return false; } std::string err; if (!buf->OpenFromFile(filename, err)) { ctx.editor.SetStatus(std::string("Reload failed: ") + err); return false; } // Try to restore the cursor to its previous position if still valid; otherwise clamp { auto &rows = buf->Rows(); const std::size_t nrows = rows.size(); if (nrows == 0) { buf->SetCursor(0, 0); } else { const std::size_t new_y = old_y < nrows ? old_y : (nrows - 1); const std::size_t line_len = rows[new_y].size(); const std::size_t new_x = old_x < line_len ? old_x : line_len; buf->SetCursor(new_x, new_y); } } ctx.editor.SetStatus(std::string("Reloaded ") + filename); ensure_cursor_visible(ctx.editor, *buf); return true; } static bool cmd_mark_all_and_jump_end(CommandContext &ctx) { Buffer *buf = ctx.editor.CurrentBuffer(); if (!buf) return false; ensure_at_least_one_line(*buf); buf->SetMark(0, 0); auto &rows = buf->Rows(); std::size_t last_y = rows.empty() ? 0 : rows.size() - 1; std::size_t last_x = last_y < rows.size() ? rows[last_y].size() : 0; buf->SetCursor(last_x, last_y); ensure_cursor_visible(ctx.editor, *buf); return true; } std::vector & CommandRegistry::storage_() { static std::vector cmds; return cmds; } void CommandRegistry::Register(const Command &cmd) { auto &v = storage_(); // Replace existing with same id or name auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { return c.id == cmd.id || c.name == cmd.name; }); if (it != v.end()) { *it = cmd; } else { v.push_back(cmd); } } const Command * CommandRegistry::FindById(CommandId id) { auto &v = storage_(); auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { return c.id == id; }); return it == v.end() ? nullptr : &*it; } const Command * CommandRegistry::FindByName(const std::string &name) { auto &v = storage_(); auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { return c.name == name; }); return it == v.end() ? nullptr : &*it; } const std::vector & CommandRegistry::All() { return storage_(); } void InstallDefaultCommands() { CommandRegistry::Register({CommandId::Save, "save", "Save current buffer", cmd_save}); CommandRegistry::Register({CommandId::SaveAs, "save-as", "Save current buffer as...", cmd_save_as}); CommandRegistry::Register({CommandId::Quit, "quit", "Quit editor (request)", cmd_quit}); CommandRegistry::Register({CommandId::QuitNow, "quit-now", "Quit editor immediately", cmd_quit_now}); CommandRegistry::Register({CommandId::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit}); CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh}); CommandRegistry::Register( {CommandId::KPrefix, "k-prefix", "Entering k-command prefix (show hint)", cmd_kprefix, false, false}); CommandRegistry::Register({ CommandId::UnknownKCommand, "unknown-k", "Unknown k-command (status)", cmd_unknown_kcommand, false, false }); CommandRegistry::Register({ CommandId::UnknownEscCommand, "unknown-esc", "Unknown ESC command (status)", cmd_unknown_esc_command, false, false }); CommandRegistry::Register({ CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start, false, false }); CommandRegistry::Register({ CommandId::RegexFindStart, "regex-find-start", "Begin regex search", cmd_regex_find_start, false, false }); CommandRegistry::Register({ CommandId::RegexpReplace, "regex-replace", "Begin regex search & replace", cmd_regex_replace_start, false, false }); CommandRegistry::Register({ CommandId::SearchReplace, "search-replace", "Begin search & replace", cmd_search_replace_start, false, false }); CommandRegistry::Register({ CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start, false, false }); // Buffers CommandRegistry::Register({ CommandId::BufferSwitchStart, "buffer-switch-start", "Begin buffer switch prompt", cmd_buffer_switch_start, false, false }); CommandRegistry::Register({CommandId::BufferNew, "buffer-new", "Open new empty buffer", cmd_buffer_new}); CommandRegistry::Register({CommandId::BufferNext, "buffer-next", "Switch to next buffer", cmd_buffer_next}); CommandRegistry::Register({CommandId::BufferPrev, "buffer-prev", "Switch to previous buffer", cmd_buffer_prev}); CommandRegistry::Register({ CommandId::BufferClose, "buffer-close", "Close current buffer", cmd_buffer_close, false, false }); // Editing CommandRegistry::Register({ CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text, false, true }); CommandRegistry::Register({CommandId::Newline, "newline", "Insert newline at cursor", cmd_newline}); CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace}); CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char}); CommandRegistry::Register({CommandId::KillToEOL, "kill-to-eol", "Delete to end of line", cmd_kill_to_eol}); CommandRegistry::Register({CommandId::KillLine, "kill-line", "Delete entire line", cmd_kill_line}); CommandRegistry::Register({CommandId::Yank, "yank", "Yank from kill ring", cmd_yank}); // Marks/regions and file boundaries CommandRegistry::Register({ CommandId::MoveFileStart, "file-start", "Move to beginning of file", cmd_move_file_start }); CommandRegistry::Register({CommandId::MoveFileEnd, "file-end", "Move to end of file", cmd_move_file_end}); CommandRegistry::Register({CommandId::ToggleMark, "toggle-mark", "Toggle mark at cursor", cmd_toggle_mark}); CommandRegistry::Register({ CommandId::JumpToMark, "jump-to-mark", "Jump to mark (swap mark)", cmd_jump_to_mark }); CommandRegistry::Register({CommandId::KillRegion, "kill-region", "Kill region to kill ring", cmd_kill_region}); CommandRegistry::Register({CommandId::CopyRegion, "copy-region", "Copy region to kill ring", cmd_copy_region}); CommandRegistry::Register({ CommandId::FlushKillRing, "flush-kill-ring", "Flush kill ring", cmd_flush_kill_ring }); // Navigation CommandRegistry::Register({CommandId::MoveLeft, "left", "Move cursor left", cmd_move_left}); CommandRegistry::Register({CommandId::MoveRight, "right", "Move cursor right", cmd_move_right}); CommandRegistry::Register({CommandId::MoveUp, "up", "Move cursor up", cmd_move_up}); CommandRegistry::Register({CommandId::MoveDown, "down", "Move cursor down", cmd_move_down}); CommandRegistry::Register({CommandId::MoveHome, "home", "Move to beginning of line", cmd_move_home}); CommandRegistry::Register({CommandId::MoveEnd, "end", "Move to end of line", cmd_move_end}); CommandRegistry::Register({CommandId::PageUp, "page-up", "Page up", cmd_page_up}); CommandRegistry::Register({CommandId::PageDown, "page-down", "Page down", cmd_page_down}); CommandRegistry::Register({CommandId::ScrollUp, "scroll-up", "Scroll viewport up", cmd_scroll_up}); CommandRegistry::Register({CommandId::ScrollDown, "scroll-down", "Scroll viewport down", cmd_scroll_down}); CommandRegistry::Register({CommandId::WordPrev, "word-prev", "Move to previous word", cmd_word_prev}); CommandRegistry::Register({CommandId::WordNext, "word-next", "Move to next word", cmd_word_next}); CommandRegistry::Register({ CommandId::DeleteWordPrev, "delete-word-prev", "Delete previous word", cmd_delete_word_prev }); CommandRegistry::Register({ CommandId::DeleteWordNext, "delete-word-next", "Delete next word", cmd_delete_word_next }); CommandRegistry::Register({ CommandId::MoveCursorTo, "move-cursor-to", "Move cursor to y:x", cmd_move_cursor_to, false, false }); // Direct navigation by line number CommandRegistry::Register({ CommandId::JumpToLine, "goto-line", "Prompt for line and jump", cmd_jump_to_line_start, false, false }); // Undo/Redo CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo, false, true}); CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo, false, true}); // Region formatting CommandRegistry::Register({CommandId::IndentRegion, "indent-region", "Indent region", cmd_indent_region}); CommandRegistry::Register( {CommandId::UnindentRegion, "unindent-region", "Unindent region", cmd_unindent_region}); CommandRegistry::Register({ CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph }); // Read-only CommandRegistry::Register({ CommandId::ToggleReadOnly, "toggle-read-only", "Toggle buffer read-only", cmd_toggle_read_only }); // GUI Themes CommandRegistry::Register({CommandId::ThemeNext, "theme-next", "Cycle to next GUI theme", cmd_theme_next}); CommandRegistry::Register({CommandId::ThemePrev, "theme-prev", "Cycle to previous GUI theme", cmd_theme_prev}); // Theme by name (public in command prompt) CommandRegistry::Register({ CommandId::ThemeSetByName, "theme", "Set GUI theme by name", cmd_theme_set_by_name, true, false }); // Font by name (public) CommandRegistry::Register({ CommandId::FontSetByName, "font", "Set GUI font by name", cmd_font_set_by_name, true, false }); // Font size (public) CommandRegistry::Register({ CommandId::FontSetSize, "font-size", "Set GUI font size (pixels)", cmd_font_set_size, true, false }); // Background light/dark (public) CommandRegistry::Register({ CommandId::BackgroundSet, "background", "Set GUI background light|dark", cmd_background_set, true, false }); // Generic command prompt (C-k ;) CommandRegistry::Register({ CommandId::CommandPromptStart, "command-prompt-start", "Start generic command prompt", cmd_command_prompt_start, false, false }); // Buffer operations CommandRegistry::Register({ CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer, false, false }); // Help CommandRegistry::Register({ CommandId::ShowHelp, "help", "+HELP+ buffer with manual text", cmd_show_help, false, false }); CommandRegistry::Register({ CommandId::MarkAllAndJumpEnd, "mark-all-jump-end", "Set mark at beginning and jump to end", cmd_mark_all_and_jump_end }); // GUI CommandRegistry::Register({ CommandId::VisualFilePickerToggle, "file-picker-toggle", "Toggle visual file picker", cmd_visual_file_picker_toggle, false, false }); CommandRegistry::Register({ CommandId::VisualFontPickerToggle, "font-picker-toggle", "Show visual font picker", cmd_visual_font_picker_toggle, false, false }); // Working directory CommandRegistry::Register({ CommandId::ShowWorkingDirectory, "show-working-directory", "Show current working directory", cmd_show_working_directory, false, false }); CommandRegistry::Register({ CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory", cmd_change_working_directory_start, false, false }); // UI helpers CommandRegistry::Register( {CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status, false, false}); // Syntax highlighting (public commands) CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true}); CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true}); // Viewport control CommandRegistry::Register({ CommandId::CenterOnCursor, "center-on-cursor", "Center viewport on current line", cmd_center_on_cursor, false, false }); } bool Execute(Editor &ed, CommandId id, const std::string &arg, int count) { const Command *cmd = CommandRegistry::FindById(id); if (!cmd) return false; // If a quit confirmation was pending and the user invoked something other // than the soft quit again, cancel the pending confirmation. if (ed.QuitConfirmPending() && id != CommandId::Quit && id != CommandId::KPrefix) { ed.SetQuitConfirmPending(false); } // Reset kill chain unless this is a kill-like command (so consecutive kills append) if (id != CommandId::KillToEOL && id != CommandId::KillLine && id != CommandId::KillRegion && id != CommandId::CopyRegion && id != CommandId::DeleteWordPrev && id != CommandId::DeleteWordNext) { ed.SetKillChain(false); } // If buffer is read-only, block mutating commands outside of prompts if (!ed.PromptActive()) { Buffer *b = ed.CurrentBuffer(); if (b && b->IsReadOnly() && is_mutating_command(id)) { ed.SetStatus("Read-only buffer"); return true; // treated as handled, but no change } } // Source repeat count from editor-level universal argument per new design. int final_count = 0; if (cmd->repeatable) { final_count = ed.UArgGet(); // returns 1 if no active uarg } else { // Special-case non-repeatables that should NOT consume/clear uarg: // - KPrefix: keeps uarg for the following k-suffix command. // - UnknownKCommand / UnknownEscCommand: user mistyped; keep uarg for next try. if (id != CommandId::KPrefix && id != CommandId::UnknownKCommand && id != CommandId::UnknownEscCommand) { ed.UArgClear(); } final_count = 0; } CommandContext ctx{ed, arg, final_count}; return cmd->handler ? cmd->handler(ctx) : false; } bool Execute(Editor &ed, const std::string &name, const std::string &arg, int count) { const Command *cmd = CommandRegistry::FindByName(name); if (!cmd) return false; CommandContext ctx{ed, arg, count}; return cmd->handler ? cmd->handler(ctx) : false; }