Files
vscode/src/vs/editor/common/model/tokenizationTextModelPart.ts
T
Alex Ross 6c15586994 Add telemtry for how long it takes to parse files with tree-sitter (#213565)
* Make space for tree sitter

* Add the tree sitter wasm file

* Very naive tree-sitter syntax highlighting for html, with a layer breaker

* Update tree when content changes

* WIP for making abstract tokens class

* Handle theme changes

* Replace entire text model value with parse callback

* Perf improvements

* Add tree-sitter-typescript

* Add typescript + better initial parsing

* Refactor into tree parsing service and fix flaw in parse callback

* Remove things that aren't the parser service

* Add yielding

* Remove changes that aren't required for PR

* Remove more file changes

* Reduce yield to 50 ms

* Fix incremental parsing

* Try update node-abi

* Revert "Try update node-abi"

This reverts commit df28801e31.

* Update text buffer chunk api

* fix build

* Remove tree-sitter dependency

* Adopt new, as yet unpublished, `@vscode/tree-sitter-wasm` package

* Use published `@vscode/tree-sitter-wasm` package

* Break `TreeSitterTree` and `TreeSitterParserService` into better pieces
and:
- document the order of editor changes
- use service injection where `TextModel` is constructed

* Fix tests

* Remove unneeded import

* Fix missing tree-sitter-wasm in web and remote

* Make package.jsons match

* Add @vscode/tree-sitter-wasm to web loader config

* Try using importAMDNodeModule

* PR feedback

* Add race condition test for changing language while loading language

* Use same timeout

* Queue content changes

* Remove override dispose

* Move queue into TreeSitterTree

---------

Co-authored-by: Peng Lyu <penn.lv@gmail.com>
2024-07-23 14:59:04 +02:00

672 lines
24 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equals } from 'vs/base/common/arrays';
import { RunOnceScheduler } from 'vs/base/common/async';
import { CharCode } from 'vs/base/common/charCode';
import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableMap, MutableDisposable } from 'vs/base/common/lifecycle';
import { countEOL } from 'vs/editor/common/core/eolCounter';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IWordAtPosition, getWordAtText } from 'vs/editor/common/core/wordHelper';
import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';
import { IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { ILanguageConfigurationService, LanguageConfigurationServiceChangeEvent, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { IAttachedView } from 'vs/editor/common/model';
import { BracketPairsTextModelPart } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl';
import { AttachedViews, IAttachedViewState, TextModel } from 'vs/editor/common/model/textModel';
import { TextModelPart } from 'vs/editor/common/model/textModelPart';
import { DefaultBackgroundTokenizer, TokenizerWithStateStoreAndTextModel, TrackingTokenizationStateStore } from 'vs/editor/common/model/textModelTokens';
import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents';
import { BackgroundTokenizationState, ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart';
import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens';
import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder';
import { ContiguousTokensStore } from 'vs/editor/common/tokens/contiguousTokensStore';
import { LineTokens } from 'vs/editor/common/tokens/lineTokens';
import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens';
import { SparseTokensStore } from 'vs/editor/common/tokens/sparseTokensStore';
export class TokenizationTextModelPart extends TextModelPart implements ITokenizationTextModelPart {
private readonly _semanticTokens: SparseTokensStore = new SparseTokensStore(this._languageService.languageIdCodec);
private readonly _onDidChangeLanguage: Emitter<IModelLanguageChangedEvent> = this._register(new Emitter<IModelLanguageChangedEvent>());
public readonly onDidChangeLanguage: Event<IModelLanguageChangedEvent> = this._onDidChangeLanguage.event;
private readonly _onDidChangeLanguageConfiguration: Emitter<IModelLanguageConfigurationChangedEvent> = this._register(new Emitter<IModelLanguageConfigurationChangedEvent>());
public readonly onDidChangeLanguageConfiguration: Event<IModelLanguageConfigurationChangedEvent> = this._onDidChangeLanguageConfiguration.event;
private readonly _onDidChangeTokens: Emitter<IModelTokensChangedEvent> = this._register(new Emitter<IModelTokensChangedEvent>());
public readonly onDidChangeTokens: Event<IModelTokensChangedEvent> = this._onDidChangeTokens.event;
private readonly grammarTokens = this._register(new GrammarTokens(this._languageService.languageIdCodec, this._textModel, () => this._languageId, this._attachedViews));
constructor(
private readonly _textModel: TextModel,
private readonly _bracketPairsTextModelPart: BracketPairsTextModelPart,
private _languageId: string,
private readonly _attachedViews: AttachedViews,
@ILanguageService private readonly _languageService: ILanguageService,
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,
) {
super();
this._register(this.grammarTokens.onDidChangeTokens(e => {
this._emitModelTokensChangedEvent(e);
}));
this._register(this.grammarTokens.onDidChangeBackgroundTokenizationState(e => {
this._bracketPairsTextModelPart.handleDidChangeBackgroundTokenizationState();
}));
}
_hasListeners(): boolean {
return (this._onDidChangeLanguage.hasListeners()
|| this._onDidChangeLanguageConfiguration.hasListeners()
|| this._onDidChangeTokens.hasListeners());
}
public handleLanguageConfigurationServiceChange(e: LanguageConfigurationServiceChangeEvent): void {
if (e.affects(this._languageId)) {
this._onDidChangeLanguageConfiguration.fire({});
}
}
public handleDidChangeContent(e: IModelContentChangedEvent): void {
if (e.isFlush) {
this._semanticTokens.flush();
} else if (!e.isEolChange) { // We don't have to do anything on an EOL change
for (const c of e.changes) {
const [eolCount, firstLineLength, lastLineLength] = countEOL(c.text);
this._semanticTokens.acceptEdit(
c.range,
eolCount,
firstLineLength,
lastLineLength,
c.text.length > 0 ? c.text.charCodeAt(0) : CharCode.Null
);
}
}
this.grammarTokens.handleDidChangeContent(e);
}
public handleDidChangeAttached(): void {
this.grammarTokens.handleDidChangeAttached();
}
/**
* Includes grammar and semantic tokens.
*/
public getLineTokens(lineNumber: number): LineTokens {
this.validateLineNumber(lineNumber);
const syntacticTokens = this.grammarTokens.getLineTokens(lineNumber);
return this._semanticTokens.addSparseTokens(lineNumber, syntacticTokens);
}
private _emitModelTokensChangedEvent(e: IModelTokensChangedEvent): void {
if (!this._textModel._isDisposing()) {
this._bracketPairsTextModelPart.handleDidChangeTokens(e);
this._onDidChangeTokens.fire(e);
}
}
// #region Grammar Tokens
private validateLineNumber(lineNumber: number): void {
if (lineNumber < 1 || lineNumber > this._textModel.getLineCount()) {
throw new BugIndicatingError('Illegal value for lineNumber');
}
}
public get hasTokens(): boolean {
return this.grammarTokens.hasTokens;
}
public resetTokenization() {
this.grammarTokens.resetTokenization();
}
public get backgroundTokenizationState() {
return this.grammarTokens.backgroundTokenizationState;
}
public forceTokenization(lineNumber: number): void {
this.validateLineNumber(lineNumber);
this.grammarTokens.forceTokenization(lineNumber);
}
public hasAccurateTokensForLine(lineNumber: number): boolean {
this.validateLineNumber(lineNumber);
return this.grammarTokens.hasAccurateTokensForLine(lineNumber);
}
public isCheapToTokenize(lineNumber: number): boolean {
this.validateLineNumber(lineNumber);
return this.grammarTokens.isCheapToTokenize(lineNumber);
}
public tokenizeIfCheap(lineNumber: number): void {
this.validateLineNumber(lineNumber);
this.grammarTokens.tokenizeIfCheap(lineNumber);
}
public getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType {
return this.grammarTokens.getTokenTypeIfInsertingCharacter(lineNumber, column, character);
}
public tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null {
return this.grammarTokens.tokenizeLineWithEdit(position, length, newText);
}
// #endregion
// #region Semantic Tokens
public setSemanticTokens(tokens: SparseMultilineTokens[] | null, isComplete: boolean): void {
this._semanticTokens.set(tokens, isComplete);
this._emitModelTokensChangedEvent({
semanticTokensApplied: tokens !== null,
ranges: [{ fromLineNumber: 1, toLineNumber: this._textModel.getLineCount() }],
});
}
public hasCompleteSemanticTokens(): boolean {
return this._semanticTokens.isComplete();
}
public hasSomeSemanticTokens(): boolean {
return !this._semanticTokens.isEmpty();
}
public setPartialSemanticTokens(range: Range, tokens: SparseMultilineTokens[]): void {
if (this.hasCompleteSemanticTokens()) {
return;
}
const changedRange = this._textModel.validateRange(
this._semanticTokens.setPartial(range, tokens)
);
this._emitModelTokensChangedEvent({
semanticTokensApplied: true,
ranges: [
{
fromLineNumber: changedRange.startLineNumber,
toLineNumber: changedRange.endLineNumber,
},
],
});
}
// #endregion
// #region Utility Methods
public getWordAtPosition(_position: IPosition): IWordAtPosition | null {
this.assertNotDisposed();
const position = this._textModel.validatePosition(_position);
const lineContent = this._textModel.getLineContent(position.lineNumber);
const lineTokens = this.getLineTokens(position.lineNumber);
const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1);
// (1). First try checking right biased word
const [rbStartOffset, rbEndOffset] = TokenizationTextModelPart._findLanguageBoundaries(lineTokens, tokenIndex);
const rightBiasedWord = getWordAtText(
position.column,
this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex)).getWordDefinition(),
lineContent.substring(rbStartOffset, rbEndOffset),
rbStartOffset
);
// Make sure the result touches the original passed in position
if (
rightBiasedWord &&
rightBiasedWord.startColumn <= _position.column &&
_position.column <= rightBiasedWord.endColumn
) {
return rightBiasedWord;
}
// (2). Else, if we were at a language boundary, check the left biased word
if (tokenIndex > 0 && rbStartOffset === position.column - 1) {
// edge case, where `position` sits between two tokens belonging to two different languages
const [lbStartOffset, lbEndOffset] = TokenizationTextModelPart._findLanguageBoundaries(
lineTokens,
tokenIndex - 1
);
const leftBiasedWord = getWordAtText(
position.column,
this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex - 1)).getWordDefinition(),
lineContent.substring(lbStartOffset, lbEndOffset),
lbStartOffset
);
// Make sure the result touches the original passed in position
if (
leftBiasedWord &&
leftBiasedWord.startColumn <= _position.column &&
_position.column <= leftBiasedWord.endColumn
) {
return leftBiasedWord;
}
}
return null;
}
private getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration {
return this._languageConfigurationService.getLanguageConfiguration(languageId);
}
private static _findLanguageBoundaries(lineTokens: LineTokens, tokenIndex: number): [number, number] {
const languageId = lineTokens.getLanguageId(tokenIndex);
// go left until a different language is hit
let startOffset = 0;
for (let i = tokenIndex; i >= 0 && lineTokens.getLanguageId(i) === languageId; i--) {
startOffset = lineTokens.getStartOffset(i);
}
// go right until a different language is hit
let endOffset = lineTokens.getLineContent().length;
for (
let i = tokenIndex, tokenCount = lineTokens.getCount();
i < tokenCount && lineTokens.getLanguageId(i) === languageId;
i++
) {
endOffset = lineTokens.getEndOffset(i);
}
return [startOffset, endOffset];
}
public getWordUntilPosition(position: IPosition): IWordAtPosition {
const wordAtPosition = this.getWordAtPosition(position);
if (!wordAtPosition) {
return { word: '', startColumn: position.column, endColumn: position.column, };
}
return {
word: wordAtPosition.word.substr(0, position.column - wordAtPosition.startColumn),
startColumn: wordAtPosition.startColumn,
endColumn: position.column,
};
}
// #endregion
// #region Language Id handling
public getLanguageId(): string {
return this._languageId;
}
public getLanguageIdAtPosition(lineNumber: number, column: number): string {
const position = this._textModel.validatePosition(new Position(lineNumber, column));
const lineTokens = this.getLineTokens(position.lineNumber);
return lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1));
}
public setLanguageId(languageId: string, source: string = 'api'): void {
if (this._languageId === languageId) {
// There's nothing to do
return;
}
const e: IModelLanguageChangedEvent = {
oldLanguage: this._languageId,
newLanguage: languageId,
source
};
this._languageId = languageId;
this._bracketPairsTextModelPart.handleDidChangeLanguage(e);
this.grammarTokens.resetTokenization();
this._onDidChangeLanguage.fire(e);
this._onDidChangeLanguageConfiguration.fire({});
}
// #endregion
}
class GrammarTokens extends Disposable {
private _tokenizer: TokenizerWithStateStoreAndTextModel | null = null;
private _defaultBackgroundTokenizer: DefaultBackgroundTokenizer | null = null;
private readonly _backgroundTokenizer = this._register(new MutableDisposable<IBackgroundTokenizer>());
private readonly _tokens = new ContiguousTokensStore(this._languageIdCodec);
private _debugBackgroundTokens: ContiguousTokensStore | undefined;
private _debugBackgroundStates: TrackingTokenizationStateStore<IState> | undefined;
private readonly _debugBackgroundTokenizer = this._register(new MutableDisposable<IBackgroundTokenizer>());
private _backgroundTokenizationState = BackgroundTokenizationState.InProgress;
public get backgroundTokenizationState(): BackgroundTokenizationState {
return this._backgroundTokenizationState;
}
private readonly _onDidChangeBackgroundTokenizationState = this._register(new Emitter<void>());
/** @internal, should not be exposed by the text model! */
public readonly onDidChangeBackgroundTokenizationState: Event<void> = this._onDidChangeBackgroundTokenizationState.event;
private readonly _onDidChangeTokens = this._register(new Emitter<IModelTokensChangedEvent>());
/** @internal, should not be exposed by the text model! */
public readonly onDidChangeTokens: Event<IModelTokensChangedEvent> = this._onDidChangeTokens.event;
private readonly _attachedViewStates = this._register(new DisposableMap<IAttachedView, AttachedViewHandler>());
constructor(
private readonly _languageIdCodec: ILanguageIdCodec,
private readonly _textModel: TextModel,
private getLanguageId: () => string,
attachedViews: AttachedViews,
) {
super();
this._register(TokenizationRegistry.onDidChange((e) => {
const languageId = this.getLanguageId();
if (e.changedLanguages.indexOf(languageId) === -1) {
return;
}
this.resetTokenization();
}));
this.resetTokenization();
this._register(attachedViews.onDidChangeVisibleRanges(({ view, state }) => {
if (state) {
let existing = this._attachedViewStates.get(view);
if (!existing) {
existing = new AttachedViewHandler(() => this.refreshRanges(existing!.lineRanges));
this._attachedViewStates.set(view, existing);
}
existing.handleStateChange(state);
} else {
this._attachedViewStates.deleteAndDispose(view);
}
}));
}
public resetTokenization(fireTokenChangeEvent: boolean = true): void {
this._tokens.flush();
this._debugBackgroundTokens?.flush();
if (this._debugBackgroundStates) {
this._debugBackgroundStates = new TrackingTokenizationStateStore(this._textModel.getLineCount());
}
if (fireTokenChangeEvent) {
this._onDidChangeTokens.fire({
semanticTokensApplied: false,
ranges: [
{
fromLineNumber: 1,
toLineNumber: this._textModel.getLineCount(),
},
],
});
}
const initializeTokenization = (): [ITokenizationSupport, IState] | [null, null] => {
if (this._textModel.isTooLargeForTokenization()) {
return [null, null];
}
const tokenizationSupport = TokenizationRegistry.get(this.getLanguageId());
if (!tokenizationSupport) {
return [null, null];
}
let initialState: IState;
try {
initialState = tokenizationSupport.getInitialState();
} catch (e) {
onUnexpectedError(e);
return [null, null];
}
return [tokenizationSupport, initialState];
};
const [tokenizationSupport, initialState] = initializeTokenization();
if (tokenizationSupport && initialState) {
this._tokenizer = new TokenizerWithStateStoreAndTextModel(this._textModel.getLineCount(), tokenizationSupport, this._textModel, this._languageIdCodec);
} else {
this._tokenizer = null;
}
this._backgroundTokenizer.clear();
this._defaultBackgroundTokenizer = null;
if (this._tokenizer) {
const b: IBackgroundTokenizationStore = {
setTokens: (tokens) => {
this.setTokens(tokens);
},
backgroundTokenizationFinished: () => {
if (this._backgroundTokenizationState === BackgroundTokenizationState.Completed) {
// We already did a full tokenization and don't go back to progressing.
return;
}
const newState = BackgroundTokenizationState.Completed;
this._backgroundTokenizationState = newState;
this._onDidChangeBackgroundTokenizationState.fire();
},
setEndState: (lineNumber, state) => {
if (!this._tokenizer) { return; }
const firstInvalidEndStateLineNumber = this._tokenizer.store.getFirstInvalidEndStateLineNumber();
// Don't accept states for definitely valid states, the renderer is ahead of the worker!
if (firstInvalidEndStateLineNumber !== null && lineNumber >= firstInvalidEndStateLineNumber) {
this._tokenizer?.store.setEndState(lineNumber, state);
}
},
};
if (tokenizationSupport && tokenizationSupport.createBackgroundTokenizer && !tokenizationSupport.backgroundTokenizerShouldOnlyVerifyTokens) {
this._backgroundTokenizer.value = tokenizationSupport.createBackgroundTokenizer(this._textModel, b);
}
if (!this._backgroundTokenizer.value && !this._textModel.isTooLargeForTokenization()) {
this._backgroundTokenizer.value = this._defaultBackgroundTokenizer =
new DefaultBackgroundTokenizer(this._tokenizer, b);
this._defaultBackgroundTokenizer.handleChanges();
}
if (tokenizationSupport?.backgroundTokenizerShouldOnlyVerifyTokens && tokenizationSupport.createBackgroundTokenizer) {
this._debugBackgroundTokens = new ContiguousTokensStore(this._languageIdCodec);
this._debugBackgroundStates = new TrackingTokenizationStateStore(this._textModel.getLineCount());
this._debugBackgroundTokenizer.clear();
this._debugBackgroundTokenizer.value = tokenizationSupport.createBackgroundTokenizer(this._textModel, {
setTokens: (tokens) => {
this._debugBackgroundTokens?.setMultilineTokens(tokens, this._textModel);
},
backgroundTokenizationFinished() {
// NO OP
},
setEndState: (lineNumber, state) => {
this._debugBackgroundStates?.setEndState(lineNumber, state);
},
});
} else {
this._debugBackgroundTokens = undefined;
this._debugBackgroundStates = undefined;
this._debugBackgroundTokenizer.value = undefined;
}
}
this.refreshAllVisibleLineTokens();
}
public handleDidChangeAttached() {
this._defaultBackgroundTokenizer?.handleChanges();
}
public handleDidChangeContent(e: IModelContentChangedEvent): void {
if (e.isFlush) {
// Don't fire the event, as the view might not have got the text change event yet
this.resetTokenization(false);
} else if (!e.isEolChange) { // We don't have to do anything on an EOL change
for (const c of e.changes) {
const [eolCount, firstLineLength] = countEOL(c.text);
this._tokens.acceptEdit(c.range, eolCount, firstLineLength);
this._debugBackgroundTokens?.acceptEdit(c.range, eolCount, firstLineLength);
}
this._debugBackgroundStates?.acceptChanges(e.changes);
if (this._tokenizer) {
this._tokenizer.store.acceptChanges(e.changes);
}
this._defaultBackgroundTokenizer?.handleChanges();
}
}
private setTokens(tokens: ContiguousMultilineTokens[]): { changes: { fromLineNumber: number; toLineNumber: number }[] } {
const { changes } = this._tokens.setMultilineTokens(tokens, this._textModel);
if (changes.length > 0) {
this._onDidChangeTokens.fire({ semanticTokensApplied: false, ranges: changes, });
}
return { changes: changes };
}
private refreshAllVisibleLineTokens(): void {
const ranges = LineRange.joinMany([...this._attachedViewStates].map(([_, s]) => s.lineRanges));
this.refreshRanges(ranges);
}
private refreshRanges(ranges: readonly LineRange[]): void {
for (const range of ranges) {
this.refreshRange(range.startLineNumber, range.endLineNumberExclusive - 1);
}
}
private refreshRange(startLineNumber: number, endLineNumber: number): void {
if (!this._tokenizer) {
return;
}
startLineNumber = Math.max(1, Math.min(this._textModel.getLineCount(), startLineNumber));
endLineNumber = Math.min(this._textModel.getLineCount(), endLineNumber);
const builder = new ContiguousMultilineTokensBuilder();
const { heuristicTokens } = this._tokenizer.tokenizeHeuristically(builder, startLineNumber, endLineNumber);
const changedTokens = this.setTokens(builder.finalize());
if (heuristicTokens) {
// We overrode tokens with heuristically computed ones.
// Because old states might get reused (thus stopping invalidation),
// we have to explicitly request the tokens for the changed ranges again.
for (const c of changedTokens.changes) {
this._backgroundTokenizer.value?.requestTokens(c.fromLineNumber, c.toLineNumber + 1);
}
}
this._defaultBackgroundTokenizer?.checkFinished();
}
public forceTokenization(lineNumber: number): void {
const builder = new ContiguousMultilineTokensBuilder();
this._tokenizer?.updateTokensUntilLine(builder, lineNumber);
this.setTokens(builder.finalize());
this._defaultBackgroundTokenizer?.checkFinished();
}
public hasAccurateTokensForLine(lineNumber: number): boolean {
if (!this._tokenizer) {
return true;
}
return this._tokenizer.hasAccurateTokensForLine(lineNumber);
}
public isCheapToTokenize(lineNumber: number): boolean {
if (!this._tokenizer) {
return true;
}
return this._tokenizer.isCheapToTokenize(lineNumber);
}
public tokenizeIfCheap(lineNumber: number): void {
if (this.isCheapToTokenize(lineNumber)) {
this.forceTokenization(lineNumber);
}
}
public getLineTokens(lineNumber: number): LineTokens {
const lineText = this._textModel.getLineContent(lineNumber);
const result = this._tokens.getTokens(
this._textModel.getLanguageId(),
lineNumber - 1,
lineText
);
if (this._debugBackgroundTokens && this._debugBackgroundStates && this._tokenizer) {
if (this._debugBackgroundStates.getFirstInvalidEndStateLineNumberOrMax() > lineNumber && this._tokenizer.store.getFirstInvalidEndStateLineNumberOrMax() > lineNumber) {
const backgroundResult = this._debugBackgroundTokens.getTokens(
this._textModel.getLanguageId(),
lineNumber - 1,
lineText
);
if (!result.equals(backgroundResult) && this._debugBackgroundTokenizer.value?.reportMismatchingTokens) {
this._debugBackgroundTokenizer.value.reportMismatchingTokens(lineNumber);
}
}
}
return result;
}
public getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType {
if (!this._tokenizer) {
return StandardTokenType.Other;
}
const position = this._textModel.validatePosition(new Position(lineNumber, column));
this.forceTokenization(position.lineNumber);
return this._tokenizer.getTokenTypeIfInsertingCharacter(position, character);
}
public tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null {
if (!this._tokenizer) {
return null;
}
const validatedPosition = this._textModel.validatePosition(position);
this.forceTokenization(validatedPosition.lineNumber);
return this._tokenizer.tokenizeLineWithEdit(validatedPosition, length, newText);
}
public get hasTokens(): boolean {
return this._tokens.hasTokens;
}
}
class AttachedViewHandler extends Disposable {
private readonly runner = this._register(new RunOnceScheduler(() => this.update(), 50));
private _computedLineRanges: readonly LineRange[] = [];
private _lineRanges: readonly LineRange[] = [];
public get lineRanges(): readonly LineRange[] { return this._lineRanges; }
constructor(private readonly _refreshTokens: () => void) {
super();
}
private update(): void {
if (equals(this._computedLineRanges, this._lineRanges, (a, b) => a.equals(b))) {
return;
}
this._computedLineRanges = this._lineRanges;
this._refreshTokens();
}
public handleStateChange(state: IAttachedViewState): void {
this._lineRanges = state.visibleLineRanges;
if (state.stabilized) {
this.runner.cancel();
this.update();
} else {
this.runner.schedule();
}
}
}