Highlighting: invalidate from the edited row, not the whole buffer

Every edit bumped the buffer version (through SetDirty) and called
InvalidateFrom(0), so all cached spans and line states were discarded
and stateful highlighters re-lexed from row 0 to the viewport on each
keystroke.

Buffer mutators now report the row they changed (edited_at_), which
bumps the version and tells the engine to drop only rows from there
down; a row's highlighting depends only on rows above it. A version
change that arrives without that notice still clears everything, so a
missed path cannot show stale spans. The engine keeps states as a
contiguous vector and caches spans for every row it lexes. SetDirty no
longer touches the version: the edits themselves do, including undo
and redo back to the saved state (D9 still holds; its test now checks
the highlighting itself).

Typing near the end of a 100k-line C++ file: 46.3 ms -> 0.14 ms of
highlighting work per keystroke.

Audit finding P3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent 4c42761fc0
commit f853fa60b1
5 changed files with 177 additions and 135 deletions
+19 -12
View File
@@ -2,8 +2,8 @@
#pragma once
#include <cstdint>
#include <map>
#include <memory>
#include <unordered_map>
#include <vector>
#include <mutex>
@@ -29,6 +29,12 @@ public:
// Invalidate cached lines from row (inclusive)
void InvalidateFrom(int row);
// The buffer changed at `row` and is now at `buf_version`. Highlighting
// of a row depends only on the rows above it, so rows before `row` stay
// cached. A version change that arrives without OnEdit (a path that
// changed content without reporting it) still clears the whole cache.
void OnEdit(int row, std::uint64_t buf_version);
bool HasHighlighter() const
{
@@ -46,20 +52,21 @@ public:
private:
std::unique_ptr<LanguageHighlighter> hl_;
// Simple cache by row index (mutable to allow caching in const GetLine)
mutable std::unordered_map<int, LineHighlight> cache_;
// Cached spans by row, valid for version_ (ordered so InvalidateFrom can
// drop a tail cheaply).
mutable std::map<int, LineHighlight> cache_;
// For stateful highlighters, remember per-line state (state after finishing that row)
struct StateEntry {
std::uint64_t version{0};
// Using the interface type; forward-declare via header
StatefulHighlighter::LineState state;
};
// For stateful highlighters: states_[r] is the state after row r, for
// every row 0..states_.size()-1 (always a contiguous prefix).
mutable std::vector<StatefulHighlighter::LineState> states_;
mutable std::unordered_map<int, StateEntry> state_cache_;
// Buffer version the caches describe.
mutable std::uint64_t version_{0};
mutable bool have_version_{false};
// Track best known contiguous state row for a given version to avoid O(n) scans
mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
void clear_caches_locked() const;
void invalidate_from_locked(int row) const;
// Guards the caches above.
mutable std::mutex mtx_;