perf(highlight): precompute SGR at compile, sorted span claiming, stream ESC-free fast path

- compileRules precomputes the full SGR open sequence per rule and per band;
  wrap() is now pure concatenation, bandOpen() a table lookup (output bytes
  unchanged, bg keeps its unvalidated white-fallback)
- claim() replaces the linear overlaps() scan: claimed spans stay sorted by
  start, O(1) append on the common left-to-right sweep plus one binary search
  otherwise (match-dense 200KB payload: -19% one-shot, -56% streamed)
- HighlightStream: bufferHasEsc flag skips the O(buffer) escape rescans when
  neither the held text nor the chunk contains ESC, fixing quadratic rescan on
  escape-free floods (plain 200KB streamed: -54%)
- applyHighlights tracks the consumed match budget incrementally instead of
  two budgets.reduce() passes per text token
- ESCAPE_RE now covers DCS/APC/PM/SOS (BEL or ST terminated) and single-char
  Fe escapes (DECSC/DECRC/NEL/RI/RIS, orphan ST); isIncompleteEscape holds cut
  tails of the new families; split-smoke exercises every cut point through them
This commit is contained in:
Bill committed 2026-09-28 13:06:29 +08:00
1 parent 4e5377f49c
commit 462da60977
3 files changed
+220 -57

No files matched your search

+48 -1
View File
@@ -12,7 +12,7 @@
* --outfile=tests/.hl-rules.cjs --alias:@shared=./src/shared
* node tests/hl-rules.mjs
*/
import { compileRules, applyHighlights } from '../src/renderer/src/terminal/highlightEngine.ts'
import { compileRules, applyHighlights, __testHooks as hooks } from '../src/renderer/src/terminal/highlightEngine.ts'
import { previewSpans } from '../src/renderer/src/terminal/highlightEngine.ts'
import { DEFAULT_HIGHLIGHT_RULES } from '../src/shared/settings.ts'
import { exportHighlightRules, mergeRules, parseHighlightRules } from '../src/shared/highlightIO.ts'
@@ -739,5 +739,52 @@ console.log('[profiles]')
)
}
// ---- 19. escape sequences pass through untouched ---------------------------
console.log('[escape sequences]')
{
// DCS / APC / PM / SOS bodies and single-char Fe escapes must keep their
// bytes raw — never wrapped in SGR — while text around them stays highlighted.
const families = [
['DCS (ST)', '\x1bP0;1;0q#0;2;0;0;0~..\x1b\\'],
['DCS (BEL)', '\x1bP1;2|9/9\x07'],
['APC (kitty graphics)', '\x1b_Gf=32,s=10,v=10,m=1\x1b\\'],
['PM', '\x1b^weather\x1b\\'],
['SOS', '\x1bXsos-frame\x1b\\']
]
// one coloured span, exactly as the engine emits it (ESC-free span text)
const SPAN_RE = /\x1b\[38;2;\d+;\d+;\d+(?:;48;2;\d+;\d+;\d+)?m[^\x1b]*\x1b\[0m/g
for (const [name, seq] of families) {
const out = applyHighlights(`DONE ${seq} ERROR`, rules)
ok(out.includes(`${sgr(OK_GREEN)}DONE\x1b[0m`), `${name}: text before it keeps its highlight`)
ok(out.includes(`${sgr(BAD_RED)}ERROR\x1b[0m`), `${name}: text after it keeps its highlight`)
ok(
out.replace(SPAN_RE, '').includes(seq),
`${name}: sequence bytes pass through raw, never wrapped in SGR`
)
}
const fe = applyHighlights('\x1b7DONE\x1b8 1', rules)
ok(fe.includes(`${sgr(OK_GREEN)}DONE\x1b[0m`), 'text between DECSC/DECRC keeps its highlight')
ok(fe.includes('\x1b7') && fe.includes('\x1b8'), 'DECSC/DECRC bytes pass through byte-for-byte')
// backstop: a text run holding a residual (unterminated) ESC passes through
// raw byte-for-byte — never partially highlighted, never corrupted
const raw = 'DONE \x1bP0;1;0qcut ERROR'
ok(applyHighlights(raw, rules) === raw, 'an unterminated DCS keeps its run byte-for-byte raw')
// the stream treats a cut DCS/APC/PM/SOS tail as incomplete, like OSC
ok(hooks.isIncompleteEscape('\x1b') === true, 'a lone trailing ESC stays incomplete')
ok(hooks.isIncompleteEscape('\x1bP0;1;0qcut') === true, 'a cut DCS tail counts as incomplete')
ok(hooks.isIncompleteEscape('\x1b_Gf=32') === true, 'a cut APC tail counts as incomplete')
ok(
hooks.isIncompleteEscape('\x1b^w') === true && hooks.isIncompleteEscape('\x1bXs') === true,
'cut PM/SOS tails count as incomplete'
)
ok(hooks.isIncompleteEscape('\x1bP0;1;0qx\x1b\\') === false, 'a terminated DCS is complete')
ok(hooks.isIncompleteEscape('\x1b7') === false, 'a complete two-byte Fe escape is complete')
ok(hooks.findIncompleteIndex('DONE \x1bP0;1;0qcut') === 5, 'the stream holds from the DCS initiator')
ok(hooks.findIncompleteIndex('DONE \x1b7') === -1, 'a trailing DECSC is not held as incomplete')
}
console.log(failed === 0 ? '\n[hl-rules] ALL CHECKS PASSED' : `\n[hl-rules] ${failed} CHECK(S) FAILED`)
process.exit(failed === 0 ? 0 : 1)
+14 -2
View File
@@ -7,12 +7,24 @@ import { DEFAULT_HIGHLIGHT_RULES } from '../src/shared/settings.ts'
const rules = compileRules(DEFAULT_HIGHLIGHT_RULES)
// kimi-like: truecolor SGR, OSC title, box drawing, per-line repaints
// kimi-like: truecolor SGR, OSC title, box drawing, per-line repaints.
// The tail line exercises the string-terminated families (DCS/sixel, APC/kitty
// graphics, PM, SOS), a BEL-terminated DCS, and DECSC/DECRC (`\x1b7`/`\x1b8`)
// so every cut point through them is checked against the reference.
const banner =
'\x1b]0;kimi — session\x07' +
'\x1b[38;2;79;168;255m╭───────╮\x1b[0m\r\n' +
'\x1b[38;2;136;136;136mWelcome to Kimi Code!\x1b[0m \x1b[38;2;79;168;255m│\x1b[0m send /help\r\n' +
'\x1b[38;2;224;224;224mModel: MiniMax-M3 0.41.038 ERROR 99% https://example.com/x\x1b[0m\r\n'
'\x1b[38;2;224;224;224mModel: MiniMax-M3 0.41.038 ERROR 99% https://example.com/x\x1b[0m\r\n' +
'\x1b7' +
'\x1b[38;2;224;224;224mERROR 85% exit code 1\x1b[0m' +
'\x1bP0;1;0q#0;2;0;0;0~-..\x1b\\' +
'\x1bP1;2|9/9\x07' +
'\x1b_Gf=32,s=10,v=10,m=1\x1b\\' +
'\x1b^pm-note\x1b\\' +
'\x1bXsos-frame\x1b\\' +
'\x1b[38;2;136;136;136mdone \u2713\x1b[0m' +
'\x1b8\r\n'
const reference = applyHighlights(banner, rules)