range map provider

This commit is contained in:
rebornix
2020-08-17 22:04:24 -07:00
parent eefe53f072
commit ba92c47e81
7 changed files with 310 additions and 149 deletions
+9 -2
View File
@@ -54,6 +54,10 @@ export interface IListViewOptionsUpdate {
readonly horizontalScrolling?: boolean;
}
export interface IListViewRangeMapProvider {
(): RangeMap;
}
export interface IListViewOptions<T> extends IListViewOptionsUpdate {
readonly dnd?: IListViewDragAndDrop<T>;
readonly useShadows?: boolean;
@@ -64,6 +68,7 @@ export interface IListViewOptions<T> extends IListViewOptionsUpdate {
readonly mouseSupport?: boolean;
readonly accessibilityProvider?: IListViewAccessibilityProvider<T>;
readonly transformOptimization?: boolean;
readonly rangeMapProvider?: IListViewRangeMapProvider;
}
const DefaultOptions = {
@@ -209,6 +214,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
private items: IItem<T>[];
private itemId: number;
private rangeMap: RangeMap;
private rangeMapProvider: IListViewRangeMapProvider;
private cache: RowCache<T>;
private renderers = new Map<string, IListRenderer<any /* TODO@joao */, any>>();
private lastRenderTop: number;
@@ -289,7 +295,8 @@ export class ListView<T> implements ISpliceable<T>, 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<T> implements ISpliceable<T>, 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 = [];
+2 -1
View File
@@ -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<T> {
readonly horizontalScrolling?: boolean;
readonly additionalScrollHeight?: number;
readonly transformOptimization?: boolean;
readonly rangeMapProvider?: IListViewRangeMapProvider;
readonly smoothScrolling?: boolean;
}
+4 -144
View File
@@ -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;
@@ -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);
}
@@ -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,
@@ -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<IRangedGroup>(g => ({ range: shift(g.range, diff), size: g.size }));
const middle = items.map<IRangedGroup>((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;
}
}
@@ -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;
}