diff --git a/src/vs/editor/common/model/mirrorTextModel.ts b/src/vs/editor/common/model/mirrorTextModel.ts index 4fefa551a1a..9ff680f395f 100644 --- a/src/vs/editor/common/model/mirrorTextModel.ts +++ b/src/vs/editor/common/model/mirrorTextModel.ts @@ -106,7 +106,7 @@ export class MirrorTextModel implements IMirrorTextModel { this._lines[lineIndex] = newValue; if (this._lineStarts) { // update prefix sum - this._lineStarts.changeValue(lineIndex, this._lines[lineIndex].length + this._eol.length); + this._lineStarts.setValue(lineIndex, this._lines[lineIndex].length + this._eol.length); } } diff --git a/src/vs/editor/common/viewModel/prefixSumComputer.ts b/src/vs/editor/common/viewModel/prefixSumComputer.ts index c64f57195b9..52d6f316dcf 100644 --- a/src/vs/editor/common/viewModel/prefixSumComputer.ts +++ b/src/vs/editor/common/viewModel/prefixSumComputer.ts @@ -3,20 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { arrayInsert } from 'vs/base/common/arrays'; import { toUint32 } from 'vs/base/common/uint'; -export class PrefixSumIndexOfResult { - _prefixSumIndexOfResultBrand: void = undefined; - - index: number; - remainder: number; - - constructor(index: number, remainder: number) { - this.index = index; - this.remainder = remainder; - } -} - export class PrefixSumComputer { /** @@ -71,7 +60,7 @@ export class PrefixSumComputer { return true; } - public changeValue(index: number, value: number): boolean { + public setValue(index: number, value: number): boolean { index = toUint32(index); value = toUint32(value); @@ -187,3 +176,119 @@ export class PrefixSumComputer { return new PrefixSumIndexOfResult(mid, sum - midStart); } } + +/** + * {@link getIndexOf} has an amortized runtime complexity of O(1). + * + * ({@link PrefixSumComputer.getIndexOf} is just O(log n)) +*/ +export class ConstantTimePrefixSumComputer { + private _values: number[]; + private _isValid: boolean; + private _validEndIndex: number; + + /** + * _prefixSum[i] = SUM(values[j]), 0 <= j <= i + */ + private _prefixSum: number[]; + + /** + * _indexBySum[sum] = idx => _prefixSum[idx - 1] <= sum < _prefixSum[idx] + */ + private _indexBySum: number[]; + + constructor(values: number[]) { + this._values = values; + this._isValid = false; + this._validEndIndex = -1; + this._prefixSum = []; + this._indexBySum = []; + } + + /** + * @returns SUM(0 <= j < values.length, values[j]) + */ + public getTotalSum(): number { + this._ensureValid(); + return this._indexBySum.length; + } + + /** + * @returns `SUM(0 <= j <= index, values[j])`. Includes `values[index]`! + */ + public getPrefixSum(index: number): number { + this._ensureValid(); + return this._prefixSum[index]; + } + + /** + * @returns `result`, such that `getPrefixSum(result.index - 1) + result.remainder = sum` + */ + public getIndexOf(sum: number): PrefixSumIndexOfResult { + this._ensureValid(); + const idx = this._indexBySum[sum]; + const viewLinesAbove = idx > 0 ? this._prefixSum[idx - 1] : 0; + return new PrefixSumIndexOfResult(idx, sum - viewLinesAbove); + } + + public removeValues(start: number, deleteCount: number): void { + this._values.splice(start, deleteCount); + this._invalidate(start); + } + + public insertValues(insertIndex: number, insertArr: number[]): void { + this._values = arrayInsert(this._values, insertIndex, insertArr); + this._invalidate(insertIndex); + } + + private _invalidate(index: number): void { + this._isValid = false; + this._validEndIndex = Math.min(this._validEndIndex, index - 1); + } + + private _ensureValid(): void { + if (this._isValid) { + return; + } + + for (let i = this._validEndIndex + 1, len = this._values.length; i < len; i++) { + const value = this._values[i]; + const sumAbove = i > 0 ? this._prefixSum[i - 1] : 0; + + this._prefixSum[i] = sumAbove + value; + for (let j = 0; j < value; j++) { + this._indexBySum[sumAbove + j] = i; + } + } + + // trim things + this._prefixSum.length = this._values.length; + this._indexBySum.length = this._prefixSum[this._prefixSum.length - 1]; + + // mark as valid + this._isValid = true; + this._validEndIndex = this._values.length - 1; + } + + public setValue(index: number, value: number): void { + if (this._values[index] === value) { + // no change + return; + } + this._values[index] = value; + this._invalidate(index); + } +} + + +export class PrefixSumIndexOfResult { + _prefixSumIndexOfResultBrand: void = undefined; + + constructor( + public readonly index: number, + public readonly remainder: number + ) { + this.index = index; + this.remainder = remainder; + } +} diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 6fb158d78a3..e24e889f1be 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -11,11 +11,11 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { BracketGuideOptions, EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, IndentGuide, IndentGuideHorizontalLine, ITextModel, PositionAffinity } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer'; import { ICoordinatesConverter, InjectedText, ILineBreaksComputer, LineBreakData, SingleLineInlineDecoration, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { IDisposable } from 'vs/base/common/lifecycle'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { LineInjectedText } from 'vs/editor/common/model/textModelEvents'; +import { ConstantTimePrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; export interface ILineBreaksComputerFactory { createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent): ILineBreaksComputer; @@ -144,89 +144,6 @@ const enum IndentGuideRepeatOption { BlockAll = 2 } -class LineNumberMapper { - - private _counts: number[]; - private _isValid: boolean; - private _validEndIndex: number; - - private _modelToView: number[]; - private _viewToModel: number[]; - - constructor(viewLineCounts: number[]) { - this._counts = viewLineCounts; - this._isValid = false; - this._validEndIndex = -1; - this._modelToView = []; - this._viewToModel = []; - } - - private _invalidate(index: number): void { - this._isValid = false; - this._validEndIndex = Math.min(this._validEndIndex, index - 1); - } - - private _ensureValid(): void { - if (this._isValid) { - return; - } - - for (let i = this._validEndIndex + 1, len = this._counts.length; i < len; i++) { - const viewLineCount = this._counts[i]; - const viewLinesAbove = (i > 0 ? this._modelToView[i - 1] : 0); - - this._modelToView[i] = viewLinesAbove + viewLineCount; - for (let j = 0; j < viewLineCount; j++) { - this._viewToModel[viewLinesAbove + j] = i; - } - } - - // trim things - this._modelToView.length = this._counts.length; - this._viewToModel.length = this._modelToView[this._modelToView.length - 1]; - - // mark as valid - this._isValid = true; - this._validEndIndex = this._counts.length - 1; - } - - public changeValue(index: number, value: number): void { - if (this._counts[index] === value) { - // no change - return; - } - this._counts[index] = value; - this._invalidate(index); - } - - public removeValues(start: number, deleteCount: number): void { - this._counts.splice(start, deleteCount); - this._invalidate(start); - } - - public insertValues(insertIndex: number, insertArr: number[]): void { - this._counts = arrays.arrayInsert(this._counts, insertIndex, insertArr); - this._invalidate(insertIndex); - } - - public getTotalValue(): number { - this._ensureValid(); - return this._viewToModel.length; - } - - public getAccumulatedValue(index: number): number { - this._ensureValid(); - return this._modelToView[index]; - } - - public getIndexOf(accumulatedValue: number): PrefixSumIndexOfResult { - this._ensureValid(); - const modelLineIndex = this._viewToModel[accumulatedValue]; - const viewLinesAbove = (modelLineIndex > 0 ? this._modelToView[modelLineIndex - 1] : 0); - return new PrefixSumIndexOfResult(modelLineIndex, accumulatedValue - viewLinesAbove); - } -} - export class SplitLinesCollection implements IViewModelLinesCollection { private readonly _editorId: number; @@ -243,7 +160,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { private wrappingStrategy: 'simple' | 'advanced'; private lines!: ISplitLine[]; - private prefixSumComputer!: LineNumberMapper; + private prefixSumComputer!: ConstantTimePrefixSumComputer; private hiddenAreasIds!: string[]; @@ -324,7 +241,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { this._validModelVersionId = this.model.getVersionId(); - this.prefixSumComputer = new LineNumberMapper(values); + this.prefixSumComputer = new ConstantTimePrefixSumComputer(values); } public getHiddenAreas(): Range[] { @@ -422,7 +339,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } if (lineChanged) { let newOutputLineCount = this.lines[i].getViewLineCount(); - this.prefixSumComputer.changeValue(i, newOutputLineCount); + this.prefixSumComputer.setValue(i, newOutputLineCount); } } @@ -510,8 +427,8 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return null; } - let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1); - let outputToLineNumber = this.prefixSumComputer.getAccumulatedValue(toLineNumber - 1); + let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getPrefixSum(fromLineNumber - 2) + 1); + let outputToLineNumber = this.prefixSumComputer.getPrefixSum(toLineNumber - 1); this.lines.splice(fromLineNumber - 1, toLineNumber - fromLineNumber + 1); this.prefixSumComputer.removeValues(fromLineNumber - 1, toLineNumber - fromLineNumber + 1); @@ -529,7 +446,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { // cannot use this.getHiddenAreas() because those decorations have already seen the effect of this model change const isInHiddenArea = (fromLineNumber > 2 && !this.lines[fromLineNumber - 2].isVisible()); - let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1); + let outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getPrefixSum(fromLineNumber - 2) + 1); let totalOutputLineCount = 0; let insertLines: ISplitLine[] = []; @@ -576,23 +493,23 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let deleteTo = -1; if (oldOutputLineCount > newOutputLineCount) { - changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(lineNumber - 2) + 1); + changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getPrefixSum(lineNumber - 2) + 1); changeTo = changeFrom + newOutputLineCount - 1; deleteFrom = changeTo + 1; deleteTo = deleteFrom + (oldOutputLineCount - newOutputLineCount) - 1; lineMappingChanged = true; } else if (oldOutputLineCount < newOutputLineCount) { - changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(lineNumber - 2) + 1); + changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getPrefixSum(lineNumber - 2) + 1); changeTo = changeFrom + oldOutputLineCount - 1; insertFrom = changeTo + 1; insertTo = insertFrom + (newOutputLineCount - oldOutputLineCount) - 1; lineMappingChanged = true; } else { - changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(lineNumber - 2) + 1); + changeFrom = (lineNumber === 1 ? 1 : this.prefixSumComputer.getPrefixSum(lineNumber - 2) + 1); changeTo = changeFrom + newOutputLineCount - 1; } - this.prefixSumComputer.changeValue(lineIndex, newOutputLineCount); + this.prefixSumComputer.setValue(lineIndex, newOutputLineCount); const viewLinesChangedEvent = (changeFrom <= changeTo ? new viewEvents.ViewLinesChangedEvent(changeFrom, changeTo) : null); const viewLinesInsertedEvent = (insertFrom <= insertTo ? new viewEvents.ViewLinesInsertedEvent(insertFrom, insertTo) : null); @@ -610,7 +527,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineCount(): number { - return this.prefixSumComputer.getTotalValue(); + return this.prefixSumComputer.getTotalSum(); } private _toValidViewLineNumber(viewLineNumber: number): number { @@ -1000,7 +917,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { // console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + 1 + ',' + 1); return new Position(1, 1); } - const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getPrefixSum(lineIndex - 1)); let r: Position; if (lineIndexChanged) { @@ -1031,7 +948,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let lineIndex = inputLineNumber - 1; if (this.lines[lineIndex].isVisible()) { // this model line is visible - const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getPrefixSum(lineIndex - 1)); return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, inputColumn); } @@ -1043,7 +960,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { // Could not reach a real line return 1; } - const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getPrefixSum(lineIndex - 1)); return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); } diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index 0ac5ed2672d..f8dfc3eceec 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -58,7 +58,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 2, 2, 1, 3] - psc.changeValue(1, 2); + psc.setValue(1, 2); assert.strictEqual(psc.getTotalSum(), 9); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 3); @@ -67,7 +67,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(psc.getPrefixSum(4), 9); // [1, 0, 2, 1, 3] - psc.changeValue(1, 0); + psc.setValue(1, 0); assert.strictEqual(psc.getTotalSum(), 7); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 1); @@ -100,7 +100,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 1, 3] - psc.changeValue(2, 0); + psc.setValue(2, 0); assert.strictEqual(psc.getTotalSum(), 5); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 1); @@ -127,7 +127,7 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 0, 0, 0, 3] - psc.changeValue(3, 0); + psc.setValue(3, 0); assert.strictEqual(psc.getTotalSum(), 4); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 1); @@ -151,9 +151,9 @@ suite('Editor ViewModel - PrefixSumComputer', () => { assert.strictEqual(indexOfResult.remainder, 3); // [1, 1, 0, 1, 1] - psc.changeValue(1, 1); - psc.changeValue(3, 1); - psc.changeValue(4, 1); + psc.setValue(1, 1); + psc.setValue(3, 1); + psc.setValue(4, 1); assert.strictEqual(psc.getTotalSum(), 4); assert.strictEqual(psc.getPrefixSum(0), 1); assert.strictEqual(psc.getPrefixSum(1), 2); diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts index bf2bbb30e97..aa0d9c70151 100644 --- a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts @@ -42,8 +42,8 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD this._disposables.add(extHostDocuments.onDidChangeDocument(e => { const cellIdx = this._cellUris.get(e.document.uri); if (cellIdx !== undefined) { - this._cellLengths.changeValue(cellIdx, this._cells[cellIdx].document.getText().length + 1); - this._cellLines.changeValue(cellIdx, this._cells[cellIdx].document.lineCount); + this._cellLengths.setValue(cellIdx, this._cells[cellIdx].document.getText().length + 1); + this._cellLines.setValue(cellIdx, this._cells[cellIdx].document.lineCount); this._versionId += 1; this._onDidChange.fire(undefined); } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts index 1d2e892001c..771b9a3c060 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts @@ -119,7 +119,7 @@ export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCe this._ensureOutputsTop(); this._outputCollection[index] = height; - if (this._outputsTop!.changeValue(index, height)) { + if (this._outputsTop!.setValue(index, height)) { this._onDidChangeOutputLayout.fire(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index eadd68541a7..6cfcdb12f91 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -383,7 +383,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } this._outputCollection[index] = height; - if (this._outputsTop!.changeValue(index, height)) { + if (this._outputsTop!.setValue(index, height)) { this.layoutChange({ outputHeight: true }, source); } }