diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 7421ac40d40..5a14b2d4bec 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -54,6 +54,10 @@ export interface IListViewOptionsUpdate { readonly horizontalScrolling?: boolean; } +export interface IListViewRangeMapProvider { + (): RangeMap; +} + export interface IListViewOptions extends IListViewOptionsUpdate { readonly dnd?: IListViewDragAndDrop; readonly useShadows?: boolean; @@ -64,6 +68,7 @@ export interface IListViewOptions extends IListViewOptionsUpdate { readonly mouseSupport?: boolean; readonly accessibilityProvider?: IListViewAccessibilityProvider; readonly transformOptimization?: boolean; + readonly rangeMapProvider?: IListViewRangeMapProvider; } const DefaultOptions = { @@ -209,6 +214,7 @@ export class ListView implements ISpliceable, IDisposable { private items: IItem[]; private itemId: number; private rangeMap: RangeMap; + private rangeMapProvider: IListViewRangeMapProvider; private cache: RowCache; private renderers = new Map>(); private lastRenderTop: number; @@ -289,7 +295,8 @@ export class ListView implements ISpliceable, IDisposable { this.items = []; this.itemId = 0; - this.rangeMap = new RangeMap(); + this.rangeMapProvider = getOrDefault(options, o => o.rangeMapProvider, () => new RangeMap()); + this.rangeMap = this.rangeMapProvider(); for (const renderer of renderers) { this.renderers.set(renderer.templateId, renderer); @@ -469,7 +476,7 @@ export class ListView implements ISpliceable, IDisposable { // TODO@joao: improve this optimization to catch even more cases if (start === 0 && deleteCount >= this.items.length) { - this.rangeMap = new RangeMap(); + this.rangeMap = this.rangeMapProvider(); this.rangeMap.splice(0, 0, inserted); this.items = inserted; deleted = []; diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 252150b28d7..e5fa1ebe7b9 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -16,7 +16,7 @@ import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardE import { Event, Emitter, EventBufferer } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListError, IKeyboardNavigationDelegate } from './list'; -import { ListView, IListViewOptions, IListViewDragAndDrop, IListViewAccessibilityProvider, IListViewOptionsUpdate } from './listView'; +import { ListView, IListViewOptions, IListViewDragAndDrop, IListViewAccessibilityProvider, IListViewOptionsUpdate, IListViewRangeMapProvider } from './listView'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable'; @@ -854,6 +854,7 @@ export interface IListOptions { readonly horizontalScrolling?: boolean; readonly additionalScrollHeight?: number; readonly transformOptimization?: boolean; + readonly rangeMapProvider?: IListViewRangeMapProvider; readonly smoothScrolling?: boolean; } diff --git a/src/vs/base/browser/ui/list/rangeMap.ts b/src/vs/base/browser/ui/list/rangeMap.ts index d6764a778b7..e9360d1173f 100644 --- a/src/vs/base/browser/ui/list/rangeMap.ts +++ b/src/vs/base/browser/ui/list/rangeMap.ts @@ -83,25 +83,13 @@ export function consolidate(groups: IRangedGroup[]): IRangedGroup[] { * Concatenates several collections of ranged groups into a single * collection. */ -function concat(...groups: IRangedGroup[][]): IRangedGroup[] { +export function concat(...groups: IRangedGroup[][]): IRangedGroup[] { return consolidate(groups.reduce((r, g) => r.concat(g), [])); } -export class ListWhitespace { - - constructor( - public afterIndex: number, - public height: number, - // height of all whitespaces before this whitespace (inclusive) - public prefixSum: number - ) { } -} - -// [ { start: 0, len: 2, size: 2 }, { start: 2, len: 1, size: 3 }, {} ] export class RangeMap { private groups: IRangedGroup[] = []; - private whitespaces: ListWhitespace[] = []; private _size = 0; splice(index: number, deleteCount: number, items: IItem[] = []): void { @@ -117,86 +105,6 @@ export class RangeMap { this.groups = concat(before, middle, after); this._size = this.groups.reduce((t, g) => t + (g.size * (g.range.end - g.range.start)), 0); - - const deleteRange = deleteCount > 0 ? [index, index + deleteCount - 1] : []; - const indexDelta = items.length - deleteCount; - let prefixSumDelta = 0; - const pendingRemovalWhitespace: number[] = []; - for (let i = 0; i < this.whitespaces.length; i++) { - const whitespace = this.whitespaces[i]; - - if (whitespace.afterIndex < index) { - continue; - } else if (deleteRange.length > 0 && whitespace.afterIndex >= deleteRange[0] && whitespace.afterIndex <= deleteRange[1]) { - // should be deleted - pendingRemovalWhitespace.push(i); - prefixSumDelta += whitespace.height; - } else { - whitespace.afterIndex += indexDelta; - whitespace.prefixSum -= prefixSumDelta; - } - } - - pendingRemovalWhitespace.reverse().forEach(index => { - this.whitespaces.splice(index, 1); - }); - } - - public static findInsertionIndex(arr: ListWhitespace[], afterIndex: number): number { - let low = 0; - let high = arr.length; - - while (low < high) { - const mid = ((low + high) >>> 1); - - if (afterIndex === arr[mid].afterIndex) { - low = mid; - break; - } else if (afterIndex < arr[mid].afterIndex) { - high = mid; - } else { - low = mid + 1; - } - } - - return low; - } - - insertWhitespace(afterIndex: number, height: number) { - // TODO - // 2. delay prefix sum update - const insertIndex = RangeMap.findInsertionIndex(this.whitespaces, afterIndex); - const prefixSum = insertIndex > 0 ? this.whitespaces[insertIndex - 1].prefixSum + height : height; - const insertedItem = new ListWhitespace(afterIndex, height, prefixSum); - this.whitespaces.splice(insertIndex, 0, insertedItem); - - for (let i = insertIndex + 1; i < this.whitespaces.length; i++) { - this.whitespaces[i].prefixSum += height; - } - } - - // todo, allow multiple whitespaces after one index - updateWhitespace(afterIndex: number, newHeight: number) { - let delta = 0; - let findWhitespace = false; - for (let i = 0; i < this.whitespaces.length; i++) { - if (this.whitespaces[i].afterIndex === afterIndex) { - delta = newHeight - this.whitespaces[i].height; - this.whitespaces[i].height = newHeight; - this.whitespaces[i].prefixSum += delta; - findWhitespace = true; - } else if (this.whitespaces[i].afterIndex > afterIndex) { - if (!findWhitespace) { - this.insertWhitespace(afterIndex, newHeight); - return; - } - this.whitespaces[i].prefixSum += delta; - } - } - - if (!findWhitespace) { - this.insertWhitespace(afterIndex, newHeight); - } } /** @@ -216,42 +124,7 @@ export class RangeMap { * Returns the sum of the sizes of all items in the range map. */ get size(): number { - return this._size - + (this.whitespaces.length ? this.whitespaces[this.whitespaces.length - 1]?.prefixSum : 0); - } - - private _getWhitespaceAccumulatedHeightBeforeIndex(index: number): number { - const lastWhitespaceBeforeLineNumber = this._findLastWhitespaceBeforeIndex(index); - - if (lastWhitespaceBeforeLineNumber === -1) { - return 0; - } - - return this.whitespaces[lastWhitespaceBeforeLineNumber].prefixSum; - } - - private _findLastWhitespaceBeforeIndex(index: number): number { - const arr = this.whitespaces; - let low = 0; - let high = arr.length - 1; - - while (low <= high) { - const delta = (high - low) | 0; - const halfDelta = (delta / 2) | 0; - const mid = (low + halfDelta) | 0; - - if (arr[mid].afterIndex < index) { - if (mid + 1 >= arr.length || arr[mid + 1].afterIndex >= index) { - return mid; - } else { - low = (mid + 1) | 0; - } - } else { - high = (mid - 1) | 0; - } - } - - return -1; + return this._size; } /** @@ -269,20 +142,7 @@ export class RangeMap { const count = group.range.end - group.range.start; const newSize = size + (count * group.size); - if (position < newSize + this._getWhitespaceAccumulatedHeightBeforeIndex(group.range.end + 1)) { - // try to find the right index - let currSize = size; - // position > currSize + all whitespaces before current range - for (let j = group.range.start; j < group.range.end; j++) { - currSize = currSize + group.size; - - if (position >= currSize + this._getWhitespaceAccumulatedHeightBeforeIndex(j + 1)) { - continue; - } else { - return j; - } - } - + if (position < newSize) { return index + Math.floor((position - size) / group.size); } @@ -317,7 +177,7 @@ export class RangeMap { const newCount = count + groupCount; if (index < newCount) { - return position + ((index - count) * group.size) + this._getWhitespaceAccumulatedHeightBeforeIndex(index); + return position + ((index - count) * group.size); } position += groupCount * group.size; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditor.ts index 590ee9b0db9..e3a815104a5 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditor.ts @@ -24,6 +24,7 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; +import { DiffComputer } from 'vs/editor/common/diff/diffComputer'; export class NotebookDiffEditor extends BaseEditor { @@ -171,6 +172,47 @@ export class NotebookDiffEditor extends BaseEditor { ]); }); + // console.log(diffResult); + + // diffResult.changes.forEach(change => { + // if (change.modifiedLength === 0) { + // // deletion ... + // return; + // } + + // if (change.originalLength === 0) { + // // insertion + // return; + // } + + // for (let i = 0, len = Math.min(change.modifiedLength, change.originalLength); i < len; i++) { + // let originalIndex = change.originalStart + i; + // let modifiedIndex = change.modifiedStart + i; + + // const originalCell = this._originalWidget!.viewModel!.viewCells[originalIndex]; + // const modifiedCell = this._widget!.viewModel!.viewCells[modifiedIndex]; + + // if (originalCell.getText() !== modifiedCell.getText()) { + // console.log(`original cell ${originalIndex} content change`); + // const originalLines = originalCell.textBuffer.getLinesContent(); + // const modifiedLines = modifiedCell.textBuffer.getLinesContent(); + // const diffComputer = new DiffComputer(originalLines, modifiedLines, { + // shouldComputeCharChanges: true, + // shouldPostProcessCharChanges: true, + // shouldIgnoreTrimWhitespace: false, + // shouldMakePrettyDiff: true, + // maxComputationTime: 5000 + // }); + + // const diffResult = diffComputer.computeDiff(); + // console.log(diffResult); + // } else { + // console.log(`original cell ${originalIndex} metadata change`) + // } + + // } + // }); + this._originalCellDecorations = this._originalWidget.deltaCellDecorations(this._originalCellDecorations, originalDecorations); this._cellDecorations = this._widget.deltaCellDecorations(this._cellDecorations, modifiedDecorations); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index b03dfcc5622..5fbd4dc1e41 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -60,6 +60,7 @@ import { isMacintosh, isNative } from 'vs/base/common/platform'; import { getTitleBarStyle } from 'vs/platform/windows/common/windows'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd'; +import { RangeMapWithWhitespace } from 'vs/workbench/contrib/notebook/browser/view/rangeMapWithWhitespace'; const $ = DOM.$; @@ -413,6 +414,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor enableKeyboardNavigation: true, additionalScrollHeight: 0, transformOptimization: (isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', + rangeMapProvider: () => { + return new RangeMapWithWhitespace(); + }, styleController: (_suffix: string) => { return this._list!; }, overrideStyles: { listBackground: editorBackground, diff --git a/src/vs/workbench/contrib/notebook/browser/view/rangeMapWithWhitespace.ts b/src/vs/workbench/contrib/notebook/browser/view/rangeMapWithWhitespace.ts new file mode 100644 index 00000000000..bf2adb1b1a0 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/rangeMapWithWhitespace.ts @@ -0,0 +1,247 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { concat, groupIntersect, IItem, IRangedGroup, shift } from 'vs/base/browser/ui/list/rangeMap'; + +export class ListWhitespace { + + constructor( + public afterIndex: number, + public height: number, + // height of all whitespaces before this whitespace (inclusive) + public prefixSum: number + ) { } +} + +// [ { start: 0, len: 2, size: 2 }, { start: 2, len: 1, size: 3 }, {} ] +export class RangeMapWithWhitespace { + + private groups: IRangedGroup[] = []; + private whitespaces: ListWhitespace[] = []; + private _size = 0; + + splice(index: number, deleteCount: number, items: IItem[] = []): void { + const diff = items.length - deleteCount; + const before = groupIntersect({ start: 0, end: index }, this.groups); + const after = groupIntersect({ start: index + deleteCount, end: Number.POSITIVE_INFINITY }, this.groups) + .map(g => ({ range: shift(g.range, diff), size: g.size })); + + const middle = items.map((item, i) => ({ + range: { start: index + i, end: index + i + 1 }, + size: item.size + })); + + this.groups = concat(before, middle, after); + this._size = this.groups.reduce((t, g) => t + (g.size * (g.range.end - g.range.start)), 0); + + const deleteRange = deleteCount > 0 ? [index, index + deleteCount - 1] : []; + const indexDelta = items.length - deleteCount; + let prefixSumDelta = 0; + const pendingRemovalWhitespace: number[] = []; + for (let i = 0; i < this.whitespaces.length; i++) { + const whitespace = this.whitespaces[i]; + + if (whitespace.afterIndex < index) { + continue; + } else if (deleteRange.length > 0 && whitespace.afterIndex >= deleteRange[0] && whitespace.afterIndex <= deleteRange[1]) { + // should be deleted + pendingRemovalWhitespace.push(i); + prefixSumDelta += whitespace.height; + } else { + whitespace.afterIndex += indexDelta; + whitespace.prefixSum -= prefixSumDelta; + } + } + + pendingRemovalWhitespace.reverse().forEach(index => { + this.whitespaces.splice(index, 1); + }); + } + + public static findInsertionIndex(arr: ListWhitespace[], afterIndex: number): number { + let low = 0; + let high = arr.length; + + while (low < high) { + const mid = ((low + high) >>> 1); + + if (afterIndex === arr[mid].afterIndex) { + low = mid; + break; + } else if (afterIndex < arr[mid].afterIndex) { + high = mid; + } else { + low = mid + 1; + } + } + + return low; + } + + insertWhitespace(afterIndex: number, height: number) { + // TODO + // 2. delay prefix sum update + const insertIndex = RangeMapWithWhitespace.findInsertionIndex(this.whitespaces, afterIndex); + const prefixSum = insertIndex > 0 ? this.whitespaces[insertIndex - 1].prefixSum + height : height; + const insertedItem = new ListWhitespace(afterIndex, height, prefixSum); + this.whitespaces.splice(insertIndex, 0, insertedItem); + + for (let i = insertIndex + 1; i < this.whitespaces.length; i++) { + this.whitespaces[i].prefixSum += height; + } + } + + // todo, allow multiple whitespaces after one index + updateWhitespace(afterIndex: number, newHeight: number) { + let delta = 0; + let findWhitespace = false; + for (let i = 0; i < this.whitespaces.length; i++) { + if (this.whitespaces[i].afterIndex === afterIndex) { + delta = newHeight - this.whitespaces[i].height; + this.whitespaces[i].height = newHeight; + this.whitespaces[i].prefixSum += delta; + findWhitespace = true; + } else if (this.whitespaces[i].afterIndex > afterIndex) { + if (!findWhitespace) { + this.insertWhitespace(afterIndex, newHeight); + return; + } + this.whitespaces[i].prefixSum += delta; + } + } + + if (!findWhitespace) { + this.insertWhitespace(afterIndex, newHeight); + } + } + + /** + * Returns the number of items in the range map. + */ + get count(): number { + const len = this.groups.length; + + if (!len) { + return 0; + } + + return this.groups[len - 1].range.end; + } + + /** + * Returns the sum of the sizes of all items in the range map. + */ + get size(): number { + return this._size + + (this.whitespaces.length ? this.whitespaces[this.whitespaces.length - 1]?.prefixSum : 0); + } + + private _getWhitespaceAccumulatedHeightBeforeIndex(index: number): number { + const lastWhitespaceBeforeLineNumber = this._findLastWhitespaceBeforeIndex(index); + + if (lastWhitespaceBeforeLineNumber === -1) { + return 0; + } + + return this.whitespaces[lastWhitespaceBeforeLineNumber].prefixSum; + } + + private _findLastWhitespaceBeforeIndex(index: number): number { + const arr = this.whitespaces; + let low = 0; + let high = arr.length - 1; + + while (low <= high) { + const delta = (high - low) | 0; + const halfDelta = (delta / 2) | 0; + const mid = (low + halfDelta) | 0; + + if (arr[mid].afterIndex < index) { + if (mid + 1 >= arr.length || arr[mid + 1].afterIndex >= index) { + return mid; + } else { + low = (mid + 1) | 0; + } + } else { + high = (mid - 1) | 0; + } + } + + return -1; + } + + /** + * Returns the index of the item at the given position. + */ + indexAt(position: number): number { + if (position < 0) { + return -1; + } + + let index = 0; + let size = 0; + + for (let group of this.groups) { + const count = group.range.end - group.range.start; + const newSize = size + (count * group.size); + + if (position < newSize + this._getWhitespaceAccumulatedHeightBeforeIndex(group.range.end + 1)) { + // try to find the right index + let currSize = size; + // position > currSize + all whitespaces before current range + for (let j = group.range.start; j < group.range.end; j++) { + currSize = currSize + group.size; + + if (position >= currSize + this._getWhitespaceAccumulatedHeightBeforeIndex(j + 1)) { + continue; + } else { + return j; + } + } + + return index + Math.floor((position - size) / group.size); + } + + index += count; + size = newSize; + } + + return index; + } + + /** + * Returns the index of the item right after the item at the + * index of the given position. + */ + indexAfter(position: number): number { + return Math.min(this.indexAt(position) + 1, this.count); + } + + /** + * Returns the start position of the item at the given index. + */ + positionAt(index: number): number { + if (index < 0) { + return -1; + } + + let position = 0; + let count = 0; + + for (let group of this.groups) { + const groupCount = group.range.end - group.range.start; + const newCount = count + groupCount; + + if (index < newCount) { + return position + ((index - count) * group.size) + this._getWhitespaceAccumulatedHeightBeforeIndex(index); + } + + position += groupCount * group.size; + count = newCount; + } + + return -1; + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index f2c035c987b..43356373703 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -108,8 +108,8 @@ export class NotebookCellTextModel extends Disposable implements ICell { return this._hash; } - // this._hash = hash([hash(this.getValue()), this._metadata]); - this._hash = hash(this.getValue()); + this._hash = hash([hash(this.getValue()), this._metadata]); + // this._hash = hash(this.getValue()); return this._hash; }