Path: blob/main/src/vs/editor/common/languages/supports/tokenization.ts
5251 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Color } from '../../../../base/common/color.js';6import { IFontTokenOptions } from '../../../../platform/theme/common/themeService.js';7import { LanguageId, FontStyle, ColorId, StandardTokenType, MetadataConsts } from '../../encodedTokenAttributes.js';89export interface ITokenThemeRule {10token: string;11foreground?: string;12background?: string;13fontStyle?: string;14}1516export class ParsedTokenThemeRule {17_parsedThemeRuleBrand: void = undefined;1819readonly token: string;20readonly index: number;2122/**23* -1 if not set. An or mask of `FontStyle` otherwise.24*/25readonly fontStyle: FontStyle;26readonly foreground: string | null;27readonly background: string | null;2829constructor(30token: string,31index: number,32fontStyle: number,33foreground: string | null,34background: string | null,35) {36this.token = token;37this.index = index;38this.fontStyle = fontStyle;39this.foreground = foreground;40this.background = background;41}42}4344/**45* Parse a raw theme into rules.46*/47export function parseTokenTheme(source: ITokenThemeRule[]): ParsedTokenThemeRule[] {48if (!source || !Array.isArray(source)) {49return [];50}51const result: ParsedTokenThemeRule[] = [];52let resultLen = 0;53for (let i = 0, len = source.length; i < len; i++) {54const entry = source[i];5556let fontStyle: number = FontStyle.NotSet;57if (typeof entry.fontStyle === 'string') {58fontStyle = FontStyle.None;5960const segments = entry.fontStyle.split(' ');61for (let j = 0, lenJ = segments.length; j < lenJ; j++) {62const segment = segments[j];63switch (segment) {64case 'italic':65fontStyle = fontStyle | FontStyle.Italic;66break;67case 'bold':68fontStyle = fontStyle | FontStyle.Bold;69break;70case 'underline':71fontStyle = fontStyle | FontStyle.Underline;72break;73case 'strikethrough':74fontStyle = fontStyle | FontStyle.Strikethrough;75break;76}77}78}7980let foreground: string | null = null;81if (typeof entry.foreground === 'string') {82foreground = entry.foreground;83}8485let background: string | null = null;86if (typeof entry.background === 'string') {87background = entry.background;88}8990result[resultLen++] = new ParsedTokenThemeRule(91entry.token || '',92i,93fontStyle,94foreground,95background96);97}9899return result;100}101102/**103* Resolve rules (i.e. inheritance).104*/105function resolveParsedTokenThemeRules(parsedThemeRules: ParsedTokenThemeRule[], customTokenColors: string[]): TokenTheme {106107// Sort rules lexicographically, and then by index if necessary108parsedThemeRules.sort((a, b) => {109const r = strcmp(a.token, b.token);110if (r !== 0) {111return r;112}113return a.index - b.index;114});115116// Determine defaults117let defaultFontStyle = FontStyle.None;118let defaultForeground = '000000';119let defaultBackground = 'ffffff';120while (parsedThemeRules.length >= 1 && parsedThemeRules[0].token === '') {121const incomingDefaults = parsedThemeRules.shift()!;122if (incomingDefaults.fontStyle !== FontStyle.NotSet) {123defaultFontStyle = incomingDefaults.fontStyle;124}125if (incomingDefaults.foreground !== null) {126defaultForeground = incomingDefaults.foreground;127}128if (incomingDefaults.background !== null) {129defaultBackground = incomingDefaults.background;130}131}132const colorMap = new ColorMap();133134// start with token colors from custom token themes135for (const color of customTokenColors) {136colorMap.getId(color);137}138139140const foregroundColorId = colorMap.getId(defaultForeground);141const backgroundColorId = colorMap.getId(defaultBackground);142143const defaults = new ThemeTrieElementRule(defaultFontStyle, foregroundColorId, backgroundColorId);144const root = new ThemeTrieElement(defaults);145for (let i = 0, len = parsedThemeRules.length; i < len; i++) {146const rule = parsedThemeRules[i];147root.insert(rule.token, rule.fontStyle, colorMap.getId(rule.foreground), colorMap.getId(rule.background));148}149150return new TokenTheme(colorMap, root);151}152153const colorRegExp = /^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/;154155export class ColorMap {156157private _lastColorId: number;158private readonly _id2color: Color[];159private readonly _color2id: Map<string, ColorId>;160161constructor() {162this._lastColorId = 0;163this._id2color = [];164this._color2id = new Map<string, ColorId>();165}166167public getId(color: string | null): ColorId {168if (color === null) {169return 0;170}171const match = color.match(colorRegExp);172if (!match) {173throw new Error('Illegal value for token color: ' + color);174}175color = match[1].toUpperCase();176let value = this._color2id.get(color);177if (value) {178return value;179}180value = ++this._lastColorId;181this._color2id.set(color, value);182this._id2color[value] = Color.fromHex('#' + color);183return value;184}185186public getColorMap(): Color[] {187return this._id2color.slice(0);188}189190}191192export class TokenTheme {193194public static createFromRawTokenTheme(source: ITokenThemeRule[], customTokenColors: string[]): TokenTheme {195return this.createFromParsedTokenTheme(parseTokenTheme(source), customTokenColors);196}197198public static createFromParsedTokenTheme(source: ParsedTokenThemeRule[], customTokenColors: string[]): TokenTheme {199return resolveParsedTokenThemeRules(source, customTokenColors);200}201202private readonly _colorMap: ColorMap;203private readonly _root: ThemeTrieElement;204private readonly _cache: Map<string, number>;205206constructor(colorMap: ColorMap, root: ThemeTrieElement) {207this._colorMap = colorMap;208this._root = root;209this._cache = new Map<string, number>();210}211212public getColorMap(): Color[] {213return this._colorMap.getColorMap();214}215216/**217* used for testing purposes218*/219public getThemeTrieElement(): ExternalThemeTrieElement {220return this._root.toExternalThemeTrieElement();221}222223public _match(token: string): ThemeTrieElementRule {224return this._root.match(token);225}226227public match(languageId: LanguageId, token: string): number {228// The cache contains the metadata without the language bits set.229let result = this._cache.get(token);230if (typeof result === 'undefined') {231const rule = this._match(token);232const standardToken = toStandardTokenType(token);233result = (234rule.metadata235| (standardToken << MetadataConsts.TOKEN_TYPE_OFFSET)236) >>> 0;237this._cache.set(token, result);238}239240return (241result242| (languageId << MetadataConsts.LANGUAGEID_OFFSET)243) >>> 0;244}245}246247const STANDARD_TOKEN_TYPE_REGEXP = /\b(comment|string|regex|regexp)\b/;248export function toStandardTokenType(tokenType: string): StandardTokenType {249const m = tokenType.match(STANDARD_TOKEN_TYPE_REGEXP);250if (!m) {251return StandardTokenType.Other;252}253switch (m[1]) {254case 'comment':255return StandardTokenType.Comment;256case 'string':257return StandardTokenType.String;258case 'regex':259return StandardTokenType.RegEx;260case 'regexp':261return StandardTokenType.RegEx;262}263throw new Error('Unexpected match for standard token type!');264}265266export function strcmp(a: string, b: string): number {267if (a < b) {268return -1;269}270if (a > b) {271return 1;272}273return 0;274}275276export class ThemeTrieElementRule {277_themeTrieElementRuleBrand: void = undefined;278279private _fontStyle: FontStyle;280private _foreground: ColorId;281private _background: ColorId;282public metadata: number;283284constructor(fontStyle: FontStyle, foreground: ColorId, background: ColorId) {285this._fontStyle = fontStyle;286this._foreground = foreground;287this._background = background;288this.metadata = (289(this._fontStyle << MetadataConsts.FONT_STYLE_OFFSET)290| (this._foreground << MetadataConsts.FOREGROUND_OFFSET)291| (this._background << MetadataConsts.BACKGROUND_OFFSET)292) >>> 0;293}294295public clone(): ThemeTrieElementRule {296return new ThemeTrieElementRule(this._fontStyle, this._foreground, this._background);297}298299public acceptOverwrite(fontStyle: FontStyle, foreground: ColorId, background: ColorId): void {300if (fontStyle !== FontStyle.NotSet) {301this._fontStyle = fontStyle;302}303if (foreground !== ColorId.None) {304this._foreground = foreground;305}306if (background !== ColorId.None) {307this._background = background;308}309this.metadata = (310(this._fontStyle << MetadataConsts.FONT_STYLE_OFFSET)311| (this._foreground << MetadataConsts.FOREGROUND_OFFSET)312| (this._background << MetadataConsts.BACKGROUND_OFFSET)313) >>> 0;314}315}316317export class ExternalThemeTrieElement {318319public readonly mainRule: ThemeTrieElementRule;320public readonly children: Map<string, ExternalThemeTrieElement>;321322constructor(323mainRule: ThemeTrieElementRule,324children: Map<string, ExternalThemeTrieElement> | { [key: string]: ExternalThemeTrieElement } = new Map<string, ExternalThemeTrieElement>()325) {326this.mainRule = mainRule;327if (children instanceof Map) {328this.children = children;329} else {330this.children = new Map<string, ExternalThemeTrieElement>();331for (const key in children) {332this.children.set(key, children[key]);333}334}335}336}337338export class ThemeTrieElement {339_themeTrieElementBrand: void = undefined;340341private readonly _mainRule: ThemeTrieElementRule;342private readonly _children: Map<string, ThemeTrieElement>;343344constructor(mainRule: ThemeTrieElementRule) {345this._mainRule = mainRule;346this._children = new Map<string, ThemeTrieElement>();347}348349/**350* used for testing purposes351*/352public toExternalThemeTrieElement(): ExternalThemeTrieElement {353const children = new Map<string, ExternalThemeTrieElement>();354this._children.forEach((element, index) => {355children.set(index, element.toExternalThemeTrieElement());356});357return new ExternalThemeTrieElement(this._mainRule, children);358}359360public match(token: string): ThemeTrieElementRule {361if (token === '') {362return this._mainRule;363}364365const dotIndex = token.indexOf('.');366let head: string;367let tail: string;368if (dotIndex === -1) {369head = token;370tail = '';371} else {372head = token.substring(0, dotIndex);373tail = token.substring(dotIndex + 1);374}375376const child = this._children.get(head);377if (typeof child !== 'undefined') {378return child.match(tail);379}380381return this._mainRule;382}383384public insert(token: string, fontStyle: FontStyle, foreground: ColorId, background: ColorId): void {385if (token === '') {386// Merge into the main rule387this._mainRule.acceptOverwrite(fontStyle, foreground, background);388return;389}390391const dotIndex = token.indexOf('.');392let head: string;393let tail: string;394if (dotIndex === -1) {395head = token;396tail = '';397} else {398head = token.substring(0, dotIndex);399tail = token.substring(dotIndex + 1);400}401402let child = this._children.get(head);403if (typeof child === 'undefined') {404child = new ThemeTrieElement(this._mainRule.clone());405this._children.set(head, child);406}407408child.insert(tail, fontStyle, foreground, background);409}410}411412export function generateTokensCSSForColorMap(colorMap: readonly Color[]): string {413const rules: string[] = [];414for (let i = 1, len = colorMap.length; i < len; i++) {415const color = colorMap[i];416rules[i] = `.mtk${i} { color: ${color}; }`;417}418rules.push('.mtki { font-style: italic; }');419rules.push('.mtkb { font-weight: bold; }');420rules.push('.mtku { text-decoration: underline; text-underline-position: under; }');421rules.push('.mtks { text-decoration: line-through; }');422rules.push('.mtks.mtku { text-decoration: underline line-through; text-underline-position: under; }');423return rules.join('\n');424}425426export function generateTokensCSSForFontMap(fontMap: readonly IFontTokenOptions[]): string {427const rules: string[] = [];428const fonts = new Set<string>();429for (let i = 1, len = fontMap.length; i < len; i++) {430const font = fontMap[i];431if (!font.fontFamily && !font.fontSizeMultiplier) {432continue;433}434const className = classNameForFontTokenDecorations(font.fontFamily ?? '', font.fontSizeMultiplier ?? 0);435if (fonts.has(className)) {436continue;437}438fonts.add(className);439let rule = `.${className} {`;440if (font.fontFamily) {441rule += `font-family: ${font.fontFamily};`;442}443if (font.fontSizeMultiplier) {444rule += `font-size: calc(var(--editor-font-size)*${font.fontSizeMultiplier});`;445}446rule += `}`;447rules.push(rule);448}449return rules.join('\n');450}451452export function classNameForFontTokenDecorations(fontFamily: string, fontSize: number): string {453const safeFontFamily = sanitizeFontFamilyForClassName(fontFamily);454return cleanClassName(`font-decoration-${safeFontFamily}-${fontSize}`);455}456457function sanitizeFontFamilyForClassName(fontFamily: string): string {458const normalized = fontFamily.toLowerCase().trim();459if (!normalized) {460return 'default';461}462return cleanClassName(normalized);463}464465function cleanClassName(className: string): string {466return className.replace(/[^a-z0-9_-]/gi, '-');467}468469470