Repository :
https://github.com/colorer/Colorer-library
On branch : master
Link :
https://github.com/colorer/Colorer-library/commit/0b1fc0446df5d4ea232bb62feaabad35d53bbe6e
>---------------------------------------------------------------
commit 0b1fc0446df5d4ea232bb62feaabad35d53bbe6e
Author: Aleksey Dobrunov <
cta...@ctapmex.com>
Date: Sun Sep 6 14:04:45 2026 +0500
Resume ParseCache search from a sibling cursor
>---------------------------------------------------------------
0b1fc0446df5d4ea232bb62feaabad35d53bbe6e
.agents/skills/colorer-hrc/core-parse.md | 2 +-
src/colorer/parsers/TextParserHelpers.cpp | 91 ++++++++++++++++++++++++-------
src/colorer/parsers/TextParserHelpers.h | 11 ++++
src/colorer/parsers/TextParserImpl.cpp | 14 ++---
src/colorer/parsers/TextParserImpl.h | 5 +-
tests/unit/test_textparser.cpp | 61 +++++++++++++++++++++
6 files changed, 153 insertions(+), 31 deletions(-)
diff --git a/.agents/skills/colorer-hrc/core-parse.md b/.agents/skills/colorer-hrc/core-parse.md
index 2ed4a4a..85d4743 100644
--- a/.agents/skills/colorer-hrc/core-parse.md
+++ b/.agents/skills/colorer-hrc/core-parse.md
@@ -76,7 +76,7 @@ Pick the candidate slice, then first match wins (HRC order inside the slice):
### ParseCache
-Multi-line blocks are a tree (`sline`/`eline`, scheme, start-RE `SMatches`, optional `backLine`, virtual table snapshot). Single-line matches are not cached.
+Multi-line blocks are a tree (`sline`/`eline`, scheme, start-RE `SMatches`, optional `backLine`, virtual table snapshot). Single-line matches are not cached. Siblings are ordered by `sline` and do not overlap. `searchLine` keeps a per-parent cursor (`search_child`) so forward `idleJob` chunks do not rescan the whole list; a long jump toward the start restarts from the head.
`parse(from, num, mode)`:
diff --git a/src/colorer/parsers/TextParserHelpers.cpp b/src/colorer/parsers/TextParserHelpers.cpp
index 0dd8c40..495ce55 100644
--- a/src/colorer/parsers/TextParserHelpers.cpp
+++ b/src/colorer/parsers/TextParserHelpers.cpp
@@ -6,8 +6,9 @@
ParseCache::~ParseCache()
{
// COLORER_LOG_DEEPTRACE("[TPCache] ~ParseCache():%,%-%", *scheme->getName(), sline, eline);
+ ParseCache* previous = prev;
delete backLine;
- delete children;
+ dropChildren();
prev = nullptr;
if (next) {
@@ -22,36 +23,84 @@ ParseCache::~ParseCache()
tmp->next = nullptr;
}
delete next;
+ next = nullptr;
}
delete[] vcache;
+ if (parent && parent->search_child == this) {
+ parent->search_child = previous;
+ }
+}
+
+void ParseCache::dropChildren()
+{
+ delete children;
+ children = nullptr;
+ search_child = nullptr;
+}
+
+void ParseCache::dropNext()
+{
+ delete next;
+ next = nullptr;
}
+namespace {
+
+ParseCache* rightmostAtOrBefore(ParseCache* node, int ln)
+{
+ while (node->next && node->next->sline <= ln) {
+ node = node->next;
+ }
+ return node;
+}
+
+} // namespace
+
ParseCache* ParseCache::searchLine(int ln, ParseCache** cache)
{
- ParseCache* r1 = nullptr;
- ParseCache* r2 = nullptr;
- ParseCache* tmp = this;
*cache = nullptr;
- while (tmp) {
- COLORER_LOG_DEEPTRACE("[TPCache] searchLine() tmp:%,%-%", *tmp->scheme->getName(), tmp->sline, tmp->eline);
- if (tmp->sline <= ln && tmp->eline >= ln) {
- if (tmp->children) {
- r1 = tmp->children->searchLine(ln, &r2);
- }
- if (r1) {
- *cache = r2;
- return r1;
- }
- *cache = r2; // last child
- return tmp;
- }
- if (tmp->sline <= ln) {
- *cache = tmp;
+
+ ParseCache* node = this;
+ if (parent && parent->search_child) {
+ node = parent->search_child;
+ }
+
+ if (node->sline <= ln) {
+ node = rightmostAtOrBefore(node, ln);
+ }
+ else if (node->prev && node->prev->sline <= ln) {
+ // One sibling back: tryParseLine(line+1) then searchLine(line).
+ node = node->prev;
+ }
+ else {
+ // Long jump toward the start: walk from the head, not back from EOF.
+ node = rightmostAtOrBefore(this, ln);
+ }
+
+ if (parent) {
+ parent->search_child = node;
+ }
+ if (node->sline > ln) {
+ return nullptr;
+ }
+
+ COLORER_LOG_DEEPTRACE("[TPCache] searchLine() tmp:%,%-%", *node->scheme->getName(), node->sline, node->eline);
+ if (node->eline < ln) {
+ *cache = node;
+ return nullptr;
+ }
+
+ ParseCache* child_cache = nullptr;
+ if (node->children) {
+ if (auto* found = node->children->searchLine(ln, &child_cache)) {
+ *cache = child_cache;
+ return found;
}
- tmp = tmp->next;
}
- return nullptr;
+
+ *cache = child_cache; // last child
+ return node;
}
/////////////////////////////////////////////////////////////////////////
diff --git a/src/colorer/parsers/TextParserHelpers.h b/src/colorer/parsers/TextParserHelpers.h
index 5edf7bd..9e19256 100644
--- a/src/colorer/parsers/TextParserHelpers.h
+++ b/src/colorer/parsers/TextParserHelpers.h
@@ -88,11 +88,22 @@ class ParseCache
~ParseCache();
/**
* Searched a cache position for the specified line number.
+ * Siblings are ordered by sline and do not overlap; the rightmost
+ * node with sline <= ln is the unique covering candidate (or a
+ * predecessor when that node ends before ln).
* @param ln Line number to search for
* @param cache Cache entry, filled with last child cache entry.
* @return Cache entry, assigned to the specified line number
*/
ParseCache* searchLine(int ln, ParseCache** cache);
+ /** Delete the children list and clear the search cursor into it. */
+ void dropChildren();
+ /** Delete the sibling suffix starting at next. */
+ void dropNext();
+
+ private:
+ /** Last searched immediate child. idleJob's forward chunks resume here. */
+ ParseCache* search_child = nullptr;
};
#endif // COLORER_TEXTPARSERPELPERS_H
diff --git a/src/colorer/parsers/TextParserImpl.cpp b/src/colorer/parsers/TextParserImpl.cpp
index 96dce96..98c312d 100644
--- a/src/colorer/parsers/TextParserImpl.cpp
+++ b/src/colorer/parsers/TextParserImpl.cpp
@@ -89,14 +89,12 @@ int TextParser::Impl::parse(int from, int num, TextParseMode mode)
return from;
}
if (updateCache) {
- delete parent->children;
- parent->children = nullptr;
+ parent->dropChildren();
}
}
else {
if (updateCache) {
- delete forward->next;
- forward->next = nullptr;
+ forward->dropNext();
}
}
baseScheme = parent->scheme;
@@ -476,12 +474,14 @@ int TextParser::Impl::searchBL(SchemeNodeBlock* node, int no, int lowLen, int hi
if (updateCache) {
if (old_gy == current_parse_line) {
- delete OldCacheF;
if (ResF) {
- ResF->next = nullptr;
+ ResF->dropNext();
}
else if (ResP) {
- ResP->children = nullptr;
+ ResP->dropChildren();
+ }
+ else {
+ delete OldCacheF;
}
forward = ResF;
parent = ResP;
diff --git a/src/colorer/parsers/TextParserImpl.h b/src/colorer/parsers/TextParserImpl.h
index 056129e..0dc54ba 100644
--- a/src/colorer/parsers/TextParserImpl.h
+++ b/src/colorer/parsers/TextParserImpl.h
@@ -12,8 +12,9 @@
* works with parsed internal HRC structure and colorisez
* text in a target editor system.
*
- * Hot path: parse() walks ParseCache to the scheme covering `from`,
- * then colorize() on each line. One scheme is active; gx is the column.
+ * Hot path: parse() walks ParseCache to the scheme covering `from`
+ * (searchLine resumes from a per-parent sibling cursor), then
+ * colorize() on each line. One scheme is active; gx is the column.
* Per line the ASCII occupancy mask (str_chars) is computed once and
* passed into every CRegExp::mayMatch/parse. searchMatch() uses the
* scheme's searchDispatch (first-character candidate list) then tries
diff --git a/tests/unit/test_textparser.cpp b/tests/unit/test_textparser.cpp
index e840348..8b194c8 100644
--- a/tests/unit/test_textparser.cpp
+++ b/tests/unit/test_textparser.cpp
@@ -383,6 +383,67 @@ TEST_CASE("tryParseLine accepts content edits and rejects block-boundary edits",
}
}
+TEST_CASE("Chunked cache updates keep searchLine covering the right block", "[textparser]")
+{
+ auto hrc_path = fs::path(__FILE__).parent_path() / "data" / "type_tryline.hrc";
+ XmlInputSource input(UnicodeString(hrc_path.c_str()));
+ HrcLibrary lib;
+ lib.loadSource(&input);
+ auto* file_type = lib.getFileType(UnicodeString("try_line"));
+ REQUIRE(file_type != nullptr);
+ REQUIRE(file_type->getBaseScheme() != nullptr);
+
+ constexpr int kBlocks = 40;
+ std::vector<UnicodeString> lines;
+ lines.reserve(static_cast<size_t>(kBlocks) * 4);
+ for (int i = 0; i < kBlocks; i++) {
+ lines.emplace_back(u"int x");
+ lines.emplace_back(u"/*");
+ lines.emplace_back(u" cmt");
+ lines.emplace_back(u"*/");
+ }
+ const int n = static_cast<int>(lines.size());
+ const int last_cmt = (kBlocks - 1) * 4 + 2;
+
+ InvalidatingLineSource source(std::move(lines));
+ CollectHandler handler;
+ TextParser parser;
+ parser.setFileType(file_type);
+ parser.setLineSource(&source);
+ parser.setRegionHandler(&handler);
+
+ int pos = 0;
+ while (pos < n) {
+ const int chunk = n - pos < 3 ? n - pos : 3;
+ pos = parser.parse(pos, chunk, TextParser::TextParseMode::TPM_CACHE_UPDATE) + 1;
+ }
+
+ SECTION("tryParseLine still matches the stack in an early and a late comment")
+ {
+ REQUIRE(parser.tryParseLine(2));
+ REQUIRE(parser.tryParseLine(last_cmt));
+ REQUIRE(parser.tryParseLine(0));
+ }
+
+ SECTION("CACHE_READ after warming to EOF still colors the first line")
+ {
+ handler.hits.clear();
+ parser.parse(0, 1, TextParser::TextParseMode::TPM_CACHE_READ);
+ REQUIRE_FALSE(handler.hits.empty());
+ REQUIRE(handler.hits.front().name.compare(UnicodeString("try_line:Kw")) == 0);
+ }
+
+ SECTION("CACHE_READ of a late comment after a jump to line 0 stays in that block")
+ {
+ handler.hits.clear();
+ parser.parse(0, 1, TextParser::TextParseMode::TPM_CACHE_READ);
+ handler.hits.clear();
+ parser.parse(last_cmt, 1, TextParser::TextParseMode::TPM_CACHE_READ);
+ REQUIRE_FALSE(handler.hits.empty());
+ REQUIRE(handler.hits.front().name.compare(UnicodeString("try_line:Cmt")) == 0);
+ }
+}
+
TEST_CASE("Long lines keep coloring past the maxBlockSize window", "[textparser]")
{
auto hrc_path = fs::path(__FILE__).parent_path() / "data" / "type_longline.hrc";