Path: blob/main/src/vs/editor/common/languages/supports/tokenization.ts
3296 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 { LanguageId, FontStyle, ColorId, StandardTokenType, MetadataConsts } from '../../encodedTokenAttributes.js';78export interface ITokenThemeRule {9token: string;10foreground?: string;11background?: string;12fontStyle?: string;13}1415export class ParsedTokenThemeRule {16_parsedThemeRuleBrand: void = undefined;1718readonly token: string;19readonly index: number;2021/**22* -1 if not set. An or mask of `FontStyle` otherwise.23*/24readonly fontStyle: FontStyle;25readonly foreground: string | null;26readonly background: string | null;2728constructor(29token: string,30index: number,31fontStyle: number,32foreground: string | null,33background: string | null,34) {35this.token = token;36this.index = index;37this.fontStyle = fontStyle;38this.foreground = foreground;39this.background = background;40}41}4243/**44* Parse a raw theme into rules.45*/46export function parseTokenTheme(source: ITokenThemeRule[]): ParsedTokenThemeRule[] {47if (!source || !Array.isArray(source)) {48return [];49}50const result: ParsedTokenThemeRule[] = [];51let resultLen = 0;52for (let i = 0, len = source.length; i < len; i++) {53const entry = source[i];5455let fontStyle: number = FontStyle.NotSet;56if (typeof entry.fontStyle === 'string') {57fontStyle = FontStyle.None;5859const segments = entry.fontStyle.split(' ');60for (let j = 0, lenJ = segments.length; j < lenJ; j++) {61const segment = segments[j];62switch (segment) {63case 'italic':64fontStyle = fontStyle | FontStyle.Italic;65break;66case 'bold':67fontStyle = fontStyle | FontStyle.Bold;68break;69case 'underline':70fontStyle = fontStyle | FontStyle.Underline;71break;72case 'strikethrough':73fontStyle = fontStyle | FontStyle.Strikethrough;74break;75}76}77}7879let foreground: string | null = null;80if (typeof entry.foreground === 'string') {81foreground = entry.foreground;82}8384let background: string | null = null;85if (typeof entry.background === 'string') {86background = entry.background;87}8889result[resultLen++] = new ParsedTokenThemeRule(90entry.token || '',91i,92fontStyle,93foreground,94background95);96}9798return result;99}100101/**102* Resolve rules (i.e. inheritance).103*/104function resolveParsedTokenThemeRules(parsedThemeRules: ParsedTokenThemeRule[], customTokenColors: string[]): TokenTheme {105106// Sort rules lexicographically, and then by index if necessary107parsedThemeRules.sort((a, b) => {108const r = strcmp(a.token, b.token);109if (r !== 0) {110return r;111}112return a.index - b.index;113});114115// Determine defaults116let defaultFontStyle = FontStyle.None;117let defaultForeground = '000000';118let defaultBackground = 'ffffff';119while (parsedThemeRules.length >= 1 && parsedThemeRules[0].token === '') {120const incomingDefaults = parsedThemeRules.shift()!;121if (incomingDefaults.fontStyle !== FontStyle.NotSet) {122defaultFontStyle = incomingDefaults.fontStyle;123}124if (incomingDefaults.foreground !== null) {125defaultForeground = incomingDefaults.foreground;126}127if (incomingDefaults.background !== null) {128defaultBackground = incomingDefaults.background;129}130}131const colorMap = new ColorMap();132133// start with token colors from custom token themes134for (const color of customTokenColors) {135colorMap.getId(color);136}137138139const foregroundColorId = colorMap.getId(defaultForeground);140const backgroundColorId = colorMap.getId(defaultBackground);141142const defaults = new ThemeTrieElementRule(defaultFontStyle, foregroundColorId, backgroundColorId);143const root = new ThemeTrieElement(defaults);144for (let i = 0, len = parsedThemeRules.length; i < len; i++) {145const rule = parsedThemeRules[i];146root.insert(rule.token, rule.fontStyle, colorMap.getId(rule.foreground), colorMap.getId(rule.background));147}148149return new TokenTheme(colorMap, root);150}151152const colorRegExp = /^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/;153154export class ColorMap {155156private _lastColorId: number;157private readonly _id2color: Color[];158private readonly _color2id: Map<string, ColorId>;159160constructor() {161this._lastColorId = 0;162this._id2color = [];163this._color2id = new Map<string, ColorId>();164}165166public getId(color: string | null): ColorId {167if (color === null) {168return 0;169}170const match = color.match(colorRegExp);171if (!match) {172throw new Error('Illegal value for token color: ' + color);173}174color = match[1].toUpperCase();175let value = this._color2id.get(color);176if (value) {177return value;178}179value = ++this._lastColorId;180this._color2id.set(color, value);181this._id2color[value] = Color.fromHex('#' + color);182return value;183}184185public getColorMap(): Color[] {186return this._id2color.slice(0);187}188189}190191export class TokenTheme {192193public static createFromRawTokenTheme(source: ITokenThemeRule[], customTokenColors: string[]): TokenTheme {194return this.createFromParsedTokenTheme(parseTokenTheme(source), customTokenColors);195}196197public static createFromParsedTokenTheme(source: ParsedTokenThemeRule[], customTokenColors: string[]): TokenTheme {198return resolveParsedTokenThemeRules(source, customTokenColors);199}200201private readonly _colorMap: ColorMap;202private readonly _root: ThemeTrieElement;203private readonly _cache: Map<string, number>;204205constructor(colorMap: ColorMap, root: ThemeTrieElement) {206this._colorMap = colorMap;207this._root = root;208this._cache = new Map<string, number>();209}210211public getColorMap(): Color[] {212return this._colorMap.getColorMap();213}214215/**216* used for testing purposes217*/218public getThemeTrieElement(): ExternalThemeTrieElement {219return this._root.toExternalThemeTrieElement();220}221222public _match(token: string): ThemeTrieElementRule {223return this._root.match(token);224}225226public match(languageId: LanguageId, token: string): number {227// The cache contains the metadata without the language bits set.228let result = this._cache.get(token);229if (typeof result === 'undefined') {230const rule = this._match(token);231const standardToken = toStandardTokenType(token);232result = (233rule.metadata234| (standardToken << MetadataConsts.TOKEN_TYPE_OFFSET)235) >>> 0;236this._cache.set(token, result);237}238239return (240result241| (languageId << MetadataConsts.LANGUAGEID_OFFSET)242) >>> 0;243}244}245246const STANDARD_TOKEN_TYPE_REGEXP = /\b(comment|string|regex|regexp)\b/;247export function toStandardTokenType(tokenType: string): StandardTokenType {248const m = tokenType.match(STANDARD_TOKEN_TYPE_REGEXP);249if (!m) {250return StandardTokenType.Other;251}252switch (m[1]) {253case 'comment':254return StandardTokenType.Comment;255case 'string':256return StandardTokenType.String;257case 'regex':258return StandardTokenType.RegEx;259case 'regexp':260return StandardTokenType.RegEx;261}262throw new Error('Unexpected match for standard token type!');263}264265export function strcmp(a: string, b: string): number {266if (a < b) {267return -1;268}269if (a > b) {270return 1;271}272return 0;273}274275export class ThemeTrieElementRule {276_themeTrieElementRuleBrand: void = undefined;277278private _fontStyle: FontStyle;279private _foreground: ColorId;280private _background: ColorId;281public metadata: number;282283constructor(fontStyle: FontStyle, foreground: ColorId, background: ColorId) {284this._fontStyle = fontStyle;285this._foreground = foreground;286this._background = background;287this.metadata = (288(this._fontStyle << MetadataConsts.FONT_STYLE_OFFSET)289| (this._foreground << MetadataConsts.FOREGROUND_OFFSET)290| (this._background << MetadataConsts.BACKGROUND_OFFSET)291) >>> 0;292}293294public clone(): ThemeTrieElementRule {295return new ThemeTrieElementRule(this._fontStyle, this._foreground, this._background);296}297298public acceptOverwrite(fontStyle: FontStyle, foreground: ColorId, background: ColorId): void {299if (fontStyle !== FontStyle.NotSet) {300this._fontStyle = fontStyle;301}302if (foreground !== ColorId.None) {303this._foreground = foreground;304}305if (background !== ColorId.None) {306this._background = background;307}308this.metadata = (309(this._fontStyle << MetadataConsts.FONT_STYLE_OFFSET)310| (this._foreground << MetadataConsts.FOREGROUND_OFFSET)311| (this._background << MetadataConsts.BACKGROUND_OFFSET)312) >>> 0;313}314}315316export class ExternalThemeTrieElement {317318public readonly mainRule: ThemeTrieElementRule;319public readonly children: Map<string, ExternalThemeTrieElement>;320321constructor(322mainRule: ThemeTrieElementRule,323children: Map<string, ExternalThemeTrieElement> | { [key: string]: ExternalThemeTrieElement } = new Map<string, ExternalThemeTrieElement>()324) {325this.mainRule = mainRule;326if (children instanceof Map) {327this.children = children;328} else {329this.children = new Map<string, ExternalThemeTrieElement>();330for (const key in children) {331this.children.set(key, children[key]);332}333}334}335}336337export class ThemeTrieElement {338_themeTrieElementBrand: void = undefined;339340private readonly _mainRule: ThemeTrieElementRule;341private readonly _children: Map<string, ThemeTrieElement>;342343constructor(mainRule: ThemeTrieElementRule) {344this._mainRule = mainRule;345this._children = new Map<string, ThemeTrieElement>();346}347348/**349* used for testing purposes350*/351public toExternalThemeTrieElement(): ExternalThemeTrieElement {352const children = new Map<string, ExternalThemeTrieElement>();353this._children.forEach((element, index) => {354children.set(index, element.toExternalThemeTrieElement());355});356return new ExternalThemeTrieElement(this._mainRule, children);357}358359public match(token: string): ThemeTrieElementRule {360if (token === '') {361return this._mainRule;362}363364const dotIndex = token.indexOf('.');365let head: string;366let tail: string;367if (dotIndex === -1) {368head = token;369tail = '';370} else {371head = token.substring(0, dotIndex);372tail = token.substring(dotIndex + 1);373}374375const child = this._children.get(head);376if (typeof child !== 'undefined') {377return child.match(tail);378}379380return this._mainRule;381}382383public insert(token: string, fontStyle: FontStyle, foreground: ColorId, background: ColorId): void {384if (token === '') {385// Merge into the main rule386this._mainRule.acceptOverwrite(fontStyle, foreground, background);387return;388}389390const dotIndex = token.indexOf('.');391let head: string;392let tail: string;393if (dotIndex === -1) {394head = token;395tail = '';396} else {397head = token.substring(0, dotIndex);398tail = token.substring(dotIndex + 1);399}400401let child = this._children.get(head);402if (typeof child === 'undefined') {403child = new ThemeTrieElement(this._mainRule.clone());404this._children.set(head, child);405}406407child.insert(tail, fontStyle, foreground, background);408}409}410411export function generateTokensCSSForColorMap(colorMap: readonly Color[]): string {412const rules: string[] = [];413for (let i = 1, len = colorMap.length; i < len; i++) {414const color = colorMap[i];415rules[i] = `.mtk${i} { color: ${color}; }`;416}417rules.push('.mtki { font-style: italic; }');418rules.push('.mtkb { font-weight: bold; }');419rules.push('.mtku { text-decoration: underline; text-underline-position: under; }');420rules.push('.mtks { text-decoration: line-through; }');421rules.push('.mtks.mtku { text-decoration: underline line-through; text-underline-position: under; }');422return rules.join('\n');423}424425426