Add regex search, search/replace, and buffer read-only mode functionality with help text
This commit is contained in:
23
.idea/workspace.xml
generated
23
.idea/workspace.xml
generated
@@ -33,9 +33,13 @@
|
|||||||
</configurations>
|
</configurations>
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Fix void crash in kge.">
|
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="add regex and search/replace functionality to editor">
|
||||||
|
<change afterPath="$PROJECT_DIR$/HelpText.cc" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/HelpText.h" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
|
||||||
@@ -43,7 +47,6 @@
|
|||||||
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/ROADMAP.md" beforeDir="false" afterPath="$PROJECT_DIR$/ROADMAP.md" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/ROADMAP.md" beforeDir="false" afterPath="$PROJECT_DIR$/ROADMAP.md" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/TerminalRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cc" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/TerminalRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/default.nix" beforeDir="false" afterPath="$PROJECT_DIR$/default.nix" afterDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -70,6 +73,7 @@
|
|||||||
<component name="HighlightingSettingsPerFile">
|
<component name="HighlightingSettingsPerFile">
|
||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||||
</component>
|
</component>
|
||||||
<component name="OptimizeOnSaveOptions">
|
<component name="OptimizeOnSaveOptions">
|
||||||
<option name="myRunOnSave" value="true" />
|
<option name="myRunOnSave" value="true" />
|
||||||
@@ -180,7 +184,7 @@
|
|||||||
<workItem from="1764539556448" duration="156000" />
|
<workItem from="1764539556448" duration="156000" />
|
||||||
<workItem from="1764539725338" duration="1075000" />
|
<workItem from="1764539725338" duration="1075000" />
|
||||||
<workItem from="1764542392763" duration="3512000" />
|
<workItem from="1764542392763" duration="3512000" />
|
||||||
<workItem from="1764548345516" duration="16453000" />
|
<workItem from="1764548345516" duration="28341000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -302,7 +306,15 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1764568264996</updated>
|
<updated>1764568264996</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="16" />
|
<task id="LOCAL-00016" summary="add regex and search/replace functionality to editor">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1764574397967</created>
|
||||||
|
<option name="number" value="00016" />
|
||||||
|
<option name="presentableId" value="LOCAL-00016" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1764574397967</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="17" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -331,7 +343,8 @@
|
|||||||
<MESSAGE value="Add buffer position display and documentation improvements. - Display buffer position prefix "[x/N]" in GUI and terminal renderers. - Improve `kte` and `kge` man pages with frontend usage details and project homepage. - Update README with GUI invocation instructions. - Bump version to 1.0.0." />
|
<MESSAGE value="Add buffer position display and documentation improvements. - Display buffer position prefix "[x/N]" in GUI and terminal renderers. - Improve `kte` and `kge` man pages with frontend usage details and project homepage. - Update README with GUI invocation instructions. - Bump version to 1.0.0." />
|
||||||
<MESSAGE value="Actually add the screenshot." />
|
<MESSAGE value="Actually add the screenshot." />
|
||||||
<MESSAGE value="Fix void crash in kge." />
|
<MESSAGE value="Fix void crash in kge." />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="Fix void crash in kge." />
|
<MESSAGE value="add regex and search/replace functionality to editor" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="add regex and search/replace functionality to editor" />
|
||||||
</component>
|
</component>
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
<expand />
|
<expand />
|
||||||
|
|||||||
10
Buffer.cc
10
Buffer.cc
@@ -36,6 +36,7 @@ Buffer::Buffer(const Buffer &other)
|
|||||||
filename_ = other.filename_;
|
filename_ = other.filename_;
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
|
read_only_ = other.read_only_;
|
||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_cury_ = other.mark_cury_;
|
||||||
@@ -60,6 +61,7 @@ Buffer::operator=(const Buffer &other)
|
|||||||
filename_ = other.filename_;
|
filename_ = other.filename_;
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
|
read_only_ = other.read_only_;
|
||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_cury_ = other.mark_cury_;
|
||||||
@@ -82,6 +84,7 @@ Buffer::Buffer(Buffer &&other) noexcept
|
|||||||
filename_(std::move(other.filename_)),
|
filename_(std::move(other.filename_)),
|
||||||
is_file_backed_(other.is_file_backed_),
|
is_file_backed_(other.is_file_backed_),
|
||||||
dirty_(other.dirty_),
|
dirty_(other.dirty_),
|
||||||
|
read_only_(other.read_only_),
|
||||||
mark_set_(other.mark_set_),
|
mark_set_(other.mark_set_),
|
||||||
mark_curx_(other.mark_curx_),
|
mark_curx_(other.mark_curx_),
|
||||||
mark_cury_(other.mark_cury_),
|
mark_cury_(other.mark_cury_),
|
||||||
@@ -112,6 +115,7 @@ Buffer::operator=(Buffer &&other) noexcept
|
|||||||
filename_ = std::move(other.filename_);
|
filename_ = std::move(other.filename_);
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
|
read_only_ = other.read_only_;
|
||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_cury_ = other.mark_cury_;
|
||||||
@@ -366,7 +370,7 @@ Buffer::insert_text(int row, int col, std::string_view text)
|
|||||||
// Split line at x
|
// Split line at x
|
||||||
std::string tail = rows_[y].substr(x);
|
std::string tail = rows_[y].substr(x);
|
||||||
rows_[y].erase(x);
|
rows_[y].erase(x);
|
||||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
|
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
||||||
y += 1;
|
y += 1;
|
||||||
x = 0;
|
x = 0;
|
||||||
remain.erase(0, pos + 1);
|
remain.erase(0, pos + 1);
|
||||||
@@ -427,7 +431,7 @@ Buffer::split_line(int row, const int col)
|
|||||||
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
||||||
const auto tail = rows_[y].substr(x);
|
const auto tail = rows_[y].substr(x);
|
||||||
rows_[y].erase(x);
|
rows_[y].erase(x);
|
||||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
|
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -455,7 +459,7 @@ Buffer::insert_row(int row, const std::string_view text)
|
|||||||
row = 0;
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) > rows_.size())
|
if (static_cast<std::size_t>(row) > rows_.size())
|
||||||
row = static_cast<int>(rows_.size());
|
row = static_cast<int>(rows_.size());
|
||||||
rows_.insert(rows_.begin() + row, std::string(text));
|
rows_.insert(rows_.begin() + row, Line(std::string(text)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
25
Buffer.h
25
Buffer.h
@@ -262,6 +262,14 @@ public:
|
|||||||
return filename_;
|
return filename_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
|
||||||
|
// This does not mark the buffer as file-backed.
|
||||||
|
void SetVirtualName(const std::string &name)
|
||||||
|
{
|
||||||
|
filename_ = name;
|
||||||
|
is_file_backed_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] bool IsFileBacked() const
|
[[nodiscard]] bool IsFileBacked() const
|
||||||
{
|
{
|
||||||
@@ -274,6 +282,22 @@ public:
|
|||||||
return dirty_;
|
return dirty_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read-only flag
|
||||||
|
[[nodiscard]] bool IsReadOnly() const
|
||||||
|
{
|
||||||
|
return read_only_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetReadOnly(bool ro)
|
||||||
|
{
|
||||||
|
read_only_ = ro;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToggleReadOnly()
|
||||||
|
{
|
||||||
|
read_only_ = !read_only_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetCursor(const std::size_t x, const std::size_t y)
|
void SetCursor(const std::size_t x, const std::size_t y)
|
||||||
{
|
{
|
||||||
@@ -365,6 +389,7 @@ private:
|
|||||||
std::string filename_;
|
std::string filename_;
|
||||||
bool is_file_backed_ = false;
|
bool is_file_backed_ = false;
|
||||||
bool dirty_ = false;
|
bool dirty_ = false;
|
||||||
|
bool read_only_ = false;
|
||||||
bool mark_set_ = false;
|
bool mark_set_ = false;
|
||||||
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ set(KTE_VERSION "1.0.4")
|
|||||||
|
|
||||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||||
set(BUILD_GUI OFF CACHE BOOL "Enable building the graphical version.")
|
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
|
||||||
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
||||||
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
|
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
|
||||||
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
||||||
@@ -55,6 +55,7 @@ set(COMMON_SOURCES
|
|||||||
Buffer.cc
|
Buffer.cc
|
||||||
Editor.cc
|
Editor.cc
|
||||||
Command.cc
|
Command.cc
|
||||||
|
HelpText.cc
|
||||||
KKeymap.cc
|
KKeymap.cc
|
||||||
TerminalInputHandler.cc
|
TerminalInputHandler.cc
|
||||||
TerminalRenderer.cc
|
TerminalRenderer.cc
|
||||||
@@ -74,6 +75,7 @@ set(COMMON_HEADERS
|
|||||||
Editor.h
|
Editor.h
|
||||||
AppendBuffer.h
|
AppendBuffer.h
|
||||||
Command.h
|
Command.h
|
||||||
|
HelpText.h
|
||||||
KKeymap.h
|
KKeymap.h
|
||||||
InputHandler.h
|
InputHandler.h
|
||||||
TerminalInputHandler.h
|
TerminalInputHandler.h
|
||||||
|
|||||||
426
Command.cc
426
Command.cc
@@ -2,11 +2,14 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
|
#include "HelpText.h"
|
||||||
|
|
||||||
|
|
||||||
// Keep buffer viewport offsets so that the cursor stays within the visible
|
// Keep buffer viewport offsets so that the cursor stays within the visible
|
||||||
@@ -62,7 +65,7 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf)
|
|||||||
std::size_t rx = 0;
|
std::size_t rx = 0;
|
||||||
const auto &lines = buf.Rows();
|
const auto &lines = buf.Rows();
|
||||||
if (cury < lines.size()) {
|
if (cury < lines.size()) {
|
||||||
rx = compute_render_x(lines[cury], curx, 8);
|
rx = compute_render_x(static_cast<std::string>(lines[cury]), curx, 8);
|
||||||
}
|
}
|
||||||
if (rx < coloffs) {
|
if (rx < coloffs) {
|
||||||
coloffs = rx;
|
coloffs = rx;
|
||||||
@@ -84,6 +87,31 @@ ensure_at_least_one_line(Buffer &buf)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 ---
|
// --- UI/status helpers ---
|
||||||
static bool
|
static bool
|
||||||
@@ -149,7 +177,7 @@ extract_region_text(const Buffer &buf, std::size_t sx, std::size_t sy, std::size
|
|||||||
}
|
}
|
||||||
// middle lines full
|
// middle lines full
|
||||||
for (std::size_t y = sy + 1; y < ey; ++y) {
|
for (std::size_t y = sy + 1; y < ey; ++y) {
|
||||||
out += rows[y];
|
out += static_cast<std::string>(rows[y]);
|
||||||
out += '\n';
|
out += '\n';
|
||||||
}
|
}
|
||||||
// last line head
|
// last line head
|
||||||
@@ -239,7 +267,7 @@ insert_text_at_cursor(Buffer &buf, const std::string &text)
|
|||||||
std::string after = rows[cur_y].substr(cur_x);
|
std::string after = rows[cur_y].substr(cur_x);
|
||||||
rows[cur_y].erase(cur_x);
|
rows[cur_y].erase(cur_x);
|
||||||
// create new line after current with the 'after' tail
|
// create new line after current with the 'after' tail
|
||||||
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(cur_y + 1), after);
|
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(cur_y + 1), Buffer::Line(after));
|
||||||
// move to start of next line
|
// move to start of next line
|
||||||
cur_y += 1;
|
cur_y += 1;
|
||||||
cur_x = 0;
|
cur_x = 0;
|
||||||
@@ -328,7 +356,7 @@ cmd_move_cursor_to(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
if (by >= lines2.size())
|
if (by >= lines2.size())
|
||||||
by = lines2.size() - 1;
|
by = lines2.size() - 1;
|
||||||
const std::string &line2 = lines2[by];
|
std::string line2 = static_cast<std::string>(lines2[by]);
|
||||||
std::size_t rx_target = bco + vx;
|
std::size_t rx_target = bco + vx;
|
||||||
std::size_t sx = inverse_render_to_source_col(line2, rx_target, 8);
|
std::size_t sx = inverse_render_to_source_col(line2, rx_target, 8);
|
||||||
row = by;
|
row = by;
|
||||||
@@ -348,7 +376,7 @@ cmd_move_cursor_to(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
if (row >= lines.size())
|
if (row >= lines.size())
|
||||||
row = lines.size() - 1;
|
row = lines.size() - 1;
|
||||||
const std::string &line = lines[row];
|
std::string line = static_cast<std::string>(lines[row]);
|
||||||
if (col > line.size())
|
if (col > line.size())
|
||||||
col = line.size();
|
col = line.size();
|
||||||
buf->SetCursor(col, row);
|
buf->SetCursor(col, row);
|
||||||
@@ -366,7 +394,7 @@ search_compute_matches(const Buffer &buf, const std::string &q)
|
|||||||
return out;
|
return out;
|
||||||
const auto &rows = buf.Rows();
|
const auto &rows = buf.Rows();
|
||||||
for (std::size_t y = 0; y < rows.size(); ++y) {
|
for (std::size_t y = 0; y < rows.size(); ++y) {
|
||||||
const std::string &line = rows[y];
|
std::string line = static_cast<std::string>(rows[y]);
|
||||||
std::size_t pos = 0;
|
std::size_t pos = 0;
|
||||||
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
|
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
|
||||||
out.emplace_back(y, pos);
|
out.emplace_back(y, pos);
|
||||||
@@ -384,6 +412,7 @@ struct RegexMatch {
|
|||||||
std::size_t len;
|
std::size_t len;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
static std::vector<RegexMatch>
|
static std::vector<RegexMatch>
|
||||||
search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std::string &err_out)
|
search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std::string &err_out)
|
||||||
{
|
{
|
||||||
@@ -395,11 +424,13 @@ search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std:
|
|||||||
const std::regex rx(pattern);
|
const std::regex rx(pattern);
|
||||||
const auto &rows = buf.Rows();
|
const auto &rows = buf.Rows();
|
||||||
for (std::size_t y = 0; y < rows.size(); ++y) {
|
for (std::size_t y = 0; y < rows.size(); ++y) {
|
||||||
const std::string &line = rows[y];
|
std::string line = static_cast<std::string>(rows[y]);
|
||||||
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
||||||
it != std::sregex_iterator(); ++it) {
|
it != std::sregex_iterator(); ++it) {
|
||||||
const auto &m = *it;
|
const auto &m = *it;
|
||||||
out.push_back(RegexMatch{y, static_cast<std::size_t>(m.position()), static_cast<std::size_t>(m.length())});
|
out.push_back(RegexMatch{
|
||||||
|
y, static_cast<std::size_t>(m.position()), static_cast<std::size_t>(m.length())
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (const std::regex_error &e) {
|
} catch (const std::regex_error &e) {
|
||||||
@@ -409,6 +440,7 @@ search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std:
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
search_apply_match_regex(Editor &ed, Buffer &buf, const std::vector<RegexMatch> &matches)
|
search_apply_match_regex(Editor &ed, Buffer &buf, const std::vector<RegexMatch> &matches)
|
||||||
{
|
{
|
||||||
@@ -727,6 +759,7 @@ cmd_find_start(CommandContext &ctx)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_regex_find_start(CommandContext &ctx)
|
cmd_regex_find_start(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -774,6 +807,30 @@ cmd_search_replace_start(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_regex_replace_start(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
|
static bool
|
||||||
cmd_open_file_start(CommandContext &ctx)
|
cmd_open_file_start(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -1065,13 +1122,17 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
// If it's a search prompt, mirror text to search state
|
// If it's a search prompt, mirror text to search state
|
||||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
|
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind) {
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind ||
|
||||||
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
|
||||||
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
|
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
|
||||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||||
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
|
||||||
std::string err;
|
std::string err;
|
||||||
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
|
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
|
||||||
if (!err.empty()) {
|
if (!err.empty()) {
|
||||||
ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
|
ctx.editor.SetStatus(
|
||||||
|
std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err +
|
||||||
|
"]");
|
||||||
}
|
}
|
||||||
if (ctx.editor.SearchIndex() >= static_cast<int>(rmatches.size()))
|
if (ctx.editor.SearchIndex() >= static_cast<int>(rmatches.size()))
|
||||||
ctx.editor.SetSearchIndex(rmatches.empty() ? -1 : 0);
|
ctx.editor.SetSearchIndex(rmatches.empty() ? -1 : 0);
|
||||||
@@ -1100,7 +1161,7 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
std::vector<std::pair<std::size_t, std::size_t> > matches;
|
std::vector<std::pair<std::size_t, std::size_t> > matches;
|
||||||
if (!q.empty()) {
|
if (!q.empty()) {
|
||||||
for (std::size_t y = 0; y < rows.size(); ++y) {
|
for (std::size_t y = 0; y < rows.size(); ++y) {
|
||||||
const std::string &line = rows[y];
|
std::string line = static_cast<std::string>(rows[y]);
|
||||||
std::size_t pos = 0;
|
std::size_t pos = 0;
|
||||||
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
|
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
|
||||||
matches.emplace_back(y, pos);
|
matches.emplace_back(y, pos);
|
||||||
@@ -1162,6 +1223,204 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
return true;
|
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<Buffer> &bufs = ctx.editor.Buffers();
|
||||||
|
std::size_t help_index = static_cast<std::size_t>(-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<char>(std::toupper(static_cast<unsigned char>(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<char>(in)), std::istreambuf_iterator<char>());
|
||||||
|
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) {
|
||||||
|
auto &rows = b.Rows();
|
||||||
|
rows.clear();
|
||||||
|
std::string line;
|
||||||
|
line.reserve(128);
|
||||||
|
for (char ch : text) {
|
||||||
|
if (ch == '\n') {
|
||||||
|
rows.emplace_back(line);
|
||||||
|
line.clear();
|
||||||
|
} else if (ch != '\r') {
|
||||||
|
line.push_back(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add last line (even if empty)
|
||||||
|
rows.emplace_back(line);
|
||||||
|
b.SetDirty(false);
|
||||||
|
b.SetCursor(0, 0);
|
||||||
|
b.SetOffsets(0, 0);
|
||||||
|
b.SetRenderX(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (help_index != static_cast<std::size_t>(-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
|
static bool
|
||||||
cmd_newline(CommandContext &ctx)
|
cmd_newline(CommandContext &ctx)
|
||||||
@@ -1198,6 +1457,16 @@ cmd_newline(CommandContext &ctx)
|
|||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
if (!buf)
|
if (!buf)
|
||||||
return false;
|
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 find = ctx.editor.ReplaceFindTmp();
|
||||||
const std::string with = value;
|
const std::string with = value;
|
||||||
ctx.editor.SetReplaceWithTmp(with);
|
ctx.editor.SetReplaceWithTmp(with);
|
||||||
@@ -1217,7 +1486,8 @@ cmd_newline(CommandContext &ctx)
|
|||||||
auto &rows = buf->Rows();
|
auto &rows = buf->Rows();
|
||||||
std::size_t total = 0;
|
std::size_t total = 0;
|
||||||
UndoSystem *u = buf->Undo();
|
UndoSystem *u = buf->Undo();
|
||||||
if (u) u->commit(); // end any pending batch
|
if (u)
|
||||||
|
u->commit(); // end any pending batch
|
||||||
for (std::size_t y = 0; y < rows.size(); ++y) {
|
for (std::size_t y = 0; y < rows.size(); ++y) {
|
||||||
std::size_t pos = 0;
|
std::size_t pos = 0;
|
||||||
while (!find.empty()) {
|
while (!find.empty()) {
|
||||||
@@ -1457,6 +1727,84 @@ cmd_newline(CommandContext &ctx)
|
|||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
ctx.editor.SetStatus(std::string("chdir failed: ") + e.what());
|
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;
|
||||||
|
}
|
||||||
|
auto &rows = buf->Rows();
|
||||||
|
std::size_t changed = 0;
|
||||||
|
for (auto &line : rows) {
|
||||||
|
std::string before = static_cast<std::string>(line);
|
||||||
|
std::string after = std::regex_replace(before, rx, repl);
|
||||||
|
if (after != before) {
|
||||||
|
line = 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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1490,7 +1838,7 @@ cmd_newline(CommandContext &ctx)
|
|||||||
tail = line.substr(x);
|
tail = line.substr(x);
|
||||||
line.erase(x);
|
line.erase(x);
|
||||||
}
|
}
|
||||||
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
|
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(y + 1), Buffer::Line(tail));
|
||||||
y += 1;
|
y += 1;
|
||||||
x = 0;
|
x = 0;
|
||||||
}
|
}
|
||||||
@@ -1514,15 +1862,19 @@ cmd_backspace(CommandContext &ctx)
|
|||||||
ctx.editor.BackspacePromptText();
|
ctx.editor.BackspacePromptText();
|
||||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
|
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind ||
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind ||
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||||
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
|
||||||
Buffer *buf2 = ctx.editor.CurrentBuffer();
|
Buffer *buf2 = ctx.editor.CurrentBuffer();
|
||||||
if (buf2) {
|
if (buf2) {
|
||||||
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
|
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
|
||||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||||
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
|
||||||
std::string err;
|
std::string err;
|
||||||
auto rm = search_compute_matches_regex(*buf2, ctx.editor.SearchQuery(), err);
|
auto rm = search_compute_matches_regex(*buf2, ctx.editor.SearchQuery(), err);
|
||||||
if (!err.empty()) {
|
if (!err.empty()) {
|
||||||
ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
|
ctx.editor.SetStatus(
|
||||||
|
std::string("Regex: ") + ctx.editor.PromptText() + " [error: "
|
||||||
|
+ err + "]");
|
||||||
}
|
}
|
||||||
search_apply_match_regex(ctx.editor, *buf2, rm);
|
search_apply_match_regex(ctx.editor, *buf2, rm);
|
||||||
} else {
|
} else {
|
||||||
@@ -1742,12 +2094,12 @@ cmd_kill_line(CommandContext &ctx)
|
|||||||
break;
|
break;
|
||||||
if (rows.size() == 1) {
|
if (rows.size() == 1) {
|
||||||
// last remaining line: clear its contents
|
// last remaining line: clear its contents
|
||||||
killed_total += rows[0];
|
killed_total += static_cast<std::string>(rows[0]);
|
||||||
rows[0].Clear();
|
rows[0].Clear();
|
||||||
y = 0;
|
y = 0;
|
||||||
} else if (y < rows.size()) {
|
} else if (y < rows.size()) {
|
||||||
// erase current line; keep y pointing at the next line
|
// erase current line; keep y pointing at the next line
|
||||||
killed_total += rows[y];
|
killed_total += static_cast<std::string>(rows[y]);
|
||||||
killed_total += "\n";
|
killed_total += "\n";
|
||||||
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y));
|
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y));
|
||||||
if (y >= rows.size()) {
|
if (y >= rows.size()) {
|
||||||
@@ -1950,7 +2302,8 @@ cmd_move_left(CommandContext &ctx)
|
|||||||
std::string err;
|
std::string err;
|
||||||
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
|
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
|
||||||
if (!err.empty()) {
|
if (!err.empty()) {
|
||||||
ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
|
ctx.editor.SetStatus(
|
||||||
|
std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
|
||||||
}
|
}
|
||||||
if (!rmatches.empty()) {
|
if (!rmatches.empty()) {
|
||||||
int idx = ctx.editor.SearchIndex();
|
int idx = ctx.editor.SearchIndex();
|
||||||
@@ -2026,7 +2379,8 @@ cmd_move_right(CommandContext &ctx)
|
|||||||
std::string err;
|
std::string err;
|
||||||
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
|
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
|
||||||
if (!err.empty()) {
|
if (!err.empty()) {
|
||||||
ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
|
ctx.editor.SetStatus(
|
||||||
|
std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
|
||||||
}
|
}
|
||||||
if (!rmatches.empty()) {
|
if (!rmatches.empty()) {
|
||||||
int idx = ctx.editor.SearchIndex();
|
int idx = ctx.editor.SearchIndex();
|
||||||
@@ -2470,7 +2824,7 @@ cmd_delete_word_prev(CommandContext &ctx)
|
|||||||
// Then collect complete lines between y and start_y
|
// Then collect complete lines between y and start_y
|
||||||
for (std::size_t ly = y + 1; ly < start_y; ++ly) {
|
for (std::size_t ly = y + 1; ly < start_y; ++ly) {
|
||||||
deleted += "\n";
|
deleted += "\n";
|
||||||
deleted += rows[ly];
|
deleted += static_cast<std::string>(rows[ly]);
|
||||||
}
|
}
|
||||||
// Finally, collect from beginning of start_y to start_x
|
// Finally, collect from beginning of start_y to start_x
|
||||||
if (start_y < rows.size()) {
|
if (start_y < rows.size()) {
|
||||||
@@ -2567,7 +2921,7 @@ cmd_delete_word_next(CommandContext &ctx)
|
|||||||
// Then collect complete lines between start_y and y
|
// Then collect complete lines between start_y and y
|
||||||
for (std::size_t ly = start_y + 1; ly < y; ++ly) {
|
for (std::size_t ly = start_y + 1; ly < y; ++ly) {
|
||||||
deleted += "\n";
|
deleted += "\n";
|
||||||
deleted += rows[ly];
|
deleted += static_cast<std::string>(rows[ly]);
|
||||||
}
|
}
|
||||||
// Finally, collect from beginning of y to x
|
// Finally, collect from beginning of y to x
|
||||||
if (y < rows.size()) {
|
if (y < rows.size()) {
|
||||||
@@ -2837,8 +3191,15 @@ InstallDefaultCommands()
|
|||||||
cmd_unknown_kcommand
|
cmd_unknown_kcommand
|
||||||
});
|
});
|
||||||
CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start});
|
CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start});
|
||||||
CommandRegistry::Register({CommandId::RegexFindStart, "regex-find-start", "Begin regex search", cmd_regex_find_start});
|
CommandRegistry::Register({
|
||||||
CommandRegistry::Register({CommandId::SearchReplace, "search-replace", "Begin search & replace", cmd_search_replace_start});
|
CommandId::RegexFindStart, "regex-find-start", "Begin regex search", cmd_regex_find_start
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::RegexpReplace, "regex-replace", "Begin regex search & replace", cmd_regex_replace_start
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::SearchReplace, "search-replace", "Begin search & replace", cmd_search_replace_start
|
||||||
|
});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start
|
CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start
|
||||||
});
|
});
|
||||||
@@ -2908,9 +3269,15 @@ InstallDefaultCommands()
|
|||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph
|
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});
|
||||||
// Buffer operations
|
// Buffer operations
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer
|
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer
|
||||||
|
});
|
||||||
|
// Help
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::ShowHelp, "help", "+HELP+ buffer with manual text", cmd_show_help
|
||||||
});
|
});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::MarkAllAndJumpEnd, "mark-all-jump-end", "Set mark at beginning and jump to end",
|
CommandId::MarkAllAndJumpEnd, "mark-all-jump-end", "Set mark at beginning and jump to end",
|
||||||
@@ -2952,6 +3319,15 @@ Execute(Editor &ed, CommandId id, const std::string &arg, int count)
|
|||||||
CommandId::CopyRegion && id != CommandId::DeleteWordPrev && id != CommandId::DeleteWordNext) {
|
CommandId::CopyRegion && id != CommandId::DeleteWordPrev && id != CommandId::DeleteWordNext) {
|
||||||
ed.SetKillChain(false);
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CommandContext ctx{ed, arg, count};
|
CommandContext ctx{ed, arg, count};
|
||||||
return cmd->handler ? cmd->handler(ctx) : false;
|
return cmd->handler ? cmd->handler(ctx) : false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ enum class CommandId {
|
|||||||
KPrefix, // show "C-k _" prompt in status when entering k-command
|
KPrefix, // show "C-k _" prompt in status when entering k-command
|
||||||
FindStart, // begin incremental search (placeholder)
|
FindStart, // begin incremental search (placeholder)
|
||||||
RegexFindStart, // begin regex search (C-r)
|
RegexFindStart, // begin regex search (C-r)
|
||||||
|
RegexpReplace, // begin regex search & replace (C-t)
|
||||||
SearchReplace, // begin search & replace (two-step prompt)
|
SearchReplace, // begin search & replace (two-step prompt)
|
||||||
OpenFileStart, // begin open-file prompt
|
OpenFileStart, // begin open-file prompt
|
||||||
VisualFilePickerToggle,
|
VisualFilePickerToggle,
|
||||||
@@ -72,6 +73,8 @@ enum class CommandId {
|
|||||||
IndentRegion, // indent region (C-k =)
|
IndentRegion, // indent region (C-k =)
|
||||||
UnindentRegion, // unindent region (C-k -)
|
UnindentRegion, // unindent region (C-k -)
|
||||||
ReflowParagraph, // reflow paragraph to column width (ESC q)
|
ReflowParagraph, // reflow paragraph to column width (ESC q)
|
||||||
|
// Read-only buffers
|
||||||
|
ToggleReadOnly, // toggle current buffer read-only (C-k ')
|
||||||
// Buffer operations
|
// Buffer operations
|
||||||
ReloadBuffer, // reload buffer from disk (C-k l)
|
ReloadBuffer, // reload buffer from disk (C-k l)
|
||||||
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a)
|
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a)
|
||||||
@@ -79,6 +82,8 @@ enum class CommandId {
|
|||||||
JumpToLine, // prompt for line and jump (C-k g)
|
JumpToLine, // prompt for line and jump (C-k g)
|
||||||
ShowWorkingDirectory, // Display the current working directory in the editor message.
|
ShowWorkingDirectory, // Display the current working directory in the editor message.
|
||||||
ChangeWorkingDirectory, // Change the editor's current directory.
|
ChangeWorkingDirectory, // Change the editor's current directory.
|
||||||
|
// Help
|
||||||
|
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
|
||||||
// Meta
|
// Meta
|
||||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||||
};
|
};
|
||||||
|
|||||||
2
Editor.h
2
Editor.h
@@ -306,6 +306,8 @@ public:
|
|||||||
None = 0,
|
None = 0,
|
||||||
Search,
|
Search,
|
||||||
RegexSearch,
|
RegexSearch,
|
||||||
|
RegexReplaceFind, // step 1 of Regex Search & Replace: find pattern
|
||||||
|
RegexReplaceWith, // step 2 of Regex Search & Replace: replacement text
|
||||||
OpenFile,
|
OpenFile,
|
||||||
SaveAs,
|
SaveAs,
|
||||||
Confirm,
|
Confirm,
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
} else {
|
} else {
|
||||||
// Convert pixel X to a render-column target including horizontal col offset
|
// Convert pixel X to a render-column target including horizontal col offset
|
||||||
// Use our own tab expansion of width 8 to match command layer logic.
|
// Use our own tab expansion of width 8 to match command layer logic.
|
||||||
const std::string &line_clicked = lines[by];
|
std::string line_clicked = static_cast<std::string>(lines[by]);
|
||||||
const std::size_t tabw = 8;
|
const std::size_t tabw = 8;
|
||||||
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
|
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
|
||||||
// then translate to viewport-space by subtracting Coloffs.
|
// then translate to viewport-space by subtracting Coloffs.
|
||||||
@@ -245,7 +245,7 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||||
// Capture the screen position before drawing the line
|
// Capture the screen position before drawing the line
|
||||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||||
const std::string &line = lines[i];
|
std::string line = static_cast<std::string>(lines[i]);
|
||||||
|
|
||||||
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
||||||
const std::size_t tabw = 8;
|
const std::size_t tabw = 8;
|
||||||
@@ -256,8 +256,8 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||||
std::vector<std::pair<std::size_t, std::size_t>> hl_src_ranges;
|
std::vector<std::pair<std::size_t, std::size_t>> hl_src_ranges;
|
||||||
if (search_mode) {
|
if (search_mode) {
|
||||||
// If we're in RegexSearch mode, compute ranges using regex; otherwise plain substring
|
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
|
||||||
if (ed.PromptActive() && ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||||
try {
|
try {
|
||||||
std::regex rx(ed.SearchQuery());
|
std::regex rx(ed.SearchQuery());
|
||||||
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
||||||
@@ -366,8 +366,7 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
||||||
// If a prompt is active, replace the entire status bar with the prompt text
|
// If a prompt is active, replace the entire status bar with the prompt text
|
||||||
if (ed.PromptActive()) {
|
if (ed.PromptActive()) {
|
||||||
std::string msg = ed.PromptLabel();
|
std::string label = ed.PromptLabel();
|
||||||
if (!msg.empty()) msg += ": ";
|
|
||||||
std::string ptext = ed.PromptText();
|
std::string ptext = ed.PromptText();
|
||||||
auto kind = ed.CurrentPromptKind();
|
auto kind = ed.CurrentPromptKind();
|
||||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||||
@@ -384,14 +383,62 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
msg += ptext;
|
|
||||||
|
|
||||||
float pad = 6.f;
|
float pad = 6.f;
|
||||||
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
|
||||||
float left_x = p0.x + pad;
|
float left_x = p0.x + pad;
|
||||||
|
float right_x = p1.x - pad;
|
||||||
|
float max_px = std::max(0.0f, right_x - left_x);
|
||||||
|
|
||||||
|
std::string prefix;
|
||||||
|
if (!label.empty()) prefix = label + ": ";
|
||||||
|
|
||||||
|
// Compose showing right-end of filename portion when too long for space
|
||||||
|
std::string final_msg;
|
||||||
|
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
|
||||||
|
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
|
||||||
|
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && avail_px > 0.0f) {
|
||||||
|
// Trim from left until it fits by pixel width
|
||||||
|
std::string tail = ptext;
|
||||||
|
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
|
||||||
|
if (tail_sz.x > avail_px) {
|
||||||
|
// Remove leading chars until it fits
|
||||||
|
// Use a simple loop; text lengths are small here
|
||||||
|
size_t start = 0;
|
||||||
|
// To avoid O(n^2) worst-case, remove chunks
|
||||||
|
while (start < tail.size()) {
|
||||||
|
// Estimate how many chars to skip based on ratio
|
||||||
|
float ratio = tail_sz.x / avail_px;
|
||||||
|
size_t skip = ratio > 1.5f ? std::min(tail.size() - start, (size_t)std::max<size_t>(1, (size_t)(tail.size() / 4))) : 1;
|
||||||
|
start += skip;
|
||||||
|
std::string candidate = tail.substr(start);
|
||||||
|
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
|
||||||
|
if (cand_sz.x <= avail_px) {
|
||||||
|
tail = candidate;
|
||||||
|
tail_sz = cand_sz;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui::CalcTextSize(tail.c_str()).x > avail_px && !tail.empty()) {
|
||||||
|
// As a last resort, ensure fit by chopping exactly
|
||||||
|
// binary reduce
|
||||||
|
size_t lo = 0, hi = tail.size();
|
||||||
|
while (lo < hi) {
|
||||||
|
size_t mid = (lo + hi) / 2;
|
||||||
|
std::string cand = tail.substr(mid);
|
||||||
|
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px) hi = mid; else lo = mid + 1;
|
||||||
|
}
|
||||||
|
tail = tail.substr(lo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final_msg = prefix + tail;
|
||||||
|
} else {
|
||||||
|
final_msg = prefix + ptext;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 msg_sz = ImGui::CalcTextSize(final_msg.c_str());
|
||||||
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
|
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
||||||
ImGui::TextUnformatted(msg.c_str());
|
ImGui::TextUnformatted(final_msg.c_str());
|
||||||
ImGui::PopClipRect();
|
ImGui::PopClipRect();
|
||||||
// Advance cursor to after the bar to keep layout consistent
|
// Advance cursor to after the bar to keep layout consistent
|
||||||
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
||||||
|
|||||||
55
HelpText.cc
Normal file
55
HelpText.cc
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* HelpText.cc - embedded/customizable help content
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "HelpText.h"
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
HelpText::Text()
|
||||||
|
{
|
||||||
|
// Customize the help text here. This string will be used by C-k h first.
|
||||||
|
// You can keep it empty to fall back to the manpage or built-in defaults.
|
||||||
|
// Note: keep newline characters as-is; the renderer splits lines on '\n'.
|
||||||
|
|
||||||
|
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 ' Toggle read-only\n"
|
||||||
|
" C-k - Unindent region\n"
|
||||||
|
" C-k = Indent region\n"
|
||||||
|
" C-k C-d Kill entire line\n"
|
||||||
|
" C-k C-q Quit now (no confirm)\n"
|
||||||
|
" C-k a Mark all and jump to end\n"
|
||||||
|
" C-k b Switch buffer\n"
|
||||||
|
" C-k c Close current buffer\n"
|
||||||
|
" C-k d Kill to end of line\n"
|
||||||
|
" C-k e Open file (prompt)\n"
|
||||||
|
" C-k g Jump to line\n"
|
||||||
|
" C-k h Show this help\n"
|
||||||
|
" C-k l Reload buffer from disk\n"
|
||||||
|
" C-k n Previous buffer\n"
|
||||||
|
" C-k o Change working directory (prompt)\n"
|
||||||
|
" C-k p Next buffer\n"
|
||||||
|
" C-k q Quit (confirm if dirty)\n"
|
||||||
|
" C-k r Redo\n"
|
||||||
|
" C-k s Save buffer\n"
|
||||||
|
" C-k u Undo\n"
|
||||||
|
" C-k v Toggle visual file picker (GUI)\n"
|
||||||
|
" C-k w Show working directory\n"
|
||||||
|
" C-k x Save and quit\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"
|
||||||
|
);
|
||||||
|
}
|
||||||
17
HelpText.h
Normal file
17
HelpText.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* HelpText.h - embedded/customizable help content
|
||||||
|
*/
|
||||||
|
#ifndef KTE_HELPTEXT_H
|
||||||
|
#define KTE_HELPTEXT_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class HelpText {
|
||||||
|
public:
|
||||||
|
// Returns the embedded help text as a single string with newlines.
|
||||||
|
// Project maintainers can customize the returned string below
|
||||||
|
// (in HelpText.cc) without touching the help command logic.
|
||||||
|
static std::string Text();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KTE_HELPTEXT_H
|
||||||
10
KKeymap.cc
10
KKeymap.cc
@@ -33,6 +33,10 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
out = CommandId::Redo; // C-k r (redo)
|
out = CommandId::Redo; // C-k r (redo)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (ascii_key == '\'') {
|
||||||
|
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
switch (k_lower) {
|
switch (k_lower) {
|
||||||
case 'a':
|
case 'a':
|
||||||
@@ -59,6 +63,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
case 'g':
|
case 'g':
|
||||||
out = CommandId::JumpToLine;
|
out = CommandId::JumpToLine;
|
||||||
return true;
|
return true;
|
||||||
|
case 'h':
|
||||||
|
out = CommandId::ShowHelp;
|
||||||
|
return true;
|
||||||
case 'j':
|
case 'j':
|
||||||
out = CommandId::JumpToMark;
|
out = CommandId::JumpToMark;
|
||||||
return true;
|
return true;
|
||||||
@@ -148,6 +155,9 @@ KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
|
|||||||
case 'r':
|
case 'r':
|
||||||
out = CommandId::RegexFindStart; // C-r regex search
|
out = CommandId::RegexFindStart; // C-r regex search
|
||||||
return true;
|
return true;
|
||||||
|
case 't':
|
||||||
|
out = CommandId::RegexpReplace; // C-t regex search & replace
|
||||||
|
return true;
|
||||||
case 'h':
|
case 'h':
|
||||||
out = CommandId::SearchReplace; // C-h: search & replace
|
out = CommandId::SearchReplace; // C-h: search & replace
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
12
ROADMAP.md
12
ROADMAP.md
@@ -1,10 +1,10 @@
|
|||||||
ROADMAP / TODO:
|
ROADMAP / TODO:
|
||||||
|
|
||||||
- [x] Search + Replace
|
- [x] Search + Replace
|
||||||
- [ ] Regex search + replace
|
- [x] Regex search + replace
|
||||||
- [ ] The undo system should actually work
|
- [ ] The undo system should actually work
|
||||||
- [ ] Able to mark buffers as read-only
|
- [x] Able to mark buffers as read-only
|
||||||
- [ ] Built-in help text
|
- [x] Built-in help text
|
||||||
- [ ] Shorten paths in the homedir with ~
|
- [x] Shorten paths in the homedir with ~
|
||||||
- [ ] When the filename is longer than the message window, scoot left to
|
- [x] When the filename is longer than the message window, scoot left to
|
||||||
to keep it in view
|
keep it in view
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||||
std::vector<std::pair<std::size_t, std::size_t>> ranges; // [start, end)
|
std::vector<std::pair<std::size_t, std::size_t>> ranges; // [start, end)
|
||||||
if (search_mode && li < lines.size()) {
|
if (search_mode && li < lines.size()) {
|
||||||
const std::string &sline = lines[li];
|
std::string sline = static_cast<std::string>(lines[li]);
|
||||||
// If regex search prompt is active, use regex to compute highlight ranges
|
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
|
||||||
if (ed.PromptActive() && ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||||
try {
|
try {
|
||||||
std::regex rx(ed.SearchQuery());
|
std::regex rx(ed.SearchQuery());
|
||||||
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
|
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
|
||||||
@@ -93,7 +93,7 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
bool cur_on = false;
|
bool cur_on = false;
|
||||||
int written = 0;
|
int written = 0;
|
||||||
if (li < lines.size()) {
|
if (li < lines.size()) {
|
||||||
const std::string &line = lines[li];
|
std::string line = static_cast<std::string>(lines[li]);
|
||||||
src_i = 0;
|
src_i = 0;
|
||||||
render_col = 0;
|
render_col = 0;
|
||||||
while (written < cols) {
|
while (written < cols) {
|
||||||
@@ -202,9 +202,7 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
// If a prompt is active, replace the status bar with the full prompt text
|
// If a prompt is active, replace the status bar with the full prompt text
|
||||||
if (ed.PromptActive()) {
|
if (ed.PromptActive()) {
|
||||||
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
|
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
|
||||||
std::string msg = ed.PromptLabel();
|
std::string label = ed.PromptLabel();
|
||||||
if (!msg.empty())
|
|
||||||
msg += ": ";
|
|
||||||
std::string ptext = ed.PromptText();
|
std::string ptext = ed.PromptText();
|
||||||
auto kind = ed.CurrentPromptKind();
|
auto kind = ed.CurrentPromptKind();
|
||||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||||
@@ -222,7 +220,30 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Prefer keeping the tail of the filename visible when it exceeds the window
|
||||||
|
std::string msg;
|
||||||
|
if (!label.empty()) {
|
||||||
|
msg = label + ": ";
|
||||||
|
}
|
||||||
|
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
|
||||||
|
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && cols > 0) {
|
||||||
|
int avail = cols - static_cast<int>(msg.size());
|
||||||
|
if (avail <= 0) {
|
||||||
|
// No room for label; fall back to showing the rightmost portion of the whole string
|
||||||
|
std::string whole = msg + ptext;
|
||||||
|
if ((int)whole.size() > cols)
|
||||||
|
whole = whole.substr(whole.size() - cols);
|
||||||
|
msg = whole;
|
||||||
|
} else {
|
||||||
|
if ((int)ptext.size() > avail) {
|
||||||
|
ptext = ptext.substr(ptext.size() - avail);
|
||||||
|
}
|
||||||
msg += ptext;
|
msg += ptext;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-file prompts: simple concatenation and clip by terminal
|
||||||
|
msg += ptext;
|
||||||
|
}
|
||||||
|
|
||||||
// Draw left-aligned, clipped to width
|
// Draw left-aligned, clipped to width
|
||||||
if (!msg.empty())
|
if (!msg.empty())
|
||||||
@@ -274,6 +295,9 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
left += fname;
|
left += fname;
|
||||||
if (b && b->Dirty())
|
if (b && b->Dirty())
|
||||||
left += " *";
|
left += " *";
|
||||||
|
// Append read-only indicator
|
||||||
|
if (b && b->IsReadOnly())
|
||||||
|
left += " [RO]";
|
||||||
// Append total line count as "<n>L"
|
// Append total line count as "<n>L"
|
||||||
if (b) {
|
if (b) {
|
||||||
unsigned long lcount = static_cast<unsigned long>(b->Rows().size());
|
unsigned long lcount = static_cast<unsigned long>(b->Rows().size());
|
||||||
|
|||||||
Reference in New Issue
Block a user