Path: blob/main/src/vs/editor/common/tokens/sparseTokensStore.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 * as arrays from '../../../base/common/arrays.js';6import { IRange, Range } from '../core/range.js';7import { LineTokens } from './lineTokens.js';8import { SparseMultilineTokens } from './sparseMultilineTokens.js';9import { ILanguageIdCodec } from '../languages.js';10import { MetadataConsts } from '../encodedTokenAttributes.js';11import { ITextModel } from '../model.js';1213/**14* Represents sparse tokens in a text model.15*/16export class SparseTokensStore {1718private _pieces: SparseMultilineTokens[];19private _isComplete: boolean;20private readonly _languageIdCodec: ILanguageIdCodec;2122constructor(languageIdCodec: ILanguageIdCodec) {23this._pieces = [];24this._isComplete = false;25this._languageIdCodec = languageIdCodec;26}2728public flush(): void {29this._pieces = [];30this._isComplete = false;31}3233public isEmpty(): boolean {34return (this._pieces.length === 0);35}3637public set(pieces: SparseMultilineTokens[] | null, isComplete: boolean, textModel: ITextModel | undefined = undefined): void {38this._pieces = pieces || [];39this._isComplete = isComplete;4041if (textModel) {42for (const p of this._pieces) {43p.reportIfInvalid(textModel);44}45}46}4748public setPartial(_range: Range, pieces: SparseMultilineTokens[]): Range {49// console.log(`setPartial ${_range} ${pieces.map(p => p.toString()).join(', ')}`);5051let range = _range;52if (pieces.length > 0) {53const _firstRange = pieces[0].getRange();54const _lastRange = pieces[pieces.length - 1].getRange();55if (!_firstRange || !_lastRange) {56return _range;57}58range = _range.plusRange(_firstRange).plusRange(_lastRange);59}6061let insertPosition: { index: number } | null = null;62for (let i = 0, len = this._pieces.length; i < len; i++) {63const piece = this._pieces[i];64if (piece.endLineNumber < range.startLineNumber) {65// this piece is before the range66continue;67}6869if (piece.startLineNumber > range.endLineNumber) {70// this piece is after the range, so mark the spot before this piece71// as a good insertion position and stop looping72insertPosition = insertPosition || { index: i };73break;74}7576// this piece might intersect with the range77piece.removeTokens(range);7879if (piece.isEmpty()) {80// remove the piece if it became empty81this._pieces.splice(i, 1);82i--;83len--;84continue;85}8687if (piece.endLineNumber < range.startLineNumber) {88// after removal, this piece is before the range89continue;90}9192if (piece.startLineNumber > range.endLineNumber) {93// after removal, this piece is after the range94insertPosition = insertPosition || { index: i };95continue;96}9798// after removal, this piece contains the range99const [a, b] = piece.split(range);100if (a.isEmpty()) {101// this piece is actually after the range102insertPosition = insertPosition || { index: i };103continue;104}105if (b.isEmpty()) {106// this piece is actually before the range107continue;108}109this._pieces.splice(i, 1, a, b);110i++;111len++;112113insertPosition = insertPosition || { index: i };114}115116insertPosition = insertPosition || { index: this._pieces.length };117118if (pieces.length > 0) {119this._pieces = arrays.arrayInsert(this._pieces, insertPosition.index, pieces);120}121122// console.log(`I HAVE ${this._pieces.length} pieces`);123// console.log(`${this._pieces.map(p => p.toString()).join('\n')}`);124125return range;126}127128public isComplete(): boolean {129return this._isComplete;130}131132public addSparseTokens(lineNumber: number, aTokens: LineTokens): LineTokens {133if (aTokens.getTextLength() === 0) {134// Don't do anything for empty lines135return aTokens;136}137138const pieces = this._pieces;139140if (pieces.length === 0) {141return aTokens;142}143144const pieceIndex = SparseTokensStore._findFirstPieceWithLine(pieces, lineNumber);145const bTokens = pieces[pieceIndex].getLineTokens(lineNumber);146147if (!bTokens) {148return aTokens;149}150151const aLen = aTokens.getCount();152const bLen = bTokens.getCount();153154let aIndex = 0;155const result: number[] = [];156let resultLen = 0;157let lastEndOffset = 0;158159const emitToken = (endOffset: number, metadata: number) => {160if (endOffset === lastEndOffset) {161return;162}163lastEndOffset = endOffset;164result[resultLen++] = endOffset;165result[resultLen++] = metadata;166};167168for (let bIndex = 0; bIndex < bLen; bIndex++) {169// bTokens is not validated yet, but aTokens is. We want to make sure that the LineTokens we return170// are valid, so we clamp the ranges to ensure that.171const bStartCharacter = Math.min(bTokens.getStartCharacter(bIndex), aTokens.getTextLength());172const bEndCharacter = Math.min(bTokens.getEndCharacter(bIndex), aTokens.getTextLength());173const bMetadata = bTokens.getMetadata(bIndex);174175const bMask = (176((bMetadata & MetadataConsts.SEMANTIC_USE_ITALIC) ? MetadataConsts.ITALIC_MASK : 0)177| ((bMetadata & MetadataConsts.SEMANTIC_USE_BOLD) ? MetadataConsts.BOLD_MASK : 0)178| ((bMetadata & MetadataConsts.SEMANTIC_USE_UNDERLINE) ? MetadataConsts.UNDERLINE_MASK : 0)179| ((bMetadata & MetadataConsts.SEMANTIC_USE_STRIKETHROUGH) ? MetadataConsts.STRIKETHROUGH_MASK : 0)180| ((bMetadata & MetadataConsts.SEMANTIC_USE_FOREGROUND) ? MetadataConsts.FOREGROUND_MASK : 0)181| ((bMetadata & MetadataConsts.SEMANTIC_USE_BACKGROUND) ? MetadataConsts.BACKGROUND_MASK : 0)182) >>> 0;183const aMask = (~bMask) >>> 0;184185// push any token from `a` that is before `b`186while (aIndex < aLen && aTokens.getEndOffset(aIndex) <= bStartCharacter) {187emitToken(aTokens.getEndOffset(aIndex), aTokens.getMetadata(aIndex));188aIndex++;189}190191// push the token from `a` if it intersects the token from `b`192if (aIndex < aLen && aTokens.getStartOffset(aIndex) < bStartCharacter) {193emitToken(bStartCharacter, aTokens.getMetadata(aIndex));194}195196// skip any tokens from `a` that are contained inside `b`197while (aIndex < aLen && aTokens.getEndOffset(aIndex) < bEndCharacter) {198emitToken(aTokens.getEndOffset(aIndex), (aTokens.getMetadata(aIndex) & aMask) | (bMetadata & bMask));199aIndex++;200}201202if (aIndex < aLen) {203emitToken(bEndCharacter, (aTokens.getMetadata(aIndex) & aMask) | (bMetadata & bMask));204if (aTokens.getEndOffset(aIndex) === bEndCharacter) {205// `a` ends exactly at the same spot as `b`!206aIndex++;207}208} else {209const aMergeIndex = Math.min(Math.max(0, aIndex - 1), aLen - 1);210211// push the token from `b`212emitToken(bEndCharacter, (aTokens.getMetadata(aMergeIndex) & aMask) | (bMetadata & bMask));213}214}215216// push the remaining tokens from `a`217while (aIndex < aLen) {218emitToken(aTokens.getEndOffset(aIndex), aTokens.getMetadata(aIndex));219aIndex++;220}221222return new LineTokens(new Uint32Array(result), aTokens.getLineContent(), this._languageIdCodec);223}224225private static _findFirstPieceWithLine(pieces: SparseMultilineTokens[], lineNumber: number): number {226let low = 0;227let high = pieces.length - 1;228229while (low < high) {230let mid = low + Math.floor((high - low) / 2);231232if (pieces[mid].endLineNumber < lineNumber) {233low = mid + 1;234} else if (pieces[mid].startLineNumber > lineNumber) {235high = mid - 1;236} else {237while (mid > low && pieces[mid - 1].startLineNumber <= lineNumber && lineNumber <= pieces[mid - 1].endLineNumber) {238mid--;239}240return mid;241}242}243244return low;245}246247public acceptEdit(range: IRange, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void {248for (const piece of this._pieces) {249piece.acceptEdit(range, eolCount, firstLineLength, lastLineLength, firstCharCode);250}251}252}253254255