add regex and search/replace functionality to editor

This commit is contained in:
2025-11-30 23:33:17 -08:00
parent 24c8040d8a
commit 69457c424c
10 changed files with 623 additions and 199 deletions

31
.idea/workspace.xml generated
View File

@@ -33,10 +33,17 @@
</configurations> </configurations>
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Actually add the screenshot."> <list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Fix void crash in kge.">
<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$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" 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$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" /> <change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.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$/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" />
@@ -67,6 +74,9 @@
<component name="OptimizeOnSaveOptions"> <component name="OptimizeOnSaveOptions">
<option name="myRunOnSave" value="true" /> <option name="myRunOnSave" value="true" />
</component> </component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="AISelfReview" />
</component>
<component name="ProjectApplicationVersion"> <component name="ProjectApplicationVersion">
<option name="ide" value="CLion" /> <option name="ide" value="CLion" />
<option name="majorVersion" value="2025" /> <option name="majorVersion" value="2025" />
@@ -139,7 +149,7 @@
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" /> <option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method> </method>
</configuration> </configuration>
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge"> <configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="$PROJECT_DIR$/cmake-build-debug/test.txt" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
<method v="2"> <method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" /> <option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method> </method>
@@ -170,7 +180,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="12773000" /> <workItem from="1764548345516" duration="16453000" />
</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" />
@@ -284,7 +294,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1764557759844</updated> <updated>1764557759844</updated>
</task> </task>
<option name="localTasksCounter" value="15" /> <task id="LOCAL-00015" summary="Fix void crash in kge.">
<option name="closed" value="true" />
<created>1764568264996</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1764568264996</updated>
</task>
<option name="localTasksCounter" value="16" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -312,7 +330,8 @@
<MESSAGE value="Introduce file picker and GUI configuration with enhancements.&#10;&#10;- Add visual file picker for GUI with toggle support.&#10;- Introduce `GUIConfig` class for loading GUI settings from configuration file.&#10;- Refactor window initialization to support dynamic sizing based on configuration.&#10;- Add macOS-specific handling for fullscreen behavior.&#10;- Improve header inclusion order and minor code cleanup." /> <MESSAGE value="Introduce file picker and GUI configuration with enhancements.&#10;&#10;- Add visual file picker for GUI with toggle support.&#10;- Introduce `GUIConfig` class for loading GUI settings from configuration file.&#10;- Refactor window initialization to support dynamic sizing based on configuration.&#10;- Add macOS-specific handling for fullscreen behavior.&#10;- Improve header inclusion order and minor code cleanup." />
<MESSAGE value="Add buffer position display and documentation improvements.&#10;&#10;- Display buffer position prefix &quot;[x/N]&quot; in GUI and terminal renderers.&#10;- Improve `kte` and `kge` man pages with frontend usage details and project homepage.&#10;- Update README with GUI invocation instructions.&#10;- Bump version to 1.0.0." /> <MESSAGE value="Add buffer position display and documentation improvements.&#10;&#10;- Display buffer position prefix &quot;[x/N]&quot; in GUI and terminal renderers.&#10;- Improve `kte` and `kge` man pages with frontend usage details and project homepage.&#10;- Update README with GUI invocation instructions.&#10;- Bump version to 1.0.0." />
<MESSAGE value="Actually add the screenshot." /> <MESSAGE value="Actually add the screenshot." />
<option name="LAST_COMMIT_MESSAGE" value="Actually add the screenshot." /> <MESSAGE value="Fix void crash in kge." />
<option name="LAST_COMMIT_MESSAGE" value="Fix void crash in kge." />
</component> </component>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />

View File

@@ -77,13 +77,13 @@ public:
Line() = default; Line() = default;
Line(const char *s) explicit Line(const char *s)
{ {
assign_from(s ? std::string(s) : std::string()); assign_from(s ? std::string(s) : std::string());
} }
Line(const std::string &s) explicit Line(const std::string &s)
{ {
assign_from(s); assign_from(s);
} }
@@ -139,29 +139,38 @@ public:
// conversions // conversions
operator std::string() const explicit operator std::string() const
{ {
return std::string(buf_.Data() ? buf_.Data() : "", buf_.Size()); return {buf_.Data() ? buf_.Data() : "", buf_.Size()};
} }
// string-like API used by command/renderer layers (implemented via materialization for now) // string-like API used by command/renderer layers (implemented via materialization for now)
std::string substr(std::size_t pos) const [[nodiscard]] std::string substr(std::size_t pos) const
{ {
const std::size_t n = buf_.Size(); const std::size_t n = buf_.Size();
if (pos >= n) if (pos >= n)
return std::string(); return {};
return std::string(buf_.Data() + pos, n - pos); return {buf_.Data() + pos, n - pos};
} }
std::string substr(std::size_t pos, std::size_t len) const [[nodiscard]] std::string substr(std::size_t pos, std::size_t len) const
{ {
const std::size_t n = buf_.Size(); const std::size_t n = buf_.Size();
if (pos >= n) if (pos >= n)
return std::string(); return {};
const std::size_t take = (pos + len > n) ? (n - pos) : len; const std::size_t take = (pos + len > n) ? (n - pos) : len;
return std::string(buf_.Data() + pos, take); return {buf_.Data() + pos, take};
}
// minimal find() to support search within a line
[[nodiscard]] std::size_t find(const std::string &needle, const std::size_t pos = 0) const
{
// Materialize to std::string for now; Line is backed by AppendBuffer
const auto s = static_cast<std::string>(*this);
return s.find(needle, pos);
} }
@@ -266,20 +275,20 @@ public:
} }
void SetCursor(std::size_t x, std::size_t y) void SetCursor(const std::size_t x, const std::size_t y)
{ {
curx_ = x; curx_ = x;
cury_ = y; cury_ = y;
} }
void SetRenderX(std::size_t rx) void SetRenderX(const std::size_t rx)
{ {
rx_ = rx; rx_ = rx;
} }
void SetOffsets(std::size_t row, std::size_t col) void SetOffsets(const std::size_t row, const std::size_t col)
{ {
rowoffs_ = row; rowoffs_ = row;
coloffs_ = col; coloffs_ = col;
@@ -299,7 +308,7 @@ public:
} }
void SetMark(std::size_t x, std::size_t y) void SetMark(const std::size_t x, const std::size_t y)
{ {
mark_set_ = true; mark_set_ = true;
mark_curx_ = x; mark_curx_ = x;
@@ -344,7 +353,7 @@ public:
// Undo system accessors (created per-buffer) // Undo system accessors (created per-buffer)
UndoSystem *Undo(); UndoSystem *Undo();
const UndoSystem *Undo() const; [[nodiscard]] const UndoSystem *Undo() const;
private: private:
// State mirroring original C struct (without undo_tree) // State mirroring original C struct (without undo_tree)

View File

@@ -1,6 +1,7 @@
#include <algorithm> #include <algorithm>
#include <filesystem> #include <filesystem>
#include <cstdlib> #include <cstdlib>
#include <regex>
#include "Command.h" #include "Command.h"
#include "Editor.h" #include "Editor.h"
@@ -376,6 +377,67 @@ search_compute_matches(const Buffer &buf, const std::string &q)
} }
// 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<RegexMatch>
search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std::string &err_out)
{
std::vector<RegexMatch> 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) {
const std::string &line = 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<std::size_t>(m.position()), static_cast<std::size_t>(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<RegexMatch> &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<int>(matches.size()))
idx = 0;
const auto &m = matches[static_cast<std::size_t>(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 static void
search_apply_match(Editor &ed, Buffer &buf, const std::vector<std::pair<std::size_t, std::size_t> > &matches) search_apply_match(Editor &ed, Buffer &buf, const std::vector<std::pair<std::size_t, std::size_t> > &matches)
{ {
@@ -665,6 +727,52 @@ cmd_find_start(CommandContext &ctx)
return true; return true;
} }
static bool
cmd_regex_find_start(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(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 static bool
cmd_open_file_start(CommandContext &ctx) cmd_open_file_start(CommandContext &ctx)
@@ -955,13 +1063,26 @@ cmd_insert_text(CommandContext &ctx)
ctx.editor.AppendPromptText(ctx.arg); ctx.editor.AppendPromptText(ctx.arg);
// 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::ReplaceFind) {
ctx.editor.SetSearchQuery(ctx.editor.PromptText()); ctx.editor.SetSearchQuery(ctx.editor.PromptText());
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
// Keep index stable unless out of range std::string err;
if (ctx.editor.SearchIndex() >= static_cast<int>(matches.size())) auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
ctx.editor.SetSearchIndex(matches.empty() ? -1 : 0); if (!err.empty()) {
search_apply_match(ctx.editor, *buf, matches); ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
}
if (ctx.editor.SearchIndex() >= static_cast<int>(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<int>(matches.size()))
ctx.editor.SetSearchIndex(matches.empty() ? -1 : 0);
search_apply_match(ctx.editor, *buf, matches);
}
} else { } else {
// For other prompts, just echo label:text in status // For other prompts, just echo label:text in status
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText()); ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
@@ -1045,21 +1166,108 @@ cmd_insert_text(CommandContext &ctx)
static bool static bool
cmd_newline(CommandContext &ctx) cmd_newline(CommandContext &ctx)
{ {
// If a prompt is active, accept it and perform the associated action // If a prompt is active, accept it and perform the associated action
if (ctx.editor.PromptActive()) { if (ctx.editor.PromptActive()) {
Editor::PromptKind kind = ctx.editor.CurrentPromptKind(); Editor::PromptKind kind = ctx.editor.CurrentPromptKind();
std::string value = ctx.editor.PromptText(); std::string value = ctx.editor.PromptText();
ctx.editor.AcceptPrompt(); ctx.editor.AcceptPrompt();
if (kind == Editor::PromptKind::Search) { if (kind == Editor::PromptKind::Search || kind == Editor::PromptKind::RegexSearch) {
// Finish search: keep cursor where it is, clear search UI prompt // Finish search: keep cursor where it is, clear search UI prompt
ctx.editor.SetSearchActive(false); ctx.editor.SetSearchActive(false);
ctx.editor.SetSearchMatch(0, 0, 0); ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.ClearSearchOrigin(); ctx.editor.ClearSearchOrigin();
ctx.editor.SetStatus("Find done"); ctx.editor.SetStatus(kind == Editor::PromptKind::RegexSearch ? "Regex find done" : "Find done");
Buffer *b = ctx.editor.CurrentBuffer(); Buffer *b = ctx.editor.CurrentBuffer();
if (b) if (b)
ensure_cursor_visible(ctx.editor, *b); ensure_cursor_visible(ctx.editor, *b);
} else if (kind == Editor::PromptKind::OpenFile) { } 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;
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();
auto &rows = buf->Rows();
std::size_t total = 0;
UndoSystem *u = buf->Undo();
if (u) u->commit(); // end any pending batch
for (std::size_t y = 0; y < rows.size(); ++y) {
std::size_t pos = 0;
while (!find.empty()) {
pos = rows[y].find(find, pos);
if (pos == std::string::npos)
break;
// Perform delete of matched segment
rows[y].erase(pos, find.size());
if (u) {
buf->SetCursor(pos, y);
u->Begin(UndoType::Delete);
u->Append(std::string_view(find));
}
// Insert replacement
if (!with.empty()) {
rows[y].insert(pos, with);
if (u) {
buf->SetCursor(pos, y);
u->Begin(UndoType::Insert);
u->Append(std::string_view(with));
}
pos += with.size();
}
++total;
if (with.empty()) {
// Avoid infinite loop when replacing with empty
// pos remains the same; move forward by 1 to continue search
if (pos < rows[y].size())
++pos;
else
break;
}
}
}
buf->SetDirty(true);
// Restore original cursor
if (orig_y < 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; std::string err;
// Expand "~" to the user's home directory // Expand "~" to the user's home directory
auto expand_user_path = [](const std::string &in) -> std::string { auto expand_user_path = [](const std::string &in) -> std::string {
@@ -1304,12 +1512,23 @@ cmd_backspace(CommandContext &ctx)
// If a prompt is active, backspace edits the prompt text // If a prompt is active, backspace edits the prompt text
if (ctx.editor.PromptActive()) { if (ctx.editor.PromptActive()) {
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::RegexSearch) {
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());
auto matches = search_compute_matches(*buf2, ctx.editor.SearchQuery()); if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
search_apply_match(ctx.editor, *buf2, matches); 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 { } else {
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText()); ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
@@ -1723,20 +1942,41 @@ cmd_move_left(CommandContext &ctx)
if (auto *u = buf->Undo()) if (auto *u = buf->Undo())
u->commit(); u->commit();
// If a prompt is active and it's search, go to previous match // If a prompt is active and it's search, go to previous match
if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) { if (ctx.editor.PromptActive() &&
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
if (!matches.empty()) { ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
int idx = ctx.editor.SearchIndex(); ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind)) {
if (idx < 0) if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
idx = 0; std::string err;
idx = (idx - 1 + static_cast<int>(matches.size())) % static_cast<int>(matches.size()); auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
ctx.editor.SetSearchIndex(idx); if (!err.empty()) {
search_apply_match(ctx.editor, *buf, matches); ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
} else { }
search_apply_match(ctx.editor, *buf, matches); if (!rmatches.empty()) {
} int idx = ctx.editor.SearchIndex();
return true; if (idx < 0)
} idx = 0;
idx = (idx - 1 + static_cast<int>(rmatches.size())) % static_cast<int>(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<int>(matches.size())) % static_cast<int>(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()) { if (ctx.editor.SearchActive()) {
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
if (!matches.empty()) { if (!matches.empty()) {
@@ -1778,20 +2018,41 @@ cmd_move_right(CommandContext &ctx)
return false; return false;
if (auto *u = buf->Undo()) if (auto *u = buf->Undo())
u->commit(); u->commit();
if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) { if (ctx.editor.PromptActive() &&
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
if (!matches.empty()) { ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
int idx = ctx.editor.SearchIndex(); ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind)) {
if (idx < 0) if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
idx = 0; std::string err;
idx = (idx + 1) % static_cast<int>(matches.size()); auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
ctx.editor.SetSearchIndex(idx); if (!err.empty()) {
search_apply_match(ctx.editor, *buf, matches); ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
} else { }
search_apply_match(ctx.editor, *buf, matches); if (!rmatches.empty()) {
} int idx = ctx.editor.SearchIndex();
return true; if (idx < 0)
} idx = 0;
idx = (idx + 1) % static_cast<int>(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<int>(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()) { if (ctx.editor.SearchActive()) {
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery()); auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
if (!matches.empty()) { if (!matches.empty()) {
@@ -2575,7 +2836,9 @@ InstallDefaultCommands()
CommandId::UnknownKCommand, "unknown-k", "Unknown k-command (status)", CommandId::UnknownKCommand, "unknown-k", "Unknown k-command (status)",
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({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
}); });

View File

@@ -23,8 +23,9 @@ enum class CommandId {
Refresh, // force redraw Refresh, // force redraw
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)
SearchReplace, // begin search & replace (two-step prompt)
OpenFileStart, // begin open-file prompt OpenFileStart, // begin open-file prompt
// GUI: visual file picker
VisualFilePickerToggle, VisualFilePickerToggle,
// Buffers // Buffers
BufferSwitchStart, // begin buffer switch prompt BufferSwitchStart, // begin buffer switch prompt

View File

@@ -301,8 +301,20 @@ public:
} }
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) --- // --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch, GotoLine, Chdir }; enum class PromptKind {
None = 0,
Search,
RegexSearch,
OpenFile,
SaveAs,
Confirm,
BufferSwitch,
GotoLine,
Chdir,
ReplaceFind, // step 1 of Search & Replace: find what
ReplaceWith // step 2 of Search & Replace: replace with
};
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial) void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
@@ -504,9 +516,20 @@ private:
std::string prompt_text_; std::string prompt_text_;
std::string pending_overwrite_path_; std::string pending_overwrite_path_;
// GUI-only state (safe no-op in terminal builds) // GUI-only state (safe no-op in terminal builds)
bool file_picker_visible_ = false; bool file_picker_visible_ = false;
std::string file_picker_dir_; std::string file_picker_dir_;
// Temporary state for Search & Replace flow
public:
void SetReplaceFindTmp(const std::string &s) { replace_find_tmp_ = s; }
void SetReplaceWithTmp(const std::string &s) { replace_with_tmp_ = s; }
[[nodiscard]] const std::string &ReplaceFindTmp() const { return replace_find_tmp_; }
[[nodiscard]] const std::string &ReplaceWithTmp() const { return replace_with_tmp_; }
private:
std::string replace_find_tmp_;
std::string replace_with_tmp_;
}; };
#endif // KTE_EDITOR_H #endif // KTE_EDITOR_H

View File

@@ -7,6 +7,7 @@
#include <string> #include <string>
#include <imgui.h> #include <imgui.h>
#include <regex>
#include "GUIRenderer.h" #include "GUIRenderer.h"
#include "Buffer.h" #include "Buffer.h"
@@ -241,31 +242,92 @@ GUIRenderer::Draw(Editor &ed)
} }
// Cache current horizontal offset in rendered columns // Cache current horizontal offset in rendered columns
const std::size_t coloffs_now = buf->Coloffs(); const std::size_t coloffs_now = buf->Coloffs();
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]; const std::string &line = 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;
std::string expanded; std::string expanded;
expanded.reserve(line.size() + 16); expanded.reserve(line.size() + 16);
std::size_t rx_abs_draw = 0; // rendered column for drawing std::size_t rx_abs_draw = 0; // rendered column for drawing
// Emit entire line (ImGui child scrolling will handle clipping) // Compute search highlight ranges for this line in source indices
for (std::size_t src = 0; src < line.size(); ++src) { bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
char c = line[src]; std::vector<std::pair<std::size_t, std::size_t>> hl_src_ranges;
if (c == '\t') { if (search_mode) {
std::size_t adv = (tabw - (rx_abs_draw % tabw)); // If we're in RegexSearch mode, compute ranges using regex; otherwise plain substring
// Emit spaces for the tab if (ed.PromptActive() && ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
expanded.append(adv, ' '); try {
rx_abs_draw += adv; std::regex rx(ed.SearchQuery());
} else { for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
expanded.push_back(c); it != std::sregex_iterator(); ++it) {
rx_abs_draw += 1; const auto &m = *it;
} std::size_t sx = static_cast<std::size_t>(m.position());
} std::size_t ex = sx + static_cast<std::size_t>(m.length());
hl_src_ranges.emplace_back(sx, ex);
}
} catch (const std::regex_error &) {
// ignore invalid patterns here; status line already shows the error
}
} else {
const std::string &q = ed.SearchQuery();
std::size_t pos = 0;
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
hl_src_ranges.emplace_back(pos, pos + q.size());
pos += q.size();
}
}
}
auto src_to_rx = [&](std::size_t upto_src_exclusive) -> std::size_t {
std::size_t rx = 0;
std::size_t s = 0;
while (s < upto_src_exclusive && s < line.size()) {
if (line[s] == '\t')
rx += (tabw - (rx % tabw));
else
rx += 1;
++s;
}
return rx;
};
// Draw background highlights (under text)
if (search_mode && !hl_src_ranges.empty()) {
// Current match emphasis
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
for (const auto &rg : hl_src_ranges) {
std::size_t sx = rg.first, ex = rg.second;
std::size_t rx_start = src_to_rx(sx);
std::size_t rx_end = src_to_rx(ex);
// Apply horizontal scroll offset
if (rx_end <= coloffs_now) continue; // fully left of view
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
std::size_t vx1 = rx_end - coloffs_now;
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w, line_pos.y + line_h);
// Choose color: current match stronger
bool is_current = has_current && sx == cur_x && ex == cur_end;
ImU32 col = is_current ? IM_COL32(255, 220, 120, 140) : IM_COL32(200, 200, 0, 90);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
}
}
// Emit entire line (ImGui child scrolling will handle clipping)
for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src];
if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw));
// Emit spaces for the tab
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
ImGui::TextUnformatted(expanded.c_str()); ImGui::TextUnformatted(expanded.c_str());
// Draw a visible cursor indicator on the current line // Draw a visible cursor indicator on the current line
if (i == cy) { if (i == cy) {

View File

@@ -145,6 +145,12 @@ KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
case 's': case 's':
out = CommandId::FindStart; out = CommandId::FindStart;
return true; return true;
case 'r':
out = CommandId::RegexFindStart; // C-r regex search
return true;
case 'h':
out = CommandId::SearchReplace; // C-h: search & replace
return true;
case 'l': case 'l':
out = CommandId::Refresh; out = CommandId::Refresh;
return true; return true;

View File

@@ -1,6 +1,6 @@
ROADMAP / TODO: ROADMAP / TODO:
- [ ] Search + Replace - [x] Search + Replace
- [ ] Regex search + replace - [ ] Regex search + replace
- [ ] The undo system should actually work - [ ] The undo system should actually work
- [ ] Able to mark buffers as read-only - [ ] Able to mark buffers as read-only

View File

@@ -3,6 +3,7 @@
#include <filesystem> #include <filesystem>
#include <cstdlib> #include <cstdlib>
#include <ncurses.h> #include <ncurses.h>
#include <regex>
#include <string> #include <string>
#include "TerminalRenderer.h" #include "TerminalRenderer.h"
@@ -40,100 +41,140 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t coloffs = buf->Coloffs(); std::size_t coloffs = buf->Coloffs();
const int tabw = 8; const int tabw = 8;
for (int r = 0; r < content_rows; ++r) { for (int r = 0; r < content_rows; ++r) {
move(r, 0); move(r, 0);
std::size_t li = rowoffs + static_cast<std::size_t>(r); std::size_t li = rowoffs + static_cast<std::size_t>(r);
std::size_t render_col = 0; std::size_t render_col = 0;
std::size_t src_i = 0; std::size_t src_i = 0;
bool do_hl = ed.SearchActive() && li == ed.SearchMatchY() && ed.SearchMatchLen() > 0; // Compute matches for this line if search highlighting is active
std::size_t mx = do_hl ? ed.SearchMatchX() : 0; bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::size_t mlen = do_hl ? ed.SearchMatchLen() : 0; std::vector<std::pair<std::size_t, std::size_t>> ranges; // [start, end)
bool hl_on = false; if (search_mode && li < lines.size()) {
int written = 0; const std::string &sline = lines[li];
if (li < lines.size()) { // If regex search prompt is active, use regex to compute highlight ranges
const std::string &line = lines[li]; if (ed.PromptActive() && ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
src_i = 0; try {
render_col = 0; std::regex rx(ed.SearchQuery());
while (written < cols) { for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
char ch = ' '; it != std::sregex_iterator(); ++it) {
bool from_src = false; const auto &m = *it;
if (src_i < line.size()) { std::size_t sx = static_cast<std::size_t>(m.position());
unsigned char c = static_cast<unsigned char>(line[src_i]); std::size_t ex = sx + static_cast<std::size_t>(m.length());
if (c == '\t') { ranges.emplace_back(sx, ex);
std::size_t next_tab = tabw - (render_col % tabw); }
if (render_col + next_tab <= coloffs) { } catch (const std::regex_error &) {
render_col += next_tab; // ignore invalid patterns here; status shows error
++src_i; }
continue; } else {
} const std::string &q = ed.SearchQuery();
// Emit spaces for tab std::size_t pos = 0;
if (render_col < coloffs) { while (!q.empty() && (pos = sline.find(q, pos)) != std::string::npos) {
// skip to coloffs ranges.emplace_back(pos, pos + q.size());
std::size_t to_skip = std::min<std::size_t>( pos += q.size();
next_tab, coloffs - render_col); }
render_col += to_skip; }
next_tab -= to_skip; }
} auto is_src_in_hl = [&](std::size_t si) -> bool {
// Now render visible spaces if (ranges.empty()) return false;
while (next_tab > 0 && written < cols) { // ranges are non-overlapping and ordered by construction
bool in_hl = do_hl && src_i >= mx && src_i < mx + mlen; // linear scan is fine for now
// highlight by source index for (const auto &rg : ranges) {
if (in_hl && !hl_on) { if (si < rg.first) break;
attron(A_STANDOUT); if (si >= rg.first && si < rg.second) return true;
hl_on = true; }
} return false;
if (!in_hl && hl_on) { };
attroff(A_STANDOUT); // Track current-match to optionally emphasize
hl_on = false; const bool has_current = ed.SearchActive() && ed.SearchMatchLen() > 0;
} const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
addch(' '); const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
++written; const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
++render_col; bool hl_on = false;
--next_tab; bool cur_on = false;
} int written = 0;
++src_i; if (li < lines.size()) {
continue; const std::string &line = lines[li];
} else { src_i = 0;
// normal char render_col = 0;
if (render_col < coloffs) { while (written < cols) {
++render_col; char ch = ' ';
++src_i; bool from_src = false;
continue; if (src_i < line.size()) {
} unsigned char c = static_cast<unsigned char>(line[src_i]);
ch = static_cast<char>(c); if (c == '\t') {
from_src = true; std::size_t next_tab = tabw - (render_col % tabw);
} if (render_col + next_tab <= coloffs) {
} else { render_col += next_tab;
// beyond EOL, fill spaces ++src_i;
ch = ' '; continue;
from_src = false; }
} // Emit spaces for tab
if (do_hl) { if (render_col < coloffs) {
bool in_hl = from_src && src_i >= mx && src_i < mx + mlen; // skip to coloffs
if (in_hl && !hl_on) { std::size_t to_skip = std::min<std::size_t>(
attron(A_STANDOUT); next_tab, coloffs - render_col);
hl_on = true; render_col += to_skip;
} next_tab -= to_skip;
if (!in_hl && hl_on) { }
attroff(A_STANDOUT); // Now render visible spaces
hl_on = false; while (next_tab > 0 && written < cols) {
} bool in_hl = search_mode && is_src_in_hl(src_i);
} bool in_cur = has_current && li == cur_my && src_i >= cur_mx && src_i < cur_mend;
addch(static_cast<unsigned char>(ch)); // Toggle highlight attributes
++written; int attr = 0;
++render_col; if (in_hl) attr |= A_STANDOUT;
if (from_src) if (in_cur) attr |= A_BOLD;
++src_i; if ((attr & A_STANDOUT) && !hl_on) { attron(A_STANDOUT); hl_on = true; }
if (src_i >= line.size() && written >= cols) if (!(attr & A_STANDOUT) && hl_on) { attroff(A_STANDOUT); hl_on = false; }
break; if ((attr & A_BOLD) && !cur_on) { attron(A_BOLD); cur_on = true; }
} if (!(attr & A_BOLD) && cur_on) { attroff(A_BOLD); cur_on = false; }
} addch(' ');
if (hl_on) { ++written;
attroff(A_STANDOUT); ++render_col;
hl_on = false; --next_tab;
} }
clrtoeol(); ++src_i;
} continue;
} else {
// normal char
if (render_col < coloffs) {
++render_col;
++src_i;
continue;
}
ch = static_cast<char>(c);
from_src = true;
}
} else {
// beyond EOL, fill spaces
ch = ' ';
from_src = false;
}
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
bool in_cur = has_current && li == cur_my && from_src && src_i >= cur_mx && src_i < cur_mend;
if (in_hl && !hl_on) { attron(A_STANDOUT); hl_on = true; }
if (!in_hl && hl_on) { attroff(A_STANDOUT); hl_on = false; }
if (in_cur && !cur_on) { attron(A_BOLD); cur_on = true; }
if (!in_cur && cur_on) { attroff(A_BOLD); cur_on = false; }
addch(static_cast<unsigned char>(ch));
++written;
++render_col;
if (from_src)
++src_i;
if (src_i >= line.size() && written >= cols)
break;
}
}
if (hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (cur_on) {
attroff(A_BOLD);
cur_on = false;
}
clrtoeol();
}
// Place terminal cursor at logical position accounting for tabs and coloffs // Place terminal cursor at logical position accounting for tabs and coloffs
std::size_t cy = buf->Cury(); std::size_t cy = buf->Cury();

View File

@@ -8,7 +8,7 @@
xorg, xorg,
installShellFiles, installShellFiles,
graphical ? true, graphical ? false,
... ...
}: }:
let let