diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 81a7be60be805e04d8dad7ce6a5e13993c26aa6b..9774fc60eaf18aa8da8cd27df3530906e9c8464c 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -34,7 +34,6 @@ import { withUndefinedAsNull } from 'vs/base/common/types'; import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer'; import { TokensStore, MultilineTokens, countEOL, MultilineTokens2, TokensStore2 } from 'vs/editor/common/model/tokensStore'; import { Color } from 'vs/base/common/color'; -import { Constants } from 'vs/base/common/uint'; import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { TextChange } from 'vs/editor/common/model/textChange'; @@ -172,6 +171,21 @@ const enum StringOffsetValidationType { SurrogatePairs = 1, } +type ContinueBracketSearchPredicate = null | (() => boolean); + +class BracketSearchCanceled { + public static INSTANCE = new BracketSearchCanceled(); + _searchCanceledBrand = undefined; + private constructor() { } +} + +function stripBracketSearchCanceled(result: T | null | BracketSearchCanceled): T | null { + if (result instanceof BracketSearchCanceled) { + return null; + } + return result; +} + export class TextModel extends Disposable implements model.ITextModel { private static readonly MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB @@ -2014,7 +2028,7 @@ export class TextModel extends Disposable implements model.ITextModel { return null; } - return this._findMatchingBracketUp(data, position); + return stripBracketSearchCanceled(this._findMatchingBracketUp(data, position, null)); } public matchBracket(position: IPosition): [Range, Range] | null { @@ -2062,8 +2076,11 @@ export class TextModel extends Disposable implements model.ITextModel { // check that we didn't hit a bracket too far away from position if (foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); - const r = this._matchFoundBracket(foundBracket, currentModeBrackets.textIsBracket[foundBracketText], currentModeBrackets.textIsOpenBracket[foundBracketText]); + const r = this._matchFoundBracket(foundBracket, currentModeBrackets.textIsBracket[foundBracketText], currentModeBrackets.textIsOpenBracket[foundBracketText], null); if (r) { + if (r instanceof BracketSearchCanceled) { + return null; + } bestResult = r; } } @@ -2100,8 +2117,11 @@ export class TextModel extends Disposable implements model.ITextModel { // check that we didn't hit a bracket too far away from position if (foundBracket && foundBracket.startColumn <= position.column && position.column <= foundBracket.endColumn) { const foundBracketText = lineText.substring(foundBracket.startColumn - 1, foundBracket.endColumn - 1).toLowerCase(); - const r = this._matchFoundBracket(foundBracket, prevModeBrackets.textIsBracket[foundBracketText], prevModeBrackets.textIsOpenBracket[foundBracketText]); + const r = this._matchFoundBracket(foundBracket, prevModeBrackets.textIsBracket[foundBracketText], prevModeBrackets.textIsOpenBracket[foundBracketText], null); if (r) { + if (r instanceof BracketSearchCanceled) { + return null; + } return r; } } @@ -2111,35 +2131,41 @@ export class TextModel extends Disposable implements model.ITextModel { return null; } - private _matchFoundBracket(foundBracket: Range, data: RichEditBracket, isOpen: boolean): [Range, Range] | null { + private _matchFoundBracket(foundBracket: Range, data: RichEditBracket, isOpen: boolean, continueSearchPredicate: ContinueBracketSearchPredicate): [Range, Range] | null | BracketSearchCanceled { if (!data) { return null; } - if (isOpen) { - let matched = this._findMatchingBracketDown(data, foundBracket.getEndPosition()); - if (matched) { - return [foundBracket, matched]; - } - } else { - let matched = this._findMatchingBracketUp(data, foundBracket.getStartPosition()); - if (matched) { - return [foundBracket, matched]; - } + const matched = ( + isOpen + ? this._findMatchingBracketDown(data, foundBracket.getEndPosition(), continueSearchPredicate) + : this._findMatchingBracketUp(data, foundBracket.getStartPosition(), continueSearchPredicate) + ); + + if (!matched) { + return null; } - return null; + if (matched instanceof BracketSearchCanceled) { + return matched; + } + + return [foundBracket, matched]; } - private _findMatchingBracketUp(bracket: RichEditBracket, position: Position): Range | null { + private _findMatchingBracketUp(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { // console.log('_findMatchingBracketUp: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); const languageId = bracket.languageIdentifier.id; const reversedBracketRegex = bracket.reversedRegex; let count = -1; - const searchPrevMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null => { + let totalCallCount = 0; + const searchPrevMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } const r = BracketsUtils.findPrevBracketInRange(reversedBracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (!r) { break; @@ -2214,15 +2240,19 @@ export class TextModel extends Disposable implements model.ITextModel { return null; } - private _findMatchingBracketDown(bracket: RichEditBracket, position: Position): Range | null { + private _findMatchingBracketDown(bracket: RichEditBracket, position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): Range | null | BracketSearchCanceled { // console.log('_findMatchingBracketDown: ', 'bracket: ', JSON.stringify(bracket), 'startPosition: ', String(position)); const languageId = bracket.languageIdentifier.id; const bracketRegex = bracket.forwardRegex; let count = 1; - const searchNextMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null => { + let totalCallCount = 0; + const searchNextMatchingBracketInRange = (lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): Range | null | BracketSearchCanceled => { while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } const r = BracketsUtils.findNextBracketInRange(bracketRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (!r) { break; @@ -2452,7 +2482,16 @@ export class TextModel extends Disposable implements model.ITextModel { return null; } - public findEnclosingBrackets(_position: IPosition, maxDuration = Constants.MAX_SAFE_SMALL_INTEGER): [Range, Range] | null { + public findEnclosingBrackets(_position: IPosition, maxDuration?: number): [Range, Range] | null { + let continueSearchPredicate: ContinueBracketSearchPredicate; + if (typeof maxDuration === 'undefined') { + continueSearchPredicate = null; + } else { + const startTime = Date.now(); + continueSearchPredicate = () => { + return (Date.now() - startTime <= maxDuration); + }; + } const position = this.validatePosition(_position); const lineCount = this.getLineCount(); const savedCounts = new Map(); @@ -2468,8 +2507,13 @@ export class TextModel extends Disposable implements model.ITextModel { } counts = savedCounts.get(languageId)!; }; - const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null => { + + let totalCallCount = 0; + const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null | BracketSearchCanceled => { while (true) { + if (continueSearchPredicate && (++totalCallCount) % 100 === 0 && !continueSearchPredicate()) { + return BracketSearchCanceled.INSTANCE; + } const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (!r) { break; @@ -2485,7 +2529,7 @@ export class TextModel extends Disposable implements model.ITextModel { } if (counts[bracket.index] === -1) { - return this._matchFoundBracket(r, bracket, false); + return this._matchFoundBracket(r, bracket, false, continueSearchPredicate); } } @@ -2496,12 +2540,7 @@ export class TextModel extends Disposable implements model.ITextModel { let languageId: LanguageId = -1; let modeBrackets: RichEditBrackets | null = null; - const startTime = Date.now(); for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const elapsedTime = Date.now() - startTime; - if (elapsedTime > maxDuration) { - return null; - } const lineTokens = this._getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this._buffer.getLineContent(lineNumber); @@ -2530,7 +2569,7 @@ export class TextModel extends Disposable implements model.ITextModel { if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return r; + return stripBracketSearchCanceled(r); } prevSearchInToken = false; } @@ -2555,7 +2594,7 @@ export class TextModel extends Disposable implements model.ITextModel { if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return r; + return stripBracketSearchCanceled(r); } } } @@ -2566,7 +2605,7 @@ export class TextModel extends Disposable implements model.ITextModel { if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return r; + return stripBracketSearchCanceled(r); } } }