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>
|
</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. - 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="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="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." />
|
||||||
<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 />
|
||||||
|
|||||||
39
Buffer.h
39
Buffer.h
@@ -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)
|
||||||
|
|||||||
369
Command.cc
369
Command.cc
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
33
Editor.h
33
Editor.h
@@ -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
|
||||||
|
|||||||
108
GUIRenderer.cc
108
GUIRenderer.cc
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
xorg,
|
xorg,
|
||||||
installShellFiles,
|
installShellFiles,
|
||||||
|
|
||||||
graphical ? true,
|
graphical ? false,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
|||||||
Reference in New Issue
Block a user