Path: blob/main/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.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 { Emitter } from '../../../../../base/common/event.js';6import { Disposable } from '../../../../../base/common/lifecycle.js';7import { Range } from '../../../core/range.js';8import { ITextModel } from '../../../model.js';9import { BracketInfo, BracketPairWithMinIndentationInfo, IFoundBracket } from '../../../textModelBracketPairs.js';10import { TextModel } from '../../textModel.js';11import { IModelContentChangedEvent, IModelTokensChangedEvent } from '../../../textModelEvents.js';12import { ResolvedLanguageConfiguration } from '../../../languages/languageConfigurationRegistry.js';13import { AstNode, AstNodeKind } from './ast.js';14import { TextEditInfo } from './beforeEditPositionMapper.js';15import { LanguageAgnosticBracketTokens } from './brackets.js';16import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThan, lengthLessThanEqual, lengthsToRange, lengthZero, positionToLength, toLength } from './length.js';17import { parseDocument } from './parser.js';18import { DenseKeyProvider } from './smallImmutableSet.js';19import { FastTokenizer, TextBufferTokenizer } from './tokenizer.js';20import { BackgroundTokenizationState } from '../../../tokenizationTextModelPart.js';21import { Position } from '../../../core/position.js';22import { CallbackIterable } from '../../../../../base/common/arrays.js';23import { combineTextEditInfos } from './combineTextEditInfos.js';24import { ClosingBracketKind, OpeningBracketKind } from '../../../languages/supports/languageBracketsConfiguration.js';2526export class BracketPairsTree extends Disposable {27private readonly didChangeEmitter;2829/*30There are two trees:31* The initial tree that has no token information and is used for performant initial bracket colorization.32* The tree that used token information to detect bracket pairs.3334To prevent flickering, we only switch from the initial tree to tree with token information35when tokenization completes.36Since the text can be edited while background tokenization is in progress, we need to update both trees.37*/38private initialAstWithoutTokens: AstNode | undefined;39private astWithTokens: AstNode | undefined;4041private readonly denseKeyProvider;42private readonly brackets;4344public didLanguageChange(languageId: string): boolean {45return this.brackets.didLanguageChange(languageId);46}4748public readonly onDidChange;49private queuedTextEditsForInitialAstWithoutTokens: TextEditInfo[];50private queuedTextEdits: TextEditInfo[];5152public constructor(53private readonly textModel: TextModel,54private readonly getLanguageConfiguration: (languageId: string) => ResolvedLanguageConfiguration55) {56super();57this.didChangeEmitter = new Emitter<void>();58this.denseKeyProvider = new DenseKeyProvider<string>();59this.brackets = new LanguageAgnosticBracketTokens(this.denseKeyProvider, this.getLanguageConfiguration);60this.onDidChange = this.didChangeEmitter.event;61this.queuedTextEditsForInitialAstWithoutTokens = [];62this.queuedTextEdits = [];6364if (!textModel.tokenization.hasTokens) {65const brackets = this.brackets.getSingleLanguageBracketTokens(this.textModel.getLanguageId());66const tokenizer = new FastTokenizer(this.textModel.getValue(), brackets);67this.initialAstWithoutTokens = parseDocument(tokenizer, [], undefined, true);68this.astWithTokens = this.initialAstWithoutTokens;69} else if (textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) {70// Skip the initial ast, as there is no flickering.71// Directly create the tree with token information.72this.initialAstWithoutTokens = undefined;73this.astWithTokens = this.parseDocumentFromTextBuffer([], undefined, false);74} else {75// We missed some token changes already, so we cannot use the fast tokenizer + delta increments76this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer([], undefined, true);77this.astWithTokens = this.initialAstWithoutTokens;78}79}8081//#region TextModel events8283public handleDidChangeBackgroundTokenizationState(): void {84if (this.textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) {85const wasUndefined = this.initialAstWithoutTokens === undefined;86// Clear the initial tree as we can use the tree with token information now.87this.initialAstWithoutTokens = undefined;88if (!wasUndefined) {89this.didChangeEmitter.fire();90}91}92}9394public handleDidChangeTokens({ ranges }: IModelTokensChangedEvent): void {95const edits = ranges.map(r =>96new TextEditInfo(97toLength(r.fromLineNumber - 1, 0),98toLength(r.toLineNumber, 0),99toLength(r.toLineNumber - r.fromLineNumber + 1, 0)100)101);102103this.handleEdits(edits, true);104105if (!this.initialAstWithoutTokens) {106this.didChangeEmitter.fire();107}108}109110public handleContentChanged(change: IModelContentChangedEvent) {111const edits = TextEditInfo.fromModelContentChanges(change.changes);112this.handleEdits(edits, false);113}114115private handleEdits(edits: TextEditInfo[], tokenChange: boolean): void {116// Lazily queue the edits and only apply them when the tree is accessed.117const result = combineTextEditInfos(this.queuedTextEdits, edits);118119this.queuedTextEdits = result;120if (this.initialAstWithoutTokens && !tokenChange) {121this.queuedTextEditsForInitialAstWithoutTokens = combineTextEditInfos(this.queuedTextEditsForInitialAstWithoutTokens, edits);122}123}124125//#endregion126127private flushQueue() {128if (this.queuedTextEdits.length > 0) {129this.astWithTokens = this.parseDocumentFromTextBuffer(this.queuedTextEdits, this.astWithTokens, false);130this.queuedTextEdits = [];131}132if (this.queuedTextEditsForInitialAstWithoutTokens.length > 0) {133if (this.initialAstWithoutTokens) {134this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(this.queuedTextEditsForInitialAstWithoutTokens, this.initialAstWithoutTokens, false);135}136this.queuedTextEditsForInitialAstWithoutTokens = [];137}138}139140/**141* @pure (only if isPure = true)142*/143private parseDocumentFromTextBuffer(edits: TextEditInfo[], previousAst: AstNode | undefined, immutable: boolean): AstNode {144// Is much faster if `isPure = false`.145const isPure = false;146const previousAstClone = isPure ? previousAst?.deepClone() : previousAst;147const tokenizer = new TextBufferTokenizer(this.textModel, this.brackets);148const result = parseDocument(tokenizer, edits, previousAstClone, immutable);149return result;150}151152public getBracketsInRange(range: Range, onlyColorizedBrackets: boolean): CallbackIterable<BracketInfo> {153this.flushQueue();154155const startOffset = toLength(range.startLineNumber - 1, range.startColumn - 1);156const endOffset = toLength(range.endLineNumber - 1, range.endColumn - 1);157return new CallbackIterable(cb => {158const node = this.initialAstWithoutTokens || this.astWithTokens!;159collectBrackets(node, lengthZero, node.length, startOffset, endOffset, cb, 0, 0, new Map(), onlyColorizedBrackets);160});161}162163public getBracketPairsInRange(range: Range, includeMinIndentation: boolean): CallbackIterable<BracketPairWithMinIndentationInfo> {164this.flushQueue();165166const startLength = positionToLength(range.getStartPosition());167const endLength = positionToLength(range.getEndPosition());168169return new CallbackIterable(cb => {170const node = this.initialAstWithoutTokens || this.astWithTokens!;171const context = new CollectBracketPairsContext(cb, includeMinIndentation, this.textModel);172collectBracketPairs(node, lengthZero, node.length, startLength, endLength, context, 0, new Map());173});174}175176public getFirstBracketAfter(position: Position): IFoundBracket | null {177this.flushQueue();178179const node = this.initialAstWithoutTokens || this.astWithTokens!;180return getFirstBracketAfter(node, lengthZero, node.length, positionToLength(position));181}182183public getFirstBracketBefore(position: Position): IFoundBracket | null {184this.flushQueue();185186const node = this.initialAstWithoutTokens || this.astWithTokens!;187return getFirstBracketBefore(node, lengthZero, node.length, positionToLength(position));188}189}190191function getFirstBracketBefore(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, position: Length): IFoundBracket | null {192if (node.kind === AstNodeKind.List || node.kind === AstNodeKind.Pair) {193const lengths: { nodeOffsetStart: Length; nodeOffsetEnd: Length }[] = [];194for (const child of node.children) {195nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);196lengths.push({ nodeOffsetStart, nodeOffsetEnd });197nodeOffsetStart = nodeOffsetEnd;198}199for (let i = lengths.length - 1; i >= 0; i--) {200const { nodeOffsetStart, nodeOffsetEnd } = lengths[i];201if (lengthLessThan(nodeOffsetStart, position)) {202const result = getFirstBracketBefore(node.children[i], nodeOffsetStart, nodeOffsetEnd, position);203if (result) {204return result;205}206}207}208return null;209} else if (node.kind === AstNodeKind.UnexpectedClosingBracket) {210return null;211} else if (node.kind === AstNodeKind.Bracket) {212const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd);213return {214bracketInfo: node.bracketInfo,215range216};217}218return null;219}220221function getFirstBracketAfter(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, position: Length): IFoundBracket | null {222if (node.kind === AstNodeKind.List || node.kind === AstNodeKind.Pair) {223for (const child of node.children) {224nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);225if (lengthLessThan(position, nodeOffsetEnd)) {226const result = getFirstBracketAfter(child, nodeOffsetStart, nodeOffsetEnd, position);227if (result) {228return result;229}230}231nodeOffsetStart = nodeOffsetEnd;232}233return null;234} else if (node.kind === AstNodeKind.UnexpectedClosingBracket) {235return null;236} else if (node.kind === AstNodeKind.Bracket) {237const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd);238return {239bracketInfo: node.bracketInfo,240range241};242}243return null;244}245246function collectBrackets(247node: AstNode,248nodeOffsetStart: Length,249nodeOffsetEnd: Length,250startOffset: Length,251endOffset: Length,252push: (item: BracketInfo) => boolean,253level: number,254nestingLevelOfEqualBracketType: number,255levelPerBracketType: Map<string, number>,256onlyColorizedBrackets: boolean,257parentPairIsIncomplete: boolean = false,258): boolean {259if (level > 200) {260return true;261}262263whileLoop:264while (true) {265switch (node.kind) {266case AstNodeKind.List: {267const childCount = node.childrenLength;268for (let i = 0; i < childCount; i++) {269const child = node.getChild(i);270if (!child) {271continue;272}273nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);274if (275lengthLessThanEqual(nodeOffsetStart, endOffset) &&276lengthGreaterThanEqual(nodeOffsetEnd, startOffset)277) {278const childEndsAfterEnd = lengthGreaterThanEqual(nodeOffsetEnd, endOffset);279if (childEndsAfterEnd) {280// No child after this child in the requested window, don't recurse281node = child;282continue whileLoop;283}284285const shouldContinue = collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, push, level, 0, levelPerBracketType, onlyColorizedBrackets);286if (!shouldContinue) {287return false;288}289}290nodeOffsetStart = nodeOffsetEnd;291}292return true;293}294case AstNodeKind.Pair: {295const colorize = !onlyColorizedBrackets || !node.closingBracket || (node.closingBracket.bracketInfo as ClosingBracketKind).closesColorized(node.openingBracket.bracketInfo as OpeningBracketKind);296297let levelPerBracket = 0;298if (levelPerBracketType) {299let existing = levelPerBracketType.get(node.openingBracket.text);300if (existing === undefined) {301existing = 0;302}303levelPerBracket = existing;304if (colorize) {305existing++;306levelPerBracketType.set(node.openingBracket.text, existing);307}308}309310const childCount = node.childrenLength;311for (let i = 0; i < childCount; i++) {312const child = node.getChild(i);313if (!child) {314continue;315}316nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);317if (318lengthLessThanEqual(nodeOffsetStart, endOffset) &&319lengthGreaterThanEqual(nodeOffsetEnd, startOffset)320) {321const childEndsAfterEnd = lengthGreaterThanEqual(nodeOffsetEnd, endOffset);322if (childEndsAfterEnd && child.kind !== AstNodeKind.Bracket) {323// No child after this child in the requested window, don't recurse324// Don't do this for brackets because of unclosed/unopened brackets325node = child;326if (colorize) {327level++;328nestingLevelOfEqualBracketType = levelPerBracket + 1;329} else {330nestingLevelOfEqualBracketType = levelPerBracket;331}332continue whileLoop;333}334335if (colorize || child.kind !== AstNodeKind.Bracket || !node.closingBracket) {336const shouldContinue = collectBrackets(337child,338nodeOffsetStart,339nodeOffsetEnd,340startOffset,341endOffset,342push,343colorize ? level + 1 : level,344colorize ? levelPerBracket + 1 : levelPerBracket,345levelPerBracketType,346onlyColorizedBrackets,347!node.closingBracket,348);349if (!shouldContinue) {350return false;351}352}353}354nodeOffsetStart = nodeOffsetEnd;355}356357levelPerBracketType?.set(node.openingBracket.text, levelPerBracket);358359return true;360}361case AstNodeKind.UnexpectedClosingBracket: {362const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd);363return push(new BracketInfo(range, level - 1, 0, true));364}365case AstNodeKind.Bracket: {366const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd);367return push(new BracketInfo(range, level - 1, nestingLevelOfEqualBracketType - 1, parentPairIsIncomplete));368}369case AstNodeKind.Text:370return true;371}372}373}374375class CollectBracketPairsContext {376constructor(377public readonly push: (item: BracketPairWithMinIndentationInfo) => boolean,378public readonly includeMinIndentation: boolean,379public readonly textModel: ITextModel,380) {381}382}383384function collectBracketPairs(385node: AstNode,386nodeOffsetStart: Length,387nodeOffsetEnd: Length,388startOffset: Length,389endOffset: Length,390context: CollectBracketPairsContext,391level: number,392levelPerBracketType: Map<string, number>393): boolean {394if (level > 200) {395return true;396}397398let shouldContinue = true;399400if (node.kind === AstNodeKind.Pair) {401let levelPerBracket = 0;402if (levelPerBracketType) {403let existing = levelPerBracketType.get(node.openingBracket.text);404if (existing === undefined) {405existing = 0;406}407levelPerBracket = existing;408existing++;409levelPerBracketType.set(node.openingBracket.text, existing);410}411412const openingBracketEnd = lengthAdd(nodeOffsetStart, node.openingBracket.length);413let minIndentation = -1;414if (context.includeMinIndentation) {415minIndentation = node.computeMinIndentation(416nodeOffsetStart,417context.textModel418);419}420421shouldContinue = context.push(422new BracketPairWithMinIndentationInfo(423lengthsToRange(nodeOffsetStart, nodeOffsetEnd),424lengthsToRange(nodeOffsetStart, openingBracketEnd),425node.closingBracket426? lengthsToRange(427lengthAdd(openingBracketEnd, node.child?.length || lengthZero),428nodeOffsetEnd429)430: undefined,431level,432levelPerBracket,433node,434minIndentation435)436);437438nodeOffsetStart = openingBracketEnd;439if (shouldContinue && node.child) {440const child = node.child;441nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);442if (443lengthLessThanEqual(nodeOffsetStart, endOffset) &&444lengthGreaterThanEqual(nodeOffsetEnd, startOffset)445) {446shouldContinue = collectBracketPairs(447child,448nodeOffsetStart,449nodeOffsetEnd,450startOffset,451endOffset,452context,453level + 1,454levelPerBracketType455);456if (!shouldContinue) {457return false;458}459}460}461462levelPerBracketType?.set(node.openingBracket.text, levelPerBracket);463} else {464let curOffset = nodeOffsetStart;465for (const child of node.children) {466const childOffset = curOffset;467curOffset = lengthAdd(curOffset, child.length);468469if (470lengthLessThanEqual(childOffset, endOffset) &&471lengthLessThanEqual(startOffset, curOffset)472) {473shouldContinue = collectBracketPairs(474child,475childOffset,476curOffset,477startOffset,478endOffset,479context,480level,481levelPerBracketType482);483if (!shouldContinue) {484return false;485}486}487}488}489return shouldContinue;490}491492493