Skip to content

Commit 320d758

Browse files
authored
feat: store grammar state in weakmap (#804)
1 parent ea4b8dd commit 320d758

File tree

17 files changed

+4479
-296
lines changed

17 files changed

+4479
-296
lines changed

packages/core/src/constructors/bundle-factory.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ export interface ShorthandsBundle<L extends string, T extends string> {
190190
* Shorthand for `getLastGrammarState` with auto-loaded theme and language.
191191
* A singleton highlighter it maintained internally.
192192
*/
193-
getLastGrammarState: (code: string, options: CodeToTokensBaseOptions<L, T>) => Promise<GrammarState>
193+
getLastGrammarState:
194+
| ((element: ThemedToken[][] | Root) => GrammarState)
195+
| ((code: string, options: CodeToTokensBaseOptions<L, T>) => Promise<GrammarState>)
194196
}
195197

196198
export function makeSingletonHighlighter<L extends string, T extends string>(

packages/core/src/constructors/highlighter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export async function createHighlighterCore(options: HighlighterCoreOptions = {}
2020
const internal = await createShikiInternal(options)
2121

2222
return {
23-
getLastGrammarState: (code, options) => getLastGrammarState(internal, code, options),
23+
getLastGrammarState: (...args: any[]) => getLastGrammarState(internal, ...args as [any])!,
2424
codeToTokensBase: (code, options) => codeToTokensBase(internal, code, options),
2525
codeToTokensWithThemes: (code, options) => codeToTokensWithThemes(internal, code, options),
2626
codeToTokens: (code, options) => codeToTokens(internal, code, options),
@@ -43,7 +43,7 @@ export function createHighlighterCoreSync(options: HighlighterCoreOptions<true>
4343
const internal = createShikiInternalSync(options)
4444

4545
return {
46-
getLastGrammarState: (code, options) => getLastGrammarState(internal, code, options),
46+
getLastGrammarState: (...args: any[]) => getLastGrammarState(internal, ...args as [any, any]),
4747
codeToTokensBase: (code, options) => codeToTokensBase(internal, code, options),
4848
codeToTokensWithThemes: (code, options) => codeToTokensWithThemes(internal, code, options),
4949
codeToTokens: (code, options) => codeToTokens(internal, code, options),

packages/core/src/highlight/code-to-hast.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
CodeToHastOptions,
33
CodeToHastRenderOptions,
4+
GrammarState,
45
ShikiInternal,
56
ShikiTransformerContext,
67
ShikiTransformerContextCommon,
@@ -14,7 +15,7 @@ import type {
1415
} from 'hast'
1516

1617
import { FontStyle } from '@shikijs/vscode-textmate'
17-
18+
import { getLastGrammarStateFromMap, setLastGrammarStateToMap } from '../textmate/grammar-state'
1819
import { addClassToHast, getTokenStyleObject, stringifyTokenStyle } from '../utils'
1920
import { warnDeprecated } from '../warn'
2021
import { getTransformers } from './_get-transformers'
@@ -42,6 +43,7 @@ export function codeToHast(
4243
bg,
4344
themeName,
4445
rootStyle,
46+
grammarState,
4547
} = codeToTokens(internal, input, options)
4648

4749
const {
@@ -73,13 +75,15 @@ export function codeToHast(
7375
rootStyle,
7476
},
7577
contextSource,
78+
grammarState,
7679
)
7780
}
7881

7982
export function tokensToHast(
8083
tokens: ThemedToken[][],
8184
options: CodeToHastRenderOptions,
8285
transformerContext: ShikiTransformerContextSource,
86+
grammarState: GrammarState | undefined = getLastGrammarStateFromMap(tokens),
8387
): Root {
8488
const transformers = getTransformers(options)
8589

@@ -220,6 +224,9 @@ export function tokensToHast(
220224
for (const transformer of transformers)
221225
result = transformer?.root?.call(context, result) || result
222226

227+
if (grammarState)
228+
setLastGrammarStateToMap(result, grammarState)
229+
223230
return result
224231
}
225232

packages/core/src/highlight/code-to-tokens-base.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*-------------------------------------------------------- */
44
import type {
55
CodeToTokensBaseOptions,
6+
Grammar,
7+
GrammarState,
68
ShikiInternal,
79
ThemedToken,
810
ThemedTokenScopeExplanation,
@@ -11,15 +13,15 @@ import type {
1113
} from '@shikijs/types'
1214
import type {
1315
FontStyle,
14-
IGrammar,
1516
IRawThemeSetting,
1617
StateStack,
1718
} from '@shikijs/vscode-textmate'
1819

20+
import type { Root } from 'hast'
1921
import { ShikiError } from '@shikijs/types'
20-
import { EncodedTokenMetadata, INITIAL } from '@shikijs/vscode-textmate'
2122

22-
import { getGrammarStack, GrammarState } from '../textmate/grammar-state'
23+
import { EncodedTokenMetadata, INITIAL } from '@shikijs/vscode-textmate'
24+
import { getGrammarStack, getLastGrammarStateFromMap, GrammarState as GrammarStateImpl, setLastGrammarStateToMap } from '../textmate/grammar-state'
2325
import { applyColorReplacements, isNoneTheme, isPlainLang, resolveColorReplacements, splitLines } from '../utils'
2426
import { tokenizeAnsiWithTheme } from './code-to-tokens-ansi'
2527

@@ -50,19 +52,29 @@ export function codeToTokensBase(
5052
if (options.grammarState.lang !== _grammar.name) {
5153
throw new ShikiError(`Grammar state language "${options.grammarState.lang}" does not match highlight language "${_grammar.name}"`)
5254
}
53-
if (options.grammarState.theme !== themeName) {
54-
throw new ShikiError(`Grammar state theme "${options.grammarState.theme}" does not match highlight theme "${themeName}"`)
55+
if (!options.grammarState.themes.includes(theme.name)) {
56+
throw new ShikiError(`Grammar state themes "${options.grammarState.themes}" do not contain highlight theme "${theme.name}"`)
5557
}
5658
}
5759

5860
return tokenizeWithTheme(code, _grammar, theme, colorMap, options)
5961
}
6062

63+
export function getLastGrammarState(
64+
internal: ShikiInternal,
65+
element: ThemedToken[][] | Root
66+
): GrammarState | undefined
6167
export function getLastGrammarState(
6268
internal: ShikiInternal,
6369
code: string,
64-
options: CodeToTokensBaseOptions = {},
65-
): GrammarState {
70+
options?: CodeToTokensBaseOptions
71+
): GrammarState
72+
export function getLastGrammarState(...args: any[]): GrammarState | undefined {
73+
if (args.length === 2) {
74+
return getLastGrammarStateFromMap(args[1])
75+
}
76+
77+
const [internal, code, options = {}] = args as [ShikiInternal, string, CodeToTokensBaseOptions]
6678
const {
6779
lang = 'text',
6880
theme: themeName = internal.getLoadedThemes()[0],
@@ -77,7 +89,7 @@ export function getLastGrammarState(
7789

7890
const _grammar = internal.getLanguage(lang)
7991

80-
return new GrammarState(
92+
return new GrammarStateImpl(
8193
_tokenizeWithTheme(code, _grammar, theme, colorMap, options).stateStack,
8294
_grammar.name,
8395
theme.name,
@@ -92,17 +104,27 @@ interface ThemeSettingsSelectors {
92104

93105
export function tokenizeWithTheme(
94106
code: string,
95-
grammar: IGrammar,
107+
grammar: Grammar,
96108
theme: ThemeRegistrationResolved,
97109
colorMap: string[],
98110
options: TokenizeWithThemeOptions,
99111
): ThemedToken[][] {
100-
return _tokenizeWithTheme(code, grammar, theme, colorMap, options).tokens
112+
const result = _tokenizeWithTheme(code, grammar, theme, colorMap, options)
113+
114+
const grammarState = new GrammarStateImpl(
115+
_tokenizeWithTheme(code, grammar, theme, colorMap, options).stateStack,
116+
grammar.name,
117+
theme.name,
118+
)
119+
120+
setLastGrammarStateToMap(result.tokens, grammarState)
121+
122+
return result.tokens
101123
}
102124

103125
function _tokenizeWithTheme(
104126
code: string,
105-
grammar: IGrammar,
127+
grammar: Grammar,
106128
theme: ThemeRegistrationResolved,
107129
colorMap: string[],
108130
options: TokenizeWithThemeOptions,
@@ -120,7 +142,7 @@ function _tokenizeWithTheme(
120142
const lines = splitLines(code)
121143

122144
let stateStack = options.grammarState
123-
? getGrammarStack(options.grammarState)
145+
? getGrammarStack(options.grammarState, theme.name) ?? INITIAL
124146
: options.grammarContextCode != null
125147
? _tokenizeWithTheme(
126148
options.grammarContextCode,

packages/core/src/highlight/code-to-tokens-themes.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
ThemedToken,
55
ThemedTokenWithVariants,
66
} from '@shikijs/types'
7+
import { getLastGrammarStateFromMap, GrammarState, setLastGrammarStateToMap } from '../textmate/grammar-state'
78
import { codeToTokensBase } from './code-to-tokens-base'
89

910
/**
@@ -19,11 +20,24 @@ export function codeToTokensWithThemes(
1920
.filter(i => i[1])
2021
.map(i => ({ color: i[0], theme: i[1]! }))
2122

22-
const tokens = syncThemesTokenization(
23-
...themes.map(t => codeToTokensBase(internal, code, {
23+
const themedTokens = themes.map((t) => {
24+
const tokens = codeToTokensBase(internal, code, {
2425
...options,
2526
theme: t.theme,
26-
})),
27+
})
28+
const state = getLastGrammarStateFromMap(tokens)
29+
const theme = typeof t.theme === 'string'
30+
? t.theme
31+
: t.theme.name
32+
return {
33+
tokens,
34+
state,
35+
theme,
36+
}
37+
})
38+
39+
const tokens = syncThemesTokenization(
40+
...themedTokens.map(i => i.tokens),
2741
)
2842

2943
const mergedTokens: ThemedTokenWithVariants[][] = tokens[0]
@@ -54,6 +68,15 @@ export function codeToTokensWithThemes(
5468
}),
5569
)
5670

71+
const mergedGrammarState = themedTokens[0].state
72+
? new GrammarState(
73+
Object.fromEntries(themedTokens.map(s => [s.theme, s.state?.getInternalStack(s.theme)])),
74+
themedTokens[0].state.lang,
75+
)
76+
: undefined
77+
if (mergedGrammarState)
78+
setLastGrammarStateToMap(mergedTokens, mergedGrammarState)
79+
5780
return mergedTokens
5881
}
5982

packages/core/src/highlight/code-to-tokens.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import type { CodeToTokensOptions, ShikiInternal, ThemedToken, ThemedTokenWithVariants, TokensResult } from '@shikijs/types'
2-
import { ShikiError } from '../../../types/src/error'
1+
import type { CodeToTokensOptions, GrammarState, ShikiInternal, ThemedToken, ThemedTokenWithVariants, TokensResult } from '@shikijs/types'
2+
import { ShikiError } from '@shikijs/types'
3+
import { getLastGrammarStateFromMap, setLastGrammarStateToMap } from '../textmate/grammar-state'
34
import { applyColorReplacements, getTokenStyleObject, resolveColorReplacements } from '../utils'
45
import { codeToTokensBase } from './code-to-tokens-base'
56
import { codeToTokensWithThemes } from './code-to-tokens-themes'
@@ -19,6 +20,7 @@ export function codeToTokens(
1920
let tokens: ThemedToken[][]
2021
let themeName: string
2122
let rootStyle: string | undefined
23+
let grammarState: GrammarState | undefined
2224

2325
if ('themes' in options) {
2426
const {
@@ -41,6 +43,8 @@ export function codeToTokens(
4143
options,
4244
)
4345

46+
grammarState = getLastGrammarStateFromMap(themeTokens)
47+
4448
if (defaultColor && !themes.find(t => t.color === defaultColor))
4549
throw new ShikiError(`\`themes\` option must contain the defaultColor key \`${defaultColor}\``)
4650

@@ -49,6 +53,9 @@ export function codeToTokens(
4953
tokens = themeTokens
5054
.map(line => line.map(token => mergeToken(token, themesOrder, cssVariablePrefix, defaultColor)))
5155

56+
if (grammarState)
57+
setLastGrammarStateToMap(tokens, grammarState)
58+
5259
const themeColorReplacements = themes.map(t => resolveColorReplacements(t.theme, options))
5360

5461
fg = themes.map((t, idx) => (idx === 0 && defaultColor
@@ -73,6 +80,7 @@ export function codeToTokens(
7380
bg = applyColorReplacements(_theme.bg, colorReplacements)
7481
fg = applyColorReplacements(_theme.fg, colorReplacements)
7582
themeName = _theme.name
83+
grammarState = getLastGrammarStateFromMap(tokens)
7684
}
7785
else {
7886
throw new ShikiError('Invalid options, either `theme` or `themes` must be provided')
@@ -84,6 +92,7 @@ export function codeToTokens(
8492
bg,
8593
themeName,
8694
rootStyle,
95+
grammarState,
8796
}
8897
}
8998

0 commit comments

Comments
 (0)