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:
1 parent
4e5377f49c
commit
462da60977
3 files changed
+220
-57
No files matched your search
+48
-1
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user