mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-02 09:35:40 -05:00
496 lines
15 KiB
TypeScript
496 lines
15 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
'use strict';
|
|
|
|
import { IState, FontStyle, StandardTokenType, MetadataConsts, ColorId, LanguageId, ITokenizationSupport, LanguageIdentifier } from 'vs/editor/common/modes';
|
|
import { LineTokens } from 'vs/editor/common/core/lineTokens';
|
|
import * as arrays from 'vs/base/common/arrays';
|
|
import { Position } from 'vs/editor/common/core/position';
|
|
import { Range } from 'vs/editor/common/core/range';
|
|
import { IModelTokensChangedEvent } from 'vs/editor/common/model/textModelEvents';
|
|
import { onUnexpectedError } from 'vs/base/common/errors';
|
|
import { TokenizationResult2 } from 'vs/editor/common/core/token';
|
|
import { nullTokenize2 } from 'vs/editor/common/modes/nullMode';
|
|
import { ITextBuffer } from 'vs/editor/common/model';
|
|
|
|
function getDefaultMetadata(topLevelLanguageId: LanguageId): number {
|
|
return (
|
|
(topLevelLanguageId << MetadataConsts.LANGUAGEID_OFFSET)
|
|
| (StandardTokenType.Other << MetadataConsts.TOKEN_TYPE_OFFSET)
|
|
| (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET)
|
|
| (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET)
|
|
| (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET)
|
|
) >>> 0;
|
|
}
|
|
|
|
const EMPTY_LINE_TOKENS = (new Uint32Array(0)).buffer;
|
|
|
|
class ModelLineTokens {
|
|
_state: IState;
|
|
_lineTokens: ArrayBuffer;
|
|
_invalid: boolean;
|
|
|
|
constructor(state: IState) {
|
|
this._state = state;
|
|
this._lineTokens = null;
|
|
this._invalid = true;
|
|
}
|
|
|
|
public deleteBeginning(toChIndex: number): void {
|
|
if (this._lineTokens === null || this._lineTokens === EMPTY_LINE_TOKENS) {
|
|
return;
|
|
}
|
|
this.delete(0, toChIndex);
|
|
}
|
|
|
|
public deleteEnding(fromChIndex: number): void {
|
|
if (this._lineTokens === null || this._lineTokens === EMPTY_LINE_TOKENS) {
|
|
return;
|
|
}
|
|
|
|
const tokens = new Uint32Array(this._lineTokens);
|
|
const lineTextLength = tokens[tokens.length - 2];
|
|
this.delete(fromChIndex, lineTextLength);
|
|
}
|
|
|
|
public delete(fromChIndex: number, toChIndex: number): void {
|
|
if (this._lineTokens === null || this._lineTokens === EMPTY_LINE_TOKENS || fromChIndex === toChIndex) {
|
|
return;
|
|
}
|
|
|
|
const tokens = new Uint32Array(this._lineTokens);
|
|
const tokensCount = (tokens.length >>> 1);
|
|
|
|
// special case: deleting everything
|
|
if (fromChIndex === 0 && tokens[tokens.length - 2] === toChIndex) {
|
|
this._lineTokens = EMPTY_LINE_TOKENS;
|
|
return;
|
|
}
|
|
|
|
const fromTokenIndex = LineTokens.findIndexInTokensArray(tokens, fromChIndex);
|
|
const fromTokenStartOffset = (fromTokenIndex > 0 ? tokens[(fromTokenIndex - 1) << 1] : 0);
|
|
const fromTokenEndOffset = tokens[fromTokenIndex << 1];
|
|
|
|
if (toChIndex < fromTokenEndOffset) {
|
|
// the delete range is inside a single token
|
|
const delta = (toChIndex - fromChIndex);
|
|
for (let i = fromTokenIndex; i < tokensCount; i++) {
|
|
tokens[i << 1] -= delta;
|
|
}
|
|
return;
|
|
}
|
|
|
|
let dest: number;
|
|
let lastEnd: number;
|
|
if (fromTokenStartOffset !== fromChIndex) {
|
|
tokens[fromTokenIndex << 1] = fromChIndex;
|
|
dest = ((fromTokenIndex + 1) << 1);
|
|
lastEnd = fromChIndex;
|
|
} else {
|
|
dest = (fromTokenIndex << 1);
|
|
lastEnd = fromTokenStartOffset;
|
|
}
|
|
|
|
const delta = (toChIndex - fromChIndex);
|
|
for (let tokenIndex = fromTokenIndex + 1; tokenIndex < tokensCount; tokenIndex++) {
|
|
const tokenEndOffset = tokens[tokenIndex << 1] - delta;
|
|
if (tokenEndOffset > lastEnd) {
|
|
tokens[dest++] = tokenEndOffset;
|
|
tokens[dest++] = tokens[(tokenIndex << 1) + 1];
|
|
lastEnd = tokenEndOffset;
|
|
}
|
|
}
|
|
|
|
if (dest === tokens.length) {
|
|
// nothing to trim
|
|
return;
|
|
}
|
|
|
|
let tmp = new Uint32Array(dest);
|
|
tmp.set(tokens.subarray(0, dest), 0);
|
|
this._lineTokens = tmp.buffer;
|
|
}
|
|
|
|
public append(_otherTokens: ArrayBuffer): void {
|
|
if (_otherTokens === EMPTY_LINE_TOKENS) {
|
|
return;
|
|
}
|
|
if (this._lineTokens === EMPTY_LINE_TOKENS) {
|
|
this._lineTokens = _otherTokens;
|
|
return;
|
|
}
|
|
if (this._lineTokens === null) {
|
|
return;
|
|
}
|
|
if (_otherTokens === null) {
|
|
// cannot determine combined line length...
|
|
this._lineTokens = null;
|
|
return;
|
|
}
|
|
const myTokens = new Uint32Array(this._lineTokens);
|
|
const otherTokens = new Uint32Array(_otherTokens);
|
|
const otherTokensCount = (otherTokens.length >>> 1);
|
|
|
|
let result = new Uint32Array(myTokens.length + otherTokens.length);
|
|
result.set(myTokens, 0);
|
|
let dest = myTokens.length;
|
|
const delta = myTokens[myTokens.length - 2];
|
|
for (let i = 0; i < otherTokensCount; i++) {
|
|
result[dest++] = otherTokens[(i << 1)] + delta;
|
|
result[dest++] = otherTokens[(i << 1) + 1];
|
|
}
|
|
this._lineTokens = result.buffer;
|
|
}
|
|
|
|
public insert(chIndex: number, textLength: number): void {
|
|
if (!this._lineTokens) {
|
|
// nothing to do
|
|
return;
|
|
}
|
|
|
|
const tokens = new Uint32Array(this._lineTokens);
|
|
const tokensCount = (tokens.length >>> 1);
|
|
|
|
let fromTokenIndex = LineTokens.findIndexInTokensArray(tokens, chIndex);
|
|
if (fromTokenIndex > 0) {
|
|
const fromTokenStartOffset = (fromTokenIndex > 0 ? tokens[(fromTokenIndex - 1) << 1] : 0);
|
|
if (fromTokenStartOffset === chIndex) {
|
|
fromTokenIndex--;
|
|
}
|
|
}
|
|
for (let tokenIndex = fromTokenIndex; tokenIndex < tokensCount; tokenIndex++) {
|
|
tokens[tokenIndex << 1] += textLength;
|
|
}
|
|
}
|
|
}
|
|
|
|
export class ModelLinesTokens {
|
|
|
|
public readonly languageIdentifier: LanguageIdentifier;
|
|
public readonly tokenizationSupport: ITokenizationSupport;
|
|
private _tokens: ModelLineTokens[];
|
|
private _invalidLineStartIndex: number;
|
|
private _lastState: IState;
|
|
|
|
constructor(languageIdentifier: LanguageIdentifier, tokenizationSupport: ITokenizationSupport) {
|
|
this.languageIdentifier = languageIdentifier;
|
|
this.tokenizationSupport = tokenizationSupport;
|
|
this._tokens = [];
|
|
if (this.tokenizationSupport) {
|
|
let initialState: IState = null;
|
|
try {
|
|
initialState = this.tokenizationSupport.getInitialState();
|
|
} catch (e) {
|
|
onUnexpectedError(e);
|
|
this.tokenizationSupport = null;
|
|
}
|
|
|
|
if (initialState) {
|
|
this._tokens[0] = new ModelLineTokens(initialState);
|
|
}
|
|
}
|
|
|
|
this._invalidLineStartIndex = 0;
|
|
this._lastState = null;
|
|
}
|
|
|
|
public get inValidLineStartIndex() {
|
|
return this._invalidLineStartIndex;
|
|
}
|
|
|
|
public getTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineText: string): LineTokens {
|
|
let rawLineTokens: ArrayBuffer = null;
|
|
if (lineIndex < this._tokens.length && this._tokens[lineIndex]) {
|
|
rawLineTokens = this._tokens[lineIndex]._lineTokens;
|
|
}
|
|
|
|
if (rawLineTokens !== null && rawLineTokens !== EMPTY_LINE_TOKENS) {
|
|
return new LineTokens(new Uint32Array(rawLineTokens), lineText);
|
|
}
|
|
|
|
let lineTokens = new Uint32Array(2);
|
|
lineTokens[0] = lineText.length;
|
|
lineTokens[1] = getDefaultMetadata(topLevelLanguageId);
|
|
return new LineTokens(lineTokens, lineText);
|
|
}
|
|
|
|
public isCheapToTokenize(lineNumber: number): boolean {
|
|
const firstInvalidLineNumber = this._invalidLineStartIndex + 1;
|
|
return (firstInvalidLineNumber >= lineNumber);
|
|
}
|
|
|
|
public hasLinesToTokenize(buffer: ITextBuffer): boolean {
|
|
return (this._invalidLineStartIndex < buffer.getLineCount());
|
|
}
|
|
|
|
public invalidateLine(lineIndex: number): void {
|
|
this._setIsInvalid(lineIndex, true);
|
|
if (lineIndex < this._invalidLineStartIndex) {
|
|
this._setIsInvalid(this._invalidLineStartIndex, true);
|
|
this._invalidLineStartIndex = lineIndex;
|
|
}
|
|
}
|
|
|
|
_setIsInvalid(lineIndex: number, invalid: boolean): void {
|
|
if (lineIndex < this._tokens.length && this._tokens[lineIndex]) {
|
|
this._tokens[lineIndex]._invalid = invalid;
|
|
}
|
|
}
|
|
|
|
_isInvalid(lineIndex: number): boolean {
|
|
if (lineIndex < this._tokens.length && this._tokens[lineIndex]) {
|
|
return this._tokens[lineIndex]._invalid;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
_getState(lineIndex: number): IState {
|
|
if (lineIndex < this._tokens.length && this._tokens[lineIndex]) {
|
|
return this._tokens[lineIndex]._state;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
_setTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineTextLength: number, tokens: Uint32Array): void {
|
|
let target: ModelLineTokens;
|
|
if (lineIndex < this._tokens.length && this._tokens[lineIndex]) {
|
|
target = this._tokens[lineIndex];
|
|
} else {
|
|
target = new ModelLineTokens(null);
|
|
this._tokens[lineIndex] = target;
|
|
}
|
|
|
|
if (lineTextLength === 0) {
|
|
target._lineTokens = EMPTY_LINE_TOKENS;
|
|
return;
|
|
}
|
|
|
|
if (!tokens || tokens.length === 0) {
|
|
tokens = new Uint32Array(2);
|
|
tokens[0] = 0;
|
|
tokens[1] = getDefaultMetadata(topLevelLanguageId);
|
|
}
|
|
|
|
LineTokens.convertToEndOffset(tokens, lineTextLength);
|
|
|
|
target._lineTokens = tokens.buffer;
|
|
}
|
|
|
|
_setState(lineIndex: number, state: IState): void {
|
|
if (lineIndex < this._tokens.length && this._tokens[lineIndex]) {
|
|
this._tokens[lineIndex]._state = state;
|
|
} else {
|
|
const tmp = new ModelLineTokens(state);
|
|
this._tokens[lineIndex] = tmp;
|
|
}
|
|
}
|
|
|
|
//#region Editing
|
|
|
|
public applyEdits(range: Range, eolCount: number, firstLineLength: number): void {
|
|
|
|
const deletingLinesCnt = range.endLineNumber - range.startLineNumber;
|
|
const insertingLinesCnt = eolCount;
|
|
const editingLinesCnt = Math.min(deletingLinesCnt, insertingLinesCnt);
|
|
|
|
for (let j = editingLinesCnt; j >= 0; j--) {
|
|
this.invalidateLine(range.startLineNumber + j - 1);
|
|
}
|
|
|
|
this._acceptDeleteRange(range);
|
|
this._acceptInsertText(new Position(range.startLineNumber, range.startColumn), eolCount, firstLineLength);
|
|
}
|
|
|
|
private _acceptDeleteRange(range: Range): void {
|
|
|
|
const firstLineIndex = range.startLineNumber - 1;
|
|
if (firstLineIndex >= this._tokens.length) {
|
|
return;
|
|
}
|
|
|
|
if (range.startLineNumber === range.endLineNumber) {
|
|
if (range.startColumn === range.endColumn) {
|
|
// Nothing to delete
|
|
return;
|
|
}
|
|
|
|
this._tokens[firstLineIndex].delete(range.startColumn - 1, range.endColumn - 1);
|
|
return;
|
|
}
|
|
|
|
const firstLine = this._tokens[firstLineIndex];
|
|
firstLine.deleteEnding(range.startColumn - 1);
|
|
|
|
const lastLineIndex = range.endLineNumber - 1;
|
|
let lastLineTokens: ArrayBuffer = null;
|
|
if (lastLineIndex < this._tokens.length) {
|
|
const lastLine = this._tokens[lastLineIndex];
|
|
lastLine.deleteBeginning(range.endColumn - 1);
|
|
lastLineTokens = lastLine._lineTokens;
|
|
}
|
|
|
|
// Take remaining text on last line and append it to remaining text on first line
|
|
firstLine.append(lastLineTokens);
|
|
|
|
// Delete middle lines
|
|
this._tokens.splice(range.startLineNumber, range.endLineNumber - range.startLineNumber);
|
|
}
|
|
|
|
private _acceptInsertText(position: Position, eolCount: number, firstLineLength: number): void {
|
|
|
|
if (eolCount === 0 && firstLineLength === 0) {
|
|
// Nothing to insert
|
|
return;
|
|
}
|
|
|
|
const lineIndex = position.lineNumber - 1;
|
|
if (lineIndex >= this._tokens.length) {
|
|
return;
|
|
}
|
|
|
|
if (eolCount === 0) {
|
|
// Inserting text on one line
|
|
this._tokens[lineIndex].insert(position.column - 1, firstLineLength);
|
|
return;
|
|
}
|
|
|
|
const line = this._tokens[lineIndex];
|
|
line.deleteEnding(position.column - 1);
|
|
line.insert(position.column - 1, firstLineLength);
|
|
|
|
let insert: ModelLineTokens[] = new Array<ModelLineTokens>(eolCount);
|
|
for (let i = eolCount - 1; i >= 0; i--) {
|
|
insert[i] = new ModelLineTokens(null);
|
|
}
|
|
this._tokens = arrays.arrayInsert(this._tokens, position.lineNumber, insert);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Tokenization
|
|
|
|
public _tokenizeOneLine(buffer: ITextBuffer, eventBuilder: ModelTokensChangedEventBuilder): number {
|
|
if (!this.hasLinesToTokenize(buffer)) {
|
|
return buffer.getLineCount() + 1;
|
|
}
|
|
const lineNumber = this._invalidLineStartIndex + 1;
|
|
this._updateTokensUntilLine(buffer, eventBuilder, lineNumber);
|
|
return lineNumber;
|
|
}
|
|
|
|
public _tokenizeText(buffer: ITextBuffer, text: string, state: IState): TokenizationResult2 {
|
|
let r: TokenizationResult2 = null;
|
|
|
|
try {
|
|
r = this.tokenizationSupport.tokenize2(text, state, 0);
|
|
} catch (e) {
|
|
onUnexpectedError(e);
|
|
}
|
|
|
|
if (!r) {
|
|
r = nullTokenize2(this.languageIdentifier.id, text, state, 0);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
public _updateTokensUntilLine(buffer: ITextBuffer, eventBuilder: ModelTokensChangedEventBuilder, lineNumber: number): void {
|
|
if (!this.tokenizationSupport) {
|
|
this._invalidLineStartIndex = buffer.getLineCount();
|
|
return;
|
|
}
|
|
|
|
const linesLength = buffer.getLineCount();
|
|
const endLineIndex = lineNumber - 1;
|
|
|
|
// Validate all states up to and including endLineIndex
|
|
for (let lineIndex = this._invalidLineStartIndex; lineIndex <= endLineIndex; lineIndex++) {
|
|
const endStateIndex = lineIndex + 1;
|
|
let r: TokenizationResult2 = null;
|
|
const text = buffer.getLineContent(lineIndex + 1);
|
|
|
|
try {
|
|
// Tokenize only the first X characters
|
|
let freshState = this._getState(lineIndex).clone();
|
|
r = this.tokenizationSupport.tokenize2(text, freshState, 0);
|
|
} catch (e) {
|
|
onUnexpectedError(e);
|
|
}
|
|
|
|
if (!r) {
|
|
r = nullTokenize2(this.languageIdentifier.id, text, this._getState(lineIndex), 0);
|
|
}
|
|
this._setTokens(this.languageIdentifier.id, lineIndex, text.length, r.tokens);
|
|
eventBuilder.registerChangedTokens(lineIndex + 1);
|
|
this._setIsInvalid(lineIndex, false);
|
|
|
|
if (endStateIndex < linesLength) {
|
|
if (this._getState(endStateIndex) !== null && r.endState.equals(this._getState(endStateIndex))) {
|
|
// The end state of this line remains the same
|
|
let nextInvalidLineIndex = lineIndex + 1;
|
|
while (nextInvalidLineIndex < linesLength) {
|
|
if (this._isInvalid(nextInvalidLineIndex)) {
|
|
break;
|
|
}
|
|
if (nextInvalidLineIndex + 1 < linesLength) {
|
|
if (this._getState(nextInvalidLineIndex + 1) === null) {
|
|
break;
|
|
}
|
|
} else {
|
|
if (this._lastState === null) {
|
|
break;
|
|
}
|
|
}
|
|
nextInvalidLineIndex++;
|
|
}
|
|
this._invalidLineStartIndex = Math.max(this._invalidLineStartIndex, nextInvalidLineIndex);
|
|
lineIndex = nextInvalidLineIndex - 1; // -1 because the outer loop increments it
|
|
} else {
|
|
this._setState(endStateIndex, r.endState);
|
|
}
|
|
} else {
|
|
this._lastState = r.endState;
|
|
}
|
|
}
|
|
this._invalidLineStartIndex = Math.max(this._invalidLineStartIndex, endLineIndex + 1);
|
|
}
|
|
|
|
// #endregion
|
|
}
|
|
|
|
export class ModelTokensChangedEventBuilder {
|
|
|
|
private _ranges: { fromLineNumber: number; toLineNumber: number; }[];
|
|
|
|
constructor() {
|
|
this._ranges = [];
|
|
}
|
|
|
|
public registerChangedTokens(lineNumber: number): void {
|
|
const ranges = this._ranges;
|
|
const rangesLength = ranges.length;
|
|
const previousRange = rangesLength > 0 ? ranges[rangesLength - 1] : null;
|
|
|
|
if (previousRange && previousRange.toLineNumber === lineNumber - 1) {
|
|
// extend previous range
|
|
previousRange.toLineNumber++;
|
|
} else {
|
|
// insert new range
|
|
ranges[rangesLength] = {
|
|
fromLineNumber: lineNumber,
|
|
toLineNumber: lineNumber
|
|
};
|
|
}
|
|
}
|
|
|
|
public build(): IModelTokensChangedEvent {
|
|
if (this._ranges.length === 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
ranges: this._ranges
|
|
};
|
|
}
|
|
}
|