add regex and search/replace functionality to editor
This commit is contained in:
31
.idea/workspace.xml
generated
31
.idea/workspace.xml
generated
@@ -33,10 +33,17 @@
|
||||
</configurations>
|
||||
</component>
|
||||
<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$/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$/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>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -67,6 +74,9 @@
|
||||
<component name="OptimizeOnSaveOptions">
|
||||
<option name="myRunOnSave" value="true" />
|
||||
</component>
|
||||
<component name="ProblemsViewState">
|
||||
<option name="selectedTabId" value="AISelfReview" />
|
||||
</component>
|
||||
<component name="ProjectApplicationVersion">
|
||||
<option name="ide" value="CLion" />
|
||||
<option name="majorVersion" value="2025" />
|
||||
@@ -139,7 +149,7 @@
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
</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">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
</method>
|
||||
@@ -170,7 +180,7 @@
|
||||
<workItem from="1764539556448" duration="156000" />
|
||||
<workItem from="1764539725338" duration="1075000" />
|
||||
<workItem from="1764542392763" duration="3512000" />
|
||||
<workItem from="1764548345516" duration="12773000" />
|
||||
<workItem from="1764548345516" duration="16453000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
||||
<option name="closed" value="true" />
|
||||
@@ -284,7 +294,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764557759844</updated>
|
||||
</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 />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -312,7 +330,8 @@
|
||||
<MESSAGE value="Introduce file picker and GUI configuration with enhancements. - Add visual file picker for GUI with toggle support. - Introduce `GUIConfig` class for loading GUI settings from configuration file. - Refactor window initialization to support dynamic sizing based on configuration. - Add macOS-specific handling for fullscreen behavior. - Improve header inclusion order and minor code cleanup." />
|
||||
<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." />
|
||||
<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 name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
|
||||
39
Buffer.h
39
Buffer.h
@@ -77,13 +77,13 @@ public:
|
||||
Line() = default;
|
||||
|
||||
|
||||
Line(const char *s)
|
||||
explicit Line(const char *s)
|
||||
{
|
||||
assign_from(s ? std::string(s) : std::string());
|
||||
}
|
||||
|
||||
|
||||
Line(const std::string &s)
|
||||
explicit Line(const std::string &s)
|
||||
{
|
||||
assign_from(s);
|
||||
}
|
||||
@@ -139,29 +139,38 @@ public:
|
||||
|
||||
|
||||
// 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)
|
||||
std::string substr(std::size_t pos) const
|
||||
[[nodiscard]] std::string substr(std::size_t pos) const
|
||||
{
|
||||
const std::size_t n = buf_.Size();
|
||||
if (pos >= n)
|
||||
return std::string();
|
||||
return std::string(buf_.Data() + pos, n - pos);
|
||||
return {};
|
||||
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();
|
||||
if (pos >= n)
|
||||
return std::string();
|
||||
return {};
|
||||
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;
|
||||
cury_ = y;
|
||||
}
|
||||
|
||||
|
||||
void SetRenderX(std::size_t rx)
|
||||
void SetRenderX(const std::size_t 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;
|
||||
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_curx_ = x;
|
||||
@@ -344,7 +353,7 @@ public:
|
||||
// Undo system accessors (created per-buffer)
|
||||
UndoSystem *Undo();
|
||||
|
||||
const UndoSystem *Undo() const;
|
||||
[[nodiscard]] const UndoSystem *Undo() const;
|
||||
|
||||
private:
|
||||
// State mirroring original C struct (without undo_tree)
|
||||
|
||||
369
Command.cc
369
Command.cc
@@ -1,6 +1,7 @@
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <cstdlib>
|
||||
#include <regex>
|
||||
|
||||
#include "Command.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
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
cmd_open_file_start(CommandContext &ctx)
|
||||
@@ -955,13 +1063,26 @@ cmd_insert_text(CommandContext &ctx)
|
||||
|
||||
ctx.editor.AppendPromptText(ctx.arg);
|
||||
// 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());
|
||||
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);
|
||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
||||
std::string err;
|
||||
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
|
||||
if (!err.empty()) {
|
||||
ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
|
||||
}
|
||||
if (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 {
|
||||
// For other prompts, just echo label:text in status
|
||||
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
|
||||
@@ -1045,21 +1166,108 @@ cmd_insert_text(CommandContext &ctx)
|
||||
static bool
|
||||
cmd_newline(CommandContext &ctx)
|
||||
{
|
||||
// If a prompt is active, accept it and perform the associated action
|
||||
if (ctx.editor.PromptActive()) {
|
||||
Editor::PromptKind kind = ctx.editor.CurrentPromptKind();
|
||||
std::string value = ctx.editor.PromptText();
|
||||
ctx.editor.AcceptPrompt();
|
||||
if (kind == Editor::PromptKind::Search) {
|
||||
// Finish search: keep cursor where it is, clear search UI prompt
|
||||
ctx.editor.SetSearchActive(false);
|
||||
ctx.editor.SetSearchMatch(0, 0, 0);
|
||||
ctx.editor.ClearSearchOrigin();
|
||||
ctx.editor.SetStatus("Find done");
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (b)
|
||||
ensure_cursor_visible(ctx.editor, *b);
|
||||
} else if (kind == Editor::PromptKind::OpenFile) {
|
||||
// If a prompt is active, accept it and perform the associated action
|
||||
if (ctx.editor.PromptActive()) {
|
||||
Editor::PromptKind kind = ctx.editor.CurrentPromptKind();
|
||||
std::string value = ctx.editor.PromptText();
|
||||
ctx.editor.AcceptPrompt();
|
||||
if (kind == Editor::PromptKind::Search || kind == Editor::PromptKind::RegexSearch) {
|
||||
// Finish search: keep cursor where it is, clear search UI prompt
|
||||
ctx.editor.SetSearchActive(false);
|
||||
ctx.editor.SetSearchMatch(0, 0, 0);
|
||||
ctx.editor.ClearSearchOrigin();
|
||||
ctx.editor.SetStatus(kind == Editor::PromptKind::RegexSearch ? "Regex find done" : "Find done");
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (b)
|
||||
ensure_cursor_visible(ctx.editor, *b);
|
||||
} else if (kind == Editor::PromptKind::ReplaceFind) {
|
||||
// Proceed to replacement text prompt
|
||||
ctx.editor.SetReplaceFindTmp(value);
|
||||
// Keep search highlights active using the collected find string
|
||||
ctx.editor.SetSearchActive(true);
|
||||
ctx.editor.SetSearchQuery(value);
|
||||
if (Buffer *b = ctx.editor.CurrentBuffer()) {
|
||||
auto matches = search_compute_matches(*b, ctx.editor.SearchQuery());
|
||||
search_apply_match(ctx.editor, *b, matches);
|
||||
}
|
||||
ctx.editor.StartPrompt(Editor::PromptKind::ReplaceWith, "Replace: with", "");
|
||||
ctx.editor.SetStatus("Replace: with: ");
|
||||
return true;
|
||||
} else if (kind == Editor::PromptKind::ReplaceWith) {
|
||||
// Execute replace-all
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf)
|
||||
return false;
|
||||
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;
|
||||
// Expand "~" to the user's home directory
|
||||
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 (ctx.editor.PromptActive()) {
|
||||
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();
|
||||
if (buf2) {
|
||||
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
|
||||
auto matches = search_compute_matches(*buf2, ctx.editor.SearchQuery());
|
||||
search_apply_match(ctx.editor, *buf2, matches);
|
||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
||||
std::string err;
|
||||
auto rm = search_compute_matches_regex(*buf2, ctx.editor.SearchQuery(), err);
|
||||
if (!err.empty()) {
|
||||
ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
|
||||
}
|
||||
search_apply_match_regex(ctx.editor, *buf2, rm);
|
||||
} else {
|
||||
auto matches = search_compute_matches(*buf2, ctx.editor.SearchQuery());
|
||||
search_apply_match(ctx.editor, *buf2, matches);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
|
||||
@@ -1723,20 +1942,41 @@ cmd_move_left(CommandContext &ctx)
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
// If a prompt is active and it's search, go to previous match
|
||||
if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
|
||||
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.PromptActive() &&
|
||||
(ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
|
||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind)) {
|
||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
||||
std::string err;
|
||||
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
|
||||
if (!err.empty()) {
|
||||
ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
|
||||
}
|
||||
if (!rmatches.empty()) {
|
||||
int idx = ctx.editor.SearchIndex();
|
||||
if (idx < 0)
|
||||
idx = 0;
|
||||
idx = (idx - 1 + static_cast<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()) {
|
||||
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
|
||||
if (!matches.empty()) {
|
||||
@@ -1778,20 +2018,41 @@ cmd_move_right(CommandContext &ctx)
|
||||
return false;
|
||||
if (auto *u = buf->Undo())
|
||||
u->commit();
|
||||
if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
|
||||
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.PromptActive() &&
|
||||
(ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
|
||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind)) {
|
||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
||||
std::string err;
|
||||
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
|
||||
if (!err.empty()) {
|
||||
ctx.editor.SetStatus(std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
|
||||
}
|
||||
if (!rmatches.empty()) {
|
||||
int idx = ctx.editor.SearchIndex();
|
||||
if (idx < 0)
|
||||
idx = 0;
|
||||
idx = (idx + 1) % static_cast<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()) {
|
||||
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
|
||||
if (!matches.empty()) {
|
||||
@@ -2575,7 +2836,9 @@ InstallDefaultCommands()
|
||||
CommandId::UnknownKCommand, "unknown-k", "Unknown k-command (status)",
|
||||
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({
|
||||
CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start
|
||||
});
|
||||
|
||||
@@ -23,8 +23,9 @@ enum class CommandId {
|
||||
Refresh, // force redraw
|
||||
KPrefix, // show "C-k _" prompt in status when entering k-command
|
||||
FindStart, // begin incremental search (placeholder)
|
||||
RegexFindStart, // begin regex search (C-r)
|
||||
SearchReplace, // begin search & replace (two-step prompt)
|
||||
OpenFileStart, // begin open-file prompt
|
||||
// GUI: visual file picker
|
||||
VisualFilePickerToggle,
|
||||
// Buffers
|
||||
BufferSwitchStart, // begin buffer switch prompt
|
||||
|
||||
33
Editor.h
33
Editor.h
@@ -301,8 +301,20 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
||||
enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch, GotoLine, Chdir };
|
||||
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
||||
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)
|
||||
@@ -504,9 +516,20 @@ private:
|
||||
std::string prompt_text_;
|
||||
std::string pending_overwrite_path_;
|
||||
|
||||
// GUI-only state (safe no-op in terminal builds)
|
||||
bool file_picker_visible_ = false;
|
||||
std::string file_picker_dir_;
|
||||
// GUI-only state (safe no-op in terminal builds)
|
||||
bool file_picker_visible_ = false;
|
||||
std::string file_picker_dir_;
|
||||
|
||||
// Temporary state for Search & Replace flow
|
||||
public:
|
||||
void SetReplaceFindTmp(const std::string &s) { replace_find_tmp_ = s; }
|
||||
void SetReplaceWithTmp(const std::string &s) { replace_with_tmp_ = s; }
|
||||
[[nodiscard]] const std::string &ReplaceFindTmp() const { return replace_find_tmp_; }
|
||||
[[nodiscard]] const std::string &ReplaceWithTmp() const { return replace_with_tmp_; }
|
||||
|
||||
private:
|
||||
std::string replace_find_tmp_;
|
||||
std::string replace_with_tmp_;
|
||||
};
|
||||
|
||||
#endif // KTE_EDITOR_H
|
||||
|
||||
108
GUIRenderer.cc
108
GUIRenderer.cc
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <regex>
|
||||
|
||||
#include "GUIRenderer.h"
|
||||
#include "Buffer.h"
|
||||
@@ -241,31 +242,92 @@ GUIRenderer::Draw(Editor &ed)
|
||||
}
|
||||
// Cache current horizontal offset in rendered columns
|
||||
const std::size_t coloffs_now = buf->Coloffs();
|
||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||
// Capture the screen position before drawing the line
|
||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||
const std::string &line = lines[i];
|
||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||
// Capture the screen position before drawing the line
|
||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||
const std::string &line = lines[i];
|
||||
|
||||
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
||||
const std::size_t tabw = 8;
|
||||
std::string expanded;
|
||||
expanded.reserve(line.size() + 16);
|
||||
std::size_t rx_abs_draw = 0; // rendered column for drawing
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
||||
const std::size_t tabw = 8;
|
||||
std::string expanded;
|
||||
expanded.reserve(line.size() + 16);
|
||||
std::size_t rx_abs_draw = 0; // rendered column for drawing
|
||||
// Compute search highlight ranges for this line in source indices
|
||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||
std::vector<std::pair<std::size_t, std::size_t>> hl_src_ranges;
|
||||
if (search_mode) {
|
||||
// If we're in RegexSearch mode, compute ranges using regex; otherwise plain substring
|
||||
if (ed.PromptActive() && ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
||||
try {
|
||||
std::regex rx(ed.SearchQuery());
|
||||
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
||||
it != std::sregex_iterator(); ++it) {
|
||||
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
|
||||
if (i == cy) {
|
||||
|
||||
@@ -145,6 +145,12 @@ KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
|
||||
case 's':
|
||||
out = CommandId::FindStart;
|
||||
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':
|
||||
out = CommandId::Refresh;
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ROADMAP / TODO:
|
||||
|
||||
- [ ] Search + Replace
|
||||
- [x] Search + Replace
|
||||
- [ ] Regex search + replace
|
||||
- [ ] The undo system should actually work
|
||||
- [ ] Able to mark buffers as read-only
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <filesystem>
|
||||
#include <cstdlib>
|
||||
#include <ncurses.h>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
|
||||
#include "TerminalRenderer.h"
|
||||
@@ -40,100 +41,140 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
std::size_t coloffs = buf->Coloffs();
|
||||
|
||||
const int tabw = 8;
|
||||
for (int r = 0; r < content_rows; ++r) {
|
||||
move(r, 0);
|
||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||
std::size_t render_col = 0;
|
||||
std::size_t src_i = 0;
|
||||
bool do_hl = ed.SearchActive() && li == ed.SearchMatchY() && ed.SearchMatchLen() > 0;
|
||||
std::size_t mx = do_hl ? ed.SearchMatchX() : 0;
|
||||
std::size_t mlen = do_hl ? ed.SearchMatchLen() : 0;
|
||||
bool hl_on = false;
|
||||
int written = 0;
|
||||
if (li < lines.size()) {
|
||||
const std::string &line = lines[li];
|
||||
src_i = 0;
|
||||
render_col = 0;
|
||||
while (written < cols) {
|
||||
char ch = ' ';
|
||||
bool from_src = false;
|
||||
if (src_i < line.size()) {
|
||||
unsigned char c = static_cast<unsigned char>(line[src_i]);
|
||||
if (c == '\t') {
|
||||
std::size_t next_tab = tabw - (render_col % tabw);
|
||||
if (render_col + next_tab <= coloffs) {
|
||||
render_col += next_tab;
|
||||
++src_i;
|
||||
continue;
|
||||
}
|
||||
// Emit spaces for tab
|
||||
if (render_col < coloffs) {
|
||||
// skip to coloffs
|
||||
std::size_t to_skip = std::min<std::size_t>(
|
||||
next_tab, coloffs - render_col);
|
||||
render_col += to_skip;
|
||||
next_tab -= to_skip;
|
||||
}
|
||||
// Now render visible spaces
|
||||
while (next_tab > 0 && written < cols) {
|
||||
bool in_hl = do_hl && src_i >= mx && src_i < mx + mlen;
|
||||
// highlight by source index
|
||||
if (in_hl && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
addch(' ');
|
||||
++written;
|
||||
++render_col;
|
||||
--next_tab;
|
||||
}
|
||||
++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;
|
||||
}
|
||||
if (do_hl) {
|
||||
bool in_hl = from_src && src_i >= mx && src_i < mx + mlen;
|
||||
if (in_hl && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_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;
|
||||
}
|
||||
clrtoeol();
|
||||
}
|
||||
for (int r = 0; r < content_rows; ++r) {
|
||||
move(r, 0);
|
||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||
std::size_t render_col = 0;
|
||||
std::size_t src_i = 0;
|
||||
// Compute matches for this line if search highlighting is active
|
||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||
std::vector<std::pair<std::size_t, std::size_t>> ranges; // [start, end)
|
||||
if (search_mode && li < lines.size()) {
|
||||
const std::string &sline = lines[li];
|
||||
// If regex search prompt is active, use regex to compute highlight ranges
|
||||
if (ed.PromptActive() && ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
||||
try {
|
||||
std::regex rx(ed.SearchQuery());
|
||||
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
|
||||
it != std::sregex_iterator(); ++it) {
|
||||
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());
|
||||
ranges.emplace_back(sx, ex);
|
||||
}
|
||||
} catch (const std::regex_error &) {
|
||||
// ignore invalid patterns here; status shows error
|
||||
}
|
||||
} else {
|
||||
const std::string &q = ed.SearchQuery();
|
||||
std::size_t pos = 0;
|
||||
while (!q.empty() && (pos = sline.find(q, pos)) != std::string::npos) {
|
||||
ranges.emplace_back(pos, pos + q.size());
|
||||
pos += q.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
auto is_src_in_hl = [&](std::size_t si) -> bool {
|
||||
if (ranges.empty()) return false;
|
||||
// ranges are non-overlapping and ordered by construction
|
||||
// linear scan is fine for now
|
||||
for (const auto &rg : ranges) {
|
||||
if (si < rg.first) break;
|
||||
if (si >= rg.first && si < rg.second) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Track current-match to optionally emphasize
|
||||
const bool has_current = ed.SearchActive() && ed.SearchMatchLen() > 0;
|
||||
const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
|
||||
const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
|
||||
const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
||||
bool hl_on = false;
|
||||
bool cur_on = false;
|
||||
int written = 0;
|
||||
if (li < lines.size()) {
|
||||
const std::string &line = lines[li];
|
||||
src_i = 0;
|
||||
render_col = 0;
|
||||
while (written < cols) {
|
||||
char ch = ' ';
|
||||
bool from_src = false;
|
||||
if (src_i < line.size()) {
|
||||
unsigned char c = static_cast<unsigned char>(line[src_i]);
|
||||
if (c == '\t') {
|
||||
std::size_t next_tab = tabw - (render_col % tabw);
|
||||
if (render_col + next_tab <= coloffs) {
|
||||
render_col += next_tab;
|
||||
++src_i;
|
||||
continue;
|
||||
}
|
||||
// Emit spaces for tab
|
||||
if (render_col < coloffs) {
|
||||
// skip to coloffs
|
||||
std::size_t to_skip = std::min<std::size_t>(
|
||||
next_tab, coloffs - render_col);
|
||||
render_col += to_skip;
|
||||
next_tab -= to_skip;
|
||||
}
|
||||
// Now render visible spaces
|
||||
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;
|
||||
// Toggle highlight attributes
|
||||
int attr = 0;
|
||||
if (in_hl) attr |= A_STANDOUT;
|
||||
if (in_cur) attr |= A_BOLD;
|
||||
if ((attr & A_STANDOUT) && !hl_on) { attron(A_STANDOUT); hl_on = true; }
|
||||
if (!(attr & A_STANDOUT) && hl_on) { attroff(A_STANDOUT); hl_on = false; }
|
||||
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(' ');
|
||||
++written;
|
||||
++render_col;
|
||||
--next_tab;
|
||||
}
|
||||
++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
|
||||
std::size_t cy = buf->Cury();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
xorg,
|
||||
installShellFiles,
|
||||
|
||||
graphical ? true,
|
||||
graphical ? false,
|
||||
...
|
||||
}:
|
||||
let
|
||||
|
||||
Reference in New Issue
Block a user