Path: blob/main/src/vs/editor/common/services/semanticTokensProviderStyling.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 { SemanticTokensLegend, SemanticTokens } from '../languages.js';6import { FontStyle, MetadataConsts, TokenMetadata } from '../encodedTokenAttributes.js';7import { IThemeService } from '../../../platform/theme/common/themeService.js';8import { ILogService, LogLevel } from '../../../platform/log/common/log.js';9import { SparseMultilineTokens } from '../tokens/sparseMultilineTokens.js';10import { ILanguageService } from '../languages/language.js';1112const enum SemanticTokensProviderStylingConstants {13NO_STYLING = 0b0111111111111111111111111111111114}1516const ENABLE_TRACE = false;1718export class SemanticTokensProviderStyling {1920private readonly _hashTable: HashTable;21private _hasWarnedOverlappingTokens = false;22private _hasWarnedInvalidLengthTokens = false;23private _hasWarnedInvalidEditStart = false;2425constructor(26private readonly _legend: SemanticTokensLegend,27@IThemeService private readonly _themeService: IThemeService,28@ILanguageService private readonly _languageService: ILanguageService,29@ILogService private readonly _logService: ILogService30) {31this._hashTable = new HashTable();32}3334public getMetadata(tokenTypeIndex: number, tokenModifierSet: number, languageId: string): number {35const encodedLanguageId = this._languageService.languageIdCodec.encodeLanguageId(languageId);36const entry = this._hashTable.get(tokenTypeIndex, tokenModifierSet, encodedLanguageId);37let metadata: number;38if (entry) {39metadata = entry.metadata;40if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) {41this._logService.trace(`SemanticTokensProviderStyling [CACHED] ${tokenTypeIndex} / ${tokenModifierSet}: foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`);42}43} else {44let tokenType = this._legend.tokenTypes[tokenTypeIndex];45const tokenModifiers: string[] = [];46if (tokenType) {47let modifierSet = tokenModifierSet;48for (let modifierIndex = 0; modifierSet > 0 && modifierIndex < this._legend.tokenModifiers.length; modifierIndex++) {49if (modifierSet & 1) {50tokenModifiers.push(this._legend.tokenModifiers[modifierIndex]);51}52modifierSet = modifierSet >> 1;53}54if (ENABLE_TRACE && modifierSet > 0 && this._logService.getLevel() === LogLevel.Trace) {55this._logService.trace(`SemanticTokensProviderStyling: unknown token modifier index: ${tokenModifierSet.toString(2)} for legend: ${JSON.stringify(this._legend.tokenModifiers)}`);56tokenModifiers.push('not-in-legend');57}5859const tokenStyle = this._themeService.getColorTheme().getTokenStyleMetadata(tokenType, tokenModifiers, languageId);60if (typeof tokenStyle === 'undefined') {61metadata = SemanticTokensProviderStylingConstants.NO_STYLING;62} else {63metadata = 0;64if (typeof tokenStyle.italic !== 'undefined') {65const italicBit = (tokenStyle.italic ? FontStyle.Italic : 0) << MetadataConsts.FONT_STYLE_OFFSET;66metadata |= italicBit | MetadataConsts.SEMANTIC_USE_ITALIC;67}68if (typeof tokenStyle.bold !== 'undefined') {69const boldBit = (tokenStyle.bold ? FontStyle.Bold : 0) << MetadataConsts.FONT_STYLE_OFFSET;70metadata |= boldBit | MetadataConsts.SEMANTIC_USE_BOLD;71}72if (typeof tokenStyle.underline !== 'undefined') {73const underlineBit = (tokenStyle.underline ? FontStyle.Underline : 0) << MetadataConsts.FONT_STYLE_OFFSET;74metadata |= underlineBit | MetadataConsts.SEMANTIC_USE_UNDERLINE;75}76if (typeof tokenStyle.strikethrough !== 'undefined') {77const strikethroughBit = (tokenStyle.strikethrough ? FontStyle.Strikethrough : 0) << MetadataConsts.FONT_STYLE_OFFSET;78metadata |= strikethroughBit | MetadataConsts.SEMANTIC_USE_STRIKETHROUGH;79}80if (tokenStyle.foreground) {81const foregroundBits = (tokenStyle.foreground) << MetadataConsts.FOREGROUND_OFFSET;82metadata |= foregroundBits | MetadataConsts.SEMANTIC_USE_FOREGROUND;83}84if (metadata === 0) {85// Nothing!86metadata = SemanticTokensProviderStylingConstants.NO_STYLING;87}88}89} else {90if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) {91this._logService.trace(`SemanticTokensProviderStyling: unknown token type index: ${tokenTypeIndex} for legend: ${JSON.stringify(this._legend.tokenTypes)}`);92}93metadata = SemanticTokensProviderStylingConstants.NO_STYLING;94tokenType = 'not-in-legend';95}96this._hashTable.add(tokenTypeIndex, tokenModifierSet, encodedLanguageId, metadata);9798if (ENABLE_TRACE && this._logService.getLevel() === LogLevel.Trace) {99this._logService.trace(`SemanticTokensProviderStyling ${tokenTypeIndex} (${tokenType}) / ${tokenModifierSet} (${tokenModifiers.join(' ')}): foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`);100}101}102103return metadata;104}105106public warnOverlappingSemanticTokens(lineNumber: number, startColumn: number): void {107if (!this._hasWarnedOverlappingTokens) {108this._hasWarnedOverlappingTokens = true;109this._logService.warn(`Overlapping semantic tokens detected at lineNumber ${lineNumber}, column ${startColumn}`);110}111}112113public warnInvalidLengthSemanticTokens(lineNumber: number, startColumn: number): void {114if (!this._hasWarnedInvalidLengthTokens) {115this._hasWarnedInvalidLengthTokens = true;116this._logService.warn(`Semantic token with invalid length detected at lineNumber ${lineNumber}, column ${startColumn}`);117}118}119120public warnInvalidEditStart(previousResultId: string | undefined, resultId: string | undefined, editIndex: number, editStart: number, maxExpectedStart: number): void {121if (!this._hasWarnedInvalidEditStart) {122this._hasWarnedInvalidEditStart = true;123this._logService.warn(`Invalid semantic tokens edit detected (previousResultId: ${previousResultId}, resultId: ${resultId}) at edit #${editIndex}: The provided start offset ${editStart} is outside the previous data (length ${maxExpectedStart}).`);124}125}126127}128129const enum SemanticColoringConstants {130/**131* Let's aim at having 8KB buffers if possible...132* So that would be 8192 / (5 * 4) = 409.6 tokens per area133*/134DesiredTokensPerArea = 400,135136/**137* Try to keep the total number of areas under 1024 if possible,138* simply compensate by having more tokens per area...139*/140DesiredMaxAreas = 1024,141}142143export function toMultilineTokens2(tokens: SemanticTokens, styling: SemanticTokensProviderStyling, languageId: string): SparseMultilineTokens[] {144const srcData = tokens.data;145const tokenCount = (tokens.data.length / 5) | 0;146const tokensPerArea = Math.max(Math.ceil(tokenCount / SemanticColoringConstants.DesiredMaxAreas), SemanticColoringConstants.DesiredTokensPerArea);147const result: SparseMultilineTokens[] = [];148149let tokenIndex = 0;150let lastLineNumber = 1;151let lastStartCharacter = 0;152while (tokenIndex < tokenCount) {153const tokenStartIndex = tokenIndex;154let tokenEndIndex = Math.min(tokenStartIndex + tokensPerArea, tokenCount);155156// Keep tokens on the same line in the same area...157if (tokenEndIndex < tokenCount) {158159let smallTokenEndIndex = tokenEndIndex;160while (smallTokenEndIndex - 1 > tokenStartIndex && srcData[5 * smallTokenEndIndex] === 0) {161smallTokenEndIndex--;162}163164if (smallTokenEndIndex - 1 === tokenStartIndex) {165// there are so many tokens on this line that our area would be empty, we must now go right166let bigTokenEndIndex = tokenEndIndex;167while (bigTokenEndIndex + 1 < tokenCount && srcData[5 * bigTokenEndIndex] === 0) {168bigTokenEndIndex++;169}170tokenEndIndex = bigTokenEndIndex;171} else {172tokenEndIndex = smallTokenEndIndex;173}174}175176let destData = new Uint32Array((tokenEndIndex - tokenStartIndex) * 4);177let destOffset = 0;178let areaLine = 0;179let prevLineNumber = 0;180let prevEndCharacter = 0;181while (tokenIndex < tokenEndIndex) {182const srcOffset = 5 * tokenIndex;183const deltaLine = srcData[srcOffset];184const deltaCharacter = srcData[srcOffset + 1];185// Casting both `lineNumber`, `startCharacter` and `endCharacter` here to uint32 using `|0`186// to validate below with the actual values that will be inserted in the Uint32Array result187const lineNumber = (lastLineNumber + deltaLine) | 0;188const startCharacter = (deltaLine === 0 ? (lastStartCharacter + deltaCharacter) | 0 : deltaCharacter);189const length = srcData[srcOffset + 2];190const endCharacter = (startCharacter + length) | 0;191const tokenTypeIndex = srcData[srcOffset + 3];192const tokenModifierSet = srcData[srcOffset + 4];193194if (endCharacter <= startCharacter) {195// this token is invalid (most likely a negative length casted to uint32)196styling.warnInvalidLengthSemanticTokens(lineNumber, startCharacter + 1);197} else if (prevLineNumber === lineNumber && prevEndCharacter > startCharacter) {198// this token overlaps with the previous token199styling.warnOverlappingSemanticTokens(lineNumber, startCharacter + 1);200} else {201const metadata = styling.getMetadata(tokenTypeIndex, tokenModifierSet, languageId);202203if (metadata !== SemanticTokensProviderStylingConstants.NO_STYLING) {204if (areaLine === 0) {205areaLine = lineNumber;206}207destData[destOffset] = lineNumber - areaLine;208destData[destOffset + 1] = startCharacter;209destData[destOffset + 2] = endCharacter;210destData[destOffset + 3] = metadata;211destOffset += 4;212213prevLineNumber = lineNumber;214prevEndCharacter = endCharacter;215}216}217218lastLineNumber = lineNumber;219lastStartCharacter = startCharacter;220tokenIndex++;221}222223if (destOffset !== destData.length) {224destData = destData.subarray(0, destOffset);225}226227const tokens = SparseMultilineTokens.create(areaLine, destData);228result.push(tokens);229}230231return result;232}233234class HashTableEntry {235public readonly tokenTypeIndex: number;236public readonly tokenModifierSet: number;237public readonly languageId: number;238public readonly metadata: number;239public next: HashTableEntry | null;240241constructor(tokenTypeIndex: number, tokenModifierSet: number, languageId: number, metadata: number) {242this.tokenTypeIndex = tokenTypeIndex;243this.tokenModifierSet = tokenModifierSet;244this.languageId = languageId;245this.metadata = metadata;246this.next = null;247}248}249250class HashTable {251252private static _SIZES = [3, 7, 13, 31, 61, 127, 251, 509, 1021, 2039, 4093, 8191, 16381, 32749, 65521, 131071, 262139, 524287, 1048573, 2097143];253254private _elementsCount: number;255private _currentLengthIndex: number;256private _currentLength: number;257private _growCount: number;258private _elements: (HashTableEntry | null)[];259260constructor() {261this._elementsCount = 0;262this._currentLengthIndex = 0;263this._currentLength = HashTable._SIZES[this._currentLengthIndex];264this._growCount = Math.round(this._currentLengthIndex + 1 < HashTable._SIZES.length ? 2 / 3 * this._currentLength : 0);265this._elements = [];266HashTable._nullOutEntries(this._elements, this._currentLength);267}268269private static _nullOutEntries(entries: (HashTableEntry | null)[], length: number): void {270for (let i = 0; i < length; i++) {271entries[i] = null;272}273}274275private _hash2(n1: number, n2: number): number {276return (((n1 << 5) - n1) + n2) | 0; // n1 * 31 + n2, keep as int32277}278279private _hashFunc(tokenTypeIndex: number, tokenModifierSet: number, languageId: number): number {280return this._hash2(this._hash2(tokenTypeIndex, tokenModifierSet), languageId) % this._currentLength;281}282283public get(tokenTypeIndex: number, tokenModifierSet: number, languageId: number): HashTableEntry | null {284const hash = this._hashFunc(tokenTypeIndex, tokenModifierSet, languageId);285286let p = this._elements[hash];287while (p) {288if (p.tokenTypeIndex === tokenTypeIndex && p.tokenModifierSet === tokenModifierSet && p.languageId === languageId) {289return p;290}291p = p.next;292}293294return null;295}296297public add(tokenTypeIndex: number, tokenModifierSet: number, languageId: number, metadata: number): void {298this._elementsCount++;299if (this._growCount !== 0 && this._elementsCount >= this._growCount) {300// expand!301const oldElements = this._elements;302303this._currentLengthIndex++;304this._currentLength = HashTable._SIZES[this._currentLengthIndex];305this._growCount = Math.round(this._currentLengthIndex + 1 < HashTable._SIZES.length ? 2 / 3 * this._currentLength : 0);306this._elements = [];307HashTable._nullOutEntries(this._elements, this._currentLength);308309for (const first of oldElements) {310let p = first;311while (p) {312const oldNext = p.next;313p.next = null;314this._add(p);315p = oldNext;316}317}318}319this._add(new HashTableEntry(tokenTypeIndex, tokenModifierSet, languageId, metadata));320}321322private _add(element: HashTableEntry): void {323const hash = this._hashFunc(element.tokenTypeIndex, element.tokenModifierSet, element.languageId);324element.next = this._elements[hash];325this._elements[hash] = element;326}327}328329330