Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 71 additions & 10 deletions packages/monaco/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ShikiInternal, ThemeRegistrationResolved } from '@shikijs/types'
import type monacoNs from 'monaco-editor-core'
import type { MonacoLineToken } from './types'
import { EncodedTokenMetadata, INITIAL } from '@shikijs/vscode-textmate'
import { EncodedTokenMetadata, FontStyle, INITIAL } from '@shikijs/vscode-textmate'
import { TokenizerState } from './tokenizer'
import { normalizeColor } from './utils'

Expand Down Expand Up @@ -36,11 +36,12 @@ export function textmateThemeToMonacoTheme(theme: ThemeRegistrationResolved): Mo

for (const s of scopes) {
if (s && (foreground || background || fontStyle)) {
const normalizedFontStyle = normalizeFontStyleString(fontStyle)
rules.push({
token: s,
foreground: normalizeColor(foreground),
background: normalizeColor(background),
fontStyle,
fontStyle: normalizedFontStyle,
})
}
}
Expand Down Expand Up @@ -76,7 +77,7 @@ export function shikiToMonaco(
}

const colorMap: string[] = []
const colorToScopeMap = new Map<string, string>()
const colorStyleToScopeMap = new Map<string, string>()

// Because Monaco does not have the API of reading the current theme,
// We hijack it here to keep track of the current theme.
Expand All @@ -88,20 +89,33 @@ export function shikiToMonaco(
ret.colorMap.forEach((color, i) => {
colorMap[i] = color
})
colorToScopeMap.clear()
colorStyleToScopeMap.clear()
theme?.rules.forEach((rule) => {
const c = normalizeColor(rule.foreground)
if (c && !colorToScopeMap.has(c))
colorToScopeMap.set(c, rule.token)
if (!c)
return

const normalizedStyle = normalizeFontStyleString(rule.fontStyle)

const key = normalizedStyle ? `${c}|${normalizedStyle}` : c

if (!colorStyleToScopeMap.has(key))
colorStyleToScopeMap.set(key, rule.token)
})
_setTheme(themeName)
}

// Set the first theme as the default theme
monaco.editor.setTheme(themeIds[0])

function findScopeByColor(color: string): string | undefined {
return colorToScopeMap.get(color)
function findScopeByColorAndStyle(color: string, fontStyle: FontStyle): string | undefined {
const normalizedStyle = normalizeFontStyleBits(fontStyle)
if (normalizedStyle) {
const scoped = colorStyleToScopeMap.get(`${color}|${normalizedStyle}`)
if (scoped)
return scoped
}
return colorStyleToScopeMap.get(color)
}

// Do not attempt to tokenize if a line is too long
Expand Down Expand Up @@ -141,10 +155,11 @@ export function shikiToMonaco(
const startIndex = result.tokens[2 * j]
const metadata = result.tokens[2 * j + 1]
const color = normalizeColor(colorMap[EncodedTokenMetadata.getForeground(metadata)] || '')
const fontStyle = EncodedTokenMetadata.getFontStyle(metadata)

// Because Monaco only support one scope per token,
// we workaround this to use color to trace back the scope
const scope = findScopeByColor(color) || ''
// we workaround this to use color (and font style when available) to trace back the scope
const scope = color ? (findScopeByColorAndStyle(color, fontStyle) || '') : ''
tokens.push({ startIndex, scopes: scope })
}

Expand All @@ -154,3 +169,49 @@ export function shikiToMonaco(
}
}
}

function normalizeFontStyleBits(fontStyle: FontStyle): string {
if (fontStyle <= FontStyle.None)
return ''

const styles: string[] = []

if (fontStyle & FontStyle.Italic)
styles.push('italic')
if (fontStyle & FontStyle.Bold)
styles.push('bold')
if (fontStyle & FontStyle.Underline)
styles.push('underline')
if (fontStyle & FontStyle.Strikethrough)
styles.push('strikethrough')

return styles.join(' ')
}

function normalizeFontStyleString(fontStyle?: string): string {
if (!fontStyle)
return ''

const styles = new Set(
fontStyle
.split(/[\s,]+/)
.map(style => style.trim().toLowerCase())
.filter(Boolean),
)
// Remove default or empty style markers sometimes present in theme data
styles.delete('')
styles.delete('normal')
styles.delete('none')
Comment on lines +202 to +204
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are those?

Copy link
Author

@prempyla prempyla Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antfu These lines remove redundant or default style tokens ('', normal, none) that can appear in some themes.
They don’t affect valid styles but keep normalization consistent.
I can remove them if you prefer.


const ordered: string[] = []
if (styles.has('italic'))
ordered.push('italic')
if (styles.has('bold'))
ordered.push('bold')
if (styles.has('underline'))
ordered.push('underline')
if (styles.has('strikethrough') || styles.has('line-through'))
ordered.push('strikethrough')

return ordered.join(' ')
}
Loading