/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter } from '../../../base/common/event.js'; import { Position } from '../core/position.js'; import { getWordAtText } from '../core/wordHelper.js'; import { TextModelPart } from './textModelPart.js'; import { TextModelTokenization } from './textModelTokens.js'; import { ContiguousTokensStore } from '../tokens/contiguousTokensStore.js'; import { SparseTokensStore } from '../tokens/sparseTokensStore.js'; export class TokenizationTextModelPart extends TextModelPart { constructor(_languageService, _languageConfigurationService, _textModel, bracketPairsTextModelPart, _languageId) { super(); this._languageService = _languageService; this._languageConfigurationService = _languageConfigurationService; this._textModel = _textModel; this.bracketPairsTextModelPart = bracketPairsTextModelPart; this._languageId = _languageId; this._onDidChangeLanguage = this._register(new Emitter()); this.onDidChangeLanguage = this._onDidChangeLanguage.event; this._onDidChangeLanguageConfiguration = this._register(new Emitter()); this.onDidChangeLanguageConfiguration = this._onDidChangeLanguageConfiguration.event; this._onDidChangeTokens = this._register(new Emitter()); this.onDidChangeTokens = this._onDidChangeTokens.event; this._backgroundTokenizationState = 0 /* BackgroundTokenizationState.Uninitialized */; this._onBackgroundTokenizationStateChanged = this._register(new Emitter()); this._tokens = new ContiguousTokensStore(this._languageService.languageIdCodec); this._semanticTokens = new SparseTokensStore(this._languageService.languageIdCodec); this._tokenization = new TextModelTokenization(_textModel, this, this._languageService.languageIdCodec); this._languageRegistryListener = this._languageConfigurationService.onDidChange(e => { if (e.affects(this._languageId)) { this._onDidChangeLanguageConfiguration.fire({}); } }); } acceptEdit(range, text, eolCount, firstLineLength, lastLineLength) { this._tokens.acceptEdit(range, eolCount, firstLineLength); this._semanticTokens.acceptEdit(range, eolCount, firstLineLength, lastLineLength, text.length > 0 ? text.charCodeAt(0) : 0 /* CharCode.Null */); } handleDidChangeAttached() { this._tokenization.handleDidChangeAttached(); } flush() { this._tokens.flush(); this._semanticTokens.flush(); } handleDidChangeContent(change) { this._tokenization.handleDidChangeContent(change); } dispose() { this._languageRegistryListener.dispose(); this._tokenization.dispose(); super.dispose(); } get backgroundTokenizationState() { return this._backgroundTokenizationState; } handleTokenizationProgress(completed) { if (this._backgroundTokenizationState === 2 /* BackgroundTokenizationState.Completed */) { // We already did a full tokenization and don't go back to progressing. return; } const newState = completed ? 2 /* BackgroundTokenizationState.Completed */ : 1 /* BackgroundTokenizationState.InProgress */; if (this._backgroundTokenizationState !== newState) { this._backgroundTokenizationState = newState; this.bracketPairsTextModelPart.handleDidChangeBackgroundTokenizationState(); this._onBackgroundTokenizationStateChanged.fire(); } } setTokens(tokens, backgroundTokenizationCompleted = false) { if (tokens.length !== 0) { const ranges = []; for (let i = 0, len = tokens.length; i < len; i++) { const element = tokens[i]; let minChangedLineNumber = 0; let maxChangedLineNumber = 0; let hasChange = false; for (let lineNumber = element.startLineNumber; lineNumber <= element.endLineNumber; lineNumber++) { if (hasChange) { this._tokens.setTokens(this._languageId, lineNumber - 1, this._textModel.getLineLength(lineNumber), element.getLineTokens(lineNumber), false); maxChangedLineNumber = lineNumber; } else { const lineHasChange = this._tokens.setTokens(this._languageId, lineNumber - 1, this._textModel.getLineLength(lineNumber), element.getLineTokens(lineNumber), true); if (lineHasChange) { hasChange = true; minChangedLineNumber = lineNumber; maxChangedLineNumber = lineNumber; } } } if (hasChange) { ranges.push({ fromLineNumber: minChangedLineNumber, toLineNumber: maxChangedLineNumber, }); } } if (ranges.length > 0) { this._emitModelTokensChangedEvent({ tokenizationSupportChanged: false, semanticTokensApplied: false, ranges: ranges, }); } } this.handleTokenizationProgress(backgroundTokenizationCompleted); } setSemanticTokens(tokens, isComplete) { this._semanticTokens.set(tokens, isComplete); this._emitModelTokensChangedEvent({ tokenizationSupportChanged: false, semanticTokensApplied: tokens !== null, ranges: [{ fromLineNumber: 1, toLineNumber: this._textModel.getLineCount() }], }); } hasCompleteSemanticTokens() { return this._semanticTokens.isComplete(); } hasSomeSemanticTokens() { return !this._semanticTokens.isEmpty(); } setPartialSemanticTokens(range, tokens) { if (this.hasCompleteSemanticTokens()) { return; } const changedRange = this._textModel.validateRange(this._semanticTokens.setPartial(range, tokens)); this._emitModelTokensChangedEvent({ tokenizationSupportChanged: false, semanticTokensApplied: true, ranges: [ { fromLineNumber: changedRange.startLineNumber, toLineNumber: changedRange.endLineNumber, }, ], }); } tokenizeViewport(startLineNumber, endLineNumber) { startLineNumber = Math.max(1, startLineNumber); endLineNumber = Math.min(this._textModel.getLineCount(), endLineNumber); this._tokenization.tokenizeViewport(startLineNumber, endLineNumber); } clearTokens() { this._tokens.flush(); this._emitModelTokensChangedEvent({ tokenizationSupportChanged: true, semanticTokensApplied: false, ranges: [ { fromLineNumber: 1, toLineNumber: this._textModel.getLineCount(), }, ], }); } _emitModelTokensChangedEvent(e) { if (!this._textModel._isDisposing()) { this.bracketPairsTextModelPart.handleDidChangeTokens(e); this._onDidChangeTokens.fire(e); } } resetTokenization() { this._tokenization.reset(); } forceTokenization(lineNumber) { if (lineNumber < 1 || lineNumber > this._textModel.getLineCount()) { throw new Error('Illegal value for lineNumber'); } this._tokenization.forceTokenization(lineNumber); } isCheapToTokenize(lineNumber) { return this._tokenization.isCheapToTokenize(lineNumber); } tokenizeIfCheap(lineNumber) { if (this.isCheapToTokenize(lineNumber)) { this.forceTokenization(lineNumber); } } getLineTokens(lineNumber) { if (lineNumber < 1 || lineNumber > this._textModel.getLineCount()) { throw new Error('Illegal value for lineNumber'); } return this._getLineTokens(lineNumber); } _getLineTokens(lineNumber) { const lineText = this._textModel.getLineContent(lineNumber); const syntacticTokens = this._tokens.getTokens(this._languageId, lineNumber - 1, lineText); return this._semanticTokens.addSparseTokens(lineNumber, syntacticTokens); } getTokenTypeIfInsertingCharacter(lineNumber, column, character) { const position = this._textModel.validatePosition(new Position(lineNumber, column)); return this._tokenization.getTokenTypeIfInsertingCharacter(position, character); } tokenizeLineWithEdit(position, length, newText) { const validatedPosition = this._textModel.validatePosition(position); return this._tokenization.tokenizeLineWithEdit(validatedPosition, length, newText); } getLanguageConfiguration(languageId) { return this._languageConfigurationService.getLanguageConfiguration(languageId); } // Having tokens allows implementing additional helper methods getWordAtPosition(_position) { 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; } static _findLanguageBoundaries(lineTokens, tokenIndex) { 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]; } getWordUntilPosition(position) { 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, }; } getLanguageId() { return this._languageId; } getLanguageIdAtPosition(lineNumber, column) { const position = this._textModel.validatePosition(new Position(lineNumber, column)); const lineTokens = this.getLineTokens(position.lineNumber); return lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); } setLanguageId(languageId) { if (this._languageId === languageId) { // There's nothing to do return; } const e = { oldLanguage: this._languageId, newLanguage: languageId }; this._languageId = languageId; this.bracketPairsTextModelPart.handleDidChangeLanguage(e); this._tokenization.handleDidChangeLanguage(e); this._onDidChangeLanguage.fire(e); this._onDidChangeLanguageConfiguration.fire({}); } }