Search: stop at the first match from the cursor; capped count
run_search() replaces collecting every match in the buffer (and building the full list again for every next/previous): - Plain search finds forward from the search origin (then the current match) in the materialized text, or backwards through 1 MiB blocks searched forward (string_view::rfind scans a byte at a time: 511 ms per previous-match on 105 MB, now 6 ms). Regex search walks lines from the position with early exit. - The i/N count stops at 1,000 matches, and for regex after 4 MiB; the status says 'N+' when capped. - The first match is the first at or after the cursor (it was the first in the buffer). Extending the query keeps the current match while it still matches. Left/Right also step through matches in the regex replace prompt; Up/Down in a regex prompt use the regex. On a 26 MB file: plain keystroke 30 ms -> 0.1 ms, next 24 -> 3 ms, regex keystroke 434 -> 1.6 ms, regex next 420 -> 66 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
@@ -700,6 +700,19 @@ public:
|
|||||||
return content_.Size();
|
return content_.Size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Byte offset of (row, col) in the content (clamped), and back.
|
||||||
|
[[nodiscard]] std::size_t RowColToOffset(std::size_t row, std::size_t col) const
|
||||||
|
{
|
||||||
|
return content_.LineColToByteOffset(row, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] std::pair<std::size_t, std::size_t> OffsetToRowCol(std::size_t off) const
|
||||||
|
{
|
||||||
|
return content_.ByteOffsetToLineCol(off);
|
||||||
|
}
|
||||||
|
|
||||||
// Undo system accessors (created per-buffer)
|
// Undo system accessors (created per-buffer)
|
||||||
[[nodiscard]] UndoSystem *Undo();
|
[[nodiscard]] UndoSystem *Undo();
|
||||||
|
|
||||||
|
|||||||
+337
-315
@@ -907,150 +907,344 @@ line_text_view(const Buffer &buf, const std::size_t y)
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::vector<std::pair<std::size_t, std::size_t> >
|
|
||||||
search_compute_matches(const Buffer &buf, const std::string &q)
|
|
||||||
{
|
// ===== Incremental search engine =====
|
||||||
std::vector<std::pair<std::size_t, std::size_t> > out;
|
// Finding a match stops at the first one after the current position (it
|
||||||
if (q.empty())
|
// used to collect every match in the buffer on every keystroke and every
|
||||||
return out;
|
// next/previous); the "i/N" count is capped (see search_count).
|
||||||
// Views into the materialized buffer (one copy, kept until the next
|
|
||||||
// edit): Rows() built a string per line, and kept them all, on every
|
struct SearchHit {
|
||||||
// search keystroke after an edit.
|
std::size_t y = 0;
|
||||||
// Matches never span lines (the query is matched within each line).
|
std::size_t x = 0;
|
||||||
if (q.find('\n') != std::string::npos)
|
std::size_t len = 0;
|
||||||
return out;
|
};
|
||||||
const std::size_t nrows = buf.Nrows();
|
|
||||||
if (nrows == 0)
|
// Count caps: stop at this many matches, and after scanning this many bytes
|
||||||
return out;
|
// (the regex budget is lower: std::regex is far slower than find).
|
||||||
// One scan of the whole text (find is much faster over one large range
|
static constexpr std::size_t kSearchCountCap = 1000;
|
||||||
// than restarted per line), mapping each hit to its row as we go.
|
static constexpr std::size_t kSearchCountBytesPlain = std::size_t{64} << 20;
|
||||||
const char *base = buf.GetLineView(0).data();
|
static constexpr std::size_t kSearchCountBytesRegex = std::size_t{4} << 20;
|
||||||
const auto last = buf.GetLineView(nrows - 1);
|
|
||||||
const std::string_view text(base, static_cast<std::size_t>(last.data() + last.size() - base));
|
|
||||||
std::size_t y = 0;
|
class RegexMatcher {
|
||||||
std::size_t line_start = 0;
|
public:
|
||||||
std::size_t next_nl = text.find('\n');
|
bool Compile(const std::string &pattern, std::string &err)
|
||||||
std::size_t pos = 0;
|
{
|
||||||
while ((pos = text.find(q, pos)) != std::string_view::npos) {
|
err.clear();
|
||||||
while (next_nl < pos) {
|
try {
|
||||||
++y;
|
rx_ = std::regex(pattern);
|
||||||
line_start = next_nl + 1;
|
return true;
|
||||||
next_nl = text.find('\n', line_start);
|
} catch (const std::regex_error &e) {
|
||||||
|
err = e.what();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
out.emplace_back(y, pos - line_start);
|
|
||||||
pos += q.size();
|
|
||||||
}
|
}
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Regex-based matches (per-line), capturing match length for highlighting
|
// First match in `line` starting at byte >= from.
|
||||||
struct RegexMatch {
|
bool Find(std::string_view line, std::size_t from, std::size_t &pos, std::size_t &len) const
|
||||||
std::size_t y;
|
{
|
||||||
std::size_t x;
|
if (from > line.size())
|
||||||
std::size_t len;
|
return false;
|
||||||
|
std::cmatch m;
|
||||||
|
const auto flags = from > 0 ? std::regex_constants::match_prev_avail
|
||||||
|
: std::regex_constants::match_default;
|
||||||
|
if (!std::regex_search(line.data() + from, line.data() + line.size(), m, rx_, flags))
|
||||||
|
return false;
|
||||||
|
pos = from + static_cast<std::size_t>(m.position(0));
|
||||||
|
len = static_cast<std::size_t>(m.length(0));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::regex rx_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
static std::vector<RegexMatch>
|
static std::string_view line_text_view(const Buffer &buf, std::size_t y);
|
||||||
search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std::string &err_out,
|
|
||||||
std::size_t line_limit = static_cast<std::size_t>(-1), std::size_t *skipped = nullptr)
|
|
||||||
|
// The whole text as one view into the materialized buffer.
|
||||||
|
static std::string_view
|
||||||
|
buffer_text_view(const Buffer &buf)
|
||||||
{
|
{
|
||||||
if (skipped)
|
const std::size_t nrows = buf.Nrows();
|
||||||
*skipped = 0;
|
if (nrows == 0)
|
||||||
std::vector<RegexMatch> out;
|
return {};
|
||||||
err_out.clear();
|
const char *base = buf.GetLineView(0).data();
|
||||||
if (pattern.empty())
|
const auto last = buf.GetLineView(nrows - 1);
|
||||||
return out;
|
return {base, static_cast<std::size_t>(last.data() + last.size() - base)};
|
||||||
try {
|
}
|
||||||
const std::regex rx(pattern);
|
|
||||||
const std::size_t nrows = buf.Nrows();
|
|
||||||
// std::regex recurses per matched character; see RegexGuard.h.
|
// The last occurrence of q starting before `end`. string_view::rfind scans
|
||||||
kte::RunWithLargeStack([&] {
|
// backwards a byte at a time (half a second over 100 MB); forward find is
|
||||||
for (std::size_t y = 0; y < nrows; ++y) {
|
// vectorized, so scan blocks forward, moving back one block at a time.
|
||||||
const std::string_view line = line_text_view(buf, y);
|
static std::size_t
|
||||||
// std::regex can be quadratic in line length for ordinary
|
rfind_before(std::string_view text, std::string_view q, std::size_t end)
|
||||||
// patterns (".*q" on a 40 KB minified line took over a
|
{
|
||||||
// minute, uninterruptibly), so the per-keystroke caller
|
constexpr std::size_t kBlock = std::size_t{1} << 20;
|
||||||
// passes a limit; moving to the next/previous match
|
end = std::min(end, text.size());
|
||||||
// (Left/Right in the prompt) searches every line.
|
while (end > 0) {
|
||||||
if (line.size() > line_limit) {
|
const std::size_t start = end > kBlock ? end - kBlock : 0;
|
||||||
if (skipped)
|
// Matches starting in [start, end); they may run past `end`.
|
||||||
++*skipped;
|
const std::string_view block = text.substr(start, std::min(text.size() - start, end - start + q.size() - 1));
|
||||||
continue;
|
std::size_t last = std::string_view::npos;
|
||||||
|
for (std::size_t p = block.find(q); p != std::string_view::npos && start + p < end; p = block.find(q, p + 1))
|
||||||
|
last = p;
|
||||||
|
if (last != std::string_view::npos)
|
||||||
|
return start + last;
|
||||||
|
end = start;
|
||||||
|
}
|
||||||
|
return std::string_view::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Plain text: the first match at or after (y, x) (forward), or the last one
|
||||||
|
// starting before it (backward), wrapping around the buffer.
|
||||||
|
static bool
|
||||||
|
plain_find(const Buffer &buf, const std::string &q, std::size_t y, std::size_t x, bool forward, SearchHit &hit)
|
||||||
|
{
|
||||||
|
if (q.empty() || q.find('\n') != std::string::npos)
|
||||||
|
return false; // matches never span lines
|
||||||
|
const std::string_view text = buffer_text_view(buf);
|
||||||
|
const std::size_t off = std::min(buf.RowColToOffset(y, x), text.size());
|
||||||
|
std::size_t p = std::string_view::npos;
|
||||||
|
if (forward) {
|
||||||
|
p = text.find(q, off);
|
||||||
|
if (p == std::string_view::npos) {
|
||||||
|
p = text.find(q);
|
||||||
|
if (p >= off)
|
||||||
|
p = std::string_view::npos;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p = rfind_before(text, q, off);
|
||||||
|
if (p == std::string_view::npos) {
|
||||||
|
p = rfind_before(text, q, text.size());
|
||||||
|
if (p != std::string_view::npos && p < off)
|
||||||
|
p = std::string_view::npos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p == std::string_view::npos)
|
||||||
|
return false;
|
||||||
|
const auto [ry, rx] = buf.OffsetToRowCol(p);
|
||||||
|
hit = SearchHit{ry, rx, q.size()};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Regex: like plain_find, line by line. Lines longer than line_limit are
|
||||||
|
// skipped (counted in *skipped).
|
||||||
|
static bool
|
||||||
|
regex_find(const Buffer &buf, const RegexMatcher &m, std::size_t y0, std::size_t x0, bool forward,
|
||||||
|
std::size_t line_limit, std::size_t *skipped, SearchHit &hit)
|
||||||
|
{
|
||||||
|
const std::size_t nrows = buf.Nrows();
|
||||||
|
if (nrows == 0)
|
||||||
|
return false;
|
||||||
|
y0 = std::min(y0, nrows - 1);
|
||||||
|
bool found = false;
|
||||||
|
kte::RunWithLargeStack([&] {
|
||||||
|
// nrows + 1 visits: the start row is visited again after wrapping,
|
||||||
|
// for the part on the other side of x0.
|
||||||
|
for (std::size_t i = 0; i <= nrows && !found; ++i) {
|
||||||
|
const std::size_t y = forward ? (y0 + i) % nrows : (y0 + nrows - i % nrows) % nrows;
|
||||||
|
const std::string_view line = line_text_view(buf, y);
|
||||||
|
if (line.size() > line_limit) {
|
||||||
|
if (skipped && i < nrows)
|
||||||
|
++*skipped;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool first = i == 0, again = i == nrows;
|
||||||
|
std::size_t pos = 0, len = 0;
|
||||||
|
if (forward) {
|
||||||
|
const std::size_t from = first ? x0 : 0;
|
||||||
|
if (m.Find(line, from, pos, len) && (!again || pos < x0)) {
|
||||||
|
hit = SearchHit{y, pos, len};
|
||||||
|
found = true;
|
||||||
}
|
}
|
||||||
for (auto it = std::cregex_iterator(line.data(), line.data() + line.size(), rx);
|
} else {
|
||||||
it != std::cregex_iterator(); ++it) {
|
// The last match in the line before x0 (first visit), at or
|
||||||
const auto &m = *it;
|
// after it (after wrapping), or anywhere (other rows).
|
||||||
out.push_back(RegexMatch{
|
bool have = false;
|
||||||
y, static_cast<std::size_t>(m.position()), static_cast<std::size_t>(m.length())
|
SearchHit best{};
|
||||||
});
|
std::size_t from = 0;
|
||||||
|
while (from <= line.size() && m.Find(line, from, pos, len)) {
|
||||||
|
if (first && pos >= x0)
|
||||||
|
break;
|
||||||
|
if (!again || pos >= x0) {
|
||||||
|
best = SearchHit{y, pos, len};
|
||||||
|
have = true;
|
||||||
|
}
|
||||||
|
from = pos + std::max<std::size_t>(len, 1);
|
||||||
|
}
|
||||||
|
if (have) {
|
||||||
|
hit = best;
|
||||||
|
found = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
} catch (const std::regex_error &e) {
|
});
|
||||||
err_out = e.what();
|
return found;
|
||||||
// Return empty results on error
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void
|
struct SearchCount {
|
||||||
search_apply_match_regex(Editor &ed, Buffer &buf, const std::vector<RegexMatch> &matches)
|
std::size_t before = 0; // matches before the current one
|
||||||
|
std::size_t total = 0;
|
||||||
|
bool reached = false; // the scan got as far as the current match
|
||||||
|
bool complete = false; // every match was counted
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static SearchCount
|
||||||
|
search_count(const Buffer &buf, bool regex, const std::string &q, const RegexMatcher *m, const SearchHit &cur,
|
||||||
|
std::size_t line_limit)
|
||||||
{
|
{
|
||||||
const std::string &q = ed.SearchQuery();
|
SearchCount c;
|
||||||
if (matches.empty()) {
|
const std::size_t nrows = buf.Nrows();
|
||||||
|
if (!regex) {
|
||||||
|
const std::string_view all = buffer_text_view(buf);
|
||||||
|
const std::string_view text = all.substr(0, kSearchCountBytesPlain);
|
||||||
|
const std::size_t cur_off = buf.RowColToOffset(cur.y, cur.x);
|
||||||
|
std::size_t p = 0;
|
||||||
|
while (c.total < kSearchCountCap && (p = text.find(q, p)) != std::string_view::npos) {
|
||||||
|
if (p < cur_off)
|
||||||
|
++c.before;
|
||||||
|
else
|
||||||
|
c.reached = true;
|
||||||
|
++c.total;
|
||||||
|
p += q.size();
|
||||||
|
}
|
||||||
|
if (p != std::string_view::npos && p >= cur_off)
|
||||||
|
c.reached = true;
|
||||||
|
c.complete = p == std::string_view::npos && text.size() == all.size();
|
||||||
|
if (c.complete)
|
||||||
|
c.reached = true;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
std::size_t scanned = 0;
|
||||||
|
bool stopped = false;
|
||||||
|
kte::RunWithLargeStack([&] {
|
||||||
|
for (std::size_t y = 0; y < nrows && !stopped; ++y) {
|
||||||
|
const std::string_view line = line_text_view(buf, y);
|
||||||
|
if (line.size() > line_limit)
|
||||||
|
continue;
|
||||||
|
scanned += line.size() + 1;
|
||||||
|
std::size_t from = 0, pos = 0, len = 0;
|
||||||
|
while (from <= line.size() && m->Find(line, from, pos, len)) {
|
||||||
|
if (y < cur.y || (y == cur.y && pos < cur.x))
|
||||||
|
++c.before;
|
||||||
|
else
|
||||||
|
c.reached = true;
|
||||||
|
if (++c.total >= kSearchCountCap) {
|
||||||
|
stopped = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
from = pos + std::max<std::size_t>(len, 1);
|
||||||
|
}
|
||||||
|
if (y >= cur.y)
|
||||||
|
c.reached = true;
|
||||||
|
if (scanned > kSearchCountBytesRegex && y + 1 < nrows) {
|
||||||
|
stopped = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
c.complete = !stopped;
|
||||||
|
if (c.complete)
|
||||||
|
c.reached = true;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class SearchStep { Here, Next, Prev };
|
||||||
|
|
||||||
|
|
||||||
|
// Run the search for the editor's query and show the result: move the
|
||||||
|
// cursor to the match (or back to the origin if there is none) and set the
|
||||||
|
// status. Here: from the current match (so it stays while the extended query
|
||||||
|
// still matches there) or the search origin; Next/Prev: after/before the
|
||||||
|
// current match. incremental limits regex to lines up to
|
||||||
|
// kRegexIncrementalLineLimit (search-as-you-type).
|
||||||
|
static void
|
||||||
|
run_search(Editor &ed, Buffer &buf, bool regex, SearchStep step, bool incremental)
|
||||||
|
{
|
||||||
|
const std::string q = ed.SearchQuery();
|
||||||
|
const std::string prefix = regex ? "Regex: " : "Find: ";
|
||||||
|
const bool have_cur = ed.SearchIndex() >= 0;
|
||||||
|
std::size_t y = buf.Cury(), x = buf.Curx();
|
||||||
|
if (have_cur) {
|
||||||
|
y = ed.SearchMatchY();
|
||||||
|
x = ed.SearchMatchX();
|
||||||
|
} else if (step == SearchStep::Here && ed.SearchOriginSet()) {
|
||||||
|
y = ed.SearchOrigY();
|
||||||
|
x = ed.SearchOrigX();
|
||||||
|
}
|
||||||
|
if (step == SearchStep::Next && have_cur) {
|
||||||
|
const std::size_t len = std::max<std::size_t>(ed.SearchMatchLen(), 1);
|
||||||
|
x += len;
|
||||||
|
if (x > line_text_view(buf, y).size() && y + 1 < buf.Nrows()) {
|
||||||
|
++y;
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RegexMatcher m;
|
||||||
|
std::string err;
|
||||||
|
SearchHit hit;
|
||||||
|
bool found = false;
|
||||||
|
std::size_t skipped = 0;
|
||||||
|
const std::size_t line_limit = (regex && incremental) ? kte::kRegexIncrementalLineLimit
|
||||||
|
: static_cast<std::size_t>(-1);
|
||||||
|
if (!q.empty()) {
|
||||||
|
if (!regex)
|
||||||
|
found = plain_find(buf, q, y, x, step != SearchStep::Prev, hit);
|
||||||
|
else if (m.Compile(q, err))
|
||||||
|
found = regex_find(buf, m, y, x, step != SearchStep::Prev, line_limit, &skipped, hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
ed.SetSearchMatch(0, 0, 0);
|
ed.SetSearchMatch(0, 0, 0);
|
||||||
// Restore cursor to origin if present
|
|
||||||
if (ed.SearchOriginSet()) {
|
if (ed.SearchOriginSet()) {
|
||||||
buf.SetCursor(ed.SearchOrigX(), ed.SearchOrigY());
|
buf.SetCursor(ed.SearchOrigX(), ed.SearchOrigY());
|
||||||
buf.SetOffsets(ed.SearchOrigRowoffs(), ed.SearchOrigColoffs());
|
buf.SetOffsets(ed.SearchOrigRowoffs(), ed.SearchOrigColoffs());
|
||||||
}
|
}
|
||||||
ed.SetSearchIndex(-1);
|
ed.SetSearchIndex(-1);
|
||||||
ed.SetStatus("Regex: " + q);
|
std::string status = prefix + q;
|
||||||
|
if (!err.empty())
|
||||||
|
status += " [error: " + err + "]";
|
||||||
|
else if (skipped > 0)
|
||||||
|
status += " [" + std::to_string(skipped) + " long line(s) skipped; Left/Right search all]";
|
||||||
|
ed.SetStatus(status);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int idx = ed.SearchIndex();
|
|
||||||
if (idx < 0 || idx >= static_cast<int>(matches.size()))
|
ed.SetSearchMatch(hit.y, hit.x, hit.len);
|
||||||
idx = 0;
|
buf.SetCursor(hit.x, hit.y);
|
||||||
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);
|
ensure_cursor_visible(ed, buf);
|
||||||
char tmp[64];
|
const SearchCount c = search_count(buf, regex, q, regex ? &m : nullptr, hit, line_limit);
|
||||||
snprintf(tmp, sizeof(tmp), "%d/%zu", idx + 1, matches.size());
|
ed.SetSearchIndex(static_cast<int>(c.reached ? c.before : 0));
|
||||||
ed.SetStatus(std::string("Regex: ") + q + " " + tmp);
|
std::string where;
|
||||||
ed.SetSearchIndex(idx);
|
if (c.complete)
|
||||||
|
where = std::to_string(c.before + 1) + "/" + std::to_string(c.total);
|
||||||
|
else if (c.reached)
|
||||||
|
where = std::to_string(c.before + 1) + "/" + std::to_string(c.total) + "+";
|
||||||
|
else
|
||||||
|
where = std::to_string(c.total) + "+ matches";
|
||||||
|
std::string status = prefix + q + " " + where;
|
||||||
|
if (skipped > 0)
|
||||||
|
status += " [" + std::to_string(skipped) + " long line(s) skipped; Left/Right search all]";
|
||||||
|
ed.SetStatus(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void
|
static bool
|
||||||
search_apply_match(Editor &ed, Buffer &buf, const std::vector<std::pair<std::size_t, std::size_t> > &matches)
|
regex_prompt(const Editor &ed)
|
||||||
{
|
{
|
||||||
const std::string &q = ed.SearchQuery();
|
return ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||||
if (matches.empty()) {
|
ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind;
|
||||||
ed.SetSearchMatch(0, 0, 0);
|
|
||||||
// Restore cursor to origin if present
|
|
||||||
if (ed.SearchOriginSet()) {
|
|
||||||
buf.SetCursor(ed.SearchOrigX(), ed.SearchOrigY());
|
|
||||||
buf.SetOffsets(ed.SearchOrigRowoffs(), ed.SearchOrigColoffs());
|
|
||||||
}
|
|
||||||
ed.SetSearchIndex(-1);
|
|
||||||
ed.SetStatus("Find: " + q);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int idx = ed.SearchIndex();
|
|
||||||
if (idx < 0 || idx >= static_cast<int>(matches.size()))
|
|
||||||
idx = 0;
|
|
||||||
auto [y, x] = matches[static_cast<std::size_t>(idx)];
|
|
||||||
ed.SetSearchMatch(y, x, q.size());
|
|
||||||
buf.SetCursor(x, y);
|
|
||||||
ensure_cursor_visible(ed, buf);
|
|
||||||
char tmp[64];
|
|
||||||
snprintf(tmp, sizeof(tmp), "%d/%zu", idx + 1, matches.size());
|
|
||||||
ed.SetStatus(std::string("Find: ") + q + " " + tmp);
|
|
||||||
ed.SetSearchIndex(idx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2519,33 +2713,7 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind ||
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind ||
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
|
||||||
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
|
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
|
||||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
run_search(ctx.editor, *buf, regex_prompt(ctx.editor), SearchStep::Here, true);
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
|
|
||||||
std::string err;
|
|
||||||
std::size_t skipped = 0;
|
|
||||||
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err,
|
|
||||||
kte::kRegexIncrementalLineLimit, &skipped);
|
|
||||||
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);
|
|
||||||
if (skipped > 0 && err.empty())
|
|
||||||
ctx.editor.SetStatus(ctx.editor.Status() + " [" + std::to_string(skipped) +
|
|
||||||
" long line(s) skipped; " +
|
|
||||||
(ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch
|
|
||||||
? "Left/Right search all]"
|
|
||||||
: "Enter searches all]"));
|
|
||||||
} 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());
|
||||||
@@ -2558,30 +2726,7 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
q += ctx.arg; // arg already printable text
|
q += ctx.arg; // arg already printable text
|
||||||
ctx.editor.SetSearchQuery(q);
|
ctx.editor.SetSearchQuery(q);
|
||||||
|
|
||||||
// Recompute matches and move cursor to current index
|
run_search(ctx.editor, *buf, false, SearchStep::Here, true);
|
||||||
const auto matches = search_compute_matches(*buf, q);
|
|
||||||
if (matches.empty()) {
|
|
||||||
ctx.editor.SetSearchMatch(0, 0, 0);
|
|
||||||
// Restore to origin if available
|
|
||||||
if (ctx.editor.SearchOriginSet()) {
|
|
||||||
buf->SetCursor(ctx.editor.SearchOrigX(), ctx.editor.SearchOrigY());
|
|
||||||
buf->SetOffsets(ctx.editor.SearchOrigRowoffs(), ctx.editor.SearchOrigColoffs());
|
|
||||||
}
|
|
||||||
ctx.editor.SetSearchIndex(-1);
|
|
||||||
ctx.editor.SetStatus("Find: " + q);
|
|
||||||
} else {
|
|
||||||
int idx = ctx.editor.SearchIndex();
|
|
||||||
if (idx < 0 || idx >= static_cast<int>(matches.size()))
|
|
||||||
idx = 0;
|
|
||||||
auto [y, x] = matches[static_cast<std::size_t>(idx)];
|
|
||||||
ctx.editor.SetSearchMatch(y, x, q.size());
|
|
||||||
buf->SetCursor(x, y);
|
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
|
||||||
char tmp[64];
|
|
||||||
snprintf(tmp, sizeof(tmp), "%d/%zu", idx + 1, matches.size());
|
|
||||||
ctx.editor.SetStatus(std::string("Find: ") + q + " " + tmp);
|
|
||||||
ctx.editor.SetSearchIndex(idx);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Disallow newlines in InsertText; they should come via Newline
|
// Disallow newlines in InsertText; they should come via Newline
|
||||||
@@ -2957,10 +3102,8 @@ cmd_newline(CommandContext &ctx)
|
|||||||
// Keep search highlights active using the collected find string
|
// Keep search highlights active using the collected find string
|
||||||
ctx.editor.SetSearchActive(true);
|
ctx.editor.SetSearchActive(true);
|
||||||
ctx.editor.SetSearchQuery(value);
|
ctx.editor.SetSearchQuery(value);
|
||||||
if (Buffer *b = ctx.editor.CurrentBuffer()) {
|
if (Buffer *b = ctx.editor.CurrentBuffer())
|
||||||
auto matches = search_compute_matches(*b, ctx.editor.SearchQuery());
|
run_search(ctx.editor, *b, false, SearchStep::Here, false);
|
||||||
search_apply_match(ctx.editor, *b, matches);
|
|
||||||
}
|
|
||||||
ctx.editor.StartPrompt(Editor::PromptKind::ReplaceWith, "Replace: with", "");
|
ctx.editor.StartPrompt(Editor::PromptKind::ReplaceWith, "Replace: with", "");
|
||||||
ctx.editor.SetStatus("Replace: with: ");
|
ctx.editor.SetStatus("Replace: with: ");
|
||||||
return true;
|
return true;
|
||||||
@@ -3395,14 +3538,8 @@ cmd_newline(CommandContext &ctx)
|
|||||||
// Keep search highlights active using the collected regex pattern
|
// Keep search highlights active using the collected regex pattern
|
||||||
ctx.editor.SetSearchActive(true);
|
ctx.editor.SetSearchActive(true);
|
||||||
ctx.editor.SetSearchQuery(value);
|
ctx.editor.SetSearchQuery(value);
|
||||||
if (Buffer *b = ctx.editor.CurrentBuffer()) {
|
if (Buffer *b = ctx.editor.CurrentBuffer())
|
||||||
std::string err;
|
run_search(ctx.editor, *b, true, SearchStep::Here, false);
|
||||||
auto rm = search_compute_matches_regex(*b, ctx.editor.SearchQuery(), err);
|
|
||||||
if (!err.empty()) {
|
|
||||||
ctx.editor.SetStatus(std::string("Regex: ") + value + " [error: " + err + "]");
|
|
||||||
}
|
|
||||||
search_apply_match_regex(ctx.editor, *b, rm);
|
|
||||||
}
|
|
||||||
ctx.editor.StartPrompt(Editor::PromptKind::RegexReplaceWith, "Regex replace: with", "");
|
ctx.editor.StartPrompt(Editor::PromptKind::RegexReplaceWith, "Regex replace: with", "");
|
||||||
ctx.editor.SetStatus("Regex replace: with: ");
|
ctx.editor.SetStatus("Regex replace: with: ");
|
||||||
return true;
|
return true;
|
||||||
@@ -3649,29 +3786,7 @@ cmd_backspace(CommandContext &ctx)
|
|||||||
Buffer *buf2 = ctx.editor.CurrentBuffer();
|
Buffer *buf2 = ctx.editor.CurrentBuffer();
|
||||||
if (buf2) {
|
if (buf2) {
|
||||||
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
|
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
|
||||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
run_search(ctx.editor, *buf2, regex_prompt(ctx.editor), SearchStep::Here, true);
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
|
|
||||||
std::string err;
|
|
||||||
std::size_t skipped = 0;
|
|
||||||
auto rm = search_compute_matches_regex(
|
|
||||||
*buf2, ctx.editor.SearchQuery(), err, kte::kRegexIncrementalLineLimit,
|
|
||||||
&skipped);
|
|
||||||
if (!err.empty()) {
|
|
||||||
ctx.editor.SetStatus(
|
|
||||||
std::string("Regex: ") + ctx.editor.PromptText() + " [error: "
|
|
||||||
+ err + "]");
|
|
||||||
}
|
|
||||||
search_apply_match_regex(ctx.editor, *buf2, rm);
|
|
||||||
if (skipped > 0 && err.empty())
|
|
||||||
ctx.editor.SetStatus(ctx.editor.Status() + " [" + std::to_string(skipped) +
|
|
||||||
" long line(s) skipped; " +
|
|
||||||
(ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch
|
|
||||||
? "Left/Right search all]"
|
|
||||||
: "Enter searches all]"));
|
|
||||||
} 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());
|
||||||
@@ -3685,11 +3800,8 @@ cmd_backspace(CommandContext &ctx)
|
|||||||
q.pop_back();
|
q.pop_back();
|
||||||
ctx.editor.SetSearchQuery(q);
|
ctx.editor.SetSearchQuery(q);
|
||||||
}
|
}
|
||||||
Buffer *buf2 = ctx.editor.CurrentBuffer();
|
if (Buffer *buf2 = ctx.editor.CurrentBuffer())
|
||||||
if (buf2) {
|
run_search(ctx.editor, *buf2, false, SearchStep::Here, true);
|
||||||
auto matches = search_compute_matches(*buf2, ctx.editor.SearchQuery());
|
|
||||||
search_apply_match(ctx.editor, *buf2, matches);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
@@ -4314,54 +4426,18 @@ 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
|
||||||
|
// In a search prompt (or search mode), Left/Right step to the previous/
|
||||||
|
// next match.
|
||||||
if (ctx.editor.PromptActive() &&
|
if (ctx.editor.PromptActive() &&
|
||||||
(ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
|
(ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind)) {
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind ||
|
||||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||||
std::string err;
|
run_search(ctx.editor, *buf, regex_prompt(ctx.editor), SearchStep::Prev, false);
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
if (ctx.editor.SearchActive()) {
|
if (ctx.editor.SearchActive()) {
|
||||||
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
|
run_search(ctx.editor, *buf, false, SearchStep::Prev, false);
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
@@ -4399,54 +4475,18 @@ cmd_move_right(CommandContext &ctx)
|
|||||||
return false;
|
return false;
|
||||||
if (auto *u = buf->Undo())
|
if (auto *u = buf->Undo())
|
||||||
u->commit();
|
u->commit();
|
||||||
|
// In a search prompt (or search mode), Left/Right step to the previous/
|
||||||
|
// next match.
|
||||||
if (ctx.editor.PromptActive() &&
|
if (ctx.editor.PromptActive() &&
|
||||||
(ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
|
(ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind)) {
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind ||
|
||||||
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
|
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||||
std::string err;
|
run_search(ctx.editor, *buf, regex_prompt(ctx.editor), SearchStep::Next, false);
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
if (ctx.editor.SearchActive()) {
|
if (ctx.editor.SearchActive()) {
|
||||||
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
|
run_search(ctx.editor, *buf, false, SearchStep::Next, false);
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
@@ -4480,18 +4520,9 @@ cmd_move_up(CommandContext &ctx)
|
|||||||
u->commit();
|
u->commit();
|
||||||
if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor.
|
if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor.
|
||||||
SearchActive()) {
|
SearchActive()) {
|
||||||
// Up == previous match
|
// Up == previous match (a regex query in a regex prompt)
|
||||||
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
|
run_search(ctx.editor, *buf, ctx.editor.PromptActive() && regex_prompt(ctx.editor), SearchStep::Prev,
|
||||||
if (!matches.empty()) {
|
false);
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
@@ -4529,18 +4560,9 @@ cmd_move_down(CommandContext &ctx)
|
|||||||
u->commit();
|
u->commit();
|
||||||
if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor.
|
if ((ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) || ctx.editor.
|
||||||
SearchActive()) {
|
SearchActive()) {
|
||||||
// Down == next match
|
// Down == next match (a regex query in a regex prompt)
|
||||||
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
|
run_search(ctx.editor, *buf, ctx.editor.PromptActive() && regex_prompt(ctx.editor), SearchStep::Next,
|
||||||
if (!matches.empty()) {
|
false);
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
|
|||||||
@@ -513,6 +513,19 @@ before the fix (unless noted).
|
|||||||
- replace_all_bytes clears undo history (it was recorded against, and
|
- replace_all_bytes clears undo history (it was recorded against, and
|
||||||
may reference, the previous content).
|
may reference, the previous content).
|
||||||
|
|
||||||
|
**Follow-up: search**
|
||||||
|
- Search finds the next match from the search origin (then from the
|
||||||
|
current match) and stops there; it no longer collects every match in
|
||||||
|
the buffer on every keystroke and next/previous. The i/N count stops
|
||||||
|
at 1,000 matches (and, for regex, after 4 MiB). On 26 MB: plain
|
||||||
|
keystroke 30 ms -> 0.1 ms, next 24 -> 3 ms, regex keystroke 434 ->
|
||||||
|
1.6 ms. Previous-match scans backwards in 1 MiB forward-searched
|
||||||
|
blocks (string_view::rfind: 511 ms -> 6 ms on 105 MB).
|
||||||
|
- Semantics change: the first match is the first one at or after the
|
||||||
|
cursor (it was the first in the buffer), and Left/Right also step
|
||||||
|
through matches in the regex replace prompt; Up/Down in a regex prompt
|
||||||
|
use the regex.
|
||||||
|
|
||||||
**Known limitations (not fixed)**
|
**Known limitations (not fixed)**
|
||||||
- Catastrophic regex backtracking (e.g. `(a*)*b`) can still take very
|
- Catastrophic regex backtracking (e.g. `(a*)*b`) can still take very
|
||||||
long; std::regex has no time limit.
|
long; std::regex has no time limit.
|
||||||
@@ -524,8 +537,8 @@ before the fix (unless noted).
|
|||||||
- Journals are created at the first edit, so a second kte that opens
|
- Journals are created at the first edit, so a second kte that opens
|
||||||
the file before the first one edits it is not warned at open; the
|
the file before the first one edits it is not warned at open; the
|
||||||
session that is locked out is told at its first edit instead.
|
session that is locked out is told at its first edit instead.
|
||||||
- Regex search runs std::regex over the whole buffer per keystroke
|
- A regex that matches rarely or not at all still scans the whole
|
||||||
(about 0.4 s on 26 MB).
|
buffer with std::regex (1.6 s on 105 MB).
|
||||||
- A journal whose buffer exceeds 16 MiB cannot be checkpointed; after a
|
- A journal whose buffer exceeds 16 MiB cannot be checkpointed; after a
|
||||||
lost record such a journal stays incomplete until the file is saved
|
lost record such a journal stays incomplete until the file is saved
|
||||||
(reported to the user).
|
(reported to the user).
|
||||||
|
|||||||
@@ -1294,10 +1294,14 @@ TEST(Audit_RegexSearch_LongLineFoundByNext)
|
|||||||
ASSERT_EQ(h.Buf().Curx(), (std::size_t) 25000);
|
ASSERT_EQ(h.Buf().Curx(), (std::size_t) 25000);
|
||||||
ASSERT_TRUE(h.Exec(CommandId::Refresh));
|
ASSERT_TRUE(h.Exec(CommandId::Refresh));
|
||||||
ASSERT_TRUE(!ed.PromptActive());
|
ASSERT_TRUE(!ed.PromptActive());
|
||||||
// The replace prompt has no next/previous; its Enter searches all lines.
|
// The regex replace prompt steps through matches the same way.
|
||||||
|
h.Buf().SetCursor(0, 0);
|
||||||
ASSERT_TRUE(h.Exec(CommandId::RegexpReplace));
|
ASSERT_TRUE(h.Exec(CommandId::RegexpReplace));
|
||||||
ASSERT_TRUE(h.Exec(CommandId::InsertText, "NEE+DLE"));
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, "NEE+DLE"));
|
||||||
ASSERT_TRUE(ed.Status().find("Enter searches all") != std::string::npos);
|
ASSERT_TRUE(ed.Status().find("Left/Right search all") != std::string::npos);
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::MoveRight));
|
||||||
|
ASSERT_EQ(h.Buf().Cury(), (std::size_t) 1);
|
||||||
|
ASSERT_EQ(h.Buf().Curx(), (std::size_t) 25000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1869,3 +1873,142 @@ TEST(Undo_ByteBudgetKeepsGroupsWhole)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Search stops at the first match after the search origin (it collected
|
||||||
|
// every match in the buffer on every keystroke); the count is capped.
|
||||||
|
TEST(Search_FindsFromOriginAndCapsCount)
|
||||||
|
{
|
||||||
|
TestHarness h;
|
||||||
|
Editor &ed = h.EditorRef();
|
||||||
|
h.Buf().insert_text(0, 0, "foo\nbar\nfoo\nbar\nfoo\n");
|
||||||
|
h.Buf().SetCursor(0, 1); // after the first foo
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::FindStart));
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, "fo"));
|
||||||
|
ASSERT_EQ(h.Buf().Cury(), (std::size_t) 2);
|
||||||
|
ASSERT_TRUE(ed.Status().find("2/3") != std::string::npos);
|
||||||
|
// Extending the query keeps the current match while it still matches.
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, "o"));
|
||||||
|
ASSERT_EQ(h.Buf().Cury(), (std::size_t) 2);
|
||||||
|
// Next, then wrap to the first.
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::MoveRight));
|
||||||
|
ASSERT_EQ(h.Buf().Cury(), (std::size_t) 4);
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::MoveRight));
|
||||||
|
ASSERT_EQ(h.Buf().Cury(), (std::size_t) 0);
|
||||||
|
ASSERT_TRUE(ed.Status().find("1/3") != std::string::npos);
|
||||||
|
// Previous wraps back to the last.
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::MoveLeft));
|
||||||
|
ASSERT_EQ(h.Buf().Cury(), (std::size_t) 4);
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::Refresh));
|
||||||
|
|
||||||
|
TestHarness g;
|
||||||
|
std::string many;
|
||||||
|
for (int i = 0; i < 3000; ++i)
|
||||||
|
many += "x\n";
|
||||||
|
g.Buf().insert_text(0, 0, many);
|
||||||
|
g.Buf().SetCursor(0, 0);
|
||||||
|
ASSERT_TRUE(g.Exec(CommandId::FindStart));
|
||||||
|
ASSERT_TRUE(g.Exec(CommandId::InsertText, "x"));
|
||||||
|
ASSERT_TRUE(g.EditorRef().Status().find("1/1000+") != std::string::npos);
|
||||||
|
ASSERT_TRUE(g.Exec(CommandId::Refresh));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Next/Previous visit exactly the matches a per-line scan finds, in order
|
||||||
|
// and wrapping, for plain and regex queries (including zero-width ones).
|
||||||
|
TEST(Search_NextPrevVisitReferenceMatches)
|
||||||
|
{
|
||||||
|
std::mt19937 rng(11);
|
||||||
|
const char alphabet[] = {'a', 'b', '\n', 'a', ' ', 'b'};
|
||||||
|
const char *regexes[] = {"a+", "^", "b$", "ab|ba", "a*", "\\bb"};
|
||||||
|
for (int iter = 0; iter < 60; ++iter) {
|
||||||
|
std::string text;
|
||||||
|
const int n = 20 + static_cast<int>(rng() % 120);
|
||||||
|
for (int i = 0; i < n; ++i)
|
||||||
|
text.push_back(alphabet[rng() % sizeof(alphabet)]);
|
||||||
|
const bool regex = iter % 2 == 1;
|
||||||
|
const std::string q = regex ? regexes[(iter / 2) % 6] : ((iter / 2) % 2 ? "ab" : "a");
|
||||||
|
// Reference: per line, non-overlapping, empty matches advance by one.
|
||||||
|
std::vector<std::pair<std::size_t, std::size_t> > want;
|
||||||
|
{
|
||||||
|
std::size_t y = 0, start = 0;
|
||||||
|
const std::regex rx(regex ? q : std::string("x"));
|
||||||
|
while (true) {
|
||||||
|
const std::size_t nl = text.find('\n', start);
|
||||||
|
const std::string line = text.substr(start, nl == std::string::npos ? std::string::npos : nl - start);
|
||||||
|
if (!regex) {
|
||||||
|
for (std::size_t p = line.find(q); p != std::string::npos; p = line.find(q, p + q.size()))
|
||||||
|
want.emplace_back(y, p);
|
||||||
|
} else {
|
||||||
|
std::size_t from = 0;
|
||||||
|
std::cmatch m;
|
||||||
|
while (from <= line.size() &&
|
||||||
|
std::regex_search(line.data() + from, line.data() + line.size(), m, rx,
|
||||||
|
from ? std::regex_constants::match_prev_avail
|
||||||
|
: std::regex_constants::match_default)) {
|
||||||
|
const std::size_t pos = from + static_cast<std::size_t>(m.position(0));
|
||||||
|
want.emplace_back(y, pos);
|
||||||
|
from = pos + std::max<std::size_t>(static_cast<std::size_t>(m.length(0)), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nl == std::string::npos)
|
||||||
|
break;
|
||||||
|
start = nl + 1;
|
||||||
|
++y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TestHarness h;
|
||||||
|
h.Buf().insert_text(0, 0, text);
|
||||||
|
h.Buf().SetCursor(0, 0);
|
||||||
|
ASSERT_TRUE(h.Exec(regex ? CommandId::RegexFindStart : CommandId::FindStart));
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, q));
|
||||||
|
if (want.empty()) {
|
||||||
|
ASSERT_EQ(h.EditorRef().SearchIndex(), -1);
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::Refresh));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::size_t k = want.size();
|
||||||
|
for (std::size_t i = 0; i <= k; ++i) { // one full cycle, wrapping
|
||||||
|
ASSERT_EQ(h.Buf().Cury(), want[i % k].first);
|
||||||
|
ASSERT_EQ(h.Buf().Curx(), want[i % k].second);
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::MoveRight));
|
||||||
|
}
|
||||||
|
// Now at want[1 % k]; step back around.
|
||||||
|
for (std::size_t i = 0; i <= k; ++i) {
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::MoveLeft));
|
||||||
|
const std::size_t j = (1 + 2 * k - 1 - i) % k;
|
||||||
|
ASSERT_EQ(h.Buf().Cury(), want[j].first);
|
||||||
|
ASSERT_EQ(h.Buf().Curx(), want[j].second);
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::Refresh));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Previous-match search scans backwards in 1 MiB blocks; matches straddling
|
||||||
|
// and next to block boundaries are all found, in order.
|
||||||
|
TEST(Search_PrevAcrossBlocks)
|
||||||
|
{
|
||||||
|
const std::size_t mib = std::size_t{1} << 20;
|
||||||
|
std::string text(3 * mib + 100, 'x');
|
||||||
|
for (std::size_t i = 64; i < text.size(); i += 65)
|
||||||
|
text[i] = '\n';
|
||||||
|
std::vector<std::size_t> at = {5, mib - 2, mib, 2 * mib - 1, 2 * mib + 3, 3 * mib + 50};
|
||||||
|
for (std::size_t p: at)
|
||||||
|
text.replace(p, 2, "QZ"); // may overwrite a newline: fine, offsets still match
|
||||||
|
std::vector<std::size_t> want;
|
||||||
|
for (std::size_t p = text.find("QZ"); p != std::string::npos; p = text.find("QZ", p + 2))
|
||||||
|
want.push_back(p);
|
||||||
|
TestHarness h;
|
||||||
|
h.Buf().insert_text(0, 0, text);
|
||||||
|
h.Buf().SetCursor(0, 0);
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::FindStart));
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, "QZ"));
|
||||||
|
for (std::size_t i = 0; i < want.size(); ++i) {
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::MoveLeft));
|
||||||
|
const std::size_t j = want.size() - 1 - i;
|
||||||
|
const std::size_t off = h.Buf().RowColToOffset(h.Buf().Cury(), h.Buf().Curx());
|
||||||
|
ASSERT_EQ(off, want[j]);
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::Refresh));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user