Path: blob/main/src/vs/editor/common/tokens/lineTokens.ts
3294 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 { ILanguageIdCodec } from '../languages.js';6import { FontStyle, ColorId, StandardTokenType, MetadataConsts, ITokenPresentation, TokenMetadata } from '../encodedTokenAttributes.js';7import { IPosition } from '../core/position.js';8import { ITextModel } from '../model.js';9import { OffsetRange } from '../core/ranges/offsetRange.js';10import { onUnexpectedError } from '../../../base/common/errors.js';111213export interface IViewLineTokens {14languageIdCodec: ILanguageIdCodec;15equals(other: IViewLineTokens): boolean;16getCount(): number;17getStandardTokenType(tokenIndex: number): StandardTokenType;18getForeground(tokenIndex: number): ColorId;19getEndOffset(tokenIndex: number): number;20getClassName(tokenIndex: number): string;21getInlineStyle(tokenIndex: number, colorMap: string[]): string;22getPresentation(tokenIndex: number): ITokenPresentation;23findTokenIndexAtOffset(offset: number): number;24getLineContent(): string;25getMetadata(tokenIndex: number): number;26getLanguageId(tokenIndex: number): string;27getTokenText(tokenIndex: number): string;28forEach(callback: (tokenIndex: number) => void): void;29}3031export class LineTokens implements IViewLineTokens {32public static createEmpty(lineContent: string, decoder: ILanguageIdCodec): LineTokens {33const defaultMetadata = LineTokens.defaultTokenMetadata;3435const tokens = new Uint32Array(2);36tokens[0] = lineContent.length;37tokens[1] = defaultMetadata;3839return new LineTokens(tokens, lineContent, decoder);40}4142public static createFromTextAndMetadata(data: { text: string; metadata: number }[], decoder: ILanguageIdCodec): LineTokens {43let offset: number = 0;44let fullText: string = '';45const tokens = new Array<number>();46for (const { text, metadata } of data) {47tokens.push(offset + text.length, metadata);48offset += text.length;49fullText += text;50}51return new LineTokens(new Uint32Array(tokens), fullText, decoder);52}5354public static convertToEndOffset(tokens: Uint32Array, lineTextLength: number): void {55const tokenCount = (tokens.length >>> 1);56const lastTokenIndex = tokenCount - 1;57for (let tokenIndex = 0; tokenIndex < lastTokenIndex; tokenIndex++) {58tokens[tokenIndex << 1] = tokens[(tokenIndex + 1) << 1];59}60tokens[lastTokenIndex << 1] = lineTextLength;61}6263public static findIndexInTokensArray(tokens: Uint32Array, desiredIndex: number): number {64if (tokens.length <= 2) {65return 0;66}6768let low = 0;69let high = (tokens.length >>> 1) - 1;7071while (low < high) {7273const mid = low + Math.floor((high - low) / 2);74const endOffset = tokens[(mid << 1)];7576if (endOffset === desiredIndex) {77return mid + 1;78} else if (endOffset < desiredIndex) {79low = mid + 1;80} else if (endOffset > desiredIndex) {81high = mid;82}83}8485return low;86}8788_lineTokensBrand: void = undefined;8990private readonly _tokens: Uint32Array;91private readonly _tokensCount: number;92private readonly _text: string;9394public readonly languageIdCodec: ILanguageIdCodec;9596public static defaultTokenMetadata = (97(FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET)98| (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET)99| (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET)100) >>> 0;101102constructor(tokens: Uint32Array, text: string, decoder: ILanguageIdCodec) {103const tokensLength = tokens.length > 1 ? tokens[tokens.length - 2] : 0;104if (tokensLength !== text.length) {105onUnexpectedError(new Error('Token length and text length do not match!'));106}107this._tokens = tokens;108this._tokensCount = (this._tokens.length >>> 1);109this._text = text;110this.languageIdCodec = decoder;111}112113public getTextLength(): number {114return this._text.length;115}116117public equals(other: IViewLineTokens): boolean {118if (other instanceof LineTokens) {119return this.slicedEquals(other, 0, this._tokensCount);120}121return false;122}123124public slicedEquals(other: LineTokens, sliceFromTokenIndex: number, sliceTokenCount: number): boolean {125if (this._text !== other._text) {126return false;127}128if (this._tokensCount !== other._tokensCount) {129return false;130}131const from = (sliceFromTokenIndex << 1);132const to = from + (sliceTokenCount << 1);133for (let i = from; i < to; i++) {134if (this._tokens[i] !== other._tokens[i]) {135return false;136}137}138return true;139}140141public getLineContent(): string {142return this._text;143}144145public getCount(): number {146return this._tokensCount;147}148149public getStartOffset(tokenIndex: number): number {150if (tokenIndex > 0) {151return this._tokens[(tokenIndex - 1) << 1];152}153return 0;154}155156public getMetadata(tokenIndex: number): number {157const metadata = this._tokens[(tokenIndex << 1) + 1];158return metadata;159}160161public getLanguageId(tokenIndex: number): string {162const metadata = this._tokens[(tokenIndex << 1) + 1];163const languageId = TokenMetadata.getLanguageId(metadata);164return this.languageIdCodec.decodeLanguageId(languageId);165}166167public getStandardTokenType(tokenIndex: number): StandardTokenType {168const metadata = this._tokens[(tokenIndex << 1) + 1];169return TokenMetadata.getTokenType(metadata);170}171172public getForeground(tokenIndex: number): ColorId {173const metadata = this._tokens[(tokenIndex << 1) + 1];174return TokenMetadata.getForeground(metadata);175}176177public getClassName(tokenIndex: number): string {178const metadata = this._tokens[(tokenIndex << 1) + 1];179return TokenMetadata.getClassNameFromMetadata(metadata);180}181182public getInlineStyle(tokenIndex: number, colorMap: string[]): string {183const metadata = this._tokens[(tokenIndex << 1) + 1];184return TokenMetadata.getInlineStyleFromMetadata(metadata, colorMap);185}186187public getPresentation(tokenIndex: number): ITokenPresentation {188const metadata = this._tokens[(tokenIndex << 1) + 1];189return TokenMetadata.getPresentationFromMetadata(metadata);190}191192public getEndOffset(tokenIndex: number): number {193return this._tokens[tokenIndex << 1];194}195196/**197* Find the token containing offset `offset`.198* @param offset The search offset199* @return The index of the token containing the offset.200*/201public findTokenIndexAtOffset(offset: number): number {202return LineTokens.findIndexInTokensArray(this._tokens, offset);203}204205public inflate(): IViewLineTokens {206return this;207}208209public sliceAndInflate(startOffset: number, endOffset: number, deltaOffset: number): IViewLineTokens {210return new SliceLineTokens(this, startOffset, endOffset, deltaOffset);211}212213public sliceZeroCopy(range: OffsetRange): IViewLineTokens {214return this.sliceAndInflate(range.start, range.endExclusive, 0);215}216217/**218* @pure219* @param insertTokens Must be sorted by offset.220*/221public withInserted(insertTokens: { offset: number; text: string; tokenMetadata: number }[]): LineTokens {222if (insertTokens.length === 0) {223return this;224}225226let nextOriginalTokenIdx = 0;227let nextInsertTokenIdx = 0;228let text = '';229const newTokens = new Array<number>();230231let originalEndOffset = 0;232while (true) {233const nextOriginalTokenEndOffset = nextOriginalTokenIdx < this._tokensCount ? this._tokens[nextOriginalTokenIdx << 1] : -1;234const nextInsertToken = nextInsertTokenIdx < insertTokens.length ? insertTokens[nextInsertTokenIdx] : null;235236if (nextOriginalTokenEndOffset !== -1 && (nextInsertToken === null || nextOriginalTokenEndOffset <= nextInsertToken.offset)) {237// original token ends before next insert token238text += this._text.substring(originalEndOffset, nextOriginalTokenEndOffset);239const metadata = this._tokens[(nextOriginalTokenIdx << 1) + 1];240newTokens.push(text.length, metadata);241nextOriginalTokenIdx++;242originalEndOffset = nextOriginalTokenEndOffset;243244} else if (nextInsertToken) {245if (nextInsertToken.offset > originalEndOffset) {246// insert token is in the middle of the next token.247text += this._text.substring(originalEndOffset, nextInsertToken.offset);248const metadata = this._tokens[(nextOriginalTokenIdx << 1) + 1];249newTokens.push(text.length, metadata);250originalEndOffset = nextInsertToken.offset;251}252253text += nextInsertToken.text;254newTokens.push(text.length, nextInsertToken.tokenMetadata);255nextInsertTokenIdx++;256} else {257break;258}259}260261return new LineTokens(new Uint32Array(newTokens), text, this.languageIdCodec);262}263264public getTokensInRange(range: OffsetRange): TokenArray {265const builder = new TokenArrayBuilder();266267const startTokenIndex = this.findTokenIndexAtOffset(range.start);268const endTokenIndex = this.findTokenIndexAtOffset(range.endExclusive);269270for (let tokenIndex = startTokenIndex; tokenIndex <= endTokenIndex; tokenIndex++) {271const tokenRange = new OffsetRange(this.getStartOffset(tokenIndex), this.getEndOffset(tokenIndex));272const length = tokenRange.intersectionLength(range);273if (length > 0) {274builder.add(length, this.getMetadata(tokenIndex));275}276}277278return builder.build();279}280281public getTokenText(tokenIndex: number): string {282const startOffset = this.getStartOffset(tokenIndex);283const endOffset = this.getEndOffset(tokenIndex);284const text = this._text.substring(startOffset, endOffset);285return text;286}287288public forEach(callback: (tokenIndex: number) => void): void {289const tokenCount = this.getCount();290for (let tokenIndex = 0; tokenIndex < tokenCount; tokenIndex++) {291callback(tokenIndex);292}293}294295toString(): string {296let result = '';297this.forEach((i) => {298result += `[${this.getTokenText(i)}]{${this.getClassName(i)}}`;299});300return result;301}302}303304class SliceLineTokens implements IViewLineTokens {305306private readonly _source: LineTokens;307private readonly _startOffset: number;308private readonly _endOffset: number;309private readonly _deltaOffset: number;310311private readonly _firstTokenIndex: number;312private readonly _tokensCount: number;313314public readonly languageIdCodec: ILanguageIdCodec;315316constructor(source: LineTokens, startOffset: number, endOffset: number, deltaOffset: number) {317this._source = source;318this._startOffset = startOffset;319this._endOffset = endOffset;320this._deltaOffset = deltaOffset;321this._firstTokenIndex = source.findTokenIndexAtOffset(startOffset);322this.languageIdCodec = source.languageIdCodec;323324this._tokensCount = 0;325for (let i = this._firstTokenIndex, len = source.getCount(); i < len; i++) {326const tokenStartOffset = source.getStartOffset(i);327if (tokenStartOffset >= endOffset) {328break;329}330this._tokensCount++;331}332}333334public getMetadata(tokenIndex: number): number {335return this._source.getMetadata(this._firstTokenIndex + tokenIndex);336}337338public getLanguageId(tokenIndex: number): string {339return this._source.getLanguageId(this._firstTokenIndex + tokenIndex);340}341342public getLineContent(): string {343return this._source.getLineContent().substring(this._startOffset, this._endOffset);344}345346public equals(other: IViewLineTokens): boolean {347if (other instanceof SliceLineTokens) {348return (349this._startOffset === other._startOffset350&& this._endOffset === other._endOffset351&& this._deltaOffset === other._deltaOffset352&& this._source.slicedEquals(other._source, this._firstTokenIndex, this._tokensCount)353);354}355return false;356}357358public getCount(): number {359return this._tokensCount;360}361362public getStandardTokenType(tokenIndex: number): StandardTokenType {363return this._source.getStandardTokenType(this._firstTokenIndex + tokenIndex);364}365366public getForeground(tokenIndex: number): ColorId {367return this._source.getForeground(this._firstTokenIndex + tokenIndex);368}369370public getEndOffset(tokenIndex: number): number {371const tokenEndOffset = this._source.getEndOffset(this._firstTokenIndex + tokenIndex);372return Math.min(this._endOffset, tokenEndOffset) - this._startOffset + this._deltaOffset;373}374375public getClassName(tokenIndex: number): string {376return this._source.getClassName(this._firstTokenIndex + tokenIndex);377}378379public getInlineStyle(tokenIndex: number, colorMap: string[]): string {380return this._source.getInlineStyle(this._firstTokenIndex + tokenIndex, colorMap);381}382383public getPresentation(tokenIndex: number): ITokenPresentation {384return this._source.getPresentation(this._firstTokenIndex + tokenIndex);385}386387public findTokenIndexAtOffset(offset: number): number {388return this._source.findTokenIndexAtOffset(offset + this._startOffset - this._deltaOffset) - this._firstTokenIndex;389}390391public getTokenText(tokenIndex: number): string {392const adjustedTokenIndex = this._firstTokenIndex + tokenIndex;393const tokenStartOffset = this._source.getStartOffset(adjustedTokenIndex);394const tokenEndOffset = this._source.getEndOffset(adjustedTokenIndex);395let text = this._source.getTokenText(adjustedTokenIndex);396if (tokenStartOffset < this._startOffset) {397text = text.substring(this._startOffset - tokenStartOffset);398}399if (tokenEndOffset > this._endOffset) {400text = text.substring(0, text.length - (tokenEndOffset - this._endOffset));401}402return text;403}404405public forEach(callback: (tokenIndex: number) => void): void {406for (let tokenIndex = 0; tokenIndex < this.getCount(); tokenIndex++) {407callback(tokenIndex);408}409}410}411412export function getStandardTokenTypeAtPosition(model: ITextModel, position: IPosition): StandardTokenType | undefined {413const lineNumber = position.lineNumber;414if (!model.tokenization.isCheapToTokenize(lineNumber)) {415return undefined;416}417model.tokenization.forceTokenization(lineNumber);418const lineTokens = model.tokenization.getLineTokens(lineNumber);419const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1);420const tokenType = lineTokens.getStandardTokenType(tokenIndex);421return tokenType;422}423424425426/**427* This class represents a sequence of tokens.428* Conceptually, each token has a length and a metadata number.429* A token array might be used to annotate a string with metadata.430* Use {@link TokenArrayBuilder} to efficiently create a token array.431*432* TODO: Make this class more efficient (e.g. by using a Int32Array).433*/434export class TokenArray {435public static fromLineTokens(lineTokens: LineTokens): TokenArray {436const tokenInfo: TokenInfo[] = [];437for (let i = 0; i < lineTokens.getCount(); i++) {438tokenInfo.push(new TokenInfo(lineTokens.getEndOffset(i) - lineTokens.getStartOffset(i), lineTokens.getMetadata(i)));439}440return TokenArray.create(tokenInfo);441}442443public static create(tokenInfo: TokenInfo[]): TokenArray {444return new TokenArray(tokenInfo);445}446447private constructor(448private readonly _tokenInfo: TokenInfo[]449) { }450451public toLineTokens(lineContent: string, decoder: ILanguageIdCodec): LineTokens {452return LineTokens.createFromTextAndMetadata(this.map((r, t) => ({ text: r.substring(lineContent), metadata: t.metadata })), decoder);453}454455public forEach(cb: (range: OffsetRange, tokenInfo: TokenInfo) => void): void {456let lengthSum = 0;457for (const tokenInfo of this._tokenInfo) {458const range = new OffsetRange(lengthSum, lengthSum + tokenInfo.length);459cb(range, tokenInfo);460lengthSum += tokenInfo.length;461}462}463464public map<T>(cb: (range: OffsetRange, tokenInfo: TokenInfo) => T): T[] {465const result: T[] = [];466let lengthSum = 0;467for (const tokenInfo of this._tokenInfo) {468const range = new OffsetRange(lengthSum, lengthSum + tokenInfo.length);469result.push(cb(range, tokenInfo));470lengthSum += tokenInfo.length;471}472return result;473}474475public slice(range: OffsetRange): TokenArray {476const result: TokenInfo[] = [];477let lengthSum = 0;478for (const tokenInfo of this._tokenInfo) {479const tokenStart = lengthSum;480const tokenEndEx = tokenStart + tokenInfo.length;481if (tokenEndEx > range.start) {482if (tokenStart >= range.endExclusive) {483break;484}485486const deltaBefore = Math.max(0, range.start - tokenStart);487const deltaAfter = Math.max(0, tokenEndEx - range.endExclusive);488489result.push(new TokenInfo(tokenInfo.length - deltaBefore - deltaAfter, tokenInfo.metadata));490}491492lengthSum += tokenInfo.length;493}494return TokenArray.create(result);495}496497public append(other: TokenArray): TokenArray {498const result: TokenInfo[] = this._tokenInfo.concat(other._tokenInfo);499return TokenArray.create(result);500}501}502503export type ITokenMetadata = number;504505export class TokenInfo {506constructor(507public readonly length: number,508public readonly metadata: ITokenMetadata509) { }510}511/**512* TODO: Make this class more efficient (e.g. by using a Int32Array).513*/514515export class TokenArrayBuilder {516private readonly _tokens: TokenInfo[] = [];517518public add(length: number, metadata: ITokenMetadata): void {519this._tokens.push(new TokenInfo(length, metadata));520}521522public build(): TokenArray {523return TokenArray.create(this._tokens);524}525}526527528529