1- import type { BundledLanguage , BundledTheme , CodeToTokensWithThemesOptions , HighlighterCore , ThemedTokenWithVariants } from "shiki" ;
1+ import type { BundledLanguage , BundledTheme , CodeToTokensWithThemesOptions , GrammarState , HighlighterCore , ThemedTokenWithVariants } from "shiki" ;
22import { debounce } from "./utils" ;
33
44export interface MountPlainShikiOptions {
@@ -40,11 +40,17 @@ export interface MountPlainShikiOptions {
4040
4141export type CreatePlainShikiReturns = ReturnType < typeof createPlainShiki > ;
4242
43- interface ColorLoads {
43+ interface ColorLoad {
4444 token : ThemedTokenWithVariants ;
4545 range : Range ;
4646}
4747
48+ interface LoadLine {
49+ text : string ;
50+ lastGrammarState : GrammarState ;
51+ loads : ColorLoad [ ] ;
52+ }
53+
4854export function createPlainShiki ( shiki : HighlighterCore ) {
4955 const isSupported = ( ) => "CSS" in globalThis && "highlights" in CSS ;
5056
@@ -65,6 +71,7 @@ export function createPlainShiki(shiki: HighlighterCore) {
6571 const stylesheet = new CSSStyleSheet ( ) ;
6672 document . adoptedStyleSheets . push ( stylesheet ) ;
6773
74+ const loadLines : LoadLine [ ] = [ ] ;
6875 const names = new Set < string > ( ) ;
6976
7077 if ( isSupported ( ) ) {
@@ -79,16 +86,28 @@ export function createPlainShiki(shiki: HighlighterCore) {
7986 document . adoptedStyleSheets . splice ( idx , 1 ) ;
8087 }
8188
82- function patch ( loads : ColorLoads [ ] ) {
83- for ( const name of names ) {
84- const highlight = CSS . highlights . get ( name ) ;
85- highlight ?. clear ( ) ;
89+ function resolveName ( theme : string , color : string ) {
90+ return `shiki-${ theme } -${ color . slice ( 1 ) . toLowerCase ( ) } ` ;
91+ }
92+
93+ function patch ( loads : ColorLoad [ ] , oldLoads : ColorLoad [ ] ) {
94+ for ( const { token, range } of oldLoads ) {
95+ for ( const theme in token . variants ) {
96+ const { color } = token . variants [ theme ] ;
97+ const name = resolveName ( theme , color ! ) ;
98+
99+ const highlight = CSS . highlights . get ( name ) ;
100+ if ( ! highlight ) {
101+ continue ;
102+ }
103+ highlight . delete ( range ) ;
104+ }
86105 }
87106
88107 for ( const { token, range } of loads ) {
89108 for ( const theme in token . variants ) {
90109 const { color } = token . variants [ theme ] ;
91- const name = `shiki- ${ theme } - ${ color ! . slice ( 1 ) . toLowerCase ( ) } ` ;
110+ const name = resolveName ( theme , color ! ) ;
92111 const isDefault = theme === "light" ;
93112
94113 let highlight = CSS . highlights . get ( name ) ;
@@ -110,6 +129,21 @@ export function createPlainShiki(shiki: HighlighterCore) {
110129 }
111130 }
112131
132+ function diff ( textLines : string [ ] ) {
133+ let i = 0 ;
134+ for ( ; i < textLines . length ; i ++ ) {
135+ if ( textLines [ i ] !== loadLines [ i ] ?. text ) {
136+ return i ;
137+ }
138+ for ( const { range } of loadLines [ i ] ?. loads ?? [ ] ) {
139+ if ( range . collapsed ) {
140+ return i ;
141+ }
142+ }
143+ }
144+ return i ;
145+ }
146+
113147 function update ( ) {
114148 const walker = document . createTreeWalker ( el , NodeFilter . SHOW_TEXT ) ;
115149
@@ -120,33 +154,52 @@ export function createPlainShiki(shiki: HighlighterCore) {
120154 }
121155
122156 const { innerText } = el ;
123- const tokenLines = shiki . codeToTokensWithThemes ( innerText , {
124- lang,
125- themes
126- } ) ;
157+ const textLines = innerText . split ( "\n" ) ;
127158
128- let i = 0 ;
159+ let i = diff ( textLines ) ;
160+ let lineOffset = textLines . slice ( 0 , i ) . reduce ( ( res , text ) => res + text . length + 1 , 0 ) ;
161+
162+ let j = 0 ;
129163 let offset = 0 ;
130164 let isCorrect = false ;
165+ findNodeAndOffset ( lineOffset ) ;
166+
167+ for ( ; i < textLines . length ; i ++ ) {
168+ const text = textLines [ i ] ;
131169
132- const loads : ColorLoads [ ] = [ ] ;
133- for ( const tokens of tokenLines ) {
134- for ( const token of tokens ) {
135- const [ startNode , startOffset ] = findNodeAndOffset ( token . offset ) ;
136- const [ endNode , endOffset ] = findNodeAndOffset ( token . offset + token . content . length ) ;
170+ const tokenLines = shiki . codeToTokensWithThemes ( text , {
171+ lang,
172+ themes
173+ } ) ;
174+
175+ const loads : ColorLoad [ ] = [ ] ;
176+ for ( const token of tokenLines [ 0 ] ) {
177+ const [ startNode , startOffset ] = findNodeAndOffset ( lineOffset + token . offset ) ;
178+ const [ endNode , endOffset ] = findNodeAndOffset ( lineOffset + token . offset + token . content . length ) ;
137179
138180 const range = document . createRange ( ) ;
139181 range . setStart ( startNode , startOffset ) ;
140182 range . setEnd ( endNode , endOffset ) ;
141-
142183 loads . push ( { token, range } ) ;
143184 }
185+
186+ const loadLine = loadLines [ i ] ??= { } as LoadLine ;
187+ loadLine . text = text ;
188+
189+ patch ( loads , loadLine . loads ?? [ ] ) ;
190+ loadLine . loads = loads ;
191+
192+ const lastGrammarState = shiki . getLastGrammarState ( text ?? "" , { lang } ) ;
193+ if ( loadLine . lastGrammarState !== lastGrammarState ) {
194+ loadLine . lastGrammarState = lastGrammarState ;
195+ lineOffset += text . length + 1 ;
196+ }
197+ else break ;
144198 }
145- patch ( loads ) ;
146199
147200 function findNodeAndOffset ( tokenOffset : number ) {
148- for ( ; i < textNodes . length ; i ++ ) {
149- const node = textNodes [ i ] ;
201+ for ( ; j < textNodes . length ; j ++ ) {
202+ const node = textNodes [ j ] ;
150203 const { textContent } = node ;
151204 if ( ! textContent ) {
152205 continue ;
@@ -157,15 +210,13 @@ export function createPlainShiki(shiki: HighlighterCore) {
157210 isCorrect = true ;
158211 }
159212
160- if ( offset + textContent . length >= tokenOffset ) {
161- return [ node , tokenOffset - offset ] as const ;
162- }
163- else {
213+ if ( offset + textContent . length < tokenOffset ) {
164214 offset += textContent . length ;
165215 isCorrect = false ;
166216 }
217+ else break ;
167218 }
168- throw new RangeError ( `[Plain Shiki] cannot find node at offset ${ tokenOffset } .` ) ;
219+ return [ textNodes [ j ] , tokenOffset - offset ] as const ;
169220 }
170221 }
171222
0 commit comments