From 648e6f6aff9dce84ae1a88465f78c98eac01e5a5 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 12 Oct 2017 15:46:30 +0200 Subject: [PATCH 01/64] Implement a relative interval tree --- src/vs/editor/common/model/intervalTree.ts | 626 ++++++++++++++++++ .../test/common/model/intervalTree.test.ts | 365 ++++++++++ 2 files changed, 991 insertions(+) create mode 100644 src/vs/editor/common/model/intervalTree.ts create mode 100644 src/vs/editor/test/common/model/intervalTree.test.ts diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts new file mode 100644 index 0000000000000..fb9b929435273 --- /dev/null +++ b/src/vs/editor/common/model/intervalTree.ts @@ -0,0 +1,626 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +// +// The red-black tree is based on the "Introduction to Algorithms" by Cormen, Leiserson and Rivest. +// + +export class Interval { + _intervalBrand: void; + + public start: number; + public end: number; + + constructor(start: number, end: number) { + this.start = start; + this.end = end; + } + + public compareToRelative(other: RelativeInterval, delta: number): number { + const otherStart = other.start + delta; + if (this.start === otherStart) { + const otherEnd = other.end + delta; + return this.end - otherEnd; + } + return this.start - otherStart; + } +} + +export class RelativeInterval { + _relativeIntervalBrand: void; + + public static fromInterval(interval: Interval, delta: number): RelativeInterval { + return new RelativeInterval(interval.start - delta, interval.end - delta); + } + + public start: number; + public end: number; + + constructor(start: number, end: number) { + this.start = start; + this.end = end; + } +} + +export const enum NodeColor { + Red, + Black +} + +export class IntervalNode { + + public left: IntervalNode; + public right: IntervalNode; + public parent: IntervalNode; + public color: NodeColor; + + public delta: number; + public interval: RelativeInterval; + public maxEnd: number; + + public resultInterval: Interval; + + constructor(interval: RelativeInterval, delta: number) { + this.parent = null; + this.left = null; + this.right = null; + this.color = NodeColor.Red; + + this.delta = delta; + this.interval = interval; + this.maxEnd = this.interval.end; + + this.resultInterval = null; + } + + public detach(): void { + this.parent = null; + this.left = null; + this.right = null; + } +} + +const SENTINEL: IntervalNode = new IntervalNode(new RelativeInterval(0, 0), 0); +SENTINEL.parent = SENTINEL; +SENTINEL.left = SENTINEL; +SENTINEL.right = SENTINEL; +SENTINEL.color = NodeColor.Black; + +function leftRotate(T: IntervalTree, x: IntervalNode): void { + const y = x.right; // set y. + + y.delta += x.delta; // y's delta is no longer influenced by x's delta + y.interval.start += x.delta; + y.interval.end += x.delta; + + x.right = y.left; // turn y's left subtree into x's right subtree. + if (y.left !== SENTINEL) { + y.left.parent = x; + } + y.parent = x.parent; // link x's parent to y. + if (x.parent === SENTINEL) { + T.root = y; + } else if (x === x.parent.left) { + x.parent.left = y; + } else { + x.parent.right = y; + } + + y.left = x; // put x on y's left. + x.parent = y; + + recomputeMaxEnd(x); + recomputeMaxEnd(y); +} + +function rightRotate(T: IntervalTree, y: IntervalNode): void { + const x = y.left; + + y.delta -= x.delta; + y.interval.start -= x.delta; + y.interval.end -= x.delta; + + y.left = x.right; + if (x.right !== SENTINEL) { + x.right.parent = y; + } + x.parent = y.parent; + if (y.parent === SENTINEL) { + T.root = x; + } else if (y === y.parent.right) { + y.parent.right = x; + } else { + y.parent.left = x; + } + + x.right = y; + y.parent = x; + + recomputeMaxEnd(y); + recomputeMaxEnd(x); +} + +function treeInsert(T: IntervalTree, interval: Interval): IntervalNode { + let delta: number = 0; + let z = SENTINEL; + let x = T.root; + while (true) { + let cmp = interval.compareToRelative(x.interval, delta); + if (cmp < 0) { + // this node should be inserted to the left + // => it is not affected by the node's delta + if (x.left === SENTINEL) { + const relativeInterval = RelativeInterval.fromInterval(interval, delta); + z = new IntervalNode(relativeInterval, relativeInterval.start); + x.left = z; + break; + } else { + x = x.left; + } + } else { + // this node should be inserted to the right + // => it is not affected by the node's delta + if (x.right === SENTINEL) { + const relativeInterval = RelativeInterval.fromInterval(interval, delta + x.delta); + z = new IntervalNode(relativeInterval, relativeInterval.start); + x.right = z; + break; + } else { + delta += x.delta; + x = x.right; + } + } + } + + z.parent = x; + z.left = SENTINEL; + z.right = SENTINEL; + z.color = NodeColor.Red; + return z; +} + +function leftest(node: IntervalNode): IntervalNode { + while (node.left !== SENTINEL) { + node = node.left; + } + return node; +} + +function resetSentinel(): void { + SENTINEL.parent = SENTINEL; + SENTINEL.delta = 0; // optional + SENTINEL.interval.start = 0; // optional + SENTINEL.interval.end = 0; // optional +} + +function computeMaxEnd(node: IntervalNode): number { + let maxEnd = node.interval.end; + if (node.left !== SENTINEL) { + const leftMaxEnd = node.left.maxEnd; + if (leftMaxEnd > maxEnd) { + maxEnd = leftMaxEnd; + } + } + if (node.right !== SENTINEL) { + const rightMaxEnd = node.right.maxEnd + node.delta; + if (rightMaxEnd > maxEnd) { + maxEnd = rightMaxEnd; + } + } + return maxEnd; +} + +function recomputeMaxEnd(node: IntervalNode): void { + node.maxEnd = computeMaxEnd(node); +} + +function recomputeMaxEndToRoot(node: IntervalNode): void { + while (node !== SENTINEL) { + + const maxEnd = computeMaxEnd(node); + + if (node.maxEnd === maxEnd) { + // no need to go further + return; + } + + node.maxEnd = maxEnd; + node = node.parent; + } +} + +function treeDelete(T: IntervalTree, z: IntervalNode): void { + + let x: IntervalNode; + let y: IntervalNode; + + // RB-DELETE except we don't swap z and y in case c) + // i.e. we always delete what's pointed at by z. + + if (z.left === SENTINEL) { + x = z.right; + y = z; + + // x's delta is no longer influenced by z's delta + x.delta += z.delta; + x.interval.start += z.delta; + x.interval.end += z.delta; + + } else if (z.right === SENTINEL) { + x = z.left; + y = z; + + } else { + y = leftest(z.right); + x = y.right; + + // y's delta is no longer influenced by z's delta, + // but we don't want to walk the entire right-hand-side subtree of x. + // we therefore maintain z's delta in y, and adjust only x + x.interval.start += y.delta; + x.interval.end += y.delta; + // if (x.interval.end > x.maxEnd) { + // x.maxEnd = x.interval.end; + // } + x.delta += y.delta; + + y.interval.start += z.delta; + y.interval.end += z.delta; + y.delta = z.delta; + } + + if (y === T.root) { + T.root = x; + x.color = NodeColor.Black; + + z.detach(); + resetSentinel(); + recomputeMaxEnd(x); + return; + } + + let yWasRed = (y.color === NodeColor.Red); + + if (y === y.parent.left) { + y.parent.left = x; + } else { + y.parent.right = x; + } + + if (y === z) { + x.parent = y.parent; + } else { + + if (y.parent === z) { + x.parent = y; + } else { + x.parent = y.parent; + } + + y.left = z.left; + y.right = z.right; + y.parent = z.parent; + y.color = z.color; + + if (z === T.root) { + T.root = y; + } else { + if (z === z.parent.left) { + z.parent.left = y; + } else { + z.parent.right = y; + } + } + + if (y.left !== SENTINEL) { + y.left.parent = y; + } + if (y.right !== SENTINEL) { + y.right.parent = y; + } + } + + z.detach(); + + if (yWasRed) { + recomputeMaxEndToRoot(x.parent); + if (y !== z) { + recomputeMaxEndToRoot(y); + recomputeMaxEndToRoot(y.parent); + } + resetSentinel(); + return; + } + + recomputeMaxEndToRoot(x); + recomputeMaxEndToRoot(x.parent); + if (y !== z) { + recomputeMaxEndToRoot(y); + recomputeMaxEndToRoot(y.parent); + } + + // RB-DELETE-FIXUP + let w: IntervalNode; + while (x !== T.root && x.color === NodeColor.Black) { + + if (x === x.parent.left) { + w = x.parent.right; + + if (w.color === NodeColor.Red) { + w.color = NodeColor.Black; + x.parent.color = NodeColor.Red; + leftRotate(T, x.parent); + w = x.parent.right; + } + + if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { + w.color = NodeColor.Red; + x = x.parent; + } else { + if (w.right.color === NodeColor.Black) { + w.left.color = NodeColor.Black; + w.color = NodeColor.Red; + rightRotate(T, w); + w = x.parent.right; + } + + w.color = x.parent.color; + x.parent.color = NodeColor.Black; + w.right.color = NodeColor.Black; + leftRotate(T, x.parent); + x = T.root; + } + + } else { + w = x.parent.left; + + if (w.color === NodeColor.Red) { + w.color = NodeColor.Black; + x.parent.color = NodeColor.Red; + rightRotate(T, x.parent); + w = x.parent.left; + } + + if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { + w.color = NodeColor.Red; + x = x.parent; + + } else { + if (w.left.color === NodeColor.Black) { + w.right.color = NodeColor.Black; + w.color = NodeColor.Red; + leftRotate(T, w); + w = x.parent.left; + } + + w.color = x.parent.color; + x.parent.color = NodeColor.Black; + w.left.color = NodeColor.Black; + rightRotate(T, x.parent); + x = T.root; + } + } + } + + x.color = NodeColor.Black; + resetSentinel(); +} + +export class IntervalTree { + + public root: IntervalNode; + + constructor() { + this.root = SENTINEL; + } + + // public intervalSearch(interval: Interval): IntervalNode[] { + // let result: IntervalNode[] = []; + // if (this.root !== SENTINEL) { + // this._intervalSearch(this.root, 0, interval, result); + // } + // return result; + // } + + // private _intervalSearch(node: IntervalNode, delta: number, interval: Interval, result: IntervalNode[]): void { + // // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree + // // Now, it is known that two intervals A and B overlap only when both + // // A.low ≤ B.high and A.high ≥ B.low. When searching the trees for + // // nodes overlapping with a given interval, you can immediately skip: + // // - all nodes to the right of nodes whose low value is past the end of the given interval. + // // - all nodes that have their maximum 'high' value below the start of the given interval. + + // if (delta + node.maxEnd < interval.start) { + // return; + // } + + + // } + + public insert(interval: Interval): IntervalNode { + if (this.root === SENTINEL) { + const relativeInterval = RelativeInterval.fromInterval(interval, 0); + const newNode = new IntervalNode(relativeInterval, relativeInterval.start); + newNode.parent = SENTINEL; + newNode.left = SENTINEL; + newNode.right = SENTINEL; + newNode.color = NodeColor.Black; + this.root = newNode; + return this.root; + } + + const newNode = treeInsert(this, interval); + + recomputeMaxEndToRoot(newNode.parent); + + // repair tree + let node = newNode; + while (node !== this.root && node.parent.color === NodeColor.Red) { + if (node.parent === node.parent.parent.left) { + const temp = node.parent.parent.right; + + if (temp.color === NodeColor.Red) { + node.parent.color = NodeColor.Black; + temp.color = NodeColor.Black; + node.parent.parent.color = NodeColor.Red; + node = node.parent.parent; + } else { + if (node === node.parent.right) { + node = node.parent; + leftRotate(this, node); + } + node.parent.color = NodeColor.Black; + node.parent.parent.color = NodeColor.Red; + rightRotate(this, node.parent.parent); + } + } else { + const temp = node.parent.parent.left; + + if (temp.color === NodeColor.Red) { + node.parent.color = NodeColor.Black; + temp.color = NodeColor.Black; + node.parent.parent.color = NodeColor.Red; + node = node.parent.parent; + } else { + if (node === node.parent.left) { + node = node.parent; + rightRotate(this, node); + } + node.parent.color = NodeColor.Black; + node.parent.parent.color = NodeColor.Red; + leftRotate(this, node.parent.parent); + } + } + } + + this.root.color = NodeColor.Black; + + return newNode; + } + + public delete(node: IntervalNode) { + treeDelete(this, node); + } + + public assertInvariants(): void { + assert(SENTINEL.color === NodeColor.Black); + assert(SENTINEL.parent === SENTINEL); + assert(SENTINEL.left === SENTINEL); + assert(SENTINEL.right === SENTINEL); + assert(SENTINEL.interval.start === 0); + assert(SENTINEL.interval.end === 0); + assert(SENTINEL.delta === 0); + assertValidTree(this); + } + + public getAllInOrder(): Interval[] { + let r: Interval[] = [], rLength = 0; + this.visitInOrder((n, delta) => { + r[rLength++] = new Interval(n.interval.start + delta, n.interval.end + delta); + }); + return r; + } + + public visitInOrder(visitor: (n: IntervalNode, delta: number) => void): void { + this._visitInOrder(this.root, 0, visitor); + } + + private _visitInOrder(n: IntervalNode, delta: number, visitor: (n: IntervalNode, delta: number) => void): void { + if (n.left !== SENTINEL) { + this._visitInOrder(n.left, delta, visitor); + } + + if (n !== SENTINEL) { + visitor(n, delta); + } + + if (n.right !== SENTINEL) { + this._visitInOrder(n.right, delta + n.delta, visitor); + } + } + + public print(): void { + let out: string[] = []; + this._print(this.root, '', 0, out); + console.log(out.join('')); + } + + private _print(n: IntervalNode, indent: string, delta: number, out: string[]): void { + out.push(`${indent}[${n.color === NodeColor.Red ? 'R' : 'B'},${n.delta}, ${n.interval.start}->${n.interval.end}, ${n.maxEnd}] : {${delta + n.interval.start}->${delta + n.interval.end}}, maxEnd: ${n.maxEnd + delta}\n`); + if (n.left !== SENTINEL) { + this._print(n.left, indent + ' ', delta, out); + } else { + out.push(`${indent} NIL\n`); + } + if (n.right !== SENTINEL) { + this._print(n.right, indent + ' ', delta + n.delta, out); + } else { + out.push(`${indent} NIL\n`); + } + } +} + +function depth(n: IntervalNode): number { + if (n === SENTINEL) { + // The leafs are black + return 1; + } + assert(depth(n.left) === depth(n.right)); + return (n.color === NodeColor.Black ? 1 : 0) + depth(n.left); +} + +function assertValidNode(n: IntervalNode, delta): void { + if (n === SENTINEL) { + return; + } + + let l = n.left; + let r = n.right; + + if (n.color === NodeColor.Red) { + if (l.color !== NodeColor.Black) { + assert(false); + } + if (r.color !== NodeColor.Black) { + assert(false); + } + } + + let expectedMaxEnd = n.interval.end; + if (l !== SENTINEL) { + const lValue = new Interval(l.interval.start + delta, l.interval.end + delta); + assert(lValue.compareToRelative(n.interval, delta) <= 0); + expectedMaxEnd = Math.max(expectedMaxEnd, l.maxEnd); + } + if (r !== SENTINEL) { + const nValue = new Interval(n.interval.start + delta, n.interval.end + delta); + assert(nValue.compareToRelative(r.interval, delta + n.delta) <= 0); + expectedMaxEnd = Math.max(expectedMaxEnd, r.maxEnd + n.delta); + } + assert(n.maxEnd === expectedMaxEnd); + + assertValidNode(l, delta); + assertValidNode(r, delta + n.delta); +} + +function assertValidTree(tree: IntervalTree): void { + if (tree.root === SENTINEL) { + return; + } + if (tree.root.color !== NodeColor.Black) { + assert(false); + } + if (depth(tree.root.left) !== depth(tree.root.right)) { + assert(false); + } + assertValidNode(tree.root, 0); +} + +function assert(condition: boolean): void { + if (!condition) { + throw new Error('Assertion violation'); + } +} diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts new file mode 100644 index 0000000000000..3c5caac63fc2d --- /dev/null +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -0,0 +1,365 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as assert from 'assert'; +import { IntervalTree, Interval, IntervalNode } from 'vs/editor/common/model/intervalTree'; + +const GENERATE_TESTS = false; +let TEST_COUNT = GENERATE_TESTS ? 10000 : 0; +let PRINT_TREE = false; +const MIN_INTERVAL_START = 1; +const MAX_INTERVAL_END = 100; +const MIN_INSERTS = 1; +const MAX_INSERTS = 30; + +suite('IntervalTree', () => { + + class Oracle { + public intervals: Interval[]; + + constructor() { + this.intervals = []; + } + + public insert(interval: Interval): Interval { + this.intervals.push(interval); + this.intervals.sort((a, b) => { + if (a.start === b.start) { + return a.end - b.end; + } + return a.start - b.start; + }); + return interval; + } + + public delete(interval: Interval): void { + for (let i = 0, len = this.intervals.length; i < len; i++) { + if (this.intervals[i] === interval) { + this.intervals.splice(i, 1); + return; + } + } + } + } + + class TestState { + private _oracle: Oracle = new Oracle(); + private _tree: IntervalTree = new IntervalTree(); + private _lastNodeId = -1; + private _treeNodes: IntervalNode[] = []; + private _oracleNodes: Interval[] = []; + + public acceptOp(op: IOperation): void { + + + if (op.type === 'insert') { + if (PRINT_TREE) { + console.log(`insert: {${JSON.stringify(new Interval(op.begin, op.end))}}`); + } + let nodeId = (++this._lastNodeId); + this._treeNodes[nodeId] = this._tree.insert(new Interval(op.begin, op.end)); + this._oracleNodes[nodeId] = this._oracle.insert(new Interval(op.begin, op.end)); + } else { + if (PRINT_TREE) { + console.log(`delete: {${JSON.stringify(this._oracleNodes[op.id])}}`); + } + this._tree.delete(this._treeNodes[op.id]); + this._oracle.delete(this._oracleNodes[op.id]); + + this._treeNodes[op.id] = null; + this._oracleNodes[op.id] = null; + } + + if (PRINT_TREE) { + this._tree.print(); + } + + this._tree.assertInvariants(); + + let actual = this._tree.getAllInOrder(); + let expected = this._oracle.intervals; + assert.deepEqual(actual, expected); + } + + public getExistingNodeId(index: number): number { + let currIndex = -1; + for (let i = 0; i < this._treeNodes.length; i++) { + if (this._treeNodes[i] === null) { + continue; + } + currIndex++; + if (currIndex === index) { + return i; + } + } + throw new Error('unexpected'); + } + } + + interface IInsertOperation { + type: 'insert'; + begin: number; + end: number; + } + + interface IDeleteOperation { + type: 'delete'; + id: number; + } + + type IOperation = IInsertOperation | IDeleteOperation; + + function testIntervalTree(ops: IOperation[]): void { + let state = new TestState(); + for (let i = 0; i < ops.length; i++) { + state.acceptOp(ops[i]); + } + } + + function getRandomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + class AutoTest { + private _ops: IOperation[] = []; + private _state: TestState = new TestState(); + private _insertCnt: number; + private _deleteCnt: number; + + constructor() { + this._insertCnt = getRandomInt(MIN_INSERTS, MAX_INSERTS); + this._deleteCnt = 0; + } + + public run() { + while (this._insertCnt > 0 || this._deleteCnt > 0) { + let type: 'insert' | 'delete'; + if (this._insertCnt > 0) { + type = 'insert'; + } else { + type = 'delete'; + } + if (type === 'insert') { + let begin = getRandomInt(MIN_INTERVAL_START, MAX_INTERVAL_END); + let length: number; + if (getRandomInt(1, 10) <= 2) { + // large range + length = getRandomInt(0, MAX_INTERVAL_END - begin); + } else { + // small range + length = getRandomInt(0, Math.min(MAX_INTERVAL_END - begin, 10)); + } + this._run({ + type: 'insert', + begin: begin, + end: begin + length + }); + this._insertCnt--; + this._deleteCnt++; + } else { + let idx = getRandomInt(0, this._deleteCnt - 1); + this._run({ + type: 'delete', + id: this._state.getExistingNodeId(idx) + }); + this._deleteCnt--; + // this._deleteCnt = 0; + } + } + } + + private _run(op: IOperation): void { + this._ops.push(op); + this._state.acceptOp(op); + } + + public print(): void { + console.log(`testIntervalTree(${JSON.stringify(this._ops)})`); + } + + } + + test('gen01', () => { + testIntervalTree([ + { type: 'insert', begin: 28, end: 35 }, + { type: 'insert', begin: 52, end: 54 }, + { type: 'insert', begin: 63, end: 69 } + ]); + }); + + test('gen02', () => { + testIntervalTree([ + { type: 'insert', begin: 80, end: 89 }, + { type: 'insert', begin: 92, end: 100 }, + { type: 'insert', begin: 99, end: 99 } + ]); + }); + + test('gen03', () => { + testIntervalTree([ + { type: 'insert', begin: 89, end: 96 }, + { type: 'insert', begin: 71, end: 74 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen04', () => { + testIntervalTree([ + { type: 'insert', begin: 44, end: 46 }, + { type: 'insert', begin: 85, end: 88 }, + { type: 'delete', id: 0 } + ]); + }); + + test('gen05', () => { + testIntervalTree([ + { type: 'insert', begin: 82, end: 90 }, + { type: 'insert', begin: 69, end: 73 }, + { type: 'delete', id: 0 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen06', () => { + testIntervalTree([ + { type: 'insert', begin: 41, end: 63 }, + { type: 'insert', begin: 98, end: 98 }, + { type: 'insert', begin: 47, end: 51 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen07', () => { + testIntervalTree([ + { type: 'insert', begin: 24, end: 26 }, + { type: 'insert', begin: 11, end: 28 }, + { type: 'insert', begin: 27, end: 30 }, + { type: 'insert', begin: 80, end: 85 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen08', () => { + testIntervalTree([ + { type: 'insert', begin: 100, end: 100 }, + { type: 'insert', begin: 100, end: 100 } + ]); + }); + + test('gen09', () => { + testIntervalTree([ + { type: 'insert', begin: 58, end: 65 }, + { type: 'insert', begin: 82, end: 96 }, + { type: 'insert', begin: 58, end: 65 } + ]); + }); + + test('gen10', () => { + testIntervalTree([ + { type: 'insert', begin: 32, end: 40 }, + { type: 'insert', begin: 25, end: 29 }, + { type: 'insert', begin: 24, end: 32 } + ]); + }); + + test('gen11', () => { + testIntervalTree([ + { type: 'insert', begin: 25, end: 70 }, + { type: 'insert', begin: 99, end: 100 }, + { type: 'insert', begin: 46, end: 51 }, + { type: 'insert', begin: 57, end: 57 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen12', () => { + testIntervalTree([ + { type: 'insert', begin: 20, end: 26 }, + { type: 'insert', begin: 10, end: 18 }, + { type: 'insert', begin: 99, end: 99 }, + { type: 'insert', begin: 37, end: 59 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen13', () => { + testIntervalTree([ + { type: 'insert', begin: 3, end: 91 }, + { type: 'insert', begin: 57, end: 57 }, + { type: 'insert', begin: 35, end: 44 }, + { type: 'insert', begin: 72, end: 81 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen14', () => { + testIntervalTree([ + { type: 'insert', begin: 58, end: 61 }, + { type: 'insert', begin: 34, end: 35 }, + { type: 'insert', begin: 56, end: 62 }, + { type: 'insert', begin: 69, end: 78 }, + { type: 'delete', id: 0 } + ]); + }); + + test('gen15', () => { + testIntervalTree([ + { type: 'insert', begin: 63, end: 69 }, + { type: 'insert', begin: 17, end: 24 }, + { type: 'insert', begin: 3, end: 13 }, + { type: 'insert', begin: 84, end: 94 }, + { type: 'insert', begin: 18, end: 23 }, + { type: 'insert', begin: 96, end: 98 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen16', () => { + testIntervalTree([ + { type: 'insert', begin: 27, end: 27 }, + { type: 'insert', begin: 42, end: 87 }, + { type: 'insert', begin: 42, end: 49 }, + { type: 'insert', begin: 69, end: 71 }, + { type: 'insert', begin: 20, end: 27 }, + { type: 'insert', begin: 8, end: 9 }, + { type: 'insert', begin: 42, end: 49 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen17', () => { + testIntervalTree([ + { type: 'insert', begin: 21, end: 23 }, + { type: 'insert', begin: 83, end: 87 }, + { type: 'insert', begin: 56, end: 58 }, + { type: 'insert', begin: 1, end: 55 }, + { type: 'insert', begin: 56, end: 59 }, + { type: 'insert', begin: 58, end: 60 }, + { type: 'insert', begin: 56, end: 65 }, + { type: 'delete', id: 1 }, + { type: 'delete', id: 0 }, + { type: 'delete', id: 6 } + ]); + }); + + // TEST_COUNT = 0; + // PRINT_TREE = true; + + for (let i = 0; i < TEST_COUNT; i++) { + if (i % 100 === 0) { + console.log(`TEST ${i + 1}/${TEST_COUNT}`); + } + let test = new AutoTest(); + + try { + test.run(); + } catch (err) { + console.log(err); + test.print(); + return; + } + } +}); From bc76ef88b9c09d6757e95a11e1060dd0227074e0 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 12 Oct 2017 16:29:09 +0200 Subject: [PATCH 02/64] Implement interval searching --- src/vs/editor/common/model/intervalTree.ts | 61 ++- .../test/common/model/intervalTree.test.ts | 467 +++++++++++------- 2 files changed, 334 insertions(+), 194 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index fb9b929435273..60d9f8ca73d6a 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -73,7 +73,7 @@ export class IntervalNode { this.interval = interval; this.maxEnd = this.interval.end; - this.resultInterval = null; + this.resultInterval = new Interval(0, 0); } public detach(): void { @@ -417,28 +417,51 @@ export class IntervalTree { this.root = SENTINEL; } - // public intervalSearch(interval: Interval): IntervalNode[] { - // let result: IntervalNode[] = []; - // if (this.root !== SENTINEL) { - // this._intervalSearch(this.root, 0, interval, result); - // } - // return result; - // } + public intervalSearch(interval: Interval): IntervalNode[] { + let result: IntervalNode[] = []; + if (this.root !== SENTINEL) { + this._intervalSearch(this.root, 0, interval.start, interval.end, result); + } + return result; + } - // private _intervalSearch(node: IntervalNode, delta: number, interval: Interval, result: IntervalNode[]): void { - // // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree - // // Now, it is known that two intervals A and B overlap only when both - // // A.low ≤ B.high and A.high ≥ B.low. When searching the trees for - // // nodes overlapping with a given interval, you can immediately skip: - // // - all nodes to the right of nodes whose low value is past the end of the given interval. - // // - all nodes that have their maximum 'high' value below the start of the given interval. + private _intervalSearch(node: IntervalNode, delta: number, intervalStart: number, intervalEnd: number, result: IntervalNode[]): void { + // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree + // Now, it is known that two intervals A and B overlap only when both + // A.low <= B.high and A.high >= B.low. When searching the trees for + // nodes overlapping with a given interval, you can immediately skip: + // a) all nodes to the right of nodes whose low value is past the end of the given interval. + // b) all nodes that have their maximum 'high' value below the start of the given interval. - // if (delta + node.maxEnd < interval.start) { - // return; - // } + const nodeMaxEnd = delta + node.maxEnd; + if (nodeMaxEnd < intervalStart) { + // Cover b) from above + return; + } + if (node.left !== SENTINEL) { + this._intervalSearch(node.left, delta, intervalStart, intervalEnd, result); + } + + const nodeStart = delta + node.interval.start; - // } + if (nodeStart > intervalEnd) { + // Cover a) from above + return; + } + + const nodeEnd = delta + node.interval.end; + if (nodeEnd >= intervalStart) { + // There is overlap + node.resultInterval.start = nodeStart; + node.resultInterval.end = nodeEnd; + result.push(node); + } + + if (node.right !== SENTINEL) { + this._intervalSearch(node.right, delta + node.delta, intervalStart, intervalEnd, result); + } + } public insert(interval: Interval): IntervalNode { if (this.root === SENTINEL) { diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index 3c5caac63fc2d..ec6d1dcfe1a62 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -43,6 +43,17 @@ suite('IntervalTree', () => { } } } + + public search(interval: Interval): Interval[] { + let result: Interval[] = []; + for (let i = 0, len = this.intervals.length; i < len; i++) { + let int = this.intervals[i]; + if (int.start <= interval.end && int.end >= interval.start) { + result.push(int); + } + } + return result; + } } class TestState { @@ -62,7 +73,7 @@ suite('IntervalTree', () => { let nodeId = (++this._lastNodeId); this._treeNodes[nodeId] = this._tree.insert(new Interval(op.begin, op.end)); this._oracleNodes[nodeId] = this._oracle.insert(new Interval(op.begin, op.end)); - } else { + } else if (op.type === 'delete') { if (PRINT_TREE) { console.log(`delete: {${JSON.stringify(this._oracleNodes[op.id])}}`); } @@ -71,6 +82,12 @@ suite('IntervalTree', () => { this._treeNodes[op.id] = null; this._oracleNodes[op.id] = null; + } else { + let actualNodes = this._tree.intervalSearch(new Interval(op.begin, op.end)); + let actual = actualNodes.map(n => n.resultInterval); + let expected = this._oracle.search(new Interval(op.begin, op.end)); + assert.deepEqual(actual, expected); + return; } if (PRINT_TREE) { @@ -110,7 +127,13 @@ suite('IntervalTree', () => { id: number; } - type IOperation = IInsertOperation | IDeleteOperation; + interface ISearchOperation { + type: 'search'; + begin: number; + end: number; + } + + type IOperation = IInsertOperation | IDeleteOperation | ISearchOperation; function testIntervalTree(ops: IOperation[]): void { let state = new TestState(); @@ -123,6 +146,19 @@ suite('IntervalTree', () => { return Math.floor(Math.random() * (max - min + 1)) + min; } + function getRandomRange(min: number, max: number): [number, number] { + let begin = getRandomInt(min, max); + let length: number; + if (getRandomInt(1, 10) <= 2) { + // large range + length = getRandomInt(0, max - begin); + } else { + // small range + length = getRandomInt(0, Math.min(max - begin, 10)); + } + return [begin, begin + length]; + } + class AutoTest { private _ops: IOperation[] = []; private _state: TestState = new TestState(); @@ -143,19 +179,11 @@ suite('IntervalTree', () => { type = 'delete'; } if (type === 'insert') { - let begin = getRandomInt(MIN_INTERVAL_START, MAX_INTERVAL_END); - let length: number; - if (getRandomInt(1, 10) <= 2) { - // large range - length = getRandomInt(0, MAX_INTERVAL_END - begin); - } else { - // small range - length = getRandomInt(0, Math.min(MAX_INTERVAL_END - begin, 10)); - } + let range = getRandomRange(MIN_INTERVAL_START, MAX_INTERVAL_END); this._run({ type: 'insert', - begin: begin, - end: begin + length + begin: range[0], + end: range[1] }); this._insertCnt--; this._deleteCnt++; @@ -166,8 +194,15 @@ suite('IntervalTree', () => { id: this._state.getExistingNodeId(idx) }); this._deleteCnt--; - // this._deleteCnt = 0; } + + // Let's also search for something... + let searchRange = getRandomRange(MIN_INTERVAL_START, MAX_INTERVAL_END); + this._run({ + type: 'search', + begin: searchRange[0], + end: searchRange[1] + }); } } @@ -182,167 +217,169 @@ suite('IntervalTree', () => { } - test('gen01', () => { - testIntervalTree([ - { type: 'insert', begin: 28, end: 35 }, - { type: 'insert', begin: 52, end: 54 }, - { type: 'insert', begin: 63, end: 69 } - ]); - }); - - test('gen02', () => { - testIntervalTree([ - { type: 'insert', begin: 80, end: 89 }, - { type: 'insert', begin: 92, end: 100 }, - { type: 'insert', begin: 99, end: 99 } - ]); - }); - - test('gen03', () => { - testIntervalTree([ - { type: 'insert', begin: 89, end: 96 }, - { type: 'insert', begin: 71, end: 74 }, - { type: 'delete', id: 1 } - ]); - }); - - test('gen04', () => { - testIntervalTree([ - { type: 'insert', begin: 44, end: 46 }, - { type: 'insert', begin: 85, end: 88 }, - { type: 'delete', id: 0 } - ]); - }); - - test('gen05', () => { - testIntervalTree([ - { type: 'insert', begin: 82, end: 90 }, - { type: 'insert', begin: 69, end: 73 }, - { type: 'delete', id: 0 }, - { type: 'delete', id: 1 } - ]); - }); - - test('gen06', () => { - testIntervalTree([ - { type: 'insert', begin: 41, end: 63 }, - { type: 'insert', begin: 98, end: 98 }, - { type: 'insert', begin: 47, end: 51 }, - { type: 'delete', id: 2 } - ]); - }); - - test('gen07', () => { - testIntervalTree([ - { type: 'insert', begin: 24, end: 26 }, - { type: 'insert', begin: 11, end: 28 }, - { type: 'insert', begin: 27, end: 30 }, - { type: 'insert', begin: 80, end: 85 }, - { type: 'delete', id: 1 } - ]); - }); - - test('gen08', () => { - testIntervalTree([ - { type: 'insert', begin: 100, end: 100 }, - { type: 'insert', begin: 100, end: 100 } - ]); - }); - - test('gen09', () => { - testIntervalTree([ - { type: 'insert', begin: 58, end: 65 }, - { type: 'insert', begin: 82, end: 96 }, - { type: 'insert', begin: 58, end: 65 } - ]); - }); - - test('gen10', () => { - testIntervalTree([ - { type: 'insert', begin: 32, end: 40 }, - { type: 'insert', begin: 25, end: 29 }, - { type: 'insert', begin: 24, end: 32 } - ]); - }); - - test('gen11', () => { - testIntervalTree([ - { type: 'insert', begin: 25, end: 70 }, - { type: 'insert', begin: 99, end: 100 }, - { type: 'insert', begin: 46, end: 51 }, - { type: 'insert', begin: 57, end: 57 }, - { type: 'delete', id: 2 } - ]); - }); - - test('gen12', () => { - testIntervalTree([ - { type: 'insert', begin: 20, end: 26 }, - { type: 'insert', begin: 10, end: 18 }, - { type: 'insert', begin: 99, end: 99 }, - { type: 'insert', begin: 37, end: 59 }, - { type: 'delete', id: 2 } - ]); - }); - - test('gen13', () => { - testIntervalTree([ - { type: 'insert', begin: 3, end: 91 }, - { type: 'insert', begin: 57, end: 57 }, - { type: 'insert', begin: 35, end: 44 }, - { type: 'insert', begin: 72, end: 81 }, - { type: 'delete', id: 2 } - ]); - }); - - test('gen14', () => { - testIntervalTree([ - { type: 'insert', begin: 58, end: 61 }, - { type: 'insert', begin: 34, end: 35 }, - { type: 'insert', begin: 56, end: 62 }, - { type: 'insert', begin: 69, end: 78 }, - { type: 'delete', id: 0 } - ]); - }); - - test('gen15', () => { - testIntervalTree([ - { type: 'insert', begin: 63, end: 69 }, - { type: 'insert', begin: 17, end: 24 }, - { type: 'insert', begin: 3, end: 13 }, - { type: 'insert', begin: 84, end: 94 }, - { type: 'insert', begin: 18, end: 23 }, - { type: 'insert', begin: 96, end: 98 }, - { type: 'delete', id: 1 } - ]); - }); - - test('gen16', () => { - testIntervalTree([ - { type: 'insert', begin: 27, end: 27 }, - { type: 'insert', begin: 42, end: 87 }, - { type: 'insert', begin: 42, end: 49 }, - { type: 'insert', begin: 69, end: 71 }, - { type: 'insert', begin: 20, end: 27 }, - { type: 'insert', begin: 8, end: 9 }, - { type: 'insert', begin: 42, end: 49 }, - { type: 'delete', id: 1 } - ]); - }); - - test('gen17', () => { - testIntervalTree([ - { type: 'insert', begin: 21, end: 23 }, - { type: 'insert', begin: 83, end: 87 }, - { type: 'insert', begin: 56, end: 58 }, - { type: 'insert', begin: 1, end: 55 }, - { type: 'insert', begin: 56, end: 59 }, - { type: 'insert', begin: 58, end: 60 }, - { type: 'insert', begin: 56, end: 65 }, - { type: 'delete', id: 1 }, - { type: 'delete', id: 0 }, - { type: 'delete', id: 6 } - ]); + suite('generated', () => { + test('gen01', () => { + testIntervalTree([ + { type: 'insert', begin: 28, end: 35 }, + { type: 'insert', begin: 52, end: 54 }, + { type: 'insert', begin: 63, end: 69 } + ]); + }); + + test('gen02', () => { + testIntervalTree([ + { type: 'insert', begin: 80, end: 89 }, + { type: 'insert', begin: 92, end: 100 }, + { type: 'insert', begin: 99, end: 99 } + ]); + }); + + test('gen03', () => { + testIntervalTree([ + { type: 'insert', begin: 89, end: 96 }, + { type: 'insert', begin: 71, end: 74 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen04', () => { + testIntervalTree([ + { type: 'insert', begin: 44, end: 46 }, + { type: 'insert', begin: 85, end: 88 }, + { type: 'delete', id: 0 } + ]); + }); + + test('gen05', () => { + testIntervalTree([ + { type: 'insert', begin: 82, end: 90 }, + { type: 'insert', begin: 69, end: 73 }, + { type: 'delete', id: 0 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen06', () => { + testIntervalTree([ + { type: 'insert', begin: 41, end: 63 }, + { type: 'insert', begin: 98, end: 98 }, + { type: 'insert', begin: 47, end: 51 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen07', () => { + testIntervalTree([ + { type: 'insert', begin: 24, end: 26 }, + { type: 'insert', begin: 11, end: 28 }, + { type: 'insert', begin: 27, end: 30 }, + { type: 'insert', begin: 80, end: 85 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen08', () => { + testIntervalTree([ + { type: 'insert', begin: 100, end: 100 }, + { type: 'insert', begin: 100, end: 100 } + ]); + }); + + test('gen09', () => { + testIntervalTree([ + { type: 'insert', begin: 58, end: 65 }, + { type: 'insert', begin: 82, end: 96 }, + { type: 'insert', begin: 58, end: 65 } + ]); + }); + + test('gen10', () => { + testIntervalTree([ + { type: 'insert', begin: 32, end: 40 }, + { type: 'insert', begin: 25, end: 29 }, + { type: 'insert', begin: 24, end: 32 } + ]); + }); + + test('gen11', () => { + testIntervalTree([ + { type: 'insert', begin: 25, end: 70 }, + { type: 'insert', begin: 99, end: 100 }, + { type: 'insert', begin: 46, end: 51 }, + { type: 'insert', begin: 57, end: 57 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen12', () => { + testIntervalTree([ + { type: 'insert', begin: 20, end: 26 }, + { type: 'insert', begin: 10, end: 18 }, + { type: 'insert', begin: 99, end: 99 }, + { type: 'insert', begin: 37, end: 59 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen13', () => { + testIntervalTree([ + { type: 'insert', begin: 3, end: 91 }, + { type: 'insert', begin: 57, end: 57 }, + { type: 'insert', begin: 35, end: 44 }, + { type: 'insert', begin: 72, end: 81 }, + { type: 'delete', id: 2 } + ]); + }); + + test('gen14', () => { + testIntervalTree([ + { type: 'insert', begin: 58, end: 61 }, + { type: 'insert', begin: 34, end: 35 }, + { type: 'insert', begin: 56, end: 62 }, + { type: 'insert', begin: 69, end: 78 }, + { type: 'delete', id: 0 } + ]); + }); + + test('gen15', () => { + testIntervalTree([ + { type: 'insert', begin: 63, end: 69 }, + { type: 'insert', begin: 17, end: 24 }, + { type: 'insert', begin: 3, end: 13 }, + { type: 'insert', begin: 84, end: 94 }, + { type: 'insert', begin: 18, end: 23 }, + { type: 'insert', begin: 96, end: 98 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen16', () => { + testIntervalTree([ + { type: 'insert', begin: 27, end: 27 }, + { type: 'insert', begin: 42, end: 87 }, + { type: 'insert', begin: 42, end: 49 }, + { type: 'insert', begin: 69, end: 71 }, + { type: 'insert', begin: 20, end: 27 }, + { type: 'insert', begin: 8, end: 9 }, + { type: 'insert', begin: 42, end: 49 }, + { type: 'delete', id: 1 } + ]); + }); + + test('gen17', () => { + testIntervalTree([ + { type: 'insert', begin: 21, end: 23 }, + { type: 'insert', begin: 83, end: 87 }, + { type: 'insert', begin: 56, end: 58 }, + { type: 'insert', begin: 1, end: 55 }, + { type: 'insert', begin: 56, end: 59 }, + { type: 'insert', begin: 58, end: 60 }, + { type: 'insert', begin: 56, end: 65 }, + { type: 'delete', id: 1 }, + { type: 'delete', id: 0 }, + { type: 'delete', id: 6 } + ]); + }); }); // TEST_COUNT = 0; @@ -362,4 +399,84 @@ suite('IntervalTree', () => { return; } } + + suite('searching', () => { + + function createCormenTree(): IntervalTree { + let r = new IntervalTree(); + let data: [number, number][] = [ + [16, 21], + [8, 9], + [25, 30], + [5, 8], + [15, 23], + [17, 19], + [26, 26], + [0, 3], + [6, 10], + [19, 20] + ]; + data.forEach((int) => { + r.insert(new Interval(int[0], int[1])); + }); + return r; + } + + const T = createCormenTree(); + + function assertIntervalSearch(start: number, end: number, expected: [number, number][]): void { + let actualNodes = T.intervalSearch(new Interval(start, end)); + let actual = actualNodes.map((n) => <[number, number]>[n.resultInterval.start, n.resultInterval.end]); + assert.deepEqual(actual, expected); + } + + test('cormen 1->2', () => { + assertIntervalSearch( + 1, 2, + [ + [0, 3], + ] + ); + }); + + test('cormen 4->8', () => { + assertIntervalSearch( + 4, 8, + [ + [5, 8], + [6, 10], + [8, 9], + ] + ); + }); + + test('cormen 10->15', () => { + assertIntervalSearch( + 10, 15, + [ + [6, 10], + [15, 23], + ] + ); + }); + + test('cormen 21->25', () => { + assertIntervalSearch( + 21, 25, + [ + [15, 23], + [16, 21], + [25, 30], + ] + ); + }); + + test('cormen 24->24', () => { + assertIntervalSearch( + 24, 24, + [ + ] + ); + }); + }); }); From 8e6968f75fba6e7c5122019bb0644211a6c86337 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 12 Oct 2017 16:38:34 +0200 Subject: [PATCH 03/64] Eliminate RelativeInterval --- src/vs/editor/common/model/intervalTree.ts | 152 +++++++++------------ 1 file changed, 65 insertions(+), 87 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 60d9f8ca73d6a..b612fe64607cf 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -19,32 +19,14 @@ export class Interval { this.end = end; } - public compareToRelative(other: RelativeInterval, delta: number): number { - const otherStart = other.start + delta; + public compareToRelative(otherStart: number, otherEnd: number): number { if (this.start === otherStart) { - const otherEnd = other.end + delta; return this.end - otherEnd; } return this.start - otherStart; } } -export class RelativeInterval { - _relativeIntervalBrand: void; - - public static fromInterval(interval: Interval, delta: number): RelativeInterval { - return new RelativeInterval(interval.start - delta, interval.end - delta); - } - - public start: number; - public end: number; - - constructor(start: number, end: number) { - this.start = start; - this.end = end; - } -} - export const enum NodeColor { Red, Black @@ -57,21 +39,23 @@ export class IntervalNode { public parent: IntervalNode; public color: NodeColor; + public start: number; + public end: number; public delta: number; - public interval: RelativeInterval; public maxEnd: number; public resultInterval: Interval; - constructor(interval: RelativeInterval, delta: number) { + constructor(start: number, end: number) { this.parent = null; this.left = null; this.right = null; this.color = NodeColor.Red; - this.delta = delta; - this.interval = interval; - this.maxEnd = this.interval.end; + this.start = start; + this.end = end; + this.delta = start; + this.maxEnd = end; this.resultInterval = new Interval(0, 0); } @@ -83,7 +67,7 @@ export class IntervalNode { } } -const SENTINEL: IntervalNode = new IntervalNode(new RelativeInterval(0, 0), 0); +const SENTINEL: IntervalNode = new IntervalNode(0, 0); SENTINEL.parent = SENTINEL; SENTINEL.left = SENTINEL; SENTINEL.right = SENTINEL; @@ -93,8 +77,8 @@ function leftRotate(T: IntervalTree, x: IntervalNode): void { const y = x.right; // set y. y.delta += x.delta; // y's delta is no longer influenced by x's delta - y.interval.start += x.delta; - y.interval.end += x.delta; + y.start += x.delta; + y.end += x.delta; x.right = y.left; // turn y's left subtree into x's right subtree. if (y.left !== SENTINEL) { @@ -120,8 +104,8 @@ function rightRotate(T: IntervalTree, y: IntervalNode): void { const x = y.left; y.delta -= x.delta; - y.interval.start -= x.delta; - y.interval.end -= x.delta; + y.start -= x.delta; + y.end -= x.delta; y.left = x.right; if (x.right !== SENTINEL) { @@ -148,13 +132,12 @@ function treeInsert(T: IntervalTree, interval: Interval): IntervalNode { let z = SENTINEL; let x = T.root; while (true) { - let cmp = interval.compareToRelative(x.interval, delta); + let cmp = interval.compareToRelative(x.start + delta, x.end + delta); if (cmp < 0) { // this node should be inserted to the left // => it is not affected by the node's delta if (x.left === SENTINEL) { - const relativeInterval = RelativeInterval.fromInterval(interval, delta); - z = new IntervalNode(relativeInterval, relativeInterval.start); + z = new IntervalNode(interval.start - delta, interval.end - delta); x.left = z; break; } else { @@ -164,8 +147,7 @@ function treeInsert(T: IntervalTree, interval: Interval): IntervalNode { // this node should be inserted to the right // => it is not affected by the node's delta if (x.right === SENTINEL) { - const relativeInterval = RelativeInterval.fromInterval(interval, delta + x.delta); - z = new IntervalNode(relativeInterval, relativeInterval.start); + z = new IntervalNode(interval.start - delta - x.delta, interval.end - delta - x.delta); x.right = z; break; } else { @@ -192,12 +174,12 @@ function leftest(node: IntervalNode): IntervalNode { function resetSentinel(): void { SENTINEL.parent = SENTINEL; SENTINEL.delta = 0; // optional - SENTINEL.interval.start = 0; // optional - SENTINEL.interval.end = 0; // optional + SENTINEL.start = 0; // optional + SENTINEL.end = 0; // optional } function computeMaxEnd(node: IntervalNode): number { - let maxEnd = node.interval.end; + let maxEnd = node.end; if (node.left !== SENTINEL) { const leftMaxEnd = node.left.maxEnd; if (leftMaxEnd > maxEnd) { @@ -246,8 +228,8 @@ function treeDelete(T: IntervalTree, z: IntervalNode): void { // x's delta is no longer influenced by z's delta x.delta += z.delta; - x.interval.start += z.delta; - x.interval.end += z.delta; + x.start += z.delta; + x.end += z.delta; } else if (z.right === SENTINEL) { x = z.left; @@ -260,15 +242,12 @@ function treeDelete(T: IntervalTree, z: IntervalNode): void { // y's delta is no longer influenced by z's delta, // but we don't want to walk the entire right-hand-side subtree of x. // we therefore maintain z's delta in y, and adjust only x - x.interval.start += y.delta; - x.interval.end += y.delta; - // if (x.interval.end > x.maxEnd) { - // x.maxEnd = x.interval.end; - // } + x.start += y.delta; + x.end += y.delta; x.delta += y.delta; - y.interval.start += z.delta; - y.interval.end += z.delta; + y.start += z.delta; + y.end += z.delta; y.delta = z.delta; } @@ -443,14 +422,14 @@ export class IntervalTree { this._intervalSearch(node.left, delta, intervalStart, intervalEnd, result); } - const nodeStart = delta + node.interval.start; + const nodeStart = delta + node.start; if (nodeStart > intervalEnd) { // Cover a) from above return; } - const nodeEnd = delta + node.interval.end; + const nodeEnd = delta + node.end; if (nodeEnd >= intervalStart) { // There is overlap node.resultInterval.start = nodeStart; @@ -465,8 +444,7 @@ export class IntervalTree { public insert(interval: Interval): IntervalNode { if (this.root === SENTINEL) { - const relativeInterval = RelativeInterval.fromInterval(interval, 0); - const newNode = new IntervalNode(relativeInterval, relativeInterval.start); + const newNode = new IntervalNode(interval.start, interval.end); newNode.parent = SENTINEL; newNode.left = SENTINEL; newNode.right = SENTINEL; @@ -480,41 +458,41 @@ export class IntervalTree { recomputeMaxEndToRoot(newNode.parent); // repair tree - let node = newNode; - while (node !== this.root && node.parent.color === NodeColor.Red) { - if (node.parent === node.parent.parent.left) { - const temp = node.parent.parent.right; - - if (temp.color === NodeColor.Red) { - node.parent.color = NodeColor.Black; - temp.color = NodeColor.Black; - node.parent.parent.color = NodeColor.Red; - node = node.parent.parent; + let x = newNode; + while (x !== this.root && x.parent.color === NodeColor.Red) { + if (x.parent === x.parent.parent.left) { + const y = x.parent.parent.right; + + if (y.color === NodeColor.Red) { + x.parent.color = NodeColor.Black; + y.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + x = x.parent.parent; } else { - if (node === node.parent.right) { - node = node.parent; - leftRotate(this, node); + if (x === x.parent.right) { + x = x.parent; + leftRotate(this, x); } - node.parent.color = NodeColor.Black; - node.parent.parent.color = NodeColor.Red; - rightRotate(this, node.parent.parent); + x.parent.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + rightRotate(this, x.parent.parent); } } else { - const temp = node.parent.parent.left; + const y = x.parent.parent.left; - if (temp.color === NodeColor.Red) { - node.parent.color = NodeColor.Black; - temp.color = NodeColor.Black; - node.parent.parent.color = NodeColor.Red; - node = node.parent.parent; + if (y.color === NodeColor.Red) { + x.parent.color = NodeColor.Black; + y.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + x = x.parent.parent; } else { - if (node === node.parent.left) { - node = node.parent; - rightRotate(this, node); + if (x === x.parent.left) { + x = x.parent; + rightRotate(this, x); } - node.parent.color = NodeColor.Black; - node.parent.parent.color = NodeColor.Red; - leftRotate(this, node.parent.parent); + x.parent.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + leftRotate(this, x.parent.parent); } } } @@ -533,8 +511,8 @@ export class IntervalTree { assert(SENTINEL.parent === SENTINEL); assert(SENTINEL.left === SENTINEL); assert(SENTINEL.right === SENTINEL); - assert(SENTINEL.interval.start === 0); - assert(SENTINEL.interval.end === 0); + assert(SENTINEL.start === 0); + assert(SENTINEL.end === 0); assert(SENTINEL.delta === 0); assertValidTree(this); } @@ -542,7 +520,7 @@ export class IntervalTree { public getAllInOrder(): Interval[] { let r: Interval[] = [], rLength = 0; this.visitInOrder((n, delta) => { - r[rLength++] = new Interval(n.interval.start + delta, n.interval.end + delta); + r[rLength++] = new Interval(n.start + delta, n.end + delta); }); return r; } @@ -572,7 +550,7 @@ export class IntervalTree { } private _print(n: IntervalNode, indent: string, delta: number, out: string[]): void { - out.push(`${indent}[${n.color === NodeColor.Red ? 'R' : 'B'},${n.delta}, ${n.interval.start}->${n.interval.end}, ${n.maxEnd}] : {${delta + n.interval.start}->${delta + n.interval.end}}, maxEnd: ${n.maxEnd + delta}\n`); + out.push(`${indent}[${n.color === NodeColor.Red ? 'R' : 'B'},${n.delta}, ${n.start}->${n.end}, ${n.maxEnd}] : {${delta + n.start}->${delta + n.end}}, maxEnd: ${n.maxEnd + delta}\n`); if (n.left !== SENTINEL) { this._print(n.left, indent + ' ', delta, out); } else { @@ -612,15 +590,15 @@ function assertValidNode(n: IntervalNode, delta): void { } } - let expectedMaxEnd = n.interval.end; + let expectedMaxEnd = n.end; if (l !== SENTINEL) { - const lValue = new Interval(l.interval.start + delta, l.interval.end + delta); - assert(lValue.compareToRelative(n.interval, delta) <= 0); + const lValue = new Interval(l.start + delta, l.end + delta); + assert(lValue.compareToRelative(n.start + delta, n.end + delta) <= 0); expectedMaxEnd = Math.max(expectedMaxEnd, l.maxEnd); } if (r !== SENTINEL) { - const nValue = new Interval(n.interval.start + delta, n.interval.end + delta); - assert(nValue.compareToRelative(r.interval, delta + n.delta) <= 0); + const nValue = new Interval(n.start + delta, n.end + delta); + assert(nValue.compareToRelative(r.start + delta + n.delta, r.end + delta + n.delta) <= 0); expectedMaxEnd = Math.max(expectedMaxEnd, r.maxEnd + n.delta); } assert(n.maxEnd === expectedMaxEnd); From a324329559cb6a40bce3cbd12d1488224d682001 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 12 Oct 2017 17:15:13 +0200 Subject: [PATCH 04/64] Reorganize code --- src/vs/editor/common/model/intervalTree.ts | 529 +++++++++++---------- 1 file changed, 271 insertions(+), 258 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index b612fe64607cf..4960d1274a6f4 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -73,58 +73,188 @@ SENTINEL.left = SENTINEL; SENTINEL.right = SENTINEL; SENTINEL.color = NodeColor.Black; -function leftRotate(T: IntervalTree, x: IntervalNode): void { - const y = x.right; // set y. +export class IntervalTree { - y.delta += x.delta; // y's delta is no longer influenced by x's delta - y.start += x.delta; - y.end += x.delta; + public root: IntervalNode; - x.right = y.left; // turn y's left subtree into x's right subtree. - if (y.left !== SENTINEL) { - y.left.parent = x; + constructor() { + this.root = SENTINEL; } - y.parent = x.parent; // link x's parent to y. - if (x.parent === SENTINEL) { - T.root = y; - } else if (x === x.parent.left) { - x.parent.left = y; - } else { - x.parent.right = y; + + public intervalSearch(interval: Interval): IntervalNode[] { + if (this.root === SENTINEL) { + return []; + } + let result: IntervalNode[] = []; + intervalSearchRecursive(this.root, 0, interval.start, interval.end, result); + return result; } - y.left = x; // put x on y's left. - x.parent = y; + public insert(interval: Interval): IntervalNode { + return rbTreeInsert(this, interval); + } - recomputeMaxEnd(x); - recomputeMaxEnd(y); + public delete(node: IntervalNode) { + rbTreeDelete(this, node); + } + + public assertInvariants(): void { + assert(SENTINEL.color === NodeColor.Black); + assert(SENTINEL.parent === SENTINEL); + assert(SENTINEL.left === SENTINEL); + assert(SENTINEL.right === SENTINEL); + assert(SENTINEL.start === 0); + assert(SENTINEL.end === 0); + assert(SENTINEL.delta === 0); + assertValidTree(this); + } + + public getAllInOrder(): Interval[] { + let r: Interval[] = [], rLength = 0; + this.visitInOrder((n, delta) => { + r[rLength++] = new Interval(n.start + delta, n.end + delta); + }); + return r; + } + + public visitInOrder(visitor: (n: IntervalNode, delta: number) => void): void { + this._visitInOrder(this.root, 0, visitor); + } + + private _visitInOrder(n: IntervalNode, delta: number, visitor: (n: IntervalNode, delta: number) => void): void { + if (n.left !== SENTINEL) { + this._visitInOrder(n.left, delta, visitor); + } + + if (n !== SENTINEL) { + visitor(n, delta); + } + + if (n.right !== SENTINEL) { + this._visitInOrder(n.right, delta + n.delta, visitor); + } + } + + public print(): void { + let out: string[] = []; + this._print(this.root, '', 0, out); + console.log(out.join('')); + } + + private _print(n: IntervalNode, indent: string, delta: number, out: string[]): void { + out.push(`${indent}[${n.color === NodeColor.Red ? 'R' : 'B'},${n.delta}, ${n.start}->${n.end}, ${n.maxEnd}] : {${delta + n.start}->${delta + n.end}}, maxEnd: ${n.maxEnd + delta}\n`); + if (n.left !== SENTINEL) { + this._print(n.left, indent + ' ', delta, out); + } else { + out.push(`${indent} NIL\n`); + } + if (n.right !== SENTINEL) { + this._print(n.right, indent + ' ', delta + n.delta, out); + } else { + out.push(`${indent} NIL\n`); + } + } } -function rightRotate(T: IntervalTree, y: IntervalNode): void { - const x = y.left; +//#region Searching +function intervalSearchRecursive(node: IntervalNode, delta: number, intervalStart: number, intervalEnd: number, result: IntervalNode[]): void { + // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree + // Now, it is known that two intervals A and B overlap only when both + // A.low <= B.high and A.high >= B.low. When searching the trees for + // nodes overlapping with a given interval, you can immediately skip: + // a) all nodes to the right of nodes whose low value is past the end of the given interval. + // b) all nodes that have their maximum 'high' value below the start of the given interval. + + const nodeMaxEnd = delta + node.maxEnd; + if (nodeMaxEnd < intervalStart) { + // Cover b) from above + return; + } - y.delta -= x.delta; - y.start -= x.delta; - y.end -= x.delta; + if (node.left !== SENTINEL) { + intervalSearchRecursive(node.left, delta, intervalStart, intervalEnd, result); + } - y.left = x.right; - if (x.right !== SENTINEL) { - x.right.parent = y; + const nodeStart = delta + node.start; + + if (nodeStart > intervalEnd) { + // Cover a) from above + return; } - x.parent = y.parent; - if (y.parent === SENTINEL) { - T.root = x; - } else if (y === y.parent.right) { - y.parent.right = x; - } else { - y.parent.left = x; + + const nodeEnd = delta + node.end; + if (nodeEnd >= intervalStart) { + // There is overlap + node.resultInterval.start = nodeStart; + node.resultInterval.end = nodeEnd; + result.push(node); } - x.right = y; - y.parent = x; + if (node.right !== SENTINEL) { + intervalSearchRecursive(node.right, delta + node.delta, intervalStart, intervalEnd, result); + } +} +//#endregion - recomputeMaxEnd(y); - recomputeMaxEnd(x); +//#region Insertion +function rbTreeInsert(T: IntervalTree, interval: Interval): IntervalNode { + if (T.root === SENTINEL) { + const newNode = new IntervalNode(interval.start, interval.end); + newNode.parent = SENTINEL; + newNode.left = SENTINEL; + newNode.right = SENTINEL; + newNode.color = NodeColor.Black; + T.root = newNode; + return T.root; + } + + const newNode = treeInsert(T, interval); + + recomputeMaxEndWalkToRoot(newNode.parent); + + // repair tree + let x = newNode; + while (x !== T.root && x.parent.color === NodeColor.Red) { + if (x.parent === x.parent.parent.left) { + const y = x.parent.parent.right; + + if (y.color === NodeColor.Red) { + x.parent.color = NodeColor.Black; + y.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + x = x.parent.parent; + } else { + if (x === x.parent.right) { + x = x.parent; + leftRotate(T, x); + } + x.parent.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + rightRotate(T, x.parent.parent); + } + } else { + const y = x.parent.parent.left; + + if (y.color === NodeColor.Red) { + x.parent.color = NodeColor.Black; + y.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + x = x.parent.parent; + } else { + if (x === x.parent.left) { + x = x.parent; + rightRotate(T, x); + } + x.parent.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + leftRotate(T, x.parent.parent); + } + } + } + + T.root.color = NodeColor.Black; + + return newNode; } function treeInsert(T: IntervalTree, interval: Interval): IntervalNode { @@ -163,58 +293,10 @@ function treeInsert(T: IntervalTree, interval: Interval): IntervalNode { z.color = NodeColor.Red; return z; } +//#endregion -function leftest(node: IntervalNode): IntervalNode { - while (node.left !== SENTINEL) { - node = node.left; - } - return node; -} - -function resetSentinel(): void { - SENTINEL.parent = SENTINEL; - SENTINEL.delta = 0; // optional - SENTINEL.start = 0; // optional - SENTINEL.end = 0; // optional -} - -function computeMaxEnd(node: IntervalNode): number { - let maxEnd = node.end; - if (node.left !== SENTINEL) { - const leftMaxEnd = node.left.maxEnd; - if (leftMaxEnd > maxEnd) { - maxEnd = leftMaxEnd; - } - } - if (node.right !== SENTINEL) { - const rightMaxEnd = node.right.maxEnd + node.delta; - if (rightMaxEnd > maxEnd) { - maxEnd = rightMaxEnd; - } - } - return maxEnd; -} - -function recomputeMaxEnd(node: IntervalNode): void { - node.maxEnd = computeMaxEnd(node); -} - -function recomputeMaxEndToRoot(node: IntervalNode): void { - while (node !== SENTINEL) { - - const maxEnd = computeMaxEnd(node); - - if (node.maxEnd === maxEnd) { - // no need to go further - return; - } - - node.maxEnd = maxEnd; - node = node.parent; - } -} - -function treeDelete(T: IntervalTree, z: IntervalNode): void { +//#region Deletion +function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { let x: IntervalNode; let y: IntervalNode; @@ -305,20 +387,20 @@ function treeDelete(T: IntervalTree, z: IntervalNode): void { z.detach(); if (yWasRed) { - recomputeMaxEndToRoot(x.parent); + recomputeMaxEndWalkToRoot(x.parent); if (y !== z) { - recomputeMaxEndToRoot(y); - recomputeMaxEndToRoot(y.parent); + recomputeMaxEndWalkToRoot(y); + recomputeMaxEndWalkToRoot(y.parent); } resetSentinel(); return; } - recomputeMaxEndToRoot(x); - recomputeMaxEndToRoot(x.parent); + recomputeMaxEndWalkToRoot(x); + recomputeMaxEndWalkToRoot(x.parent); if (y !== z) { - recomputeMaxEndToRoot(y); - recomputeMaxEndToRoot(y.parent); + recomputeMaxEndWalkToRoot(y); + recomputeMaxEndWalkToRoot(y.parent); } // RB-DELETE-FIXUP @@ -388,182 +470,119 @@ function treeDelete(T: IntervalTree, z: IntervalNode): void { resetSentinel(); } -export class IntervalTree { - - public root: IntervalNode; - - constructor() { - this.root = SENTINEL; - } - - public intervalSearch(interval: Interval): IntervalNode[] { - let result: IntervalNode[] = []; - if (this.root !== SENTINEL) { - this._intervalSearch(this.root, 0, interval.start, interval.end, result); - } - return result; +function leftest(node: IntervalNode): IntervalNode { + while (node.left !== SENTINEL) { + node = node.left; } + return node; +} - private _intervalSearch(node: IntervalNode, delta: number, intervalStart: number, intervalEnd: number, result: IntervalNode[]): void { - // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree - // Now, it is known that two intervals A and B overlap only when both - // A.low <= B.high and A.high >= B.low. When searching the trees for - // nodes overlapping with a given interval, you can immediately skip: - // a) all nodes to the right of nodes whose low value is past the end of the given interval. - // b) all nodes that have their maximum 'high' value below the start of the given interval. - - const nodeMaxEnd = delta + node.maxEnd; - if (nodeMaxEnd < intervalStart) { - // Cover b) from above - return; - } - - if (node.left !== SENTINEL) { - this._intervalSearch(node.left, delta, intervalStart, intervalEnd, result); - } - - const nodeStart = delta + node.start; +function resetSentinel(): void { + SENTINEL.parent = SENTINEL; + SENTINEL.delta = 0; // optional + SENTINEL.start = 0; // optional + SENTINEL.end = 0; // optional +} +//#endregion - if (nodeStart > intervalEnd) { - // Cover a) from above - return; - } +//#region Rotations +function leftRotate(T: IntervalTree, x: IntervalNode): void { + const y = x.right; // set y. - const nodeEnd = delta + node.end; - if (nodeEnd >= intervalStart) { - // There is overlap - node.resultInterval.start = nodeStart; - node.resultInterval.end = nodeEnd; - result.push(node); - } + y.delta += x.delta; // y's delta is no longer influenced by x's delta + y.start += x.delta; + y.end += x.delta; - if (node.right !== SENTINEL) { - this._intervalSearch(node.right, delta + node.delta, intervalStart, intervalEnd, result); - } + x.right = y.left; // turn y's left subtree into x's right subtree. + if (y.left !== SENTINEL) { + y.left.parent = x; + } + y.parent = x.parent; // link x's parent to y. + if (x.parent === SENTINEL) { + T.root = y; + } else if (x === x.parent.left) { + x.parent.left = y; + } else { + x.parent.right = y; } - public insert(interval: Interval): IntervalNode { - if (this.root === SENTINEL) { - const newNode = new IntervalNode(interval.start, interval.end); - newNode.parent = SENTINEL; - newNode.left = SENTINEL; - newNode.right = SENTINEL; - newNode.color = NodeColor.Black; - this.root = newNode; - return this.root; - } + y.left = x; // put x on y's left. + x.parent = y; - const newNode = treeInsert(this, interval); - - recomputeMaxEndToRoot(newNode.parent); - - // repair tree - let x = newNode; - while (x !== this.root && x.parent.color === NodeColor.Red) { - if (x.parent === x.parent.parent.left) { - const y = x.parent.parent.right; - - if (y.color === NodeColor.Red) { - x.parent.color = NodeColor.Black; - y.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - x = x.parent.parent; - } else { - if (x === x.parent.right) { - x = x.parent; - leftRotate(this, x); - } - x.parent.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - rightRotate(this, x.parent.parent); - } - } else { - const y = x.parent.parent.left; - - if (y.color === NodeColor.Red) { - x.parent.color = NodeColor.Black; - y.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - x = x.parent.parent; - } else { - if (x === x.parent.left) { - x = x.parent; - rightRotate(this, x); - } - x.parent.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; - leftRotate(this, x.parent.parent); - } - } - } + recomputeMaxEnd(x); + recomputeMaxEnd(y); +} - this.root.color = NodeColor.Black; +function rightRotate(T: IntervalTree, y: IntervalNode): void { + const x = y.left; - return newNode; - } + y.delta -= x.delta; + y.start -= x.delta; + y.end -= x.delta; - public delete(node: IntervalNode) { - treeDelete(this, node); + y.left = x.right; + if (x.right !== SENTINEL) { + x.right.parent = y; } - - public assertInvariants(): void { - assert(SENTINEL.color === NodeColor.Black); - assert(SENTINEL.parent === SENTINEL); - assert(SENTINEL.left === SENTINEL); - assert(SENTINEL.right === SENTINEL); - assert(SENTINEL.start === 0); - assert(SENTINEL.end === 0); - assert(SENTINEL.delta === 0); - assertValidTree(this); + x.parent = y.parent; + if (y.parent === SENTINEL) { + T.root = x; + } else if (y === y.parent.right) { + y.parent.right = x; + } else { + y.parent.left = x; } - public getAllInOrder(): Interval[] { - let r: Interval[] = [], rLength = 0; - this.visitInOrder((n, delta) => { - r[rLength++] = new Interval(n.start + delta, n.end + delta); - }); - return r; - } + x.right = y; + y.parent = x; - public visitInOrder(visitor: (n: IntervalNode, delta: number) => void): void { - this._visitInOrder(this.root, 0, visitor); - } + recomputeMaxEnd(y); + recomputeMaxEnd(x); +} +//#endregion - private _visitInOrder(n: IntervalNode, delta: number, visitor: (n: IntervalNode, delta: number) => void): void { - if (n.left !== SENTINEL) { - this._visitInOrder(n.left, delta, visitor); - } +//#region max end computation - if (n !== SENTINEL) { - visitor(n, delta); +function computeMaxEnd(node: IntervalNode): number { + let maxEnd = node.end; + if (node.left !== SENTINEL) { + const leftMaxEnd = node.left.maxEnd; + if (leftMaxEnd > maxEnd) { + maxEnd = leftMaxEnd; } - - if (n.right !== SENTINEL) { - this._visitInOrder(n.right, delta + n.delta, visitor); + } + if (node.right !== SENTINEL) { + const rightMaxEnd = node.right.maxEnd + node.delta; + if (rightMaxEnd > maxEnd) { + maxEnd = rightMaxEnd; } } + return maxEnd; +} - public print(): void { - let out: string[] = []; - this._print(this.root, '', 0, out); - console.log(out.join('')); - } +function recomputeMaxEnd(node: IntervalNode): void { + node.maxEnd = computeMaxEnd(node); +} - private _print(n: IntervalNode, indent: string, delta: number, out: string[]): void { - out.push(`${indent}[${n.color === NodeColor.Red ? 'R' : 'B'},${n.delta}, ${n.start}->${n.end}, ${n.maxEnd}] : {${delta + n.start}->${delta + n.end}}, maxEnd: ${n.maxEnd + delta}\n`); - if (n.left !== SENTINEL) { - this._print(n.left, indent + ' ', delta, out); - } else { - out.push(`${indent} NIL\n`); - } - if (n.right !== SENTINEL) { - this._print(n.right, indent + ' ', delta + n.delta, out); - } else { - out.push(`${indent} NIL\n`); +function recomputeMaxEndWalkToRoot(node: IntervalNode): void { + while (node !== SENTINEL) { + + const maxEnd = computeMaxEnd(node); + + if (node.maxEnd === maxEnd) { + // no need to go further + return; } + + node.maxEnd = maxEnd; + node = node.parent; } } +//#endregion + +//#region Assertion + function depth(n: IntervalNode): number { if (n === SENTINEL) { // The leafs are black @@ -582,12 +601,8 @@ function assertValidNode(n: IntervalNode, delta): void { let r = n.right; if (n.color === NodeColor.Red) { - if (l.color !== NodeColor.Black) { - assert(false); - } - if (r.color !== NodeColor.Black) { - assert(false); - } + assert(l.color === NodeColor.Black); + assert(r.color === NodeColor.Black); } let expectedMaxEnd = n.end; @@ -611,12 +626,8 @@ function assertValidTree(tree: IntervalTree): void { if (tree.root === SENTINEL) { return; } - if (tree.root.color !== NodeColor.Black) { - assert(false); - } - if (depth(tree.root.left) !== depth(tree.root.right)) { - assert(false); - } + assert(tree.root.color === NodeColor.Black); + assert(depth(tree.root.left) === depth(tree.root.right)); assertValidNode(tree.root, 0); } @@ -625,3 +636,5 @@ function assert(condition: boolean): void { throw new Error('Assertion violation'); } } + +//#endregion From d902ab8a84dd0978841d2fbcab6db5a1ee49c212 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 12 Oct 2017 17:44:29 +0200 Subject: [PATCH 05/64] Simplify IModelDecorationsChangedEvent --- src/vs/editor/common/model/textModelEvents.ts | 8 ---- .../common/model/textModelWithDecorations.ts | 41 ++++++++----------- .../common/viewModel/viewModelDecorations.ts | 20 +-------- .../common/model/modelDecorations.test.ts | 9 ---- src/vs/monaco.d.ts | 8 ---- 5 files changed, 18 insertions(+), 68 deletions(-) diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index 2acaf260be289..a4c73b4cb1395 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -88,18 +88,10 @@ export interface IModelContentChangedEvent { * An event describing that model decorations have changed. */ export interface IModelDecorationsChangedEvent { - /** - * Lists of ids for added decorations. - */ - readonly addedDecorations: string[]; /** * Lists of ids for changed decorations. */ readonly changedDecorations: string[]; - /** - * List of ids for removed decorations. - */ - readonly removedDecorations: string[]; } /** diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index d86288a6b2cf9..fc5da7ad38015 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -26,30 +26,27 @@ export const ClassName = { class DecorationsTracker { - public addedDecorations: string[]; - public addedDecorationsLen: number; + public didAddDecorations: boolean; + public didRemoveDecorations: boolean; + public changedDecorations: string[]; public changedDecorationsLen: number; - public removedDecorations: string[]; - public removedDecorationsLen: number; constructor() { - this.addedDecorations = []; - this.addedDecorationsLen = 0; + this.didAddDecorations = false; + this.didRemoveDecorations = false; this.changedDecorations = []; this.changedDecorationsLen = 0; - this.removedDecorations = []; - this.removedDecorationsLen = 0; } // --- Build decoration events - public addNewDecoration(id: string): void { - this.addedDecorations[this.addedDecorationsLen++] = id; + public markDidAddDecorations(): void { + this.didAddDecorations = true; } - public addRemovedDecoration(id: string): void { - this.removedDecorations[this.removedDecorationsLen++] = id; + public markDidRemoveDecorations(): void { + this.didRemoveDecorations = true; } public addMovedDecoration(id: string): void { @@ -494,9 +491,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed if (uniqueChangedDecorations.length > 0) { let e: textModelEvents.IModelDecorationsChangedEvent = { - addedDecorations: [], - changedDecorations: uniqueChangedDecorations, - removedDecorations: [] + changedDecorations: uniqueChangedDecorations }; this.emitModelDecorationsChangedEvent(e); } @@ -532,17 +527,15 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed private _handleTrackedDecorations(decorationsTracker: DecorationsTracker): void { if ( - decorationsTracker.addedDecorationsLen === 0 + !decorationsTracker.didAddDecorations && decorationsTracker.changedDecorationsLen === 0 - && decorationsTracker.removedDecorationsLen === 0 + && !decorationsTracker.didRemoveDecorations ) { return; } let e: textModelEvents.IModelDecorationsChangedEvent = { - addedDecorations: decorationsTracker.addedDecorations, - changedDecorations: decorationsTracker.changedDecorations, - removedDecorations: decorationsTracker.removedDecorations + changedDecorations: decorationsTracker.changedDecorations }; this.emitModelDecorationsChangedEvent(e); } @@ -592,7 +585,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed this._multiLineDecorationsMap[decorationId] = decoration; } - decorationsTracker.addNewDecoration(decorationId); + decorationsTracker.markDidAddDecorations(); return decorationId; } @@ -643,7 +636,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed this._multiLineDecorationsMap[decorationId] = decoration; } - decorationsTracker.addNewDecoration(decorationId); + decorationsTracker.markDidAddDecorations(); } return decorationIds; @@ -705,7 +698,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed delete this._internalDecorations[decoration.internalId]; if (decorationsTracker) { - decorationsTracker.addRemovedDecoration(decorationId); + decorationsTracker.markDidRemoveDecorations(); } } @@ -720,7 +713,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } if (decorationsTracker) { - decorationsTracker.addRemovedDecoration(decorationId); + decorationsTracker.markDidRemoveDecorations(); } removeMarkers[removeMarkersLen++] = decoration.startMarker; diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index f4311681f8214..9a5f7b38e7629 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -59,25 +59,7 @@ export class ViewModelDecorations implements IDisposable { } public onModelDecorationsChanged(e: IModelDecorationsChangedEvent): void { - let changedDecorations = e.changedDecorations; - for (let i = 0, len = changedDecorations.length; i < len; i++) { - let changedDecoration = changedDecorations[i]; - let myDecoration = this._decorationsCache[changedDecoration]; - if (!myDecoration) { - continue; - } - - myDecoration.range = null; - } - - let removedDecorations = e.removedDecorations; - if (this._decorationsCache !== null && this._decorationsCache !== undefined) { - for (let i = 0, len = removedDecorations.length; i < len; i++) { - let removedDecoration = removedDecorations[i]; - delete this._decorationsCache[removedDecoration]; - } - } - + this._decorationsCache = Object.create(null); this._clearCachedModelDecorationsResolver(); } diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 1b2264090f0e6..e8fd7a07654e8 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -207,9 +207,7 @@ suite('Editor Model - Model Decorations', () => { let listenerCalled = 0; thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.addedDecorations.length, 1); assert.equal(e.changedDecorations.length, 0); - assert.equal(e.removedDecorations.length, 0); }); addDecoration(thisModel, 1, 2, 3, 2, 'myType'); assert.equal(listenerCalled, 1, 'listener called'); @@ -220,10 +218,8 @@ suite('Editor Model - Model Decorations', () => { let decId = addDecoration(thisModel, 1, 2, 3, 2, 'myType'); thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.addedDecorations.length, 0); assert.equal(e.changedDecorations.length, 1); assert.equal(e.changedDecorations[0], decId); - assert.equal(e.removedDecorations.length, 0); }); thisModel.changeDecorations((changeAccessor) => { changeAccessor.changeDecoration(decId, new Range(1, 1, 1, 2)); @@ -236,10 +232,7 @@ suite('Editor Model - Model Decorations', () => { let decId = addDecoration(thisModel, 1, 2, 3, 2, 'myType'); thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.addedDecorations.length, 0); assert.equal(e.changedDecorations.length, 0); - assert.equal(e.removedDecorations.length, 1); - assert.equal(e.removedDecorations[0], decId); }); thisModel.changeDecorations((changeAccessor) => { changeAccessor.removeDecoration(decId); @@ -253,10 +246,8 @@ suite('Editor Model - Model Decorations', () => { thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.addedDecorations.length, 0); assert.equal(e.changedDecorations.length, 1); assert.equal(e.changedDecorations[0], decId); - assert.equal(e.removedDecorations.length, 0); }); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'Hallo ')]); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index c43c2f05b3471..902254d86d212 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2482,18 +2482,10 @@ declare module monaco.editor { * An event describing that model decorations have changed. */ export interface IModelDecorationsChangedEvent { - /** - * Lists of ids for added decorations. - */ - readonly addedDecorations: string[]; /** * Lists of ids for changed decorations. */ readonly changedDecorations: string[]; - /** - * List of ids for removed decorations. - */ - readonly removedDecorations: string[]; } /** From 6023aee90c9c09ea9a70aea81ab40827aa39e8d7 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 12 Oct 2017 18:08:23 +0200 Subject: [PATCH 06/64] Reduce usages of IModelDecorationsChangedEvent.changedDecorations --- .../browser/referencesWidget.ts | 17 ++++------ .../debug/browser/debugEditorModelManager.ts | 34 +++++++++++++------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts index 0d299ca85af65..bc0f080836d13 100644 --- a/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts @@ -108,20 +108,15 @@ class DecorationsManager implements IDisposable { } private _onDecorationChanged(event: IModelDecorationsChangedEvent): void { - const changedDecorations = event.changedDecorations, - toRemove: string[] = []; + const toRemove: string[] = []; - for (let i = 0, len = changedDecorations.length; i < len; i++) { - let reference = this._decorations.get(changedDecorations[i]); - if (!reference) { - continue; - } + this._decorations.forEach((reference, decorationId) => { + const newRange = this._editor.getModel().getDecorationRange(decorationId); - const newRange = this._editor.getModel().getDecorationRange(changedDecorations[i]); let ignore = false; if (Range.equalsRange(newRange, reference.range)) { - continue; + return; } else if (Range.spansMultipleLines(newRange)) { ignore = true; @@ -137,11 +132,11 @@ class DecorationsManager implements IDisposable { if (ignore) { this._decorationIgnoreSet.add(reference.id); - toRemove.push(changedDecorations[i]); + toRemove.push(decorationId); } else { reference.range = newRange; } - } + }); this._editor.changeDecorations((accessor) => { for (let i = 0, len = toRemove.length; i < len; i++) { diff --git a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts index 6fd5d7c1c2031..10fb1886dfd99 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts @@ -21,7 +21,7 @@ interface IDebugEditorModelData { toDispose: lifecycle.IDisposable[]; breakpointDecorationIds: string[]; breakpointLines: number[]; - breakpointDecorationsAsMap: Map; + breakpointDecorationsAsMap: Map; currentStackDecorations: string[]; dirty: boolean; topStackFrameRange: Range; @@ -81,11 +81,12 @@ export class DebugEditorModelManager implements IWorkbenchContribution { const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp.uri.toString() === modelUrlStr); const currentStackDecorations = model.deltaDecorations([], this.createCallStackDecorations(modelUrlStr)); - const breakPointDecorations = model.deltaDecorations([], this.createBreakpointDecorations(breakpoints)); + const desiredDecorations = this.createBreakpointDecorations(model, breakpoints); + const breakPointDecorations = model.deltaDecorations([], desiredDecorations); const toDispose: lifecycle.IDisposable[] = [model.onDidChangeDecorations((e) => this.onModelDecorationsChanged(modelUrlStr, e))]; - const breakpointDecorationsAsMap = new Map(); - breakPointDecorations.forEach(bpd => breakpointDecorationsAsMap.set(bpd, true)); + const breakpointDecorationsAsMap = new Map(); + breakPointDecorations.forEach((decorationId, index) => breakpointDecorationsAsMap.set(decorationId, desiredDecorations[index].range)); this.modelDataMap.set(modelUrlStr, { model: model, @@ -191,7 +192,17 @@ export class DebugEditorModelManager implements IWorkbenchContribution { // I have no decorations return; } - if (!e.changedDecorations.some(decorationId => modelData.breakpointDecorationsAsMap.has(decorationId))) { + let somethingChanged = false; + modelData.breakpointDecorationsAsMap.forEach((breakpointRange, decorationId) => { + if (somethingChanged) { + return; + } + const newBreakpointRange = modelData.model.getDecorationRange(decorationId); + if (newBreakpointRange && !breakpointRange.equalsRange(newBreakpointRange)) { + somethingChanged = true; + } + }); + if (!somethingChanged) { // nothing to do, my decorations did not change. return; } @@ -254,16 +265,19 @@ export class DebugEditorModelManager implements IWorkbenchContribution { } private updateBreakpoints(modelData: IDebugEditorModelData, newBreakpoints: IBreakpoint[]): void { - modelData.breakpointDecorationIds = modelData.model.deltaDecorations(modelData.breakpointDecorationIds, this.createBreakpointDecorations(newBreakpoints)); + const desiredDecorations = this.createBreakpointDecorations(modelData.model, newBreakpoints); + modelData.breakpointDecorationIds = modelData.model.deltaDecorations(modelData.breakpointDecorationIds, desiredDecorations); modelData.breakpointDecorationsAsMap.clear(); - modelData.breakpointDecorationIds.forEach(id => modelData.breakpointDecorationsAsMap.set(id, true)); + modelData.breakpointDecorationIds.forEach((decorationId, index) => modelData.breakpointDecorationsAsMap.set(decorationId, desiredDecorations[index].range)); modelData.breakpointLines = newBreakpoints.map(bp => bp.lineNumber); } - private createBreakpointDecorations(breakpoints: IBreakpoint[]): IModelDeltaDecoration[] { + private createBreakpointDecorations(model: IModel, breakpoints: IBreakpoint[]): { range: Range; options: IModelDecorationOptions; }[] { return breakpoints.map((breakpoint) => { - const range = breakpoint.column ? new Range(breakpoint.lineNumber, breakpoint.column, breakpoint.lineNumber, breakpoint.column + 1) - : new Range(breakpoint.lineNumber, 1, breakpoint.lineNumber, Constants.MAX_SAFE_SMALL_INTEGER); // Decoration has to have a width #20688 + const range = model.validateRange( + breakpoint.column ? new Range(breakpoint.lineNumber, breakpoint.column, breakpoint.lineNumber, breakpoint.column + 1) + : new Range(breakpoint.lineNumber, 1, breakpoint.lineNumber, Constants.MAX_SAFE_SMALL_INTEGER) // Decoration has to have a width #20688 + ); return { options: this.getBreakpointDecorationOptions(breakpoint), range From a47159bad1c1d0abdc5aae35b2f6f76524cfc869 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 12 Oct 2017 18:31:42 +0200 Subject: [PATCH 07/64] Eliminate IModelDecorationsChangedEvent.changedDecorations --- src/vs/editor/common/model/textModelEvents.ts | 4 -- .../common/model/textModelWithDecorations.ts | 43 +++++++------------ .../common/viewModel/viewModelDecorations.ts | 3 +- .../editor/common/viewModel/viewModelImpl.ts | 3 +- .../browser/referencesWidget.ts | 5 +-- .../common/model/modelDecorations.test.ts | 8 +--- src/vs/monaco.d.ts | 4 -- .../debug/browser/debugEditorModelManager.ts | 5 +-- 8 files changed, 22 insertions(+), 53 deletions(-) diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index a4c73b4cb1395..aaa66c7beabf5 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -88,10 +88,6 @@ export interface IModelContentChangedEvent { * An event describing that model decorations have changed. */ export interface IModelDecorationsChangedEvent { - /** - * Lists of ids for changed decorations. - */ - readonly changedDecorations: string[]; } /** diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index fc5da7ad38015..9ed9c9a16e6a8 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -28,15 +28,12 @@ class DecorationsTracker { public didAddDecorations: boolean; public didRemoveDecorations: boolean; - - public changedDecorations: string[]; - public changedDecorationsLen: number; + public didChangeDecorations: boolean; constructor() { this.didAddDecorations = false; this.didRemoveDecorations = false; - this.changedDecorations = []; - this.changedDecorationsLen = 0; + this.didChangeDecorations = false; } // --- Build decoration events @@ -49,12 +46,8 @@ class DecorationsTracker { this.didRemoveDecorations = true; } - public addMovedDecoration(id: string): void { - this.changedDecorations[this.changedDecorationsLen++] = id; - } - - public addUpdatedDecoration(id: string): void { - this.changedDecorations[this.changedDecorationsLen++] = id; + public markDidChangeDecorations(): void { + this.didChangeDecorations = true; } } @@ -456,7 +449,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } /** - * Handle changed markers (i.e. update decorations ranges and return the changed decorations, unique and sorted by id) + * Handle changed markers (i.e. update decorations ranges) */ private _handleTrackedMarkers(markersTracker: MarkersTracker): void { let changedInternalDecorationIds = markersTracker.getDecorationIds(); @@ -466,8 +459,8 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed changedInternalDecorationIds.sort(); - let uniqueChangedDecorations: string[] = [], uniqueChangedDecorationsLen = 0; let previousInternalDecorationId: number = 0; + let somethingChanged = false; for (let i = 0, len = changedInternalDecorationIds.length; i < len; i++) { let internalDecorationId = changedInternalDecorationIds[i]; if (internalDecorationId === previousInternalDecorationId) { @@ -485,15 +478,11 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed let endMarker = decoration.endMarker.position; let range = TextModelWithDecorations._createRangeFromMarkers(startMarker, endMarker); decoration.setRange(this._multiLineDecorationsMap, range); - - uniqueChangedDecorations[uniqueChangedDecorationsLen++] = decoration.id; + somethingChanged = true; } - if (uniqueChangedDecorations.length > 0) { - let e: textModelEvents.IModelDecorationsChangedEvent = { - changedDecorations: uniqueChangedDecorations - }; - this.emitModelDecorationsChangedEvent(e); + if (somethingChanged) { + this.emitModelDecorationsChangedEvent(); } } @@ -528,20 +517,18 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed private _handleTrackedDecorations(decorationsTracker: DecorationsTracker): void { if ( !decorationsTracker.didAddDecorations - && decorationsTracker.changedDecorationsLen === 0 + && !decorationsTracker.didChangeDecorations && !decorationsTracker.didRemoveDecorations ) { return; } - let e: textModelEvents.IModelDecorationsChangedEvent = { - changedDecorations: decorationsTracker.changedDecorations - }; - this.emitModelDecorationsChangedEvent(e); + this.emitModelDecorationsChangedEvent(); } - private emitModelDecorationsChangedEvent(e: textModelEvents.IModelDecorationsChangedEvent): void { + private emitModelDecorationsChangedEvent(): void { if (!this._isDisposing) { + let e: textModelEvents.IModelDecorationsChangedEvent = {}; this._eventEmitter.emit(textModelEvents.TextModelEventType.ModelDecorationsChanged, e); } } @@ -666,7 +653,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed decoration.setRange(this._multiLineDecorationsMap, newRange); - decorationsTracker.addMovedDecoration(decorationId); + decorationsTracker.markDidChangeDecorations(); } private _changeDecorationOptionsImpl(decorationsTracker: DecorationsTracker, decorationId: string, options: ModelDecorationOptions): void { @@ -682,7 +669,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed decoration.setOptions(options); - decorationsTracker.addUpdatedDecoration(decorationId); + decorationsTracker.markDidChangeDecorations(); } private _removeDecorationImpl(decorationsTracker: DecorationsTracker, decorationId: string): void { diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index 9a5f7b38e7629..59205a10171d1 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -9,7 +9,6 @@ import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { InlineDecoration, ViewModelDecoration, ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; -import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; export interface IDecorationsViewportData { /** @@ -58,7 +57,7 @@ export class ViewModelDecorations implements IDisposable { this._clearCachedModelDecorationsResolver(); } - public onModelDecorationsChanged(e: IModelDecorationsChangedEvent): void { + public onModelDecorationsChanged(): void { this._decorationsCache = Object.create(null); this._clearCachedModelDecorationsResolver(); } diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index f1a7fcbf961b3..68cdaf1ccbcad 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -282,8 +282,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel break; } case textModelEvents.TextModelEventType.ModelDecorationsChanged: { - const e = data; - this.decorations.onModelDecorationsChanged(e); + this.decorations.onModelDecorationsChanged(); eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); break; } diff --git a/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts index bc0f080836d13..42aaeb2fce05b 100644 --- a/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts @@ -38,7 +38,6 @@ import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/r import { registerColor, activeContrastBorder, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant, ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { attachListStyler, attachBadgeStyler } from 'vs/platform/theme/common/styler'; -import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; @@ -81,7 +80,7 @@ class DecorationsManager implements IDisposable { } private _addDecorations(reference: FileReferences): void { - this._callOnModelChange.push(this._editor.getModel().onDidChangeDecorations((event) => this._onDecorationChanged(event))); + this._callOnModelChange.push(this._editor.getModel().onDidChangeDecorations((event) => this._onDecorationChanged())); this._editor.changeDecorations(accessor => { @@ -107,7 +106,7 @@ class DecorationsManager implements IDisposable { }); } - private _onDecorationChanged(event: IModelDecorationsChangedEvent): void { + private _onDecorationChanged(): void { const toRemove: string[] = []; this._decorations.forEach((reference, decorationId) => { diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index e8fd7a07654e8..84defed4adaec 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -207,7 +207,6 @@ suite('Editor Model - Model Decorations', () => { let listenerCalled = 0; thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.changedDecorations.length, 0); }); addDecoration(thisModel, 1, 2, 3, 2, 'myType'); assert.equal(listenerCalled, 1, 'listener called'); @@ -218,8 +217,6 @@ suite('Editor Model - Model Decorations', () => { let decId = addDecoration(thisModel, 1, 2, 3, 2, 'myType'); thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.changedDecorations.length, 1); - assert.equal(e.changedDecorations[0], decId); }); thisModel.changeDecorations((changeAccessor) => { changeAccessor.changeDecoration(decId, new Range(1, 1, 1, 2)); @@ -232,7 +229,6 @@ suite('Editor Model - Model Decorations', () => { let decId = addDecoration(thisModel, 1, 2, 3, 2, 'myType'); thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.changedDecorations.length, 0); }); thisModel.changeDecorations((changeAccessor) => { changeAccessor.removeDecoration(decId); @@ -242,12 +238,10 @@ suite('Editor Model - Model Decorations', () => { test('decorations emit event when inserting one line text before it', () => { let listenerCalled = 0; - let decId = addDecoration(thisModel, 1, 2, 3, 2, 'myType'); + addDecoration(thisModel, 1, 2, 3, 2, 'myType'); thisModel.onDidChangeDecorations((e) => { listenerCalled++; - assert.equal(e.changedDecorations.length, 1); - assert.equal(e.changedDecorations[0], decId); }); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'Hallo ')]); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 902254d86d212..96c24eb1b18f8 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2482,10 +2482,6 @@ declare module monaco.editor { * An event describing that model decorations have changed. */ export interface IModelDecorationsChangedEvent { - /** - * Lists of ids for changed decorations. - */ - readonly changedDecorations: string[]; } /** diff --git a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts index 10fb1886dfd99..55f5e51c46fa8 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts @@ -13,7 +13,6 @@ import { IModel, TrackedRangeStickiness, IModelDeltaDecoration, IModelDecoration import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDebugService, IBreakpoint, IRawBreakpoint, State } from 'vs/workbench/parts/debug/common/debug'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { MarkdownString } from 'vs/base/common/htmlContent'; interface IDebugEditorModelData { @@ -84,7 +83,7 @@ export class DebugEditorModelManager implements IWorkbenchContribution { const desiredDecorations = this.createBreakpointDecorations(model, breakpoints); const breakPointDecorations = model.deltaDecorations([], desiredDecorations); - const toDispose: lifecycle.IDisposable[] = [model.onDidChangeDecorations((e) => this.onModelDecorationsChanged(modelUrlStr, e))]; + const toDispose: lifecycle.IDisposable[] = [model.onDidChangeDecorations((e) => this.onModelDecorationsChanged(modelUrlStr))]; const breakpointDecorationsAsMap = new Map(); breakPointDecorations.forEach((decorationId, index) => breakpointDecorationsAsMap.set(decorationId, desiredDecorations[index].range)); @@ -186,7 +185,7 @@ export class DebugEditorModelManager implements IWorkbenchContribution { } // breakpoints management. Represent data coming from the debug service and also send data back. - private onModelDecorationsChanged(modelUrlStr: string, e: IModelDecorationsChangedEvent): void { + private onModelDecorationsChanged(modelUrlStr: string): void { const modelData = this.modelDataMap.get(modelUrlStr); if (modelData.breakpointDecorationsAsMap.size === 0) { // I have no decorations From 479958b25dc41e66470ad43c35438d6261d97e5d Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 13 Oct 2017 09:45:48 +0200 Subject: [PATCH 08/64] IntervalTree.insert gets passed in the node to be inserted --- src/vs/editor/common/model/intervalTree.ts | 55 ++++++++++--------- .../test/common/model/intervalTree.test.ts | 10 ++-- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 4960d1274a6f4..7160e8b546cd5 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -18,13 +18,6 @@ export class Interval { this.start = start; this.end = end; } - - public compareToRelative(otherStart: number, otherEnd: number): number { - if (this.start === otherStart) { - return this.end - otherEnd; - } - return this.start - otherStart; - } } export const enum NodeColor { @@ -44,7 +37,7 @@ export class IntervalNode { public delta: number; public maxEnd: number; - public resultInterval: Interval; + public absoluteInterval: Interval; constructor(start: number, end: number) { this.parent = null; @@ -57,7 +50,7 @@ export class IntervalNode { this.delta = start; this.maxEnd = end; - this.resultInterval = new Interval(0, 0); + this.absoluteInterval = new Interval(0, 0); } public detach(): void { @@ -90,8 +83,8 @@ export class IntervalTree { return result; } - public insert(interval: Interval): IntervalNode { - return rbTreeInsert(this, interval); + public insert(node: IntervalNode): void { + rbTreeInsert(this, node); } public delete(node: IntervalNode) { @@ -185,8 +178,8 @@ function intervalSearchRecursive(node: IntervalNode, delta: number, intervalStar const nodeEnd = delta + node.end; if (nodeEnd >= intervalStart) { // There is overlap - node.resultInterval.start = nodeStart; - node.resultInterval.end = nodeEnd; + node.absoluteInterval.start = nodeStart; + node.absoluteInterval.end = nodeEnd; result.push(node); } @@ -197,9 +190,8 @@ function intervalSearchRecursive(node: IntervalNode, delta: number, intervalStar //#endregion //#region Insertion -function rbTreeInsert(T: IntervalTree, interval: Interval): IntervalNode { +function rbTreeInsert(T: IntervalTree, newNode: IntervalNode): IntervalNode { if (T.root === SENTINEL) { - const newNode = new IntervalNode(interval.start, interval.end); newNode.parent = SENTINEL; newNode.left = SENTINEL; newNode.right = SENTINEL; @@ -208,7 +200,7 @@ function rbTreeInsert(T: IntervalTree, interval: Interval): IntervalNode { return T.root; } - const newNode = treeInsert(T, interval); + treeInsert(T, newNode); recomputeMaxEndWalkToRoot(newNode.parent); @@ -257,17 +249,20 @@ function rbTreeInsert(T: IntervalTree, interval: Interval): IntervalNode { return newNode; } -function treeInsert(T: IntervalTree, interval: Interval): IntervalNode { +function treeInsert(T: IntervalTree, z: IntervalNode): void { let delta: number = 0; - let z = SENTINEL; let x = T.root; + const zAbsoluteStart = z.start; + const zAbsoluteEnd = z.end; while (true) { - let cmp = interval.compareToRelative(x.start + delta, x.end + delta); + const cmp = intervalCompare(zAbsoluteStart, zAbsoluteEnd, x.start + delta, x.end + delta); if (cmp < 0) { // this node should be inserted to the left // => it is not affected by the node's delta if (x.left === SENTINEL) { - z = new IntervalNode(interval.start - delta, interval.end - delta); + z.start -= delta; + z.end -= delta; + z.maxEnd -= delta; x.left = z; break; } else { @@ -277,7 +272,9 @@ function treeInsert(T: IntervalTree, interval: Interval): IntervalNode { // this node should be inserted to the right // => it is not affected by the node's delta if (x.right === SENTINEL) { - z = new IntervalNode(interval.start - delta - x.delta, interval.end - delta - x.delta); + z.start -= (delta + x.delta); + z.end -= (delta + x.delta); + z.maxEnd -= (delta + x.delta); x.right = z; break; } else { @@ -291,7 +288,6 @@ function treeInsert(T: IntervalTree, interval: Interval): IntervalNode { z.left = SENTINEL; z.right = SENTINEL; z.color = NodeColor.Red; - return z; } //#endregion @@ -581,6 +577,15 @@ function recomputeMaxEndWalkToRoot(node: IntervalNode): void { //#endregion +//#region utils +function intervalCompare(aStart: number, aEnd: number, bStart: number, bEnd: number): number { + if (aStart === bStart) { + return aEnd - bEnd; + } + return aStart - bStart; +} +//#endregion + //#region Assertion function depth(n: IntervalNode): number { @@ -607,13 +612,11 @@ function assertValidNode(n: IntervalNode, delta): void { let expectedMaxEnd = n.end; if (l !== SENTINEL) { - const lValue = new Interval(l.start + delta, l.end + delta); - assert(lValue.compareToRelative(n.start + delta, n.end + delta) <= 0); + assert(intervalCompare(l.start + delta, l.end + delta, n.start + delta, n.end + delta) <= 0); expectedMaxEnd = Math.max(expectedMaxEnd, l.maxEnd); } if (r !== SENTINEL) { - const nValue = new Interval(n.start + delta, n.end + delta); - assert(nValue.compareToRelative(r.start + delta + n.delta, r.end + delta + n.delta) <= 0); + assert(intervalCompare(n.start + delta, n.end + delta, r.start + delta + n.delta, r.end + delta + n.delta) <= 0); expectedMaxEnd = Math.max(expectedMaxEnd, r.maxEnd + n.delta); } assert(n.maxEnd === expectedMaxEnd); diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index ec6d1dcfe1a62..d8518b27a68fa 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -71,7 +71,8 @@ suite('IntervalTree', () => { console.log(`insert: {${JSON.stringify(new Interval(op.begin, op.end))}}`); } let nodeId = (++this._lastNodeId); - this._treeNodes[nodeId] = this._tree.insert(new Interval(op.begin, op.end)); + this._treeNodes[nodeId] = new IntervalNode(op.begin, op.end); + this._tree.insert(this._treeNodes[nodeId]); this._oracleNodes[nodeId] = this._oracle.insert(new Interval(op.begin, op.end)); } else if (op.type === 'delete') { if (PRINT_TREE) { @@ -84,7 +85,7 @@ suite('IntervalTree', () => { this._oracleNodes[op.id] = null; } else { let actualNodes = this._tree.intervalSearch(new Interval(op.begin, op.end)); - let actual = actualNodes.map(n => n.resultInterval); + let actual = actualNodes.map(n => n.absoluteInterval); let expected = this._oracle.search(new Interval(op.begin, op.end)); assert.deepEqual(actual, expected); return; @@ -417,7 +418,8 @@ suite('IntervalTree', () => { [19, 20] ]; data.forEach((int) => { - r.insert(new Interval(int[0], int[1])); + let node = new IntervalNode(int[0], int[1]); + r.insert(node); }); return r; } @@ -426,7 +428,7 @@ suite('IntervalTree', () => { function assertIntervalSearch(start: number, end: number, expected: [number, number][]): void { let actualNodes = T.intervalSearch(new Interval(start, end)); - let actual = actualNodes.map((n) => <[number, number]>[n.resultInterval.start, n.resultInterval.end]); + let actual = actualNodes.map((n) => <[number, number]>[n.absoluteInterval.start, n.absoluteInterval.end]); assert.deepEqual(actual, expected); } From 80859dd7deb6a456e25f69bd290e07b3ea959db2 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 13 Oct 2017 15:18:32 +0200 Subject: [PATCH 09/64] IntervalNode implements IModelDecoration --- src/vs/editor/common/model/intervalTree.ts | 100 +++++++++++++++--- .../test/common/model/intervalTree.test.ts | 13 ++- 2 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 7160e8b546cd5..d89868bc61aff 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -4,10 +4,21 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; +import { Range } from 'vs/editor/common/core/range'; +import { IModelDecoration } from 'vs/editor/common/editorCommon'; + // // The red-black tree is based on the "Introduction to Algorithms" by Cormen, Leiserson and Rivest. // +// TODO@interval!!! +export const ClassName = { + EditorInfoDecoration: 'infosquiggly', + EditorWarningDecoration: 'warningsquiggly', + EditorErrorDecoration: 'errorsquiggly' +}; + export class Interval { _intervalBrand: void; @@ -25,11 +36,11 @@ export const enum NodeColor { Black } -export class IntervalNode { +export class IntervalNode implements IModelDecoration { + public parent: IntervalNode; public left: IntervalNode; public right: IntervalNode; - public parent: IntervalNode; public color: NodeColor; public start: number; @@ -37,9 +48,17 @@ export class IntervalNode { public delta: number; public maxEnd: number; - public absoluteInterval: Interval; + public id: string; + public ownerId: number; + public options: ModelDecorationOptions; + public isForValidation: boolean; - constructor(start: number, end: number) { + public cachedVersionId: number; + public cachedAbsoluteStart: number; + public cachedAbsoluteEnd: number; + public range: Range; + + constructor(id: string, start: number, end: number) { this.parent = null; this.left = null; this.right = null; @@ -50,7 +69,34 @@ export class IntervalNode { this.delta = start; this.maxEnd = end; - this.absoluteInterval = new Interval(0, 0); + this.id = id; + this.ownerId = 0; + this.options = null; + this.isForValidation = false; + + this.cachedVersionId = 0; + this.cachedAbsoluteStart = start; + this.cachedAbsoluteEnd = end; + this.range = null; + } + + public setOptions(options: ModelDecorationOptions) { + this.options = options; + this.isForValidation = ( + this.options.className === ClassName.EditorErrorDecoration + || this.options.className === ClassName.EditorWarningDecoration + ); + } + + public setCachedOffsets(absoluteStart: number, absoluteEnd: number, cachedVersionId: number): void { + this.cachedVersionId = cachedVersionId; + if (this.cachedAbsoluteStart === absoluteStart && this.cachedAbsoluteEnd === absoluteEnd) { + // no change + return; + } + this.cachedAbsoluteStart = absoluteStart; + this.cachedAbsoluteEnd = absoluteEnd; + this.range = null; } public detach(): void { @@ -60,7 +106,7 @@ export class IntervalNode { } } -const SENTINEL: IntervalNode = new IntervalNode(0, 0); +const SENTINEL: IntervalNode = new IntervalNode(null, 0, 0); SENTINEL.parent = SENTINEL; SENTINEL.left = SENTINEL; SENTINEL.right = SENTINEL; @@ -74,12 +120,12 @@ export class IntervalTree { this.root = SENTINEL; } - public intervalSearch(interval: Interval): IntervalNode[] { + public intervalSearch(interval: Interval, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { if (this.root === SENTINEL) { return []; } let result: IntervalNode[] = []; - intervalSearchRecursive(this.root, 0, interval.start, interval.end, result); + intervalSearchRecursive(this.root, 0, interval.start, interval.end, filterOwnerId, filterOutValidation, cachedVersionId, result); return result; } @@ -87,10 +133,24 @@ export class IntervalTree { rbTreeInsert(this, node); } - public delete(node: IntervalNode) { + public delete(node: IntervalNode): void { rbTreeDelete(this, node); } + public resolveNode(node: IntervalNode, cachedVersionId: number): void { + let delta = 0; + while (node !== this.root) { + if (node === node.parent.right) { + delta += node.parent.delta; + } + node = node.parent; + } + + const nodeStart = node.start + delta; + const nodeEnd = node.end + delta; + node.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); + } + public assertInvariants(): void { assert(SENTINEL.color === NodeColor.Black); assert(SENTINEL.parent === SENTINEL); @@ -150,7 +210,7 @@ export class IntervalTree { } //#region Searching -function intervalSearchRecursive(node: IntervalNode, delta: number, intervalStart: number, intervalEnd: number, result: IntervalNode[]): void { +function intervalSearchRecursive(node: IntervalNode, delta: number, intervalStart: number, intervalEnd: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number, result: IntervalNode[]): void { // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree // Now, it is known that two intervals A and B overlap only when both // A.low <= B.high and A.high >= B.low. When searching the trees for @@ -165,7 +225,7 @@ function intervalSearchRecursive(node: IntervalNode, delta: number, intervalStar } if (node.left !== SENTINEL) { - intervalSearchRecursive(node.left, delta, intervalStart, intervalEnd, result); + intervalSearchRecursive(node.left, delta, intervalStart, intervalEnd, filterOwnerId, filterOutValidation, cachedVersionId, result); } const nodeStart = delta + node.start; @@ -178,13 +238,23 @@ function intervalSearchRecursive(node: IntervalNode, delta: number, intervalStar const nodeEnd = delta + node.end; if (nodeEnd >= intervalStart) { // There is overlap - node.absoluteInterval.start = nodeStart; - node.absoluteInterval.end = nodeEnd; - result.push(node); + node.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); + + let include = true; + if (filterOwnerId && node.ownerId && node.ownerId !== filterOwnerId) { + include = false; + } + if (filterOutValidation && node.isForValidation) { + include = false; + } + + if (include) { + result.push(node); + } } if (node.right !== SENTINEL) { - intervalSearchRecursive(node.right, delta + node.delta, intervalStart, intervalEnd, result); + intervalSearchRecursive(node.right, delta + node.delta, intervalStart, intervalEnd, filterOwnerId, filterOutValidation, cachedVersionId, result); } } //#endregion diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index d8518b27a68fa..7df8a19e0e311 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -65,13 +65,12 @@ suite('IntervalTree', () => { public acceptOp(op: IOperation): void { - if (op.type === 'insert') { if (PRINT_TREE) { console.log(`insert: {${JSON.stringify(new Interval(op.begin, op.end))}}`); } let nodeId = (++this._lastNodeId); - this._treeNodes[nodeId] = new IntervalNode(op.begin, op.end); + this._treeNodes[nodeId] = new IntervalNode(null, op.begin, op.end); this._tree.insert(this._treeNodes[nodeId]); this._oracleNodes[nodeId] = this._oracle.insert(new Interval(op.begin, op.end)); } else if (op.type === 'delete') { @@ -84,8 +83,8 @@ suite('IntervalTree', () => { this._treeNodes[op.id] = null; this._oracleNodes[op.id] = null; } else { - let actualNodes = this._tree.intervalSearch(new Interval(op.begin, op.end)); - let actual = actualNodes.map(n => n.absoluteInterval); + let actualNodes = this._tree.intervalSearch(new Interval(op.begin, op.end), 0, false, 0); + let actual = actualNodes.map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); let expected = this._oracle.search(new Interval(op.begin, op.end)); assert.deepEqual(actual, expected); return; @@ -418,7 +417,7 @@ suite('IntervalTree', () => { [19, 20] ]; data.forEach((int) => { - let node = new IntervalNode(int[0], int[1]); + let node = new IntervalNode(null, int[0], int[1]); r.insert(node); }); return r; @@ -427,8 +426,8 @@ suite('IntervalTree', () => { const T = createCormenTree(); function assertIntervalSearch(start: number, end: number, expected: [number, number][]): void { - let actualNodes = T.intervalSearch(new Interval(start, end)); - let actual = actualNodes.map((n) => <[number, number]>[n.absoluteInterval.start, n.absoluteInterval.end]); + let actualNodes = T.intervalSearch(new Interval(start, end), 0, false, 0); + let actual = actualNodes.map((n) => <[number, number]>[n.cachedAbsoluteStart, n.cachedAbsoluteEnd]); assert.deepEqual(actual, expected); } From 452e0ed86525decb6080736347a3b9486c6106be Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 13 Oct 2017 16:39:52 +0200 Subject: [PATCH 10/64] Reduce usage of `Interval` --- src/vs/editor/common/model/intervalTree.ts | 4 ++-- src/vs/editor/test/common/model/intervalTree.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index d89868bc61aff..2744c6416bfbd 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -120,12 +120,12 @@ export class IntervalTree { this.root = SENTINEL; } - public intervalSearch(interval: Interval, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { + public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { if (this.root === SENTINEL) { return []; } let result: IntervalNode[] = []; - intervalSearchRecursive(this.root, 0, interval.start, interval.end, filterOwnerId, filterOutValidation, cachedVersionId, result); + intervalSearchRecursive(this.root, 0, start, end, filterOwnerId, filterOutValidation, cachedVersionId, result); return result; } diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index 7df8a19e0e311..cc12118fee54d 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -83,7 +83,7 @@ suite('IntervalTree', () => { this._treeNodes[op.id] = null; this._oracleNodes[op.id] = null; } else { - let actualNodes = this._tree.intervalSearch(new Interval(op.begin, op.end), 0, false, 0); + let actualNodes = this._tree.intervalSearch(op.begin, op.end, 0, false, 0); let actual = actualNodes.map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); let expected = this._oracle.search(new Interval(op.begin, op.end)); assert.deepEqual(actual, expected); @@ -426,7 +426,7 @@ suite('IntervalTree', () => { const T = createCormenTree(); function assertIntervalSearch(start: number, end: number, expected: [number, number][]): void { - let actualNodes = T.intervalSearch(new Interval(start, end), 0, false, 0); + let actualNodes = T.intervalSearch(start, end, 0, false, 0); let actual = actualNodes.map((n) => <[number, number]>[n.cachedAbsoluteStart, n.cachedAbsoluteEnd]); assert.deepEqual(actual, expected); } From 6b374f36a19c0399efa7a62542e9be144396f358 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 13 Oct 2017 17:27:16 +0200 Subject: [PATCH 11/64] Implement interval searching to be non recursive and not use a stack --- src/vs/editor/common/model/intervalTree.ts | 109 +++++++++++++----- .../test/common/model/intervalTree.test.ts | 9 ++ 2 files changed, 87 insertions(+), 31 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 2744c6416bfbd..62888a2f05c97 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -58,6 +58,8 @@ export class IntervalNode implements IModelDecoration { public cachedAbsoluteEnd: number; public range: Range; + public visited: boolean; + constructor(id: string, start: number, end: number) { this.parent = null; this.left = null; @@ -78,6 +80,8 @@ export class IntervalNode implements IModelDecoration { this.cachedAbsoluteStart = start; this.cachedAbsoluteEnd = end; this.range = null; + + this.visited = false; } public setOptions(options: ModelDecorationOptions) { @@ -124,9 +128,7 @@ export class IntervalTree { if (this.root === SENTINEL) { return []; } - let result: IntervalNode[] = []; - intervalSearchRecursive(this.root, 0, start, end, filterOwnerId, filterOutValidation, cachedVersionId, result); - return result; + return intervalSearch(this, start, end, filterOwnerId, filterOutValidation, cachedVersionId); } public insert(node: IntervalNode): void { @@ -159,6 +161,7 @@ export class IntervalTree { assert(SENTINEL.start === 0); assert(SENTINEL.end === 0); assert(SENTINEL.delta === 0); + assert(this.root.parent === SENTINEL); assertValidTree(this); } @@ -210,7 +213,8 @@ export class IntervalTree { } //#region Searching -function intervalSearchRecursive(node: IntervalNode, delta: number, intervalStart: number, intervalEnd: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number, result: IntervalNode[]): void { + +function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree // Now, it is known that two intervals A and B overlap only when both // A.low <= B.high and A.high >= B.low. When searching the trees for @@ -218,45 +222,87 @@ function intervalSearchRecursive(node: IntervalNode, delta: number, intervalStar // a) all nodes to the right of nodes whose low value is past the end of the given interval. // b) all nodes that have their maximum 'high' value below the start of the given interval. - const nodeMaxEnd = delta + node.maxEnd; - if (nodeMaxEnd < intervalStart) { - // Cover b) from above - return; - } + let node = T.root; + let delta = 0; + let nodeMaxEnd = 0; + let nodeStart = 0; + let nodeEnd = 0; + let result: IntervalNode[] = []; + let resultLen = 0; + while (node !== SENTINEL) { + if (node.visited) { + // going up from this node + node.left.visited = false; + node.right.visited = false; + if (node === node.parent.right) { + delta -= node.parent.delta; + } + node = node.parent; + continue; + } - if (node.left !== SENTINEL) { - intervalSearchRecursive(node.left, delta, intervalStart, intervalEnd, filterOwnerId, filterOutValidation, cachedVersionId, result); - } + if (!node.left.visited) { + // first time seeing this node + nodeMaxEnd = delta + node.maxEnd; + if (nodeMaxEnd < intervalStart) { + // cover case b) from above + // there is no need to search this node or its children + node.visited = true; + continue; + } - const nodeStart = delta + node.start; + if (node.left !== SENTINEL) { + // go left + node = node.left; + continue; + } + } - if (nodeStart > intervalEnd) { - // Cover a) from above - return; - } + // handle current node + nodeStart = delta + node.start; + if (nodeStart > intervalEnd) { + // cover case a) from above + // there is no need to search this node or its right subtree + node.visited = true; + continue; + } - const nodeEnd = delta + node.end; - if (nodeEnd >= intervalStart) { - // There is overlap - node.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); + nodeEnd = delta + node.end; - let include = true; - if (filterOwnerId && node.ownerId && node.ownerId !== filterOwnerId) { - include = false; - } - if (filterOutValidation && node.isForValidation) { - include = false; + if (nodeEnd >= intervalStart) { + // There is overlap + node.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); + + let include = true; + if (filterOwnerId && node.ownerId && node.ownerId !== filterOwnerId) { + include = false; + } + if (filterOutValidation && node.isForValidation) { + include = false; + } + + if (include) { + result[resultLen++] = node; + } } - if (include) { - result.push(node); + node.visited = true; + + if (node.right !== SENTINEL && !node.right.visited) { + // go right + delta += node.delta; + node = node.right; + continue; } } - if (node.right !== SENTINEL) { - intervalSearchRecursive(node.right, delta + node.delta, intervalStart, intervalEnd, filterOwnerId, filterOutValidation, cachedVersionId, result); + if (T.root) { + T.root.visited = false; } + + return result; } + //#endregion //#region Insertion @@ -406,6 +452,7 @@ function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { z.detach(); resetSentinel(); recomputeMaxEnd(x); + T.root.parent = SENTINEL; return; } diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index cc12118fee54d..1ece94f8b4314 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -380,6 +380,15 @@ suite('IntervalTree', () => { { type: 'delete', id: 6 } ]); }); + + test('gen18', () => { + testIntervalTree([ + { type: 'insert', begin: 25, end: 25 }, + { type: 'insert', begin: 67, end: 79 }, + { type: 'delete', id: 0 }, + { type: 'search', begin: 65, end: 75 } + ]); + }); }); // TEST_COUNT = 0; From fedf6fdcb6e6331297e51f066d8aad8ae54bac00 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 13 Oct 2017 18:14:10 +0200 Subject: [PATCH 12/64] Add a flag that switches between decorations implementations --- src/vs/editor/common/editorCommon.ts | 7 + src/vs/editor/common/model/intervalTree.ts | 156 ++++++++++++ src/vs/editor/common/model/textModel.ts | 72 +++++- .../common/model/textModelWithDecorations.ts | 240 +++++++++++++++++- .../common/viewModel/viewModelDecorations.ts | 8 +- src/vs/monaco.d.ts | 6 + 6 files changed, 479 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index ab9db53d0ceb4..e0f3ad0a16005 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -1034,6 +1034,13 @@ export interface ITextModelWithDecorations { * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). */ getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + + /** + * Gets all the decorations that should be rendered in the overview ruler as an array. + * @param ownerId If set, it will ignore decorations belonging to other owners. + * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). + */ + getOverviewRulerDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; } /** diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 62888a2f05c97..df602db753f4a 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -131,6 +131,24 @@ export class IntervalTree { return intervalSearch(this, start, end, filterOwnerId, filterOutValidation, cachedVersionId); } + public search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] { + if (this.root === SENTINEL) { + return []; + } + return search(this, filterOwnerId, filterOutValidation, overviewRulerOnly, cachedVersionId); + } + + public count(): number { + return nodeCount(this); + } + + /** + * Will not set `cachedAbsoluteStart` nor `cachedAbsoluteEnd` on the returned nodes! + */ + public collectNodesFromOwner(ownerId: number): IntervalNode[] { + return collectNodesFromOwner(this, ownerId); + } + public insert(node: IntervalNode): void { rbTreeInsert(this, node); } @@ -214,6 +232,144 @@ export class IntervalTree { //#region Searching +function nodeCount(T: IntervalTree): number { + let node = T.root; + let count = 0; + while (node !== SENTINEL) { + if (node.visited) { + // going up from this node + node.left.visited = false; + node.right.visited = false; + node = node.parent; + continue; + } + + if (node.left !== SENTINEL && !node.left.visited) { + // go left + node = node.left; + continue; + } + + // handle current node + count++; + node.visited = true; + + if (node.right !== SENTINEL && !node.right.visited) { + // go right + node = node.right; + continue; + } + } + + if (T.root) { + T.root.visited = false; + } + + return count; +} + +function collectNodesFromOwner(T: IntervalTree, ownerId: number): IntervalNode[] { + let node = T.root; + let result: IntervalNode[] = []; + let resultLen = 0; + while (node !== SENTINEL) { + if (node.visited) { + // going up from this node + node.left.visited = false; + node.right.visited = false; + node = node.parent; + continue; + } + + if (node.left !== SENTINEL && !node.left.visited) { + // go left + node = node.left; + continue; + } + + // handle current node + if (node.ownerId === ownerId) { + result[resultLen++] = node; + } + + node.visited = true; + + if (node.right !== SENTINEL && !node.right.visited) { + // go right + node = node.right; + continue; + } + } + + if (T.root) { + T.root.visited = false; + } + + return result; +} + +function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] { + let node = T.root; + let delta = 0; + let nodeStart = 0; + let nodeEnd = 0; + let result: IntervalNode[] = []; + let resultLen = 0; + while (node !== SENTINEL) { + if (node.visited) { + // going up from this node + node.left.visited = false; + node.right.visited = false; + if (node === node.parent.right) { + delta -= node.parent.delta; + } + node = node.parent; + continue; + } + + if (node.left !== SENTINEL && !node.left.visited) { + // go left + node = node.left; + continue; + } + + // handle current node + nodeStart = delta + node.start; + nodeEnd = delta + node.end; + + node.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); + + let include = true; + if (filterOwnerId && node.ownerId && node.ownerId !== filterOwnerId) { + include = false; + } + if (filterOutValidation && node.isForValidation) { + include = false; + } + if (overviewRulerOnly && !node.options.overviewRuler.color) { + include = false; + } + if (include) { + result[resultLen++] = node; + } + + node.visited = true; + + if (node.right !== SENTINEL && !node.right.visited) { + // go right + delta += node.delta; + node = node.right; + continue; + } + } + + if (T.root) { + T.root.visited = false; + } + + return result; +} + function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree // Now, it is known that two intervals A and B overlap only when both diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 3e55882ed9605..4af332e8f1422 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -261,7 +261,7 @@ export class TextModel implements editorCommon.ITextModel { return this._alternativeVersionId; } - private _ensureLineStarts(): void { + protected _ensureLineStarts(): void { if (!this._lineStarts) { const eolLength = this._EOL.length; const linesLength = this._lines.length; @@ -607,6 +607,76 @@ export class TextModel implements editorCommon.ITextModel { return lineNumber; } + /** + * Validates `range` is within buffer bounds, but allows it to sit in between surrogate pairs, etc. + * Will try to not allocate if possible. + */ + protected _validateRangeRelaxedNoAllocations(range: IRange): Range { + const linesCount = this._lines.length; + + const initialStartLineNumber = range.startLineNumber; + const initialStartColumn = range.startColumn; + let startLineNumber: number; + let startColumn: number; + + if (initialStartLineNumber < 1) { + startLineNumber = 1; + startColumn = 1; + } else if (initialStartLineNumber > linesCount) { + startLineNumber = linesCount; + startColumn = this.getLineMaxColumn(startLineNumber); + } else { + startLineNumber = initialStartLineNumber | 0; + if (initialStartColumn <= 1) { + startColumn = 1; + } else { + const maxColumn = this.getLineMaxColumn(startLineNumber); + if (initialStartColumn >= maxColumn) { + startColumn = maxColumn; + } else { + startColumn = initialStartColumn | 0; + } + } + } + + const initialEndLineNumber = range.endLineNumber; + const initialEndColumn = range.endColumn; + let endLineNumber: number; + let endColumn: number; + + if (initialEndLineNumber < 1) { + endLineNumber = 1; + endColumn = 1; + } else if (initialEndLineNumber > linesCount) { + endLineNumber = linesCount; + endColumn = this.getLineMaxColumn(endLineNumber); + } else { + endLineNumber = initialEndLineNumber | 0; + if (initialEndColumn <= 1) { + endColumn = 1; + } else { + const maxColumn = this.getLineMaxColumn(endLineNumber); + if (initialEndColumn >= maxColumn) { + endColumn = maxColumn; + } else { + endColumn = initialEndColumn | 0; + } + } + } + + if ( + initialStartLineNumber === startLineNumber + && initialStartColumn === startColumn + && initialEndLineNumber === endLineNumber + && initialEndColumn === endColumn + && range instanceof Range + ) { + return range; + } + + return new Range(startLineNumber, startColumn, endLineNumber, endColumn); + } + /** * @param strict Do NOT allow a position inside a high-low surrogate pair */ diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 9ed9c9a16e6a8..2b9befae2f9fd 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -17,6 +17,9 @@ import { LanguageIdentifier } from 'vs/editor/common/modes'; import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { IntervalNode, IntervalTree } from 'vs/editor/common/model/intervalTree'; + +const USE_NEW_DECORATIONS = false; export const ClassName = { EditorInfoDecoration: 'infosquiggly', @@ -134,6 +137,9 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed private _currentMarkersTracker: MarkersTracker; private _currentMarkersTrackerCnt: number; + private _tree: IntervalTree; + private _treeDecorations: { [decorationId: string]: IntervalNode; }; + private _decorations: { [decorationId: string]: InternalDecoration; }; private _internalDecorations: { [internalDecorationId: number]: InternalDecoration; }; private _multiLineDecorationsMap: { [key: string]: InternalDecoration; }; @@ -151,6 +157,9 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed this._currentMarkersTracker = null; this._currentMarkersTrackerCnt = 0; + this._tree = new IntervalTree(); + this._treeDecorations = Object.create(null); + this._decorations = Object.create(null); this._internalDecorations = Object.create(null); this._multiLineDecorationsMap = Object.create(null); @@ -187,6 +196,9 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } _getTrackedRangesCount(): number { + if (USE_NEW_DECORATIONS) { + return this._tree.count(); + } return Object.keys(this._decorations).length; } @@ -208,18 +220,36 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed private _changeDecorations(decorationsTracker: DecorationsTracker, ownerId: number, callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T): T { let changeAccessor: editorCommon.IModelDecorationsChangeAccessor = { addDecoration: (range: IRange, options: editorCommon.IModelDecorationOptions): string => { + if (USE_NEW_DECORATIONS) { + return this._deltaDecorationsImpl2(decorationsTracker, [], this._normalizeDeltaDecorations2(ownerId, [{ range: range, options: options }]))[0]; + } return this._addDecorationImpl(decorationsTracker, ownerId, this.validateRange(range), _normalizeOptions(options)); }, changeDecoration: (id: string, newRange: IRange): void => { + if (USE_NEW_DECORATIONS) { + this._changeDecorationImpl2(decorationsTracker, id, newRange); + return; + } this._changeDecorationImpl(decorationsTracker, id, this.validateRange(newRange)); }, changeDecorationOptions: (id: string, options: editorCommon.IModelDecorationOptions) => { + if (USE_NEW_DECORATIONS) { + this._changeDecorationOptionsImpl2(decorationsTracker, id, _normalizeOptions(options)); + return; + } this._changeDecorationOptionsImpl(decorationsTracker, id, _normalizeOptions(options)); }, removeDecoration: (id: string): void => { + if (USE_NEW_DECORATIONS) { + this._deltaDecorationsImpl2(decorationsTracker, [id], []); + return; + } this._removeDecorationImpl(decorationsTracker, id); }, deltaDecorations: (oldDecorations: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] => { + if (USE_NEW_DECORATIONS) { + return this._deltaDecorationsImpl2(decorationsTracker, oldDecorations, this._normalizeDeltaDecorations2(ownerId, newDecorations)); + } return this._deltaDecorationsImpl(decorationsTracker, ownerId, oldDecorations, this._normalizeDeltaDecorations(newDecorations)); } }; @@ -248,6 +278,16 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } public removeAllDecorationsWithOwnerId(ownerId: number): void { + if (USE_NEW_DECORATIONS) { + const nodes = this._tree.collectNodesFromOwner(ownerId); + for (let i = 0, len = nodes.length; i < len; i++) { + const node = nodes[i]; + + this._tree.delete(node); + delete this._treeDecorations[node.id]; + } + return; + } let toRemove: string[] = []; for (let decorationId in this._decorations) { @@ -264,6 +304,13 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } public getDecorationOptions(decorationId: string): editorCommon.IModelDecorationOptions { + if (USE_NEW_DECORATIONS) { + const node = this._treeDecorations[decorationId]; + if (!node) { + return null; + } + return node.options; + } let decoration = this._decorations[decorationId]; if (!decoration) { return null; @@ -271,7 +318,34 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return decoration.options; } + private _getRangeAt(start: number, end: number): Range { + const startResult = this._lineStarts.getIndexOf(start); + const startLineLength = this._lines[startResult.index].text.length; + const startColumn = Math.min(startResult.remainder + 1, startLineLength + 1); + + const endResult = this._lineStarts.getIndexOf(end); + const endLineLength = this._lines[endResult.index].text.length; + const endColumn = Math.min(endResult.remainder + 1, endLineLength + 1); + + return new Range(startResult.index + 1, startColumn, endResult.index + 1, endColumn); + } + public getDecorationRange(decorationId: string): Range { + if (USE_NEW_DECORATIONS) { + const node = this._treeDecorations[decorationId]; + if (!node) { + return null; + } + const versionId = this.getVersionId(); + if (node.cachedVersionId !== versionId) { + this._tree.resolveNode(node, versionId); + } + if (node.range === null) { + this._ensureLineStarts(); + node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd); + } + return node.range; + } let decoration = this._decorations[decorationId]; if (!decoration) { return null; @@ -331,7 +405,31 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return result; } - private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): InternalDecoration[] { + private _ensureNodesHaveRanges(nodes: IntervalNode[]): IntervalNode[] { + this._ensureLineStarts(); + + for (let i = 0, len = nodes.length; i < len; i++) { + const node = nodes[i]; + if (node.range === null) { + node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd); + } + } + return nodes; + } + + private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): IntervalNode[] | InternalDecoration[] { + if (USE_NEW_DECORATIONS) { + this._ensureLineStarts(); + + const startOffset = this._lineStarts.getAccumulatedValue(filterRange.startLineNumber - 2) + filterRange.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(filterRange.endLineNumber - 2) + filterRange.endColumn - 1; + + const versionId = this.getVersionId(); + const result = this._tree.intervalSearch(startOffset, endOffset, filterOwnerId, filterOutValidation, versionId); + + return this._ensureNodesHaveRanges(result); + } + const filterStartLineNumber = filterRange.startLineNumber; const filterStartColumn = filterRange.startColumn; const filterEndLineNumber = filterRange.endLineNumber; @@ -405,12 +503,47 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return this._getDecorationsInRange(new Range(startLineNumber, 1, endLineNumber, endColumn), ownerId, filterOutValidation); } - public getDecorationsInRange(range: IRange, ownerId?: number, filterOutValidation?: boolean): editorCommon.IModelDecoration[] { + public getDecorationsInRange(range: IRange, ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { let validatedRange = this.validateRange(range); return this._getDecorationsInRange(validatedRange, ownerId, filterOutValidation); } + public getOverviewRulerDecorations(ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { + if (USE_NEW_DECORATIONS) { + const versionId = this.getVersionId(); + const result = this._tree.search(ownerId, filterOutValidation, true, versionId); + return this._ensureNodesHaveRanges(result); + } + let result: InternalDecoration[] = [], resultLen = 0; + + for (let decorationId in this._decorations) { + // No `hasOwnProperty` call due to using Object.create(null) + let decoration = this._decorations[decorationId]; + + if (ownerId && decoration.ownerId && decoration.ownerId !== ownerId) { + continue; + } + + if (filterOutValidation && decoration.isForValidation) { + continue; + } + + if (!decoration.options.overviewRuler.color) { + continue; + } + + result[resultLen++] = decoration; + } + + return result; + } + public getAllDecorations(ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { + if (USE_NEW_DECORATIONS) { + const versionId = this.getVersionId(); + const result = this._tree.search(ownerId, filterOutValidation, false, versionId); + return this._ensureNodesHaveRanges(result); + } let result: InternalDecoration[] = [], resultLen = 0; for (let decorationId in this._decorations) { @@ -542,6 +675,30 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return result; } + private _normalizeDeltaDecorations2(ownerId: number, deltaDecorations: editorCommon.IModelDeltaDecoration[]): IntervalNode[] { + this._ensureLineStarts(); + + const versionId = this.getVersionId(); + let result = new Array(deltaDecorations.length); + for (let i = 0, len = deltaDecorations.length; i < len; i++) { + const dec = deltaDecorations[i]; + const range = this._validateRangeRelaxedNoAllocations(dec.range); + const options = _normalizeOptions(dec.options); + + const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; + + const node = new IntervalNode(null, startOffset, endOffset); + node.ownerId = ownerId; + node.cachedVersionId = versionId; + node.range = range; + node.setOptions(options); + + result[i] = node; + } + return result; + } + private _externalDecorationId(internalId: number): string { return `${this._instanceId};${internalId}`; } @@ -629,6 +786,25 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return decorationIds; } + private _addDecorationsImpl2(decorationsTracker: DecorationsTracker, newDecorations: IntervalNode[]): string[] { + + let decorationIds: string[] = new Array(newDecorations.length); + for (let i = 0, len = newDecorations.length; i < len; i++) { + const node = newDecorations[i]; + const internalDecorationId = (++this._lastDecorationId); + const decorationId = this._externalDecorationId(internalDecorationId); + node.id = decorationId; + + this._tree.insert(node); + this._treeDecorations[decorationId] = node; + decorationIds[i] = decorationId; + } + + decorationsTracker.markDidAddDecorations(); + + return decorationIds; + } + private _changeDecorationImpl(decorationsTracker: DecorationsTracker, decorationId: string, newRange: Range): void { let decoration = this._decorations[decorationId]; if (!decoration) { @@ -656,6 +832,24 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed decorationsTracker.markDidChangeDecorations(); } + private _changeDecorationImpl2(decorationsTracker: DecorationsTracker, decorationId: string, _range: IRange): void { + const node = this._treeDecorations[decorationId]; + if (!node) { + return; + } + const range = this._validateRangeRelaxedNoAllocations(_range); + const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; + + this._tree.delete(node); + + node.setCachedOffsets(startOffset, endOffset, this.getVersionId()); + node.range = range; + this._tree.insert(node); + + decorationsTracker.markDidChangeDecorations(); + } + private _changeDecorationOptionsImpl(decorationsTracker: DecorationsTracker, decorationId: string, options: ModelDecorationOptions): void { let decoration = this._decorations[decorationId]; if (!decoration) { @@ -672,6 +866,17 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed decorationsTracker.markDidChangeDecorations(); } + private _changeDecorationOptionsImpl2(decorationsTracker: DecorationsTracker, decorationId: string, options: ModelDecorationOptions): void { + const node = this._treeDecorations[decorationId]; + if (!node) { + return; + } + + node.setOptions(options); + + decorationsTracker.markDidChangeDecorations(); + } + private _removeDecorationImpl(decorationsTracker: DecorationsTracker, decorationId: string): void { let decoration = this._decorations[decorationId]; if (!decoration) { @@ -715,6 +920,23 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } } + private _removeDecorationsImpl2(decorationsTracker: DecorationsTracker, decorationIds: string[]): void { + for (let i = 0, len = decorationIds.length; i < len; i++) { + const decorationId = decorationIds[i]; + const node = this._treeDecorations[decorationId]; + if (!node) { + continue; + } + + this._tree.delete(node); + delete this._treeDecorations[decorationId]; + } + + if (decorationsTracker) { + decorationsTracker.markDidRemoveDecorations(); + } + } + private _resolveOldDecorations(oldDecorations: string[]): InternalDecoration[] { let result: InternalDecoration[] = []; for (let i = 0, len = oldDecorations.length; i < len; i++) { @@ -816,6 +1038,18 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return result; } + + private _deltaDecorationsImpl2(decorationsTracker: DecorationsTracker, oldDecorationsIds: string[], newDecorations: IntervalNode[]): string[] { + // TODO@interval: is it worth it to compare? + if (oldDecorationsIds.length > 0) { + this._removeDecorationsImpl2(decorationsTracker, oldDecorationsIds); + } + + if (newDecorations.length > 0) { + return this._addDecorationsImpl2(decorationsTracker, newDecorations); + } + return []; + } } function cleanClassName(className: string): string { @@ -931,6 +1165,8 @@ ModelDecorationOptions.EMPTY = ModelDecorationOptions.register({}); class ModelDeltaDecoration implements editorCommon.IModelDeltaDecoration { + _modelDeltaDecorationBrand: void; + index: number; range: Range; options: ModelDecorationOptions; diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index 59205a10171d1..be7a324004a40 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -89,16 +89,10 @@ export class ViewModelDecorations implements IDisposable { } public getAllOverviewRulerDecorations(): ViewModelDecoration[] { - let modelDecorations = this.model.getAllDecorations(this.editorId, this.configuration.editor.readOnly); + let modelDecorations = this.model.getOverviewRulerDecorations(this.editorId, this.configuration.editor.readOnly); let result: ViewModelDecoration[] = [], resultLen = 0; for (let i = 0, len = modelDecorations.length; i < len; i++) { let modelDecoration = modelDecorations[i]; - let decorationOptions = modelDecoration.options; - - if (!decorationOptions.overviewRuler.color) { - continue; - } - let viewModelDecoration = this._getOrCreateViewModelDecoration(modelDecoration); result[resultLen++] = viewModelDecoration; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 96c24eb1b18f8..4b4e91c982417 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1717,6 +1717,12 @@ declare module monaco.editor { * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). */ getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + /** + * Gets all the decorations that should be rendered in the overview ruler as an array. + * @param ownerId If set, it will ignore decorations belonging to other owners. + * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). + */ + getOverviewRulerDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; } /** From 76c68ae527c087702f55f605cc2cfff2ec069ec8 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 13 Oct 2017 18:19:02 +0200 Subject: [PATCH 13/64] some cleanup in intervalTree.ts --- src/vs/editor/common/model/intervalTree.ts | 41 +++---------------- .../test/common/model/intervalTree.test.ts | 16 +++++++- 2 files changed, 19 insertions(+), 38 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index df602db753f4a..b1e0cb4fa4496 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -19,18 +19,6 @@ export const ClassName = { EditorErrorDecoration: 'errorsquiggly' }; -export class Interval { - _intervalBrand: void; - - public start: number; - public end: number; - - constructor(start: number, end: number) { - this.start = start; - this.end = end; - } -} - export const enum NodeColor { Red, Black @@ -139,6 +127,9 @@ export class IntervalTree { } public count(): number { + if (this.root === SENTINEL) { + return 0; + } return nodeCount(this); } @@ -183,30 +174,8 @@ export class IntervalTree { assertValidTree(this); } - public getAllInOrder(): Interval[] { - let r: Interval[] = [], rLength = 0; - this.visitInOrder((n, delta) => { - r[rLength++] = new Interval(n.start + delta, n.end + delta); - }); - return r; - } - - public visitInOrder(visitor: (n: IntervalNode, delta: number) => void): void { - this._visitInOrder(this.root, 0, visitor); - } - - private _visitInOrder(n: IntervalNode, delta: number, visitor: (n: IntervalNode, delta: number) => void): void { - if (n.left !== SENTINEL) { - this._visitInOrder(n.left, delta, visitor); - } - - if (n !== SENTINEL) { - visitor(n, delta); - } - - if (n.right !== SENTINEL) { - this._visitInOrder(n.right, delta + n.delta, visitor); - } + public getAllInOrder(): IntervalNode[] { + return search(this, 0, false, false, 0); } public print(): void { diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index 1ece94f8b4314..f5ab68fcb0c5a 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import { IntervalTree, Interval, IntervalNode } from 'vs/editor/common/model/intervalTree'; +import { IntervalTree, IntervalNode } from 'vs/editor/common/model/intervalTree'; const GENERATE_TESTS = false; let TEST_COUNT = GENERATE_TESTS ? 10000 : 0; @@ -17,6 +17,18 @@ const MAX_INSERTS = 30; suite('IntervalTree', () => { + class Interval { + _intervalBrand: void; + + public start: number; + public end: number; + + constructor(start: number, end: number) { + this.start = start; + this.end = end; + } + } + class Oracle { public intervals: Interval[]; @@ -96,7 +108,7 @@ suite('IntervalTree', () => { this._tree.assertInvariants(); - let actual = this._tree.getAllInOrder(); + let actual = this._tree.getAllInOrder().map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); let expected = this._oracle.intervals; assert.deepEqual(actual, expected); } From c2da4060c8c5c8dd24bc5a36a0b3ccc0d2289eea Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Oct 2017 09:20:32 +0200 Subject: [PATCH 14/64] Document current decoration behaviour --- .../common/model/modelDecorations.test.ts | 697 +++++++++++++++++- 1 file changed, 693 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 84defed4adaec..1699981d0da0b 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -27,6 +27,7 @@ function modelHasDecorations(model: Model, decorations: ILightWeightDecoration2[ className: actualDecorations[i].options.className }); } + modelDecorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); assert.deepEqual(modelDecorations, decorations, 'Model decorations'); } @@ -168,13 +169,13 @@ suite('Editor Model - Model Decorations', () => { var decId1 = addDecoration(thisModel, 1, 2, 3, 2, 'myType1'); var decId2 = addDecoration(thisModel, 1, 2, 3, 1, 'myType2'); modelHasDecorations(thisModel, [ - { - range: new Range(1, 2, 3, 2), - className: 'myType1' - }, { range: new Range(1, 2, 3, 1), className: 'myType2' + }, + { + range: new Range(1, 2, 3, 2), + className: 'myType1' } ]); thisModel.changeDecorations((changeAccessor) => { @@ -352,6 +353,694 @@ suite('Editor Model - Model Decorations', () => { }); }); +suite('Decorations and editing', () => { + + function _runTest(decRange: Range, stickiness: TrackedRangeStickiness, editRange: Range, editText: string, editForceMoveMarkers: boolean, expectedDecRange: Range, msg: string): void { + let model = Model.createFromString([ + 'My First Line', + 'My Second Line', + 'Third Line' + ].join('\n')); + + const id = model.deltaDecorations([], [{ range: decRange, options: { stickiness: stickiness } }])[0]; + model.applyEdits([{ range: editRange, text: editText, forceMoveMarkers: editForceMoveMarkers, identifier: null }]); + const actual = model.getDecorationRange(id); + assert.deepEqual(actual, expectedDecRange, msg); + + model.dispose(); + } + + function runTest(decRange: Range, editRange: Range, editText: string, expectedDecRange: Range[][]): void { + _runTest(decRange, 0, editRange, editText, false, expectedDecRange[0][0], 'no-0-AlwaysGrowsWhenTypingAtEdges'); + _runTest(decRange, 1, editRange, editText, false, expectedDecRange[0][1], 'no-1-NeverGrowsWhenTypingAtEdges'); + _runTest(decRange, 2, editRange, editText, false, expectedDecRange[0][2], 'no-2-GrowsOnlyWhenTypingBefore'); + _runTest(decRange, 3, editRange, editText, false, expectedDecRange[0][3], 'no-3-GrowsOnlyWhenTypingAfter'); + + _runTest(decRange, 0, editRange, editText, true, expectedDecRange[1][0], 'force-0-AlwaysGrowsWhenTypingAtEdges'); + _runTest(decRange, 1, editRange, editText, true, expectedDecRange[1][1], 'force-1-NeverGrowsWhenTypingAtEdges'); + _runTest(decRange, 2, editRange, editText, true, expectedDecRange[1][2], 'force-2-GrowsOnlyWhenTypingBefore'); + _runTest(decRange, 3, editRange, editText, true, expectedDecRange[1][3], 'force-3-GrowsOnlyWhenTypingAfter'); + } + + suite('insert', () => { + suite('collapsed dec', () => { + test('before', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 3), 'xx', + [ + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + ] + ); + }); + test('equal', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 4), 'xx', + [ + [new Range(1, 4, 1, 6), new Range(1, 6, 1, 6), new Range(1, 4, 1, 4), new Range(1, 6, 1, 6)], + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + ] + ); + }); + test('after', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 5), 'xx', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('before', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 3), 'xx', + [ + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + ] + ); + }); + test('start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 4), 'xx', + [ + [new Range(1, 4, 1, 11), new Range(1, 6, 1, 11), new Range(1, 4, 1, 11), new Range(1, 6, 1, 11)], + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + ] + ); + }); + test('inside', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 5), 'xx', + [ + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + ] + ); + }); + test('end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 9), 'xx', + [ + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 11)], + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + ] + ); + }); + test('after', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 10), 'xx', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + }); + }); + + suite('delete', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), '', + [ + [new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2)], + [new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), '', + [ + [new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2)], + [new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2), new Range(1, 2, 1, 2)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), '', + [ + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), '', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), '', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), '', + [ + [new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7)], + [new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), '', + [ + [new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7)], + [new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7), new Range(1, 2, 1, 7)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), '', + [ + [new Range(1, 3, 1, 7), new Range(1, 3, 1, 7), new Range(1, 3, 1, 7), new Range(1, 3, 1, 7)], + [new Range(1, 3, 1, 7), new Range(1, 3, 1, 7), new Range(1, 3, 1, 7), new Range(1, 3, 1, 7)], + ] + ); + }); + + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), '', + [ + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + ] + ); + }); + + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), '', + [ + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + ] + ); + }); + + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), '', + [ + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + ] + ); + }); + + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), '', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), '', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), '', + [ + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), '', + [ + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), '', + [ + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + ] + ); + }); + + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), '', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), '', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + }); + }); + + suite('replace short', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), 'c', + [ + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), 'c', + [ + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + [new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3), new Range(1, 3, 1, 3)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), 'c', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), 'c', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), 'c', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), 'c', + [ + [new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8)], + [new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), 'c', + [ + [new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8)], + [new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8), new Range(1, 3, 1, 8)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), 'c', + [ + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + ] + ); + }); + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), 'c', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), 'c', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), 'c', + [ + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + [new Range(1, 5, 1, 8), new Range(1, 5, 1, 8), new Range(1, 5, 1, 8), new Range(1, 5, 1, 8)], + ] + ); + }); + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), 'c', + [ + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + [new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5)], + ] + ); + }); + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), 'c', + [ + [new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5), new Range(1, 4, 1, 5)], + [new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5), new Range(1, 5, 1, 5)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), 'c', + [ + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), 'c', + [ + [new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6)], + [new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), 'c', + [ + [new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6)], + [new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6), new Range(1, 4, 1, 6)], + ] + ); + }); + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), 'c', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 10), new Range(1, 4, 1, 10), new Range(1, 4, 1, 10), new Range(1, 4, 1, 10)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), 'c', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + }); + }); + + suite('replace long', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), 'cccc', + [ + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), 'cccc', + [ + [new Range(1, 4, 1, 6), new Range(1, 6, 1, 6), new Range(1, 4, 1, 4), new Range(1, 6, 1, 6)], + [new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6), new Range(1, 6, 1, 6)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), 'cccc', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), 'cccc', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), 'cccc', + [ + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + [new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4), new Range(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), 'cccc', + [ + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), 'cccc', + [ + [new Range(1, 4, 1, 11), new Range(1, 6, 1, 11), new Range(1, 4, 1, 11), new Range(1, 6, 1, 11)], + [new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11), new Range(1, 6, 1, 11)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), 'cccc', + [ + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + [new Range(1, 7, 1, 11), new Range(1, 7, 1, 11), new Range(1, 7, 1, 11), new Range(1, 7, 1, 11)], + ] + ); + }); + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), 'cccc', + [ + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + [new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), 'cccc', + [ + [new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7), new Range(1, 4, 1, 7)], + [new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7), new Range(1, 7, 1, 7)], + ] + ); + }); + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), 'cccc', + [ + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + [new Range(1, 8, 1, 11), new Range(1, 8, 1, 11), new Range(1, 8, 1, 11), new Range(1, 8, 1, 11)], + ] + ); + }); + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), 'cccc', + [ + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + [new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8)], + ] + ); + }); + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), 'cccc', + [ + [new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8), new Range(1, 4, 1, 8)], + [new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8), new Range(1, 8, 1, 8)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), 'cccc', + [ + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + [new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11), new Range(1, 4, 1, 11)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), 'cccc', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), 'cccc', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), 'cccc', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 13), new Range(1, 4, 1, 13), new Range(1, 4, 1, 13), new Range(1, 4, 1, 13)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), 'cccc', + [ + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + [new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9), new Range(1, 4, 1, 9)], + ] + ); + }); + }); + }); +}); + interface ILightWeightDecoration { id: string; range: Range; From 10c5d96af2a9ddfe63a1dea396e2cc8d38eca9d3 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Oct 2017 18:04:00 +0200 Subject: [PATCH 15/64] Fix issue in IntervalTree.resolveNode --- src/vs/editor/common/model/intervalTree.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index b1e0cb4fa4496..f62dfd2ba3e4d 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -149,6 +149,7 @@ export class IntervalTree { } public resolveNode(node: IntervalNode, cachedVersionId: number): void { + const initialNode = node; let delta = 0; while (node !== this.root) { if (node === node.parent.right) { @@ -157,9 +158,9 @@ export class IntervalTree { node = node.parent; } - const nodeStart = node.start + delta; - const nodeEnd = node.end + delta; - node.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); + const nodeStart = initialNode.start + delta; + const nodeEnd = initialNode.end + delta; + initialNode.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); } public assertInvariants(): void { From e7ae7d781e4460461065fa9947be67011bed6485 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Oct 2017 18:05:31 +0200 Subject: [PATCH 16/64] Fix issue in changeDecoration --- src/vs/editor/common/model/textModelWithDecorations.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 2b9befae2f9fd..5579578d5ffe3 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -842,7 +842,9 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; this._tree.delete(node); - + node.start = startOffset; + node.end = endOffset; + node.maxEnd = endOffset; node.setCachedOffsets(startOffset, endOffset, this.getVersionId()); node.range = range; this._tree.insert(node); From bfedda7b2fd07c209b8271282c1654a2a676ecdf Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Oct 2017 18:07:14 +0200 Subject: [PATCH 17/64] Adjust tests for USE_NEW_DECORATIONS --- .../editor/common/model/textModelWithDecorations.ts | 2 +- .../snippet/test/browser/snippetSession.test.ts | 13 +++++++++++-- .../test/common/model/modelDecorations.test.ts | 9 +++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 5579578d5ffe3..00d521007faf0 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -19,7 +19,7 @@ import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { IntervalNode, IntervalTree } from 'vs/editor/common/model/intervalTree'; -const USE_NEW_DECORATIONS = false; +export const USE_NEW_DECORATIONS = false; export const ClassName = { EditorInfoDecoration: 'infosquiggly', diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts index 2bb0c11006dff..d29f6908dc95d 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts @@ -12,6 +12,7 @@ import { SnippetSession } from 'vs/editor/contrib/snippet/browser/snippetSession import { ICommonCodeEditor } from 'vs/editor/common/editorCommon'; import { mockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; import { Model } from 'vs/editor/common/model/model'; +import { USE_NEW_DECORATIONS } from 'vs/editor/common/model/textModelWithDecorations'; suite('SnippetSession', function () { @@ -234,9 +235,17 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); session.prev(); - assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); + if (USE_NEW_DECORATIONS) { + assertSelections(editor, new Selection(1, 7, 1, 10), new Selection(2, 11, 2, 14)); + } else { + assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); + } session.prev(); - assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); + if (USE_NEW_DECORATIONS) { + assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11)); + } else { + assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); + } session.prev(); assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(2, 5, 2, 8)); }); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 1699981d0da0b..4d0c3bbbe9512 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -10,6 +10,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/editorCommon'; import { Model } from 'vs/editor/common/model/model'; +import { USE_NEW_DECORATIONS } from 'vs/editor/common/model/textModelWithDecorations'; // --------- utils @@ -1094,7 +1095,11 @@ suite('deltaDecorations', () => { assert.equal(initialIds.length, decorations.length, 'returns expected cnt of ids'); assert.equal(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); assert.equal(initialIds.length, model._getTrackedRangesCount(), 'does not leak tracked ranges'); + if (!USE_NEW_DECORATIONS) { assert.equal(2 * initialIds.length, model._getMarkersCount(), 'does not leak markers'); + } else { + assert.equal(0, model._getMarkersCount(), 'does not leak markers'); + } actualDecorations.sort((a, b) => strcmp(a.id, b.id)); decorations.sort((a, b) => strcmp(a.id, b.id)); assert.deepEqual(actualDecorations, decorations); @@ -1105,7 +1110,11 @@ suite('deltaDecorations', () => { assert.equal(newIds.length, newDecorations.length, 'returns expected cnt of ids'); assert.equal(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); assert.equal(newIds.length, model._getTrackedRangesCount(), 'does not leak tracked ranges'); + if (!USE_NEW_DECORATIONS) { assert.equal(2 * newIds.length, model._getMarkersCount(), 'does not leak markers'); + } else { + assert.equal(0, model._getMarkersCount(), 'does not leak markers'); + } actualNewDecorations.sort((a, b) => strcmp(a.id, b.id)); newDecorations.sort((a, b) => strcmp(a.id, b.id)); assert.deepEqual(actualDecorations, decorations); From 167c2e739b7cba51a9435ac4b7ed1f82bd467924 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Oct 2017 18:09:54 +0200 Subject: [PATCH 18/64] Always emit a model decorations changed event when the model is being edited --- src/vs/editor/common/model/textModelWithDecorations.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 00d521007faf0..5232e03aaa4f2 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -137,7 +137,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed private _currentMarkersTracker: MarkersTracker; private _currentMarkersTrackerCnt: number; - private _tree: IntervalTree; + protected _tree: IntervalTree; private _treeDecorations: { [decorationId: string]: IntervalNode; }; private _decorations: { [decorationId: string]: InternalDecoration; }; @@ -587,13 +587,13 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed private _handleTrackedMarkers(markersTracker: MarkersTracker): void { let changedInternalDecorationIds = markersTracker.getDecorationIds(); if (changedInternalDecorationIds.length === 0) { + this.emitModelDecorationsChangedEvent(); return; } changedInternalDecorationIds.sort(); let previousInternalDecorationId: number = 0; - let somethingChanged = false; for (let i = 0, len = changedInternalDecorationIds.length; i < len; i++) { let internalDecorationId = changedInternalDecorationIds[i]; if (internalDecorationId === previousInternalDecorationId) { @@ -611,12 +611,9 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed let endMarker = decoration.endMarker.position; let range = TextModelWithDecorations._createRangeFromMarkers(startMarker, endMarker); decoration.setRange(this._multiLineDecorationsMap, range); - somethingChanged = true; } - if (somethingChanged) { - this.emitModelDecorationsChangedEvent(); - } + this.emitModelDecorationsChangedEvent(); } private static _createRangeFromMarkers(startPosition: Position, endPosition: Position): Range { From 1a37562eb7c4cd3f3794c1e647d94271bde721ba Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Oct 2017 19:31:36 +0200 Subject: [PATCH 19/64] Implement IntervalTree.acceptReplace --- src/vs/editor/common/model/intervalTree.ts | 282 +++++++++++++++++++++ 1 file changed, 282 insertions(+) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index f62dfd2ba3e4d..584bd513e5ad9 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -19,6 +19,17 @@ export const ClassName = { EditorErrorDecoration: 'errorsquiggly' }; +/** + * Describes the behavior of decorations when typing/editing near their edges. + * Note: Please do not edit the values, as they very carefully match `DecorationRangeBehavior` + */ +export const enum TrackedRangeStickiness { + AlwaysGrowsWhenTypingAtEdges = 0, + NeverGrowsWhenTypingAtEdges = 1, + GrowsOnlyWhenTypingBefore = 2, + GrowsOnlyWhenTypingAfter = 3, +} + export const enum NodeColor { Red, Black @@ -40,6 +51,7 @@ export class IntervalNode implements IModelDecoration { public ownerId: number; public options: ModelDecorationOptions; public isForValidation: boolean; + public stickiness: TrackedRangeStickiness; public cachedVersionId: number; public cachedAbsoluteStart: number; @@ -63,6 +75,7 @@ export class IntervalNode implements IModelDecoration { this.ownerId = 0; this.options = null; this.isForValidation = false; + this.stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; this.cachedVersionId = 0; this.cachedAbsoluteStart = start; @@ -78,6 +91,7 @@ export class IntervalNode implements IModelDecoration { this.options.className === ClassName.EditorErrorDecoration || this.options.className === ClassName.EditorWarningDecoration ); + this.stickiness = this.options.stickiness; } public setCachedOffsets(absoluteStart: number, absoluteEnd: number, cachedVersionId: number): void { @@ -163,6 +177,32 @@ export class IntervalTree { initialNode.setCachedOffsets(nodeStart, nodeEnd, cachedVersionId); } + public acceptReplace(offset: number, length: number, textLength: number, forceMoveMarkers: boolean): void { + // Our strategy is to remove all directly impacted nodes, and then add them back to the tree. + + // (1) collect all nodes that are intersecting this edit as nodes of interest + const nodesOfInterest = searchForEditing(this, offset, offset + length); + + // (2) remove all nodes that are intersecting this edit + for (let i = 0, len = nodesOfInterest.length; i < len; i++) { + const node = nodesOfInterest[i]; + rbTreeDelete(this, node); + } + + // (3) edit all tree nodes except the nodes of interest + noOverlapReplace(this, offset, offset + length, textLength); + + // (4) edit the nodes of interest and insert them back in the tree + for (let i = 0, len = nodesOfInterest.length; i < len; i++) { + const node = nodesOfInterest[i]; + node.start = node.cachedAbsoluteStart; + node.end = node.cachedAbsoluteEnd; + nodeAcceptEdit(0, node, node.start, node.end, offset, (offset + length), textLength, forceMoveMarkers); + node.maxEnd = node.end; + rbTreeInsert(this, node); + } + } + public assertInvariants(): void { assert(SENTINEL.color === NodeColor.Black); assert(SENTINEL.parent === SENTINEL); @@ -180,6 +220,10 @@ export class IntervalTree { } public print(): void { + if (this.root === SENTINEL) { + console.log(`~~ empty`); + return; + } let out: string[] = []; this._print(this.root, '', 0, out); console.log(out.join('')); @@ -200,6 +244,244 @@ export class IntervalTree { } } + +//#region Editing + +const enum MarkerMoveSemantics { + MarkerDefined = 0, + ForceMove = 1, + ForceStay = 2 +} + +function adjustMarkerBeforeColumn(markerOffset: number, markerStickToPreviousCharacter: boolean, checkOffset: number, moveSemantics: MarkerMoveSemantics): boolean { + if (markerOffset < checkOffset) { + return true; + } + if (markerOffset > checkOffset) { + return false; + } + if (moveSemantics === MarkerMoveSemantics.ForceMove) { + return false; + } + if (moveSemantics === MarkerMoveSemantics.ForceStay) { + return true; + } + return markerStickToPreviousCharacter; +}; + +function nodeAcceptEdit(delta: number, node: IntervalNode, nodeStart: number, nodeEnd: number, start: number, end: number, textLength: number, forceMoveMarkers: boolean): void { + const startStickToPreviousCharacter = ( + node.stickiness === TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + || node.stickiness === TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + ); + const endStickToPreviousCharacter = ( + node.stickiness === TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + || node.stickiness === TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + ); + + const deletingCnt = (end - start); + const insertingCnt = textLength; + const commonLength = Math.min(deletingCnt, insertingCnt); + + let startDone = false; + let endDone = false; + + { + const moveSemantics = forceMoveMarkers ? MarkerMoveSemantics.ForceMove : (deletingCnt > 0 ? MarkerMoveSemantics.ForceStay : MarkerMoveSemantics.MarkerDefined); + if (!startDone && adjustMarkerBeforeColumn(nodeStart, startStickToPreviousCharacter, start, moveSemantics)) { + startDone = true; + } + if (!endDone && adjustMarkerBeforeColumn(nodeEnd, endStickToPreviousCharacter, start, moveSemantics)) { + endDone = true; + } + } + + if (commonLength > 0 && !forceMoveMarkers) { + const moveSemantics = (deletingCnt > insertingCnt ? MarkerMoveSemantics.ForceStay : MarkerMoveSemantics.MarkerDefined); + if (!startDone && adjustMarkerBeforeColumn(nodeStart, startStickToPreviousCharacter, start + commonLength, moveSemantics)) { + startDone = true; + } + if (!endDone && adjustMarkerBeforeColumn(nodeEnd, endStickToPreviousCharacter, start + commonLength, moveSemantics)) { + endDone = true; + } + } + + { + const moveSemantics = forceMoveMarkers ? MarkerMoveSemantics.ForceMove : MarkerMoveSemantics.MarkerDefined; + if (!startDone && adjustMarkerBeforeColumn(nodeStart, startStickToPreviousCharacter, end, moveSemantics)) { + const desiredStart = start + insertingCnt; + node.start = desiredStart - delta; + startDone = true; + } + if (!endDone && adjustMarkerBeforeColumn(nodeEnd, endStickToPreviousCharacter, end, moveSemantics)) { + const desiredEnd = start + insertingCnt; + node.end = desiredEnd - delta; + endDone = true; + } + } + + // Finish + const deltaColumn = (insertingCnt - deletingCnt); + if (!startDone) { + const desiredStart = Math.max(0, nodeStart + deltaColumn); + node.start = desiredStart - delta; + startDone = true; + } + if (!endDone) { + const desiredEnd = Math.max(0, nodeEnd + deltaColumn); + node.end = desiredEnd - delta; + endDone = true; + } + + if (node.start > node.end) { + node.end = node.start; + } +} + +function searchForEditing(T: IntervalTree, start: number, end: number): IntervalNode[] { + // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree + // Now, it is known that two intervals A and B overlap only when both + // A.low <= B.high and A.high >= B.low. When searching the trees for + // nodes overlapping with a given interval, you can immediately skip: + // a) all nodes to the right of nodes whose low value is past the end of the given interval. + // b) all nodes that have their maximum 'high' value below the start of the given interval. + let node = T.root; + let delta = 0; + let nodeMaxEnd = 0; + let nodeStart = 0; + let nodeEnd = 0; + let result: IntervalNode[] = []; + let resultLen = 0; + while (node !== SENTINEL) { + if (node.visited) { + // going up from this node + node.left.visited = false; + node.right.visited = false; + if (node === node.parent.right) { + delta -= node.parent.delta; + } + node = node.parent; + continue; + } + + if (!node.left.visited) { + // first time seeing this node + nodeMaxEnd = delta + node.maxEnd; + if (nodeMaxEnd < start) { + // cover case b) from above + // there is no need to search this node or its children + node.visited = true; + continue; + } + + if (node.left !== SENTINEL) { + // go left + node = node.left; + continue; + } + } + + // handle current node + nodeStart = delta + node.start; + if (nodeStart > end) { + // cover case a) from above + // there is no need to search this node or its right subtree + node.visited = true; + continue; + } + + nodeEnd = delta + node.end; + if (nodeEnd >= start) { + node.setCachedOffsets(nodeStart, nodeEnd, 0); + result[resultLen++] = node; + } + node.visited = true; + + if (node.right !== SENTINEL && !node.right.visited) { + // go right + delta += node.delta; + node = node.right; + continue; + } + } + + if (T.root) { + T.root.visited = false; + } + + return result; +} + +function noOverlapReplace(T: IntervalTree, start: number, end: number, textLength: number): void { + // https://en.wikipedia.org/wiki/Interval_tree#Augmented_tree + // Now, it is known that two intervals A and B overlap only when both + // A.low <= B.high and A.high >= B.low. When searching the trees for + // nodes overlapping with a given interval, you can immediately skip: + // a) all nodes to the right of nodes whose low value is past the end of the given interval. + // b) all nodes that have their maximum 'high' value below the start of the given interval. + let node = T.root; + let delta = 0; + let nodeMaxEnd = 0; + let nodeStart = 0; + while (node !== SENTINEL) { + if (node.visited) { + // going up from this node + node.left.visited = false; + node.right.visited = false; + if (node === node.parent.right) { + delta -= node.parent.delta; + } + recomputeMaxEnd(node); + node = node.parent; + continue; + } + + if (!node.left.visited) { + // first time seeing this node + nodeMaxEnd = delta + node.maxEnd; + if (nodeMaxEnd < start) { + // cover case b) from above + // there is no need to search this node or its children + node.visited = true; + continue; + } + + if (node.left !== SENTINEL) { + // go left + node = node.left; + continue; + } + } + + // handle current node + nodeStart = delta + node.start; + if (nodeStart > end) { + node.start += (textLength - (end - start)); + node.end += (textLength - (end - start)); + node.delta += (textLength - (end - start)); + // cover case a) from above + // there is no need to search this node or its right subtree + node.visited = true; + continue; + } + + node.visited = true; + + if (node.right !== SENTINEL && !node.right.visited) { + // go right + delta += node.delta; + node = node.right; + continue; + } + } + + if (T.root) { + T.root.visited = false; + } +} + +//#endregion + //#region Searching function nodeCount(T: IntervalTree): number { From b179e24a85494d64aa231c6b35c91fe045b3a8d0 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Oct 2017 20:43:51 +0200 Subject: [PATCH 20/64] Update interval tree when edits occur --- .../editor/common/model/editableTextModel.ts | 10 +- src/vs/editor/common/model/intervalTree.ts | 23 +-- .../common/model/textModelWithDecorations.ts | 7 + .../common/model/editableTextModel.test.ts | 160 +++++++++--------- .../common/model/modelDecorations.test.ts | 4 +- 5 files changed, 112 insertions(+), 92 deletions(-) diff --git a/src/vs/editor/common/model/editableTextModel.ts b/src/vs/editor/common/model/editableTextModel.ts index 66566d1092b4f..e8c617fc2c230 100644 --- a/src/vs/editor/common/model/editableTextModel.ts +++ b/src/vs/editor/common/model/editableTextModel.ts @@ -23,6 +23,7 @@ export interface IValidatedEditOperation { sortIndex: number; identifier: editorCommon.ISingleEditOperationIdentifier; range: Range; + rangeOffset: number; rangeLength: number; lines: string[]; forceMoveMarkers: boolean; @@ -249,6 +250,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito sortIndex: 0, identifier: operations[0].identifier, range: entireEditRange, + rangeOffset: this.getOffsetAt(entireEditRange.getStartPosition()), rangeLength: this.getValueLengthInRange(entireEditRange), lines: result.join('').split('\n'), forceMoveMarkers: forceMoveMarkers, @@ -288,6 +290,8 @@ export class EditableTextModel extends TextModelWithDecorations implements edito return []; } + this._ensureLineStarts(); + let mightContainRTL = this._mightContainRTL; let mightContainNonBasicASCII = this._mightContainNonBasicASCII; let canReduceOperations = true; @@ -310,6 +314,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito sortIndex: i, identifier: op.identifier, range: validatedRange, + rangeOffset: this.getOffsetAt(validatedRange.getStartPosition()), rangeLength: this.getValueLengthInRange(validatedRange), lines: op.text ? op.text.split(/\r\n|\r|\n/) : null, forceMoveMarkers: op.forceMoveMarkers, @@ -675,12 +680,15 @@ export class EditableTextModel extends TextModelWithDecorations implements edito ); } + const text = (op.lines ? op.lines.join(this.getEOL()) : ''); contentChanges.push({ range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), rangeLength: op.rangeLength, - text: op.lines ? op.lines.join(this.getEOL()) : '' + text: text }); + this._tree.acceptReplace(op.rangeOffset, op.rangeLength, text.length, op.forceMoveMarkers); + // console.log('AFTER:'); // console.log('<<<\n' + this._lines.map(l => l.text).join('\n') + '\n>>>'); } diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 584bd513e5ad9..b73d5adb293d5 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -197,7 +197,7 @@ export class IntervalTree { const node = nodesOfInterest[i]; node.start = node.cachedAbsoluteStart; node.end = node.cachedAbsoluteEnd; - nodeAcceptEdit(0, node, node.start, node.end, offset, (offset + length), textLength, forceMoveMarkers); + nodeAcceptEdit(node, offset, (offset + length), textLength, forceMoveMarkers); node.maxEnd = node.end; rbTreeInsert(this, node); } @@ -269,7 +269,11 @@ function adjustMarkerBeforeColumn(markerOffset: number, markerStickToPreviousCha return markerStickToPreviousCharacter; }; -function nodeAcceptEdit(delta: number, node: IntervalNode, nodeStart: number, nodeEnd: number, start: number, end: number, textLength: number, forceMoveMarkers: boolean): void { +/** + * This is a lot more complicated than strictly necessary to maintain the same behaviour + * as when decorations were implemented using two markers. + */ +function nodeAcceptEdit(node: IntervalNode, start: number, end: number, textLength: number, forceMoveMarkers: boolean): void { const startStickToPreviousCharacter = ( node.stickiness === TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges || node.stickiness === TrackedRangeStickiness.GrowsOnlyWhenTypingBefore @@ -283,7 +287,10 @@ function nodeAcceptEdit(delta: number, node: IntervalNode, nodeStart: number, no const insertingCnt = textLength; const commonLength = Math.min(deletingCnt, insertingCnt); + const nodeStart = node.start; let startDone = false; + + const nodeEnd = node.end; let endDone = false; { @@ -309,13 +316,11 @@ function nodeAcceptEdit(delta: number, node: IntervalNode, nodeStart: number, no { const moveSemantics = forceMoveMarkers ? MarkerMoveSemantics.ForceMove : MarkerMoveSemantics.MarkerDefined; if (!startDone && adjustMarkerBeforeColumn(nodeStart, startStickToPreviousCharacter, end, moveSemantics)) { - const desiredStart = start + insertingCnt; - node.start = desiredStart - delta; + node.start = start + insertingCnt; startDone = true; } if (!endDone && adjustMarkerBeforeColumn(nodeEnd, endStickToPreviousCharacter, end, moveSemantics)) { - const desiredEnd = start + insertingCnt; - node.end = desiredEnd - delta; + node.end = start + insertingCnt; endDone = true; } } @@ -323,13 +328,11 @@ function nodeAcceptEdit(delta: number, node: IntervalNode, nodeStart: number, no // Finish const deltaColumn = (insertingCnt - deletingCnt); if (!startDone) { - const desiredStart = Math.max(0, nodeStart + deltaColumn); - node.start = desiredStart - delta; + node.start = Math.max(0, nodeStart + deltaColumn); startDone = true; } if (!endDone) { - const desiredEnd = Math.max(0, nodeEnd + deltaColumn); - node.end = desiredEnd - delta; + node.end = Math.max(0, nodeEnd + deltaColumn); endDone = true; } diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 5232e03aaa4f2..bcece19251462 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -169,6 +169,10 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed this._decorations = null; this._internalDecorations = null; this._multiLineDecorationsMap = null; + + this._tree = null; + this._treeDecorations = null; + super.dispose(); } @@ -179,6 +183,9 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed this._decorations = Object.create(null); this._internalDecorations = Object.create(null); this._multiLineDecorationsMap = Object.create(null); + + this._tree = new IntervalTree(); + this._treeDecorations = Object.create(null); } private static _shouldStartMarkerSticksToPreviousCharacter(stickiness: editorCommon.TrackedRangeStickiness): boolean { diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index 6166f3eb8e568..266b6b67f05d0 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -15,12 +15,13 @@ import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvent suite('EditorModel - EditableTextModel._getInverseEdits', () => { - function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, rangeLength: number, text: string[]): IValidatedEditOperation { + function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, text: string[]): IValidatedEditOperation { return { sortIndex: 0, identifier: null, range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), - rangeLength: rangeLength, + rangeOffset: 0, + rangeLength: 0, lines: text, forceMoveMarkers: false, isAutoWhitespaceEdit: false @@ -39,7 +40,7 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('single insert', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello']) + editOp(1, 1, 1, 1, ['hello']) ], [ inverseEditOp(1, 1, 1, 6) @@ -50,8 +51,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('Bug 19872: Undo is funky', () => { assertInverseEdits( [ - editOp(2, 1, 2, 2, 0, ['']), - editOp(3, 1, 4, 2, 0, ['']) + editOp(2, 1, 2, 2, ['']), + editOp(3, 1, 4, 2, ['']) ], [ inverseEditOp(2, 1, 2, 1), @@ -63,8 +64,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single unrelated inserts', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello']), - editOp(2, 1, 2, 1, 0, ['world']) + editOp(1, 1, 1, 1, ['hello']), + editOp(2, 1, 2, 1, ['world']) ], [ inverseEditOp(1, 1, 1, 6), @@ -76,8 +77,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single inserts 1', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello']), - editOp(1, 2, 1, 2, 0, ['world']) + editOp(1, 1, 1, 1, ['hello']), + editOp(1, 2, 1, 2, ['world']) ], [ inverseEditOp(1, 1, 1, 6), @@ -89,8 +90,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single inserts 2', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello']), - editOp(1, 4, 1, 4, 0, ['world']) + editOp(1, 1, 1, 1, ['hello']), + editOp(1, 4, 1, 4, ['world']) ], [ inverseEditOp(1, 1, 1, 6), @@ -102,7 +103,7 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('multiline insert', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello', 'world']) + editOp(1, 1, 1, 1, ['hello', 'world']) ], [ inverseEditOp(1, 1, 2, 6) @@ -113,8 +114,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two unrelated multiline inserts', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello', 'world']), - editOp(2, 1, 2, 1, 0, ['how', 'are', 'you?']), + editOp(1, 1, 1, 1, ['hello', 'world']), + editOp(2, 1, 2, 1, ['how', 'are', 'you?']), ], [ inverseEditOp(1, 1, 2, 6), @@ -126,8 +127,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two multiline inserts 1', () => { assertInverseEdits( [ - editOp(1, 1, 1, 1, 0, ['hello', 'world']), - editOp(1, 2, 1, 2, 0, ['how', 'are', 'you?']), + editOp(1, 1, 1, 1, ['hello', 'world']), + editOp(1, 2, 1, 2, ['how', 'are', 'you?']), ], [ inverseEditOp(1, 1, 2, 6), @@ -139,7 +140,7 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('single delete', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, null) + editOp(1, 1, 1, 6, null) ], [ inverseEditOp(1, 1, 1, 1) @@ -150,8 +151,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single unrelated deletes', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, null), - editOp(2, 1, 2, 6, 0, null) + editOp(1, 1, 1, 6, null), + editOp(2, 1, 2, 6, null) ], [ inverseEditOp(1, 1, 1, 1), @@ -163,8 +164,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single deletes 1', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, null), - editOp(1, 7, 1, 12, 0, null) + editOp(1, 1, 1, 6, null), + editOp(1, 7, 1, 12, null) ], [ inverseEditOp(1, 1, 1, 1), @@ -176,8 +177,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two single deletes 2', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, null), - editOp(1, 9, 1, 14, 0, null) + editOp(1, 1, 1, 6, null), + editOp(1, 9, 1, 14, null) ], [ inverseEditOp(1, 1, 1, 1), @@ -189,7 +190,7 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('multiline delete', () => { assertInverseEdits( [ - editOp(1, 1, 2, 6, 0, null) + editOp(1, 1, 2, 6, null) ], [ inverseEditOp(1, 1, 1, 1) @@ -200,8 +201,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two unrelated multiline deletes', () => { assertInverseEdits( [ - editOp(1, 1, 2, 6, 0, null), - editOp(3, 1, 5, 5, 0, null), + editOp(1, 1, 2, 6, null), + editOp(3, 1, 5, 5, null), ], [ inverseEditOp(1, 1, 1, 1), @@ -213,8 +214,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two multiline deletes 1', () => { assertInverseEdits( [ - editOp(1, 1, 2, 6, 0, null), - editOp(2, 7, 4, 5, 0, null), + editOp(1, 1, 2, 6, null), + editOp(2, 7, 4, 5, null), ], [ inverseEditOp(1, 1, 1, 1), @@ -226,7 +227,7 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('single replace', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, ['Hello world']) + editOp(1, 1, 1, 6, ['Hello world']) ], [ inverseEditOp(1, 1, 1, 12) @@ -237,8 +238,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('two replaces', () => { assertInverseEdits( [ - editOp(1, 1, 1, 6, 0, ['Hello world']), - editOp(1, 7, 1, 8, 0, ['How are you?']), + editOp(1, 1, 1, 6, ['Hello world']), + editOp(1, 7, 1, 8, ['How are you?']), ], [ inverseEditOp(1, 1, 1, 12), @@ -250,9 +251,9 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { test('many edits', () => { assertInverseEdits( [ - editOp(1, 2, 1, 2, 0, ['', ' ']), - editOp(1, 5, 1, 6, 0, ['']), - editOp(1, 9, 1, 9, 0, ['', '']) + editOp(1, 2, 1, 2, ['', ' ']), + editOp(1, 5, 1, 6, ['']), + editOp(1, 9, 1, 9, ['', '']) ], [ inverseEditOp(1, 2, 2, 3), @@ -265,11 +266,12 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { - function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, rangeLength: number, text: string[]): IValidatedEditOperation { + function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, rangeOffset: number, rangeLength: number, text: string[]): IValidatedEditOperation { return { sortIndex: 0, identifier: null, range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), + rangeOffset: rangeOffset, rangeLength: rangeLength, lines: text, forceMoveMarkers: false, @@ -297,9 +299,9 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '1' ], [ - editOp(1, 3, 1, 3, 0, [' new line', 'No longer']) + editOp(1, 3, 1, 3, 2, 0, [' new line', 'No longer']) ], - editOp(1, 3, 1, 3, 0, [' new line', 'No longer']) + editOp(1, 3, 1, 3, 2, 0, [' new line', 'No longer']) ); }); @@ -311,11 +313,11 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '', '1' ], [ - editOp(1, 1, 1, 3, 0, ['Your']), - editOp(1, 4, 1, 4, 0, ['Interesting ']), - editOp(2, 3, 2, 6, 0, null) + editOp(1, 1, 1, 3, 0, 2, ['Your']), + editOp(1, 4, 1, 4, 3, 0, ['Interesting ']), + editOp(2, 3, 2, 6, 16, 3, null) ], - editOp(1, 1, 2, 6, 19, [ + editOp(1, 1, 2, 6, 0, 19, [ 'Your Interesting First Line', '\t\t' ])); @@ -331,10 +333,10 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '1' ], [ - editOp(1, 3, 1, 3, 0, ['', '', '', '', '']), - editOp(3, 15, 3, 15, 0, ['a', 'b']) + editOp(1, 3, 1, 3, 2, 0, ['', '', '', '', '']), + editOp(3, 15, 3, 15, 45, 0, ['a', 'b']) ], - editOp(1, 3, 3, 15, 43, [ + editOp(1, 3, 3, 15, 2, 43, [ '', '', '', @@ -357,9 +359,9 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '1' ], [ - editOp(1, 1, 1, 1, 0, ['']) + editOp(1, 1, 1, 1, 0, 0, ['']) ], - editOp(1, 1, 1, 1, 0, ['']) + editOp(1, 1, 1, 1, 0, 0, ['']) ); }); @@ -373,10 +375,10 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '123' ], [ - editOp(2, 1, 2, 3, 0, ['\t']), - editOp(3, 1, 3, 5, 0, ['']) + editOp(2, 1, 2, 3, 14, 2, ['\t']), + editOp(3, 1, 3, 5, 31, 4, ['']) ], - editOp(2, 1, 3, 5, 21, ['\tMy Second Line', '']) + editOp(2, 1, 3, 5, 14, 21, ['\tMy Second Line', '']) ); }); @@ -386,11 +388,11 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '{"x" : 1}' ], [ - editOp(1, 2, 1, 2, 0, ['\n ']), - editOp(1, 5, 1, 6, 0, ['']), - editOp(1, 9, 1, 9, 0, ['\n']) + editOp(1, 2, 1, 2, 1, 0, ['\n ']), + editOp(1, 5, 1, 6, 4, 1, ['']), + editOp(1, 9, 1, 9, 8, 0, ['\n']) ], - editOp(1, 2, 1, 9, 7, [ + editOp(1, 2, 1, 9, 1, 7, [ '', ' "x": 1', '' @@ -406,11 +408,11 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '}' ], [ - editOp(1, 2, 2, 3, 0, ['']), - editOp(2, 6, 2, 6, 0, [' ']), - editOp(2, 9, 3, 1, 0, ['']) + editOp(1, 2, 2, 3, 1, 3, ['']), + editOp(2, 6, 2, 6, 7, 0, [' ']), + editOp(2, 9, 3, 1, 10, 1, ['']) ], - editOp(1, 2, 3, 1, 10, ['"x" : 1']) + editOp(1, 2, 3, 1, 1, 10, ['"x" : 1']) ); }); @@ -424,10 +426,10 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { '}' ], [ - editOp(1, 2, 2, 1, 0, ['', '\t']), - editOp(2, 11, 4, 1, 0, ['', '\t']) + editOp(1, 2, 2, 1, 1, 1, ['', '\t']), + editOp(2, 11, 4, 1, 12, 2, ['', '\t']) ], - editOp(1, 2, 4, 1, 13, [ + editOp(1, 2, 4, 1, 1, 13, [ '', '\t"a": true,', '\t' @@ -446,12 +448,12 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { 'and the last line' ], [ - editOp(1, 5, 3, 1, 0, [' text', 'some more text', 'some more text']), - editOp(3, 2, 4, 1, 0, ['o more lines', 'asd', 'asd', 'asd']), - editOp(5, 1, 5, 6, 0, ['zzzzzzzz']), - editOp(5, 11, 6, 16, 0, ['1', '2', '3', '4']) + editOp(1, 5, 3, 1, 4, 21, [' text', 'some more text', 'some more text']), + editOp(3, 2, 4, 1, 26, 23, ['o more lines', 'asd', 'asd', 'asd']), + editOp(5, 1, 5, 6, 50, 5, ['zzzzzzzz']), + editOp(5, 11, 6, 16, 60, 22, ['1', '2', '3', '4']) ], - editOp(1, 5, 6, 16, 78, [ + editOp(1, 5, 6, 16, 4, 78, [ ' text', 'some more text', 'some more textno more lines', @@ -475,17 +477,17 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { ' ,"e": /*comment*/ [null] }', ], [ - editOp(1, 1, 1, 2, 0, ['']), - editOp(1, 3, 1, 10, 0, ['', ' ']), - editOp(1, 16, 2, 14, 0, ['', ' ']), - editOp(2, 18, 3, 9, 0, ['', ' ']), - editOp(3, 22, 4, 9, 0, ['']), - editOp(4, 10, 4, 10, 0, ['', ' ']), - editOp(4, 28, 4, 28, 0, ['', ' ']), - editOp(4, 32, 4, 32, 0, ['', ' ']), - editOp(4, 33, 4, 34, 0, ['', '']) + editOp(1, 1, 1, 2, 0, 1, ['']), + editOp(1, 3, 1, 10, 2, 7, ['', ' ']), + editOp(1, 16, 2, 14, 15, 14, ['', ' ']), + editOp(2, 18, 3, 9, 33, 9, ['', ' ']), + editOp(3, 22, 4, 9, 55, 9, ['']), + editOp(4, 10, 4, 10, 65, 0, ['', ' ']), + editOp(4, 28, 4, 28, 83, 0, ['', ' ']), + editOp(4, 32, 4, 32, 87, 0, ['', ' ']), + editOp(4, 33, 4, 34, 88, 1, ['', '']) ], - editOp(1, 1, 4, 34, 89, [ + editOp(1, 1, 4, 34, 0, 89, [ '{', ' "d": [', ' null', @@ -505,11 +507,11 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { ' ,def' ], [ - editOp(1, 1, 1, 4, 0, ['']), - editOp(1, 7, 2, 2, 0, ['']), - editOp(2, 3, 2, 3, 0, ['', '']) + editOp(1, 1, 1, 4, 0, 3, ['']), + editOp(1, 7, 2, 2, 6, 2, ['']), + editOp(2, 3, 2, 3, 9, 0, ['', '']) ], - editOp(1, 1, 2, 3, 9, [ + editOp(1, 1, 2, 3, 0, 9, [ 'abc,', '' ]) diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 4d0c3bbbe9512..8e808191925ea 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -1096,7 +1096,7 @@ suite('deltaDecorations', () => { assert.equal(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); assert.equal(initialIds.length, model._getTrackedRangesCount(), 'does not leak tracked ranges'); if (!USE_NEW_DECORATIONS) { - assert.equal(2 * initialIds.length, model._getMarkersCount(), 'does not leak markers'); + assert.equal(2 * initialIds.length, model._getMarkersCount(), 'does not leak markers'); } else { assert.equal(0, model._getMarkersCount(), 'does not leak markers'); } @@ -1111,7 +1111,7 @@ suite('deltaDecorations', () => { assert.equal(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); assert.equal(newIds.length, model._getTrackedRangesCount(), 'does not leak tracked ranges'); if (!USE_NEW_DECORATIONS) { - assert.equal(2 * newIds.length, model._getMarkersCount(), 'does not leak markers'); + assert.equal(2 * newIds.length, model._getMarkersCount(), 'does not leak markers'); } else { assert.equal(0, model._getMarkersCount(), 'does not leak markers'); } From d4408e2bdd2cfee7bb7f5b9d8dacb06edcc02273 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Oct 2017 22:13:48 +0200 Subject: [PATCH 21/64] Optimize deltaDecorations --- src/vs/editor/common/model/intervalTree.ts | 10 ++ .../common/model/textModelWithDecorations.ts | 141 ++++++++---------- 2 files changed, 75 insertions(+), 76 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index b73d5adb293d5..9b6970da50878 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -85,6 +85,16 @@ export class IntervalNode implements IModelDecoration { this.visited = false; } + public reset(versionId: number, start: number, end: number, range: Range): void { + this.start = start; + this.end = end; + this.maxEnd = end; + this.cachedVersionId = versionId; + this.cachedAbsoluteStart = start; + this.cachedAbsoluteEnd = end; + this.range = range; + } + public setOptions(options: ModelDecorationOptions) { this.options = options; this.isForValidation = ( diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index bcece19251462..b4289f3aedd8b 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -228,7 +228,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed let changeAccessor: editorCommon.IModelDecorationsChangeAccessor = { addDecoration: (range: IRange, options: editorCommon.IModelDecorationOptions): string => { if (USE_NEW_DECORATIONS) { - return this._deltaDecorationsImpl2(decorationsTracker, [], this._normalizeDeltaDecorations2(ownerId, [{ range: range, options: options }]))[0]; + return this._deltaDecorationsImpl2(decorationsTracker, ownerId, [], [{ range: range, options: options }])[0]; } return this._addDecorationImpl(decorationsTracker, ownerId, this.validateRange(range), _normalizeOptions(options)); }, @@ -248,14 +248,14 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed }, removeDecoration: (id: string): void => { if (USE_NEW_DECORATIONS) { - this._deltaDecorationsImpl2(decorationsTracker, [id], []); + this._deltaDecorationsImpl2(decorationsTracker, ownerId, [id], []); return; } this._removeDecorationImpl(decorationsTracker, id); }, deltaDecorations: (oldDecorations: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] => { if (USE_NEW_DECORATIONS) { - return this._deltaDecorationsImpl2(decorationsTracker, oldDecorations, this._normalizeDeltaDecorations2(ownerId, newDecorations)); + return this._deltaDecorationsImpl2(decorationsTracker, ownerId, oldDecorations, newDecorations); } return this._deltaDecorationsImpl(decorationsTracker, ownerId, oldDecorations, this._normalizeDeltaDecorations(newDecorations)); } @@ -679,30 +679,6 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return result; } - private _normalizeDeltaDecorations2(ownerId: number, deltaDecorations: editorCommon.IModelDeltaDecoration[]): IntervalNode[] { - this._ensureLineStarts(); - - const versionId = this.getVersionId(); - let result = new Array(deltaDecorations.length); - for (let i = 0, len = deltaDecorations.length; i < len; i++) { - const dec = deltaDecorations[i]; - const range = this._validateRangeRelaxedNoAllocations(dec.range); - const options = _normalizeOptions(dec.options); - - const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; - const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; - - const node = new IntervalNode(null, startOffset, endOffset); - node.ownerId = ownerId; - node.cachedVersionId = versionId; - node.range = range; - node.setOptions(options); - - result[i] = node; - } - return result; - } - private _externalDecorationId(internalId: number): string { return `${this._instanceId};${internalId}`; } @@ -790,25 +766,6 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return decorationIds; } - private _addDecorationsImpl2(decorationsTracker: DecorationsTracker, newDecorations: IntervalNode[]): string[] { - - let decorationIds: string[] = new Array(newDecorations.length); - for (let i = 0, len = newDecorations.length; i < len; i++) { - const node = newDecorations[i]; - const internalDecorationId = (++this._lastDecorationId); - const decorationId = this._externalDecorationId(internalDecorationId); - node.id = decorationId; - - this._tree.insert(node); - this._treeDecorations[decorationId] = node; - decorationIds[i] = decorationId; - } - - decorationsTracker.markDidAddDecorations(); - - return decorationIds; - } - private _changeDecorationImpl(decorationsTracker: DecorationsTracker, decorationId: string, newRange: Range): void { let decoration = this._decorations[decorationId]; if (!decoration) { @@ -846,11 +803,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; this._tree.delete(node); - node.start = startOffset; - node.end = endOffset; - node.maxEnd = endOffset; - node.setCachedOffsets(startOffset, endOffset, this.getVersionId()); - node.range = range; + node.reset(this.getVersionId(), startOffset, endOffset, range); this._tree.insert(node); decorationsTracker.markDidChangeDecorations(); @@ -926,23 +879,6 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } } - private _removeDecorationsImpl2(decorationsTracker: DecorationsTracker, decorationIds: string[]): void { - for (let i = 0, len = decorationIds.length; i < len; i++) { - const decorationId = decorationIds[i]; - const node = this._treeDecorations[decorationId]; - if (!node) { - continue; - } - - this._tree.delete(node); - delete this._treeDecorations[decorationId]; - } - - if (decorationsTracker) { - decorationsTracker.markDidRemoveDecorations(); - } - } - private _resolveOldDecorations(oldDecorations: string[]): InternalDecoration[] { let result: InternalDecoration[] = []; for (let i = 0, len = oldDecorations.length; i < len; i++) { @@ -1045,16 +981,69 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return result; } - private _deltaDecorationsImpl2(decorationsTracker: DecorationsTracker, oldDecorationsIds: string[], newDecorations: IntervalNode[]): string[] { - // TODO@interval: is it worth it to compare? - if (oldDecorationsIds.length > 0) { - this._removeDecorationsImpl2(decorationsTracker, oldDecorationsIds); - } + private _deltaDecorationsImpl2(decorationsTracker: DecorationsTracker, ownerId: number, oldDecorationsIds: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] { + this._ensureLineStarts(); + const versionId = this.getVersionId(); + + const oldDecorationsLen = oldDecorationsIds.length; + let oldDecorationIndex = 0; + + const newDecorationsLen = newDecorations.length; + let newDecorationIndex = 0; + + let result = new Array(newDecorationsLen); + while (oldDecorationIndex < oldDecorationsLen || newDecorationIndex < newDecorationsLen) { - if (newDecorations.length > 0) { - return this._addDecorationsImpl2(decorationsTracker, newDecorations); + let node: IntervalNode = null; + + if (oldDecorationIndex < oldDecorationsLen) { + // (1) get ourselves an old node + do { + node = this._treeDecorations[oldDecorationsIds[oldDecorationIndex++]]; + } while (!node && oldDecorationIndex < oldDecorationsLen); + + // (2) remove the node from the tree (if it exists) + if (node) { + this._tree.delete(node); + } + } + + if (newDecorationIndex < newDecorationsLen) { + // (3) create a new node if necessary + if (!node) { + const internalDecorationId = (++this._lastDecorationId); + const decorationId = this._externalDecorationId(internalDecorationId); + node = new IntervalNode(decorationId, 0, 0); + this._treeDecorations[decorationId] = node; + } + + // (4) initialize node + const newDecoration = newDecorations[newDecorationIndex]; + const range = this._validateRangeRelaxedNoAllocations(newDecoration.range); + const options = _normalizeOptions(newDecoration.options); + const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; + + node.ownerId = ownerId; + node.reset(versionId, startOffset, endOffset, range); + node.setOptions(options); + + this._tree.insert(node); + + result[newDecorationIndex] = node.id; + + newDecorationIndex++; + } else { + if (node) { + delete this._treeDecorations[node.id]; + } + } } - return []; + + decorationsTracker.markDidAddDecorations(); + decorationsTracker.markDidRemoveDecorations(); + + return result; } } From 324650128e3ec2442a88cf031a3207ff2a39262a Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 16 Oct 2017 22:48:45 +0200 Subject: [PATCH 22/64] Add fast path for vscode.TextEditor.setDecorations --- src/vs/editor/common/commonCodeEditor.ts | 21 ++++++++++++++ src/vs/editor/common/editorCommon.ts | 5 ++++ .../api/electron-browser/mainThreadEditor.ts | 11 ++++++++ .../api/electron-browser/mainThreadEditors.ts | 8 ++++++ src/vs/workbench/api/node/extHost.protocol.ts | 1 + .../workbench/api/node/extHostTextEditor.ts | 28 +++++++++++++++---- .../api/node/extHostTypeConverters.ts | 2 +- .../api/extHostTextEditor.test.ts | 1 + 8 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/common/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index 2c38ec938d230..4881d847e08a8 100644 --- a/src/vs/editor/common/commonCodeEditor.ts +++ b/src/vs/editor/common/commonCodeEditor.ts @@ -30,6 +30,7 @@ import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/ed import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions'; import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; let EDITOR_ID = 0; @@ -856,6 +857,26 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo this._decorationTypeKeysToIds[decorationTypeKey] = this.deltaDecorations(oldDecorationsIds, newModelDecorations); } + public setDecorationsFast(decorationTypeKey: string, ranges: IRange[]): void { + + // remove decoration sub types that are no longer used, deregister decoration type if necessary + let oldDecorationsSubTypes = this._decorationTypeSubtypes[decorationTypeKey] || {}; + for (let subType in oldDecorationsSubTypes) { + this._removeDecorationType(decorationTypeKey + '-' + subType); + } + this._decorationTypeSubtypes[decorationTypeKey] = {}; + + const opts = ModelDecorationOptions.createDynamic(this._resolveDecorationOptions(decorationTypeKey, false)); + let newModelDecorations: editorCommon.IModelDeltaDecoration[] = new Array(ranges.length); + for (let i = 0, len = ranges.length; i < len; i++) { + newModelDecorations[i] = { range: ranges[i], options: opts }; + } + + // update all decorations + let oldDecorationsIds = this._decorationTypeKeysToIds[decorationTypeKey] || []; + this._decorationTypeKeysToIds[decorationTypeKey] = this.deltaDecorations(oldDecorationsIds, newModelDecorations); + } + public removeDecorations(decorationTypeKey: string): void { // remove decorations for type and sub type let oldDecorationsIds = this._decorationTypeKeysToIds[decorationTypeKey]; diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index e0f3ad0a16005..837485b70a9a9 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -1984,6 +1984,11 @@ export interface ICommonCodeEditor extends IEditor { */ setDecorations(decorationTypeKey: string, ranges: IDecorationOptions[]): void; + /** + * @internal + */ + setDecorationsFast(decorationTypeKey: string, ranges: IRange[]): void; + /** * @internal */ diff --git a/src/vs/workbench/api/electron-browser/mainThreadEditor.ts b/src/vs/workbench/api/electron-browser/mainThreadEditor.ts index 0da01725b1756..bc23649dbe8ce 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadEditor.ts @@ -244,6 +244,17 @@ export class MainThreadTextEditor { this._codeEditor.setDecorations(key, ranges); } + public setDecorationsFast(key: string, _ranges: number[]): void { + if (!this._codeEditor) { + return; + } + let ranges: Range[] = []; + for (let i = 0, len = Math.floor(_ranges.length / 4); i < len; i++) { + ranges[i] = new Range(_ranges[4 * i], _ranges[4 * i + 1], _ranges[4 * i + 2], _ranges[4 * i + 3]); + } + this._codeEditor.setDecorationsFast(key, ranges); + } + public revealRange(range: IRange, revealType: TextEditorRevealType): void { if (!this._codeEditor) { return; diff --git a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts index df3174ae67cec..6787006461b0c 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts @@ -195,6 +195,14 @@ export class MainThreadEditors implements MainThreadEditorsShape { return TPromise.as(null); } + $trySetDecorationsFast(id: string, key: string, ranges: string): TPromise { + if (!this._documentsAndEditors.getEditor(id)) { + return TPromise.wrapError(disposed(`TextEditor(${id})`)); + } + this._documentsAndEditors.getEditor(id).setDecorationsFast(key, /*TODO: marshaller is too slow*/JSON.parse(ranges)); + return TPromise.as(null); + } + $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise { if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index a3d65955516c7..0dfea1602f338 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -210,6 +210,7 @@ export interface MainThreadEditorsShape extends IDisposable { $tryHideEditor(id: string): TPromise; $trySetOptions(id: string, options: ITextEditorConfigurationUpdate): TPromise; $trySetDecorations(id: string, key: string, ranges: editorCommon.IDecorationOptions[]): TPromise; + $trySetDecorationsFast(id: string, key: string, ranges: string): TPromise; $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise; $trySetSelections(id: string, selections: ISelection[]): TPromise; $tryApplyEdits(id: string, modelVersionId: number, edits: editorCommon.ISingleEditOperation[], opts: IApplyEditsOptions): TPromise; diff --git a/src/vs/workbench/api/node/extHostTextEditor.ts b/src/vs/workbench/api/node/extHostTextEditor.ts index 763969acc9696..9872ac721fdaa 100644 --- a/src/vs/workbench/api/node/extHostTextEditor.ts +++ b/src/vs/workbench/api/node/extHostTextEditor.ts @@ -416,11 +416,29 @@ export class ExtHostTextEditor implements vscode.TextEditor { setDecorations(decorationType: vscode.TextEditorDecorationType, ranges: Range[] | vscode.DecorationOptions[]): void { this._runOnProxy( - () => this._proxy.$trySetDecorations( - this._id, - decorationType.key, - TypeConverters.fromRangeOrRangeWithMessage(ranges) - ) + () => { + if (TypeConverters.isDecorationOptionsArr(ranges)) { + return this._proxy.$trySetDecorations( + this._id, + decorationType.key, + TypeConverters.fromRangeOrRangeWithMessage(ranges) + ); + } else { + let _ranges: number[] = new Array(4 * ranges.length); + for (let i = 0, len = ranges.length; i < len; i++) { + const range = ranges[i]; + _ranges[4 * i] = range.start.line + 1; + _ranges[4 * i + 1] = range.start.character + 1; + _ranges[4 * i + 2] = range.end.line + 1; + _ranges[4 * i + 3] = range.end.character + 1; + } + return this._proxy.$trySetDecorationsFast( + this._id, + decorationType.key, + /*TODO: marshaller is too slow*/JSON.stringify(_ranges) + ); + } + } ); } diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 54c80ba84c46d..ca9561d8d251f 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -139,7 +139,7 @@ function isDecorationOptions(something: any): something is vscode.DecorationOpti return (typeof something.range !== 'undefined'); } -function isDecorationOptionsArr(something: vscode.Range[] | vscode.DecorationOptions[]): something is vscode.DecorationOptions[] { +export function isDecorationOptionsArr(something: vscode.Range[] | vscode.DecorationOptions[]): something is vscode.DecorationOptions[] { if (something.length === 0) { return true; } diff --git a/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts index 402466a620e12..48d6e46c0c71d 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts @@ -61,6 +61,7 @@ suite('ExtHostTextEditorOptions', () => { $tryShowEditor: undefined, $tryHideEditor: undefined, $trySetDecorations: undefined, + $trySetDecorationsFast: undefined, $tryRevealRange: undefined, $trySetSelections: undefined, $tryApplyEdits: undefined, From 5aff04bc9e57a6df22e6c026ad38bdaa377c44f2 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 09:50:52 +0200 Subject: [PATCH 23/64] Capture current side editing behaviour --- .../test/common/commands/sideEditing.test.ts | 698 ++++++++++++++++++ 1 file changed, 698 insertions(+) diff --git a/src/vs/editor/test/common/commands/sideEditing.test.ts b/src/vs/editor/test/common/commands/sideEditing.test.ts index 08fe4e0c5e747..a5e2100692c2b 100644 --- a/src/vs/editor/test/common/commands/sideEditing.test.ts +++ b/src/vs/editor/test/common/commands/sideEditing.test.ts @@ -12,6 +12,10 @@ import { Selection } from 'vs/editor/common/core/selection'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; import { ILineEdit, ModelLine, LineMarker, MarkersTracker } from 'vs/editor/common/model/modelLine'; import { withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; +import { Model } from 'vs/editor/common/model/model'; +import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; +import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; +import { Cursor } from 'vs/editor/common/controller/cursor'; const NO_TAB_SIZE = 0; @@ -204,5 +208,699 @@ suite('Editor Side Editing - collapsed selection', () => { [new Selection(1, 1, 1, 1), new Selection(1, 3, 1, 3)] ); }); +}); +suite('SideEditing', () => { + + const LINES = [ + 'My First Line', + 'My Second Line', + 'Third Line' + ]; + + function _runTest(selection: Selection, editRange: Range, editText: string, editForceMoveMarkers: boolean, expected: Selection, msg: string): void { + const model = Model.createFromString(LINES.join('\n')); + const config = new TestConfiguration(null); + const viewModel = new ViewModel(0, config, model, null); + const cursor = new Cursor(config, model, viewModel); + + cursor.setSelections('tests', [selection]); + model.applyEdits([{ range: editRange, text: editText, forceMoveMarkers: editForceMoveMarkers, identifier: null }]); + const actual = cursor.getSelection(); + assert.deepEqual(actual.toString(), expected.toString(), msg); + + cursor.dispose(); + viewModel.dispose(); + config.dispose(); + model.dispose(); + } + + function runTest(selection: Range, editRange: Range, editText: string, expected: Selection[][]): void { + const sel1 = new Selection(selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn); + _runTest(sel1, editRange, editText, false, expected[0][0], '0-0-regular-no-force'); + _runTest(sel1, editRange, editText, true, expected[1][0], '1-0-regular-force'); + + // RTL selection + const sel2 = new Selection(selection.endLineNumber, selection.endColumn, selection.startLineNumber, selection.startColumn); + _runTest(sel2, editRange, editText, false, expected[0][1], '0-1-inverse-no-force'); + _runTest(sel2, editRange, editText, true, expected[1][1], '1-1-inverse-force'); + } + + suite('insert', () => { + suite('collapsed sel', () => { + test('before', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 3), 'xx', + [ + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + ] + ); + }); + test('equal', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 4), 'xx', + [ + [new Selection(1, 4, 1, 6), new Selection(1, 4, 1, 6)], + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + ] + ); + }); + test('after', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 5), 'xx', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('before', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 3), 'xx', + [ + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + ] + ); + }); + test('start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 4), 'xx', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + ] + ); + }); + test('inside', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 5), 'xx', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + ] + ); + }); + test('end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 9), 'xx', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + ] + ); + }); + test('after', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 10), 'xx', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + }); + }); + + suite('delete', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), '', + [ + [new Selection(1, 2, 1, 2), new Selection(1, 2, 1, 2)], + [new Selection(1, 2, 1, 2), new Selection(1, 2, 1, 2)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), '', + [ + [new Selection(1, 2, 1, 2), new Selection(1, 2, 1, 2)], + [new Selection(1, 2, 1, 2), new Selection(1, 2, 1, 2)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), '', + [ + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), '', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), '', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), '', + [ + [new Selection(1, 2, 1, 7), new Selection(1, 7, 1, 2)], + [new Selection(1, 2, 1, 7), new Selection(1, 7, 1, 2)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), '', + [ + [new Selection(1, 2, 1, 7), new Selection(1, 7, 1, 2)], + [new Selection(1, 2, 1, 7), new Selection(1, 7, 1, 2)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), '', + [ + [new Selection(1, 3, 1, 7), new Selection(1, 7, 1, 3)], + [new Selection(1, 3, 1, 7), new Selection(1, 7, 1, 3)], + ] + ); + }); + + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), '', + [ + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + ] + ); + }); + + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), '', + [ + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + ] + ); + }); + + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), '', + [ + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + ] + ); + }); + + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), '', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), '', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), '', + [ + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), '', + [ + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + ] + ); + }); + + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), '', + [ + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + ] + ); + }); + + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), '', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), '', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + }); + }); + + suite('replace short', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), 'c', + [ + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), 'c', + [ + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + [new Selection(1, 3, 1, 3), new Selection(1, 3, 1, 3)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), 'c', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), 'c', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 5, 1, 5), new Selection(1, 5, 1, 5)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), 'c', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), 'c', + [ + [new Selection(1, 3, 1, 8), new Selection(1, 8, 1, 3)], + [new Selection(1, 3, 1, 8), new Selection(1, 8, 1, 3)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), 'c', + [ + [new Selection(1, 3, 1, 8), new Selection(1, 8, 1, 3)], + [new Selection(1, 3, 1, 8), new Selection(1, 8, 1, 3)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), 'c', + [ + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + ] + ); + }); + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), 'c', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), 'c', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), 'c', + [ + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + [new Selection(1, 5, 1, 8), new Selection(1, 8, 1, 5)], + ] + ); + }); + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), 'c', + [ + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + [new Selection(1, 5, 1, 5), new Selection(1, 5, 1, 5)], + ] + ); + }); + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), 'c', + [ + [new Selection(1, 4, 1, 5), new Selection(1, 5, 1, 4)], + [new Selection(1, 5, 1, 5), new Selection(1, 5, 1, 5)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), 'c', + [ + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), 'c', + [ + [new Selection(1, 4, 1, 6), new Selection(1, 6, 1, 4)], + [new Selection(1, 4, 1, 6), new Selection(1, 6, 1, 4)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), 'c', + [ + [new Selection(1, 4, 1, 6), new Selection(1, 6, 1, 4)], + [new Selection(1, 4, 1, 6), new Selection(1, 6, 1, 4)], + ] + ); + }); + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), 'c', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 10), new Selection(1, 10, 1, 4)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), 'c', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + }); + }); + + suite('replace long', () => { + suite('collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 1, 1, 3), 'cccc', + [ + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 2, 1, 4), 'cccc', + [ + [new Selection(1, 4, 1, 6), new Selection(1, 4, 1, 6)], + [new Selection(1, 6, 1, 6), new Selection(1, 6, 1, 6)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 3, 1, 5), 'cccc', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 7, 1, 7), new Selection(1, 7, 1, 7)], + ] + ); + }); + test('edit.start >= range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 4, 1, 6), 'cccc', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 8, 1, 8), new Selection(1, 8, 1, 8)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 4), + new Range(1, 5, 1, 7), 'cccc', + [ + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + [new Selection(1, 4, 1, 4), new Selection(1, 4, 1, 4)], + ] + ); + }); + }); + suite('non-collapsed dec', () => { + test('edit.end < range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 1, 1, 3), 'cccc', + [ + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + ] + ); + }); + test('edit.end <= range.start', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 2, 1, 4), 'cccc', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 6, 1, 11), new Selection(1, 11, 1, 6)], + ] + ); + }); + test('edit.start < range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 5), 'cccc', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 7, 1, 11), new Selection(1, 11, 1, 7)], + ] + ); + }); + test('edit.start < range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 9), 'cccc', + [ + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + [new Selection(1, 7, 1, 7), new Selection(1, 7, 1, 7)], + ] + ); + }); + test('edit.start < range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 3, 1, 10), 'cccc', + [ + [new Selection(1, 4, 1, 7), new Selection(1, 7, 1, 4)], + [new Selection(1, 7, 1, 7), new Selection(1, 7, 1, 7)], + ] + ); + }); + test('edit.start == range.start && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 6), 'cccc', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 8, 1, 11), new Selection(1, 11, 1, 8)], + ] + ); + }); + test('edit.start == range.start && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 9), 'cccc', + [ + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + [new Selection(1, 8, 1, 8), new Selection(1, 8, 1, 8)], + ] + ); + }); + test('edit.start == range.start && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 4, 1, 10), 'cccc', + [ + [new Selection(1, 4, 1, 8), new Selection(1, 8, 1, 4)], + [new Selection(1, 8, 1, 8), new Selection(1, 8, 1, 8)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end < range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 7), 'cccc', + [ + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + [new Selection(1, 4, 1, 11), new Selection(1, 11, 1, 4)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 9), 'cccc', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + test('edit.start > range.start && edit.start < range.end && edit.end > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 5, 1, 10), 'cccc', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + test('edit.start == range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 9, 1, 11), 'cccc', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 13), new Selection(1, 13, 1, 4)], + ] + ); + }); + test('edit.start > range.end', () => { + runTest( + new Range(1, 4, 1, 9), + new Range(1, 10, 1, 11), 'cccc', + [ + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + [new Selection(1, 4, 1, 9), new Selection(1, 9, 1, 4)], + ] + ); + }); + }); + }); }); \ No newline at end of file From 400afdbd4428fb3a2e0201bd4e631edda79a51f5 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 11:33:27 +0200 Subject: [PATCH 24/64] Reduce reliance on _addMarker for cursor tracking --- src/vs/editor/common/controller/oneCursor.ts | 51 ++++++++++++--- src/vs/editor/common/editorCommon.ts | 10 +++ src/vs/editor/common/model/textModel.ts | 2 + .../common/model/textModelWithDecorations.ts | 63 ++++++++++++++++--- 4 files changed, 109 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index ae9c990ec4a3d..8e96e9130b873 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -8,6 +8,8 @@ import { SingleCursorState, CursorContext, CursorState } from 'vs/editor/common/ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; +import { TrackedRangeStickiness } from 'vs/editor/common/editorCommon'; +import { USE_NEW_DECORATIONS } from 'vs/editor/common/model/textModelWithDecorations'; export class OneCursor { @@ -17,7 +19,19 @@ export class OneCursor { private _selStartMarker: string; private _selEndMarker: string; + private _selTrackedRange: string; + constructor(context: CursorContext) { + this.modelState = null; + this.viewState = null; + + if (USE_NEW_DECORATIONS) { + this._selTrackedRange = null; + } else { + this._selStartMarker = null; + this._selEndMarker = null; + } + this._setState( context, new SingleCursorState(new Range(1, 1, 1, 1), 0, new Position(1, 1), 0), @@ -26,8 +40,12 @@ export class OneCursor { } public dispose(context: CursorContext): void { - context.model._removeMarker(this._selStartMarker); - context.model._removeMarker(this._selEndMarker); + if (USE_NEW_DECORATIONS) { + this._selTrackedRange = context.model._deltaTrackedRange(this._selTrackedRange, null, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); + } else { + context.model._removeMarker(this._selStartMarker); + context.model._removeMarker(this._selEndMarker); + } } public asCursorState(): CursorState { @@ -35,14 +53,23 @@ export class OneCursor { } public readSelectionFromMarkers(context: CursorContext): Selection { - const start = context.model._getMarker(this._selStartMarker); - const end = context.model._getMarker(this._selEndMarker); + if (USE_NEW_DECORATIONS) { + const range = context.model._getTrackedRange(this._selTrackedRange); + if (this.modelState.selection.getDirection() === SelectionDirection.LTR) { + return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + } - if (this.modelState.selection.getDirection() === SelectionDirection.LTR) { - return new Selection(start.lineNumber, start.column, end.lineNumber, end.column); - } + return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); + } else { + const start = context.model._getMarker(this._selStartMarker); + const end = context.model._getMarker(this._selEndMarker); + + if (this.modelState.selection.getDirection() === SelectionDirection.LTR) { + return new Selection(start.lineNumber, start.column, end.lineNumber, end.column); + } - return new Selection(end.lineNumber, end.column, start.lineNumber, start.column); + return new Selection(end.lineNumber, end.column, start.lineNumber, start.column); + } } public ensureValidState(context: CursorContext): void { @@ -100,8 +127,12 @@ export class OneCursor { this.modelState = modelState; this.viewState = viewState; - this._selStartMarker = this._ensureMarker(context, this._selStartMarker, this.modelState.selection.startLineNumber, this.modelState.selection.startColumn, true); - this._selEndMarker = this._ensureMarker(context, this._selEndMarker, this.modelState.selection.endLineNumber, this.modelState.selection.endColumn, false); + if (USE_NEW_DECORATIONS) { + this._selTrackedRange = context.model._deltaTrackedRange(this._selTrackedRange, this.modelState.selection, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); + } else { + this._selStartMarker = this._ensureMarker(context, this._selStartMarker, this.modelState.selection.startLineNumber, this.modelState.selection.startColumn, true); + this._selEndMarker = this._ensureMarker(context, this._selEndMarker, this.modelState.selection.endLineNumber, this.modelState.selection.endColumn, false); + } } private _ensureMarker(context: CursorContext, markerId: string, lineNumber: number, column: number, stickToPreviousCharacter: boolean): string { diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 837485b70a9a9..6fb2a0796561d 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -1041,6 +1041,16 @@ export interface ITextModelWithDecorations { * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). */ getOverviewRulerDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + + /** + * @internal + */ + _getTrackedRange(id: string): Range; + + /** + * @internal + */ + _deltaTrackedRange(id: string, newRange: Range, newStickiness: TrackedRangeStickiness): string; } /** diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 4af332e8f1422..493af0fb03778 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -8,6 +8,7 @@ import { OrderGuaranteeEventEmitter, BulkListenerCallback } from 'vs/base/common import * as strings from 'vs/base/common/strings'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { Range, IRange } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ModelLine, IModelLine, MinimalModelLine } from 'vs/editor/common/model/modelLine'; import { guessIndentation } from 'vs/editor/common/model/indentationGuesser'; @@ -670,6 +671,7 @@ export class TextModel implements editorCommon.ITextModel { && initialEndLineNumber === endLineNumber && initialEndColumn === endColumn && range instanceof Range + && !(range instanceof Selection) ) { return range; } diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index b4289f3aedd8b..fc0b8921c38c3 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -228,7 +228,9 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed let changeAccessor: editorCommon.IModelDecorationsChangeAccessor = { addDecoration: (range: IRange, options: editorCommon.IModelDecorationOptions): string => { if (USE_NEW_DECORATIONS) { - return this._deltaDecorationsImpl2(decorationsTracker, ownerId, [], [{ range: range, options: options }])[0]; + decorationsTracker.markDidAddDecorations(); + decorationsTracker.markDidRemoveDecorations(); + return this._deltaDecorationsImpl2(ownerId, [], [{ range: range, options: options }])[0]; } return this._addDecorationImpl(decorationsTracker, ownerId, this.validateRange(range), _normalizeOptions(options)); }, @@ -248,14 +250,18 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed }, removeDecoration: (id: string): void => { if (USE_NEW_DECORATIONS) { - this._deltaDecorationsImpl2(decorationsTracker, ownerId, [id], []); + decorationsTracker.markDidAddDecorations(); + decorationsTracker.markDidRemoveDecorations(); + this._deltaDecorationsImpl2(ownerId, [id], []); return; } this._removeDecorationImpl(decorationsTracker, id); }, deltaDecorations: (oldDecorations: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] => { if (USE_NEW_DECORATIONS) { - return this._deltaDecorationsImpl2(decorationsTracker, ownerId, oldDecorations, newDecorations); + decorationsTracker.markDidAddDecorations(); + decorationsTracker.markDidRemoveDecorations(); + return this._deltaDecorationsImpl2(ownerId, oldDecorations, newDecorations); } return this._deltaDecorationsImpl(decorationsTracker, ownerId, oldDecorations, this._normalizeDeltaDecorations(newDecorations)); } @@ -284,6 +290,41 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed }, ownerId); } + _getTrackedRange(id: string): Range { + return this.getDecorationRange(id); + } + + _deltaTrackedRange(id: string, newRange: Range, newStickiness: editorCommon.TrackedRangeStickiness): string { + const node = (id ? this._treeDecorations[id] : null); + + if (!node) { + if (!newRange) { + // node doesn't exist, the request is to delete => nothing to do + return null; + } + // node doesn't exist, the request is to set => add the tracked range + return this._deltaDecorationsImpl2(0, [], [{ range: newRange, options: TRACKED_RANGE_OPTIONS[newStickiness] }])[0]; + } + + if (!newRange) { + // node exists, the request is to delete => delete node + this._tree.delete(node); + delete this._treeDecorations[node.id]; + return null; + } + + // node exists, the request is to set => change the tracked range and its options + const range = this._validateRangeRelaxedNoAllocations(newRange); + this._ensureLineStarts(); + const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; + this._tree.delete(node); + node.reset(this.getVersionId(), startOffset, endOffset, range); + node.setOptions(TRACKED_RANGE_OPTIONS[newStickiness]); + this._tree.insert(node); + return node.id; + } + public removeAllDecorationsWithOwnerId(ownerId: number): void { if (USE_NEW_DECORATIONS) { const nodes = this._tree.collectNodesFromOwner(ownerId); @@ -798,6 +839,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed if (!node) { return; } + this._ensureLineStarts(); const range = this._validateRangeRelaxedNoAllocations(_range); const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; @@ -981,7 +1023,7 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return result; } - private _deltaDecorationsImpl2(decorationsTracker: DecorationsTracker, ownerId: number, oldDecorationsIds: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] { + private _deltaDecorationsImpl2(ownerId: number, oldDecorationsIds: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] { this._ensureLineStarts(); const versionId = this.getVersionId(); @@ -1040,9 +1082,6 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } } - decorationsTracker.markDidAddDecorations(); - decorationsTracker.markDidRemoveDecorations(); - return result; } } @@ -1158,6 +1197,16 @@ export class ModelDecorationOptions implements editorCommon.IModelDecorationOpti } ModelDecorationOptions.EMPTY = ModelDecorationOptions.register({}); +/** + * The order carefully matches the values of the enum. + */ +const TRACKED_RANGE_OPTIONS = [ + ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }), + ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }), + ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore }), + ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter }), +]; + class ModelDeltaDecoration implements editorCommon.IModelDeltaDecoration { _modelDeltaDecorationBrand: void; From a93953101ea49b2559f303c69f26c0d762cb31de Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 12:12:16 +0200 Subject: [PATCH 25/64] Further reduce reliance on _addMarker for editing commands selection tracking --- src/vs/editor/common/controller/cursor.ts | 113 ++++++++++++++++------ 1 file changed, 81 insertions(+), 32 deletions(-) diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 65bc4059e5c82..a6227965aa72e 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -20,6 +20,7 @@ import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import Event, { Emitter } from 'vs/base/common/event'; +import { USE_NEW_DECORATIONS } from 'vs/editor/common/model/textModelWithDecorations'; function containsLineMappingChanged(events: viewEvents.ViewEvent[]): boolean { for (let i = 0, len = events.length; i < len; i++) { @@ -557,6 +558,8 @@ interface IExecContext { readonly selectionsBefore: Selection[]; readonly selectionStartMarkers: string[]; readonly positionMarkers: string[]; + readonly trackedRanges: string[]; + readonly trackedRangesDirection: SelectionDirection[]; } interface ICommandData { @@ -577,14 +580,22 @@ class CommandExecutor { model: model, selectionsBefore: selectionsBefore, selectionStartMarkers: [], - positionMarkers: [] + positionMarkers: [], + trackedRanges: [], + trackedRangesDirection: [] }; const result = this._innerExecuteCommands(ctx, commands); - for (let i = 0; i < ctx.selectionStartMarkers.length; i++) { - ctx.model._removeMarker(ctx.selectionStartMarkers[i]); - ctx.model._removeMarker(ctx.positionMarkers[i]); + if (USE_NEW_DECORATIONS) { + for (let i = 0, len = ctx.trackedRanges.length; i < len; i++) { + ctx.model._deltaTrackedRange(ctx.trackedRanges[i], null, editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); + } + } else { + for (let i = 0; i < ctx.selectionStartMarkers.length; i++) { + ctx.model._removeMarker(ctx.selectionStartMarkers[i]); + ctx.model._removeMarker(ctx.positionMarkers[i]); + } } return result; @@ -661,9 +672,17 @@ class CommandExecutor { getTrackedSelection: (id: string) => { const idx = parseInt(id, 10); - const selectionStartMarker = ctx.model._getMarker(ctx.selectionStartMarkers[idx]); - const positionMarker = ctx.model._getMarker(ctx.positionMarkers[idx]); - return new Selection(selectionStartMarker.lineNumber, selectionStartMarker.column, positionMarker.lineNumber, positionMarker.column); + if (USE_NEW_DECORATIONS) { + const range = ctx.model._getTrackedRange(ctx.trackedRanges[idx]); + if (ctx.trackedRangesDirection[idx] === SelectionDirection.LTR) { + return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + } + return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); + } else { + const selectionStartMarker = ctx.model._getMarker(ctx.selectionStartMarkers[idx]); + const positionMarker = ctx.model._getMarker(ctx.positionMarkers[idx]); + return new Selection(selectionStartMarker.lineNumber, selectionStartMarker.column, positionMarker.lineNumber, positionMarker.column); + } } }); } else { @@ -750,38 +769,68 @@ class CommandExecutor { }; const trackSelection = (selection: Selection, trackPreviousOnEmpty?: boolean) => { - let selectionMarkerStickToPreviousCharacter: boolean; - let positionMarkerStickToPreviousCharacter: boolean; - - if (selection.isEmpty()) { - // Try to lock it with surrounding text - if (typeof trackPreviousOnEmpty === 'boolean') { - selectionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; - positionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; - } else { - const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber); - if (selection.startColumn === maxLineColumn) { - selectionMarkerStickToPreviousCharacter = true; - positionMarkerStickToPreviousCharacter = true; + if (USE_NEW_DECORATIONS) { + let stickiness: editorCommon.TrackedRangeStickiness; + if (selection.isEmpty()) { + if (typeof trackPreviousOnEmpty === 'boolean') { + if (trackPreviousOnEmpty) { + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore; + } else { + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter; + } } else { - selectionMarkerStickToPreviousCharacter = false; - positionMarkerStickToPreviousCharacter = false; + // Try to lock it with surrounding text + const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber); + if (selection.startColumn === maxLineColumn) { + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore; + } else { + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter; + } } + } else { + stickiness = editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; } + + const l = ctx.trackedRanges.length; + const id = ctx.model._deltaTrackedRange(null, selection, stickiness); + ctx.trackedRanges[l] = id; + ctx.trackedRangesDirection[l] = selection.getDirection(); + return l.toString(); + } else { - if (selection.getDirection() === SelectionDirection.LTR) { - selectionMarkerStickToPreviousCharacter = false; - positionMarkerStickToPreviousCharacter = true; + let selectionMarkerStickToPreviousCharacter: boolean; + let positionMarkerStickToPreviousCharacter: boolean; + + if (selection.isEmpty()) { + // Try to lock it with surrounding text + if (typeof trackPreviousOnEmpty === 'boolean') { + selectionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; + positionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; + } else { + const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber); + if (selection.startColumn === maxLineColumn) { + selectionMarkerStickToPreviousCharacter = true; + positionMarkerStickToPreviousCharacter = true; + } else { + selectionMarkerStickToPreviousCharacter = false; + positionMarkerStickToPreviousCharacter = false; + } + } } else { - selectionMarkerStickToPreviousCharacter = true; - positionMarkerStickToPreviousCharacter = false; + if (selection.getDirection() === SelectionDirection.LTR) { + selectionMarkerStickToPreviousCharacter = false; + positionMarkerStickToPreviousCharacter = true; + } else { + selectionMarkerStickToPreviousCharacter = true; + positionMarkerStickToPreviousCharacter = false; + } } - } - const l = ctx.selectionStartMarkers.length; - ctx.selectionStartMarkers[l] = ctx.model._addMarker(0, selection.selectionStartLineNumber, selection.selectionStartColumn, selectionMarkerStickToPreviousCharacter); - ctx.positionMarkers[l] = ctx.model._addMarker(0, selection.positionLineNumber, selection.positionColumn, positionMarkerStickToPreviousCharacter); - return l.toString(); + const l = ctx.selectionStartMarkers.length; + ctx.selectionStartMarkers[l] = ctx.model._addMarker(0, selection.selectionStartLineNumber, selection.selectionStartColumn, selectionMarkerStickToPreviousCharacter); + ctx.positionMarkers[l] = ctx.model._addMarker(0, selection.positionLineNumber, selection.positionColumn, positionMarkerStickToPreviousCharacter); + return l.toString(); + } }; const editOperationBuilder: editorCommon.IEditOperationBuilder = { From 7b9e4aebffa5a5f9b2048199051002f7a9aad891 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 12:13:17 +0200 Subject: [PATCH 26/64] Flip the switch --- src/vs/editor/common/model/textModelWithDecorations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index fc0b8921c38c3..d912eb3953096 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -19,7 +19,7 @@ import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { IntervalNode, IntervalTree } from 'vs/editor/common/model/intervalTree'; -export const USE_NEW_DECORATIONS = false; +export const USE_NEW_DECORATIONS = true; export const ClassName = { EditorInfoDecoration: 'infosquiggly', From 40f61d0b902c5f5b0933514a8afea7f25b381840 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 12:24:56 +0200 Subject: [PATCH 27/64] Remove old decorations implementation --- src/vs/editor/common/controller/cursor.ts | 102 +-- src/vs/editor/common/controller/oneCursor.ts | 55 +- src/vs/editor/common/model/textModel.ts | 1 + .../common/model/textModelWithDecorations.ts | 591 ++---------------- .../test/browser/snippetSession.test.ts | 13 +- .../common/model/modelDecorations.test.ts | 13 +- 6 files changed, 81 insertions(+), 694 deletions(-) diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index a6227965aa72e..91ea1e17dcb1d 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -20,7 +20,6 @@ import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import Event, { Emitter } from 'vs/base/common/event'; -import { USE_NEW_DECORATIONS } from 'vs/editor/common/model/textModelWithDecorations'; function containsLineMappingChanged(events: viewEvents.ViewEvent[]): boolean { for (let i = 0, len = events.length; i < len; i++) { @@ -556,8 +555,6 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { interface IExecContext { readonly model: editorCommon.IModel; readonly selectionsBefore: Selection[]; - readonly selectionStartMarkers: string[]; - readonly positionMarkers: string[]; readonly trackedRanges: string[]; readonly trackedRangesDirection: SelectionDirection[]; } @@ -579,23 +576,14 @@ class CommandExecutor { const ctx: IExecContext = { model: model, selectionsBefore: selectionsBefore, - selectionStartMarkers: [], - positionMarkers: [], trackedRanges: [], trackedRangesDirection: [] }; const result = this._innerExecuteCommands(ctx, commands); - if (USE_NEW_DECORATIONS) { - for (let i = 0, len = ctx.trackedRanges.length; i < len; i++) { - ctx.model._deltaTrackedRange(ctx.trackedRanges[i], null, editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); - } - } else { - for (let i = 0; i < ctx.selectionStartMarkers.length; i++) { - ctx.model._removeMarker(ctx.selectionStartMarkers[i]); - ctx.model._removeMarker(ctx.positionMarkers[i]); - } + for (let i = 0, len = ctx.trackedRanges.length; i < len; i++) { + ctx.model._deltaTrackedRange(ctx.trackedRanges[i], null, editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); } return result; @@ -672,17 +660,11 @@ class CommandExecutor { getTrackedSelection: (id: string) => { const idx = parseInt(id, 10); - if (USE_NEW_DECORATIONS) { - const range = ctx.model._getTrackedRange(ctx.trackedRanges[idx]); - if (ctx.trackedRangesDirection[idx] === SelectionDirection.LTR) { - return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); - } - return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); - } else { - const selectionStartMarker = ctx.model._getMarker(ctx.selectionStartMarkers[idx]); - const positionMarker = ctx.model._getMarker(ctx.positionMarkers[idx]); - return new Selection(selectionStartMarker.lineNumber, selectionStartMarker.column, positionMarker.lineNumber, positionMarker.column); + const range = ctx.model._getTrackedRange(ctx.trackedRanges[idx]); + if (ctx.trackedRangesDirection[idx] === SelectionDirection.LTR) { + return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); } + return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); } }); } else { @@ -769,68 +751,32 @@ class CommandExecutor { }; const trackSelection = (selection: Selection, trackPreviousOnEmpty?: boolean) => { - if (USE_NEW_DECORATIONS) { - let stickiness: editorCommon.TrackedRangeStickiness; - if (selection.isEmpty()) { - if (typeof trackPreviousOnEmpty === 'boolean') { - if (trackPreviousOnEmpty) { - stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore; - } else { - stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter; - } + let stickiness: editorCommon.TrackedRangeStickiness; + if (selection.isEmpty()) { + if (typeof trackPreviousOnEmpty === 'boolean') { + if (trackPreviousOnEmpty) { + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore; } else { - // Try to lock it with surrounding text - const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber); - if (selection.startColumn === maxLineColumn) { - stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore; - } else { - stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter; - } + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter; } } else { - stickiness = editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; - } - - const l = ctx.trackedRanges.length; - const id = ctx.model._deltaTrackedRange(null, selection, stickiness); - ctx.trackedRanges[l] = id; - ctx.trackedRangesDirection[l] = selection.getDirection(); - return l.toString(); - - } else { - let selectionMarkerStickToPreviousCharacter: boolean; - let positionMarkerStickToPreviousCharacter: boolean; - - if (selection.isEmpty()) { // Try to lock it with surrounding text - if (typeof trackPreviousOnEmpty === 'boolean') { - selectionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; - positionMarkerStickToPreviousCharacter = trackPreviousOnEmpty; - } else { - const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber); - if (selection.startColumn === maxLineColumn) { - selectionMarkerStickToPreviousCharacter = true; - positionMarkerStickToPreviousCharacter = true; - } else { - selectionMarkerStickToPreviousCharacter = false; - positionMarkerStickToPreviousCharacter = false; - } - } - } else { - if (selection.getDirection() === SelectionDirection.LTR) { - selectionMarkerStickToPreviousCharacter = false; - positionMarkerStickToPreviousCharacter = true; + const maxLineColumn = ctx.model.getLineMaxColumn(selection.startLineNumber); + if (selection.startColumn === maxLineColumn) { + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore; } else { - selectionMarkerStickToPreviousCharacter = true; - positionMarkerStickToPreviousCharacter = false; + stickiness = editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter; } } - - const l = ctx.selectionStartMarkers.length; - ctx.selectionStartMarkers[l] = ctx.model._addMarker(0, selection.selectionStartLineNumber, selection.selectionStartColumn, selectionMarkerStickToPreviousCharacter); - ctx.positionMarkers[l] = ctx.model._addMarker(0, selection.positionLineNumber, selection.positionColumn, positionMarkerStickToPreviousCharacter); - return l.toString(); + } else { + stickiness = editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; } + + const l = ctx.trackedRanges.length; + const id = ctx.model._deltaTrackedRange(null, selection, stickiness); + ctx.trackedRanges[l] = id; + ctx.trackedRangesDirection[l] = selection.getDirection(); + return l.toString(); }; const editOperationBuilder: editorCommon.IEditOperationBuilder = { diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index 8e96e9130b873..f4be4b16a7802 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -9,28 +9,19 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import { TrackedRangeStickiness } from 'vs/editor/common/editorCommon'; -import { USE_NEW_DECORATIONS } from 'vs/editor/common/model/textModelWithDecorations'; export class OneCursor { public modelState: SingleCursorState; public viewState: SingleCursorState; - private _selStartMarker: string; - private _selEndMarker: string; - private _selTrackedRange: string; constructor(context: CursorContext) { this.modelState = null; this.viewState = null; - if (USE_NEW_DECORATIONS) { - this._selTrackedRange = null; - } else { - this._selStartMarker = null; - this._selEndMarker = null; - } + this._selTrackedRange = null; this._setState( context, @@ -40,12 +31,7 @@ export class OneCursor { } public dispose(context: CursorContext): void { - if (USE_NEW_DECORATIONS) { - this._selTrackedRange = context.model._deltaTrackedRange(this._selTrackedRange, null, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); - } else { - context.model._removeMarker(this._selStartMarker); - context.model._removeMarker(this._selEndMarker); - } + this._selTrackedRange = context.model._deltaTrackedRange(this._selTrackedRange, null, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); } public asCursorState(): CursorState { @@ -53,23 +39,11 @@ export class OneCursor { } public readSelectionFromMarkers(context: CursorContext): Selection { - if (USE_NEW_DECORATIONS) { - const range = context.model._getTrackedRange(this._selTrackedRange); - if (this.modelState.selection.getDirection() === SelectionDirection.LTR) { - return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); - } - - return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); - } else { - const start = context.model._getMarker(this._selStartMarker); - const end = context.model._getMarker(this._selEndMarker); - - if (this.modelState.selection.getDirection() === SelectionDirection.LTR) { - return new Selection(start.lineNumber, start.column, end.lineNumber, end.column); - } - - return new Selection(end.lineNumber, end.column, start.lineNumber, start.column); + const range = context.model._getTrackedRange(this._selTrackedRange); + if (this.modelState.selection.getDirection() === SelectionDirection.LTR) { + return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); } + return new Selection(range.endLineNumber, range.endColumn, range.startLineNumber, range.startColumn); } public ensureValidState(context: CursorContext): void { @@ -127,21 +101,6 @@ export class OneCursor { this.modelState = modelState; this.viewState = viewState; - if (USE_NEW_DECORATIONS) { - this._selTrackedRange = context.model._deltaTrackedRange(this._selTrackedRange, this.modelState.selection, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); - } else { - this._selStartMarker = this._ensureMarker(context, this._selStartMarker, this.modelState.selection.startLineNumber, this.modelState.selection.startColumn, true); - this._selEndMarker = this._ensureMarker(context, this._selEndMarker, this.modelState.selection.endLineNumber, this.modelState.selection.endColumn, false); - } - } - - private _ensureMarker(context: CursorContext, markerId: string, lineNumber: number, column: number, stickToPreviousCharacter: boolean): string { - if (!markerId) { - return context.model._addMarker(0, lineNumber, column, stickToPreviousCharacter); - } else { - context.model._changeMarker(markerId, lineNumber, column); - context.model._changeMarkerStickiness(markerId, stickToPreviousCharacter); - return markerId; - } + this._selTrackedRange = context.model._deltaTrackedRange(this._selTrackedRange, this.modelState.selection, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); } } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 493af0fb03778..e932ce350c3da 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -527,6 +527,7 @@ export class TextModel implements editorCommon.ITextModel { } public setEOL(eol: editorCommon.EndOfLineSequence): void { + // TODO@interval!!! this._assertNotDisposed(); const newEOL = (eol === editorCommon.EndOfLineSequence.CRLF ? '\r\n' : '\n'); if (this._EOL === newEOL) { diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index d912eb3953096..5e651d60c5e08 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -12,15 +12,13 @@ import { Range, IRange } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { MarkersTracker, LineMarker } from 'vs/editor/common/model/modelLine'; import { Position } from 'vs/editor/common/core/position'; -import { INewMarker, TextModelWithMarkers } from 'vs/editor/common/model/textModelWithMarkers'; +import { TextModelWithMarkers } from 'vs/editor/common/model/textModelWithMarkers'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { IntervalNode, IntervalTree } from 'vs/editor/common/model/intervalTree'; -export const USE_NEW_DECORATIONS = true; - export const ClassName = { EditorInfoDecoration: 'infosquiggly', EditorWarningDecoration: 'warningsquiggly', @@ -188,25 +186,8 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed this._treeDecorations = Object.create(null); } - private static _shouldStartMarkerSticksToPreviousCharacter(stickiness: editorCommon.TrackedRangeStickiness): boolean { - if (stickiness === editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges || stickiness === editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore) { - return true; - } - return false; - } - - private static _shouldEndMarkerSticksToPreviousCharacter(stickiness: editorCommon.TrackedRangeStickiness): boolean { - if (stickiness === editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges || stickiness === editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore) { - return true; - } - return false; - } - _getTrackedRangesCount(): number { - if (USE_NEW_DECORATIONS) { - return this._tree.count(); - } - return Object.keys(this._decorations).length; + return this._tree.count(); } // --- END TrackedRanges @@ -227,43 +208,25 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed private _changeDecorations(decorationsTracker: DecorationsTracker, ownerId: number, callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T): T { let changeAccessor: editorCommon.IModelDecorationsChangeAccessor = { addDecoration: (range: IRange, options: editorCommon.IModelDecorationOptions): string => { - if (USE_NEW_DECORATIONS) { - decorationsTracker.markDidAddDecorations(); - decorationsTracker.markDidRemoveDecorations(); - return this._deltaDecorationsImpl2(ownerId, [], [{ range: range, options: options }])[0]; - } - return this._addDecorationImpl(decorationsTracker, ownerId, this.validateRange(range), _normalizeOptions(options)); + decorationsTracker.markDidAddDecorations(); + decorationsTracker.markDidRemoveDecorations(); + return this._deltaDecorationsImpl2(ownerId, [], [{ range: range, options: options }])[0]; }, changeDecoration: (id: string, newRange: IRange): void => { - if (USE_NEW_DECORATIONS) { - this._changeDecorationImpl2(decorationsTracker, id, newRange); - return; - } - this._changeDecorationImpl(decorationsTracker, id, this.validateRange(newRange)); + this._changeDecorationImpl2(decorationsTracker, id, newRange); }, changeDecorationOptions: (id: string, options: editorCommon.IModelDecorationOptions) => { - if (USE_NEW_DECORATIONS) { - this._changeDecorationOptionsImpl2(decorationsTracker, id, _normalizeOptions(options)); - return; - } - this._changeDecorationOptionsImpl(decorationsTracker, id, _normalizeOptions(options)); + this._changeDecorationOptionsImpl2(decorationsTracker, id, _normalizeOptions(options)); }, removeDecoration: (id: string): void => { - if (USE_NEW_DECORATIONS) { - decorationsTracker.markDidAddDecorations(); - decorationsTracker.markDidRemoveDecorations(); - this._deltaDecorationsImpl2(ownerId, [id], []); - return; - } - this._removeDecorationImpl(decorationsTracker, id); + decorationsTracker.markDidAddDecorations(); + decorationsTracker.markDidRemoveDecorations(); + this._deltaDecorationsImpl2(ownerId, [id], []); }, deltaDecorations: (oldDecorations: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] => { - if (USE_NEW_DECORATIONS) { - decorationsTracker.markDidAddDecorations(); - decorationsTracker.markDidRemoveDecorations(); - return this._deltaDecorationsImpl2(ownerId, oldDecorations, newDecorations); - } - return this._deltaDecorationsImpl(decorationsTracker, ownerId, oldDecorations, this._normalizeDeltaDecorations(newDecorations)); + decorationsTracker.markDidAddDecorations(); + decorationsTracker.markDidRemoveDecorations(); + return this._deltaDecorationsImpl2(ownerId, oldDecorations, newDecorations); } }; let result: T = null; @@ -326,44 +289,21 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } public removeAllDecorationsWithOwnerId(ownerId: number): void { - if (USE_NEW_DECORATIONS) { - const nodes = this._tree.collectNodesFromOwner(ownerId); - for (let i = 0, len = nodes.length; i < len; i++) { - const node = nodes[i]; - - this._tree.delete(node); - delete this._treeDecorations[node.id]; - } - return; - } - let toRemove: string[] = []; - - for (let decorationId in this._decorations) { - // No `hasOwnProperty` call due to using Object.create(null) - - let decoration = this._decorations[decorationId]; + const nodes = this._tree.collectNodesFromOwner(ownerId); + for (let i = 0, len = nodes.length; i < len; i++) { + const node = nodes[i]; - if (decoration.ownerId === ownerId) { - toRemove.push(decoration.id); - } + this._tree.delete(node); + delete this._treeDecorations[node.id]; } - - this._removeDecorationsImpl(null, toRemove); } public getDecorationOptions(decorationId: string): editorCommon.IModelDecorationOptions { - if (USE_NEW_DECORATIONS) { - const node = this._treeDecorations[decorationId]; - if (!node) { - return null; - } - return node.options; - } - let decoration = this._decorations[decorationId]; - if (!decoration) { + const node = this._treeDecorations[decorationId]; + if (!node) { return null; } - return decoration.options; + return node.options; } private _getRangeAt(start: number, end: number): Range { @@ -379,26 +319,19 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } public getDecorationRange(decorationId: string): Range { - if (USE_NEW_DECORATIONS) { - const node = this._treeDecorations[decorationId]; - if (!node) { - return null; - } - const versionId = this.getVersionId(); - if (node.cachedVersionId !== versionId) { - this._tree.resolveNode(node, versionId); - } - if (node.range === null) { - this._ensureLineStarts(); - node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd); - } - return node.range; - } - let decoration = this._decorations[decorationId]; - if (!decoration) { + const node = this._treeDecorations[decorationId]; + if (!node) { return null; } - return decoration.range; + const versionId = this.getVersionId(); + if (node.cachedVersionId !== versionId) { + this._tree.resolveNode(node, versionId); + } + if (node.range === null) { + this._ensureLineStarts(); + node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd); + } + return node.range; } public getLineDecorations(lineNumber: number, ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { @@ -409,50 +342,6 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return this.getLinesDecorations(lineNumber, lineNumber, ownerId, filterOutValidation); } - /** - * Fetch only multi-line decorations that intersect with the given line number range - */ - private _getMultiLineDecorations(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): InternalDecoration[] { - const filterStartLineNumber = filterRange.startLineNumber; - const filterStartColumn = filterRange.startColumn; - const filterEndLineNumber = filterRange.endLineNumber; - const filterEndColumn = filterRange.endColumn; - - let result: InternalDecoration[] = [], resultLen = 0; - - for (let decorationId in this._multiLineDecorationsMap) { - // No `hasOwnProperty` call due to using Object.create(null) - let decoration = this._multiLineDecorationsMap[decorationId]; - - if (filterOwnerId && decoration.ownerId && decoration.ownerId !== filterOwnerId) { - continue; - } - - if (filterOutValidation && decoration.isForValidation) { - continue; - } - - let range = decoration.range; - - if (range.startLineNumber > filterEndLineNumber) { - continue; - } - if (range.startLineNumber === filterEndLineNumber && range.startColumn > filterEndColumn) { - continue; - } - if (range.endLineNumber < filterStartLineNumber) { - continue; - } - if (range.endLineNumber === filterStartLineNumber && range.endColumn < filterStartColumn) { - continue; - } - - result[resultLen++] = decoration; - } - - return result; - } - private _ensureNodesHaveRanges(nodes: IntervalNode[]): IntervalNode[] { this._ensureLineStarts(); @@ -465,82 +354,16 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed return nodes; } - private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): IntervalNode[] | InternalDecoration[] { - if (USE_NEW_DECORATIONS) { - this._ensureLineStarts(); - - const startOffset = this._lineStarts.getAccumulatedValue(filterRange.startLineNumber - 2) + filterRange.startColumn - 1; - const endOffset = this._lineStarts.getAccumulatedValue(filterRange.endLineNumber - 2) + filterRange.endColumn - 1; - - const versionId = this.getVersionId(); - const result = this._tree.intervalSearch(startOffset, endOffset, filterOwnerId, filterOutValidation, versionId); - - return this._ensureNodesHaveRanges(result); - } - - const filterStartLineNumber = filterRange.startLineNumber; - const filterStartColumn = filterRange.startColumn; - const filterEndLineNumber = filterRange.endLineNumber; - const filterEndColumn = filterRange.endColumn; - - let result = this._getMultiLineDecorations(filterRange, filterOwnerId, filterOutValidation); - let resultLen = result.length; - let resultMap: { [decorationId: string]: boolean; } = {}; - - for (let i = 0, len = resultLen; i < len; i++) { - resultMap[result[i].id] = true; - } - - for (let lineNumber = filterStartLineNumber; lineNumber <= filterEndLineNumber; lineNumber++) { - let lineMarkers = this._lines[lineNumber - 1].getMarkers(); - if (lineMarkers === null) { - continue; - } - for (let i = 0, len = lineMarkers.length; i < len; i++) { - let lineMarker = lineMarkers[i]; - let internalDecorationId = lineMarker.internalDecorationId; - - if (internalDecorationId === 0) { - // marker does not belong to any decoration - continue; - } - - let decoration = this._internalDecorations[internalDecorationId]; - - if (resultMap.hasOwnProperty(decoration.id)) { - // decoration already in result - continue; - } - - if (filterOwnerId && decoration.ownerId && decoration.ownerId !== filterOwnerId) { - continue; - } - - if (filterOutValidation && decoration.isForValidation) { - continue; - } + private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): IntervalNode[] { + this._ensureLineStarts(); - let range = decoration.range; + const startOffset = this._lineStarts.getAccumulatedValue(filterRange.startLineNumber - 2) + filterRange.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(filterRange.endLineNumber - 2) + filterRange.endColumn - 1; - if (range.startLineNumber > filterEndLineNumber) { - continue; - } - if (range.startLineNumber === filterEndLineNumber && range.startColumn > filterEndColumn) { - continue; - } - if (range.endLineNumber < filterStartLineNumber) { - continue; - } - if (range.endLineNumber === filterStartLineNumber && range.endColumn < filterStartColumn) { - continue; - } - - result[resultLen++] = decoration; - resultMap[decoration.id] = true; - } - } + const versionId = this.getVersionId(); + const result = this._tree.intervalSearch(startOffset, endOffset, filterOwnerId, filterOutValidation, versionId); - return result; + return this._ensureNodesHaveRanges(result); } public getLinesDecorations(_startLineNumber: number, _endLineNumber: number, ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { @@ -557,59 +380,15 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } public getOverviewRulerDecorations(ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { - if (USE_NEW_DECORATIONS) { - const versionId = this.getVersionId(); - const result = this._tree.search(ownerId, filterOutValidation, true, versionId); - return this._ensureNodesHaveRanges(result); - } - let result: InternalDecoration[] = [], resultLen = 0; - - for (let decorationId in this._decorations) { - // No `hasOwnProperty` call due to using Object.create(null) - let decoration = this._decorations[decorationId]; - - if (ownerId && decoration.ownerId && decoration.ownerId !== ownerId) { - continue; - } - - if (filterOutValidation && decoration.isForValidation) { - continue; - } - - if (!decoration.options.overviewRuler.color) { - continue; - } - - result[resultLen++] = decoration; - } - - return result; + const versionId = this.getVersionId(); + const result = this._tree.search(ownerId, filterOutValidation, true, versionId); + return this._ensureNodesHaveRanges(result); } public getAllDecorations(ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { - if (USE_NEW_DECORATIONS) { - const versionId = this.getVersionId(); - const result = this._tree.search(ownerId, filterOutValidation, false, versionId); - return this._ensureNodesHaveRanges(result); - } - let result: InternalDecoration[] = [], resultLen = 0; - - for (let decorationId in this._decorations) { - // No `hasOwnProperty` call due to using Object.create(null) - let decoration = this._decorations[decorationId]; - - if (ownerId && decoration.ownerId && decoration.ownerId !== ownerId) { - continue; - } - - if (filterOutValidation && decoration.isForValidation) { - continue; - } - - result[resultLen++] = decoration; - } - - return result; + const versionId = this.getVersionId(); + const result = this._tree.search(ownerId, filterOutValidation, false, versionId); + return this._ensureNodesHaveRanges(result); } protected _acquireMarkersTracker(): MarkersTracker { @@ -711,129 +490,10 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed } } - private _normalizeDeltaDecorations(deltaDecorations: editorCommon.IModelDeltaDecoration[]): ModelDeltaDecoration[] { - let result: ModelDeltaDecoration[] = []; - for (let i = 0, len = deltaDecorations.length; i < len; i++) { - let deltaDecoration = deltaDecorations[i]; - result.push(new ModelDeltaDecoration(i, this.validateRange(deltaDecoration.range), _normalizeOptions(deltaDecoration.options))); - } - return result; - } - private _externalDecorationId(internalId: number): string { return `${this._instanceId};${internalId}`; } - private _addDecorationImpl(decorationsTracker: DecorationsTracker, ownerId: number, _range: Range, options: ModelDecorationOptions): string { - let range = this.validateRange(_range); - - let internalDecorationId = (++this._lastDecorationId); - let decorationId = this._externalDecorationId(internalDecorationId); - - let markers = this._addMarkers([ - { - internalDecorationId: internalDecorationId, - position: new Position(range.startLineNumber, range.startColumn), - stickToPreviousCharacter: TextModelWithDecorations._shouldStartMarkerSticksToPreviousCharacter(options.stickiness) - }, - { - internalDecorationId: internalDecorationId, - position: new Position(range.endLineNumber, range.endColumn), - stickToPreviousCharacter: TextModelWithDecorations._shouldEndMarkerSticksToPreviousCharacter(options.stickiness) - } - ]); - - let decoration = new InternalDecoration(decorationId, internalDecorationId, ownerId, range, markers[0], markers[1], options); - this._decorations[decorationId] = decoration; - this._internalDecorations[internalDecorationId] = decoration; - if (range.startLineNumber !== range.endLineNumber) { - this._multiLineDecorationsMap[decorationId] = decoration; - } - - decorationsTracker.markDidAddDecorations(); - - return decorationId; - } - - private _addDecorationsImpl(decorationsTracker: DecorationsTracker, ownerId: number, newDecorations: ModelDeltaDecoration[]): string[] { - let internalDecorationIds: number[] = []; - let decorationIds: string[] = []; - let newMarkers: INewMarker[] = []; - - for (let i = 0, len = newDecorations.length; i < len; i++) { - let newDecoration = newDecorations[i]; - let range = newDecoration.range; - let stickiness = newDecoration.options.stickiness; - - let internalDecorationId = (++this._lastDecorationId); - let decorationId = this._externalDecorationId(internalDecorationId); - - internalDecorationIds[i] = internalDecorationId; - decorationIds[i] = decorationId; - - newMarkers[2 * i] = { - internalDecorationId: internalDecorationId, - position: new Position(range.startLineNumber, range.startColumn), - stickToPreviousCharacter: TextModelWithDecorations._shouldStartMarkerSticksToPreviousCharacter(stickiness) - }; - - newMarkers[2 * i + 1] = { - internalDecorationId: internalDecorationId, - position: new Position(range.endLineNumber, range.endColumn), - stickToPreviousCharacter: TextModelWithDecorations._shouldEndMarkerSticksToPreviousCharacter(stickiness) - }; - } - - let markerIds = this._addMarkers(newMarkers); - - for (let i = 0, len = newDecorations.length; i < len; i++) { - let newDecoration = newDecorations[i]; - let range = newDecoration.range; - let internalDecorationId = internalDecorationIds[i]; - let decorationId = decorationIds[i]; - let startMarker = markerIds[2 * i]; - let endMarker = markerIds[2 * i + 1]; - - let decoration = new InternalDecoration(decorationId, internalDecorationId, ownerId, range, startMarker, endMarker, newDecoration.options); - this._decorations[decorationId] = decoration; - this._internalDecorations[internalDecorationId] = decoration; - if (range.startLineNumber !== range.endLineNumber) { - this._multiLineDecorationsMap[decorationId] = decoration; - } - - decorationsTracker.markDidAddDecorations(); - } - - return decorationIds; - } - - private _changeDecorationImpl(decorationsTracker: DecorationsTracker, decorationId: string, newRange: Range): void { - let decoration = this._decorations[decorationId]; - if (!decoration) { - return; - } - - let startMarker = decoration.startMarker; - if (newRange.startLineNumber !== startMarker.position.lineNumber) { - // move marker between lines - this._lines[startMarker.position.lineNumber - 1].removeMarker(startMarker); - this._lines[newRange.startLineNumber - 1].addMarker(startMarker); - } - startMarker.setPosition(new Position(newRange.startLineNumber, newRange.startColumn)); - - let endMarker = decoration.endMarker; - if (newRange.endLineNumber !== endMarker.position.lineNumber) { - // move marker between lines - this._lines[endMarker.position.lineNumber - 1].removeMarker(endMarker); - this._lines[newRange.endLineNumber - 1].addMarker(endMarker); - } - endMarker.setPosition(new Position(newRange.endLineNumber, newRange.endColumn)); - - decoration.setRange(this._multiLineDecorationsMap, newRange); - - decorationsTracker.markDidChangeDecorations(); - } - private _changeDecorationImpl2(decorationsTracker: DecorationsTracker, decorationId: string, _range: IRange): void { const node = this._treeDecorations[decorationId]; if (!node) { @@ -851,22 +511,6 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed decorationsTracker.markDidChangeDecorations(); } - private _changeDecorationOptionsImpl(decorationsTracker: DecorationsTracker, decorationId: string, options: ModelDecorationOptions): void { - let decoration = this._decorations[decorationId]; - if (!decoration) { - return; - } - - if (decoration.options.stickiness !== options.stickiness) { - decoration.startMarker.stickToPreviousCharacter = TextModelWithDecorations._shouldStartMarkerSticksToPreviousCharacter(options.stickiness); - decoration.endMarker.stickToPreviousCharacter = TextModelWithDecorations._shouldEndMarkerSticksToPreviousCharacter(options.stickiness); - } - - decoration.setOptions(options); - - decorationsTracker.markDidChangeDecorations(); - } - private _changeDecorationOptionsImpl2(decorationsTracker: DecorationsTracker, decorationId: string, options: ModelDecorationOptions): void { const node = this._treeDecorations[decorationId]; if (!node) { @@ -878,151 +522,6 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed decorationsTracker.markDidChangeDecorations(); } - private _removeDecorationImpl(decorationsTracker: DecorationsTracker, decorationId: string): void { - let decoration = this._decorations[decorationId]; - if (!decoration) { - return; - } - - this._removeMarkers([decoration.startMarker, decoration.endMarker]); - - delete this._multiLineDecorationsMap[decorationId]; - delete this._decorations[decorationId]; - delete this._internalDecorations[decoration.internalId]; - - if (decorationsTracker) { - decorationsTracker.markDidRemoveDecorations(); - } - } - - private _removeDecorationsImpl(decorationsTracker: DecorationsTracker, decorationIds: string[]): void { - let removeMarkers: LineMarker[] = [], removeMarkersLen = 0; - - for (let i = 0, len = decorationIds.length; i < len; i++) { - let decorationId = decorationIds[i]; - let decoration = this._decorations[decorationId]; - if (!decoration) { - continue; - } - - if (decorationsTracker) { - decorationsTracker.markDidRemoveDecorations(); - } - - removeMarkers[removeMarkersLen++] = decoration.startMarker; - removeMarkers[removeMarkersLen++] = decoration.endMarker; - delete this._multiLineDecorationsMap[decorationId]; - delete this._decorations[decorationId]; - delete this._internalDecorations[decoration.internalId]; - } - - if (removeMarkers.length > 0) { - this._removeMarkers(removeMarkers); - } - } - - private _resolveOldDecorations(oldDecorations: string[]): InternalDecoration[] { - let result: InternalDecoration[] = []; - for (let i = 0, len = oldDecorations.length; i < len; i++) { - let id = oldDecorations[i]; - let decoration = this._decorations[id]; - if (!decoration) { - continue; - } - - result.push(decoration); - } - return result; - } - - private _deltaDecorationsImpl(decorationsTracker: DecorationsTracker, ownerId: number, oldDecorationsIds: string[], newDecorations: ModelDeltaDecoration[]): string[] { - - if (oldDecorationsIds.length === 0) { - // Nothing to remove - return this._addDecorationsImpl(decorationsTracker, ownerId, newDecorations); - } - - if (newDecorations.length === 0) { - // Nothing to add - this._removeDecorationsImpl(decorationsTracker, oldDecorationsIds); - return []; - } - - let oldDecorations = this._resolveOldDecorations(oldDecorationsIds); - - oldDecorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - newDecorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - - let result: string[] = [], - oldDecorationsIndex = 0, - oldDecorationsLength = oldDecorations.length, - newDecorationsIndex = 0, - newDecorationsLength = newDecorations.length, - decorationsToAdd: ModelDeltaDecoration[] = [], - decorationsToRemove: string[] = []; - - while (oldDecorationsIndex < oldDecorationsLength && newDecorationsIndex < newDecorationsLength) { - let oldDecoration = oldDecorations[oldDecorationsIndex]; - let newDecoration = newDecorations[newDecorationsIndex]; - let comparison = Range.compareRangesUsingStarts(oldDecoration.range, newDecoration.range); - - if (comparison < 0) { - // `oldDecoration` is before `newDecoration` => remove `oldDecoration` - decorationsToRemove.push(oldDecoration.id); - oldDecorationsIndex++; - continue; - } - - if (comparison > 0) { - // `newDecoration` is before `oldDecoration` => add `newDecoration` - decorationsToAdd.push(newDecoration); - newDecorationsIndex++; - continue; - } - - // The ranges of `oldDecoration` and `newDecoration` are equal - - if (!oldDecoration.options.equals(newDecoration.options)) { - // The options do not match => remove `oldDecoration` - decorationsToRemove.push(oldDecoration.id); - oldDecorationsIndex++; - continue; - } - - // Bingo! We can reuse `oldDecoration` for `newDecoration` - result[newDecoration.index] = oldDecoration.id; - oldDecorationsIndex++; - newDecorationsIndex++; - } - - while (oldDecorationsIndex < oldDecorationsLength) { - // No more new decorations => remove decoration at `oldDecorationsIndex` - decorationsToRemove.push(oldDecorations[oldDecorationsIndex].id); - oldDecorationsIndex++; - } - - while (newDecorationsIndex < newDecorationsLength) { - // No more old decorations => add decoration at `newDecorationsIndex` - decorationsToAdd.push(newDecorations[newDecorationsIndex]); - newDecorationsIndex++; - } - - // Remove `decorationsToRemove` - if (decorationsToRemove.length > 0) { - this._removeDecorationsImpl(decorationsTracker, decorationsToRemove); - } - - // Add `decorationsToAdd` - if (decorationsToAdd.length > 0) { - let newIds = this._addDecorationsImpl(decorationsTracker, ownerId, decorationsToAdd); - for (let i = 0, len = decorationsToAdd.length; i < len; i++) { - result[decorationsToAdd[i].index] = newIds[i]; - } - } - - return result; - } - private _deltaDecorationsImpl2(ownerId: number, oldDecorationsIds: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] { this._ensureLineStarts(); const versionId = this.getVersionId(); diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts index d29f6908dc95d..05d96ea0b304e 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts @@ -12,7 +12,6 @@ import { SnippetSession } from 'vs/editor/contrib/snippet/browser/snippetSession import { ICommonCodeEditor } from 'vs/editor/common/editorCommon'; import { mockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; import { Model } from 'vs/editor/common/model/model'; -import { USE_NEW_DECORATIONS } from 'vs/editor/common/model/textModelWithDecorations'; suite('SnippetSession', function () { @@ -235,17 +234,9 @@ suite('SnippetSession', function () { assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); session.prev(); - if (USE_NEW_DECORATIONS) { - assertSelections(editor, new Selection(1, 7, 1, 10), new Selection(2, 11, 2, 14)); - } else { - assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); - } + assertSelections(editor, new Selection(1, 7, 1, 10), new Selection(2, 11, 2, 14)); session.prev(); - if (USE_NEW_DECORATIONS) { - assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11)); - } else { - assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); - } + assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11)); session.prev(); assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(2, 5, 2, 8)); }); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 8e808191925ea..54fd5d1742484 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -10,7 +10,6 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/editorCommon'; import { Model } from 'vs/editor/common/model/model'; -import { USE_NEW_DECORATIONS } from 'vs/editor/common/model/textModelWithDecorations'; // --------- utils @@ -1095,11 +1094,7 @@ suite('deltaDecorations', () => { assert.equal(initialIds.length, decorations.length, 'returns expected cnt of ids'); assert.equal(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); assert.equal(initialIds.length, model._getTrackedRangesCount(), 'does not leak tracked ranges'); - if (!USE_NEW_DECORATIONS) { - assert.equal(2 * initialIds.length, model._getMarkersCount(), 'does not leak markers'); - } else { - assert.equal(0, model._getMarkersCount(), 'does not leak markers'); - } + assert.equal(0, model._getMarkersCount(), 'does not leak markers'); actualDecorations.sort((a, b) => strcmp(a.id, b.id)); decorations.sort((a, b) => strcmp(a.id, b.id)); assert.deepEqual(actualDecorations, decorations); @@ -1110,11 +1105,7 @@ suite('deltaDecorations', () => { assert.equal(newIds.length, newDecorations.length, 'returns expected cnt of ids'); assert.equal(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); assert.equal(newIds.length, model._getTrackedRangesCount(), 'does not leak tracked ranges'); - if (!USE_NEW_DECORATIONS) { - assert.equal(2 * newIds.length, model._getMarkersCount(), 'does not leak markers'); - } else { - assert.equal(0, model._getMarkersCount(), 'does not leak markers'); - } + assert.equal(0, model._getMarkersCount(), 'does not leak markers'); actualNewDecorations.sort((a, b) => strcmp(a.id, b.id)); newDecorations.sort((a, b) => strcmp(a.id, b.id)); assert.deepEqual(actualDecorations, decorations); From a0a5f18f7271c239b83523498ccf60680a00757a Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 12:40:45 +0200 Subject: [PATCH 28/64] Remove TextModelWithMarkers --- src/vs/editor/common/editorCommon.ts | 30 +- .../editor/common/model/editableTextModel.ts | 25 -- src/vs/editor/common/model/model.ts | 2 +- .../common/model/textModelWithDecorations.ts | 113 +------- .../common/model/textModelWithMarkers.ts | 174 ------------ .../common/model/editableTextModel.test.ts | 257 +----------------- .../model/editableTextModelTestUtils.ts | 1 - .../common/model/modelDecorations.test.ts | 2 - src/vs/monaco.d.ts | 10 +- 9 files changed, 9 insertions(+), 605 deletions(-) delete mode 100644 src/vs/editor/common/model/textModelWithMarkers.ts diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 6fb2a0796561d..6b46304bbebfb 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -915,32 +915,6 @@ export interface ITokenizedModel extends ITextModel { getLineIndentGuide(lineNumber: number): number; } -/** - * A model that can track markers. - */ -export interface ITextModelWithMarkers extends ITextModel { - /** - * @internal - */ - _addMarker(internalDecorationId: number, lineNumber: number, column: number, stickToPreviousCharacter: boolean): string; - /** - * @internal - */ - _changeMarker(id: string, newLineNumber: number, newColumn: number): void; - /** - * @internal - */ - _changeMarkerStickiness(id: string, newStickToPreviousCharacter: boolean): void; - /** - * @internal - */ - _getMarker(id: string): Position; - /** - * @internal - */ - _removeMarker(id: string): void; -} - /** * Describes the behavior of decorations when typing/editing near their edges. * Note: Please do not edit the values, as they very carefully match `DecorationRangeBehavior` @@ -1056,7 +1030,7 @@ export interface ITextModelWithDecorations { /** * An editable text model. */ -export interface IEditableTextModel extends ITextModelWithMarkers { +export interface IEditableTextModel extends ITextModel { /** * Normalize a string containing whitespace according to indentation rules (converts to spaces or to tabs). @@ -1139,7 +1113,7 @@ export interface IEditableTextModel extends ITextModelWithMarkers { /** * A model. */ -export interface IModel extends IReadOnlyModel, IEditableTextModel, ITextModelWithMarkers, ITokenizedModel, ITextModelWithDecorations { +export interface IModel extends IReadOnlyModel, IEditableTextModel, ITokenizedModel, ITextModelWithDecorations { /** * @deprecated Please use `onDidChangeContent` instead. * An event emitted when the contents of the model have changed. diff --git a/src/vs/editor/common/model/editableTextModel.ts b/src/vs/editor/common/model/editableTextModel.ts index e8c617fc2c230..0880fc0824b29 100644 --- a/src/vs/editor/common/model/editableTextModel.ts +++ b/src/vs/editor/common/model/editableTextModel.ts @@ -730,31 +730,6 @@ export class EditableTextModel extends TextModelWithDecorations implements edito this._resetIndentRanges(); } - public _assertLineNumbersOK(): void { - let foundMarkersCnt = 0; - for (let i = 0, len = this._lines.length; i < len; i++) { - let line = this._lines[i]; - let lineNumber = i + 1; - - let markers = line.getMarkers(); - if (markers !== null) { - for (let j = 0, lenJ = markers.length; j < lenJ; j++) { - foundMarkersCnt++; - let markerId = markers[j].id; - let marker = this._markerIdToMarker[markerId]; - if (marker.position.lineNumber !== lineNumber) { - throw new Error('Misplaced marker with id ' + markerId); - } - } - } - } - - let totalMarkersCnt = Object.keys(this._markerIdToMarker).length; - if (totalMarkersCnt !== foundMarkersCnt) { - throw new Error('There are misplaced markers!'); - } - } - private _undo(): Selection[] { this._isUndoing = true; let r = this._commandManager.undo(); diff --git a/src/vs/editor/common/model/model.ts b/src/vs/editor/common/model/model.ts index 1228592fe0fb5..2e1410fc830bb 100644 --- a/src/vs/editor/common/model/model.ts +++ b/src/vs/editor/common/model/model.ts @@ -16,7 +16,7 @@ import { IRawTextSource, RawTextSource } from 'vs/editor/common/model/textSource import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; // The hierarchy is: -// Model -> EditableTextModel -> TextModelWithDecorations -> TextModelWithTrackedRanges -> TextModelWithMarkers -> TextModelWithTokens -> TextModel +// Model -> EditableTextModel -> TextModelWithDecorations -> TextModelWithTokens -> TextModel var MODEL_ID = 0; diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 5e651d60c5e08..de20a2b6aec54 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -10,9 +10,8 @@ import * as strings from 'vs/base/common/strings'; import { CharCode } from 'vs/base/common/charCode'; import { Range, IRange } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { MarkersTracker, LineMarker } from 'vs/editor/common/model/modelLine'; -import { Position } from 'vs/editor/common/core/position'; -import { TextModelWithMarkers } from 'vs/editor/common/model/textModelWithMarkers'; +import { MarkersTracker } from 'vs/editor/common/model/modelLine'; +import { TextModelWithTokens } from 'vs/editor/common/model/textModelWithTokens'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; @@ -52,57 +51,6 @@ class DecorationsTracker { } } -export class InternalDecoration implements editorCommon.IModelDecoration { - _internalDecorationBrand: void; - - public readonly id: string; - public readonly internalId: number; - public readonly ownerId: number; - public readonly startMarker: LineMarker; - public readonly endMarker: LineMarker; - public options: ModelDecorationOptions; - public isForValidation: boolean; - public range: Range; - - constructor(id: string, internalId: number, ownerId: number, range: Range, startMarker: LineMarker, endMarker: LineMarker, options: ModelDecorationOptions) { - this.id = id; - this.internalId = internalId; - this.ownerId = ownerId; - this.range = range; - this.startMarker = startMarker; - this.endMarker = endMarker; - this.setOptions(options); - } - - public setOptions(options: ModelDecorationOptions) { - this.options = options; - this.isForValidation = ( - this.options.className === ClassName.EditorErrorDecoration - || this.options.className === ClassName.EditorWarningDecoration - ); - } - - public setRange(multiLineDecorationsMap: { [key: string]: InternalDecoration; }, range: Range): void { - if (this.range.equalsRange(range)) { - return; - } - - let rangeWasMultiLine = (this.range.startLineNumber !== this.range.endLineNumber); - this.range = range; - let rangeIsMultiline = (this.range.startLineNumber !== this.range.endLineNumber); - - if (rangeWasMultiLine === rangeIsMultiline) { - return; - } - - if (rangeIsMultiline) { - multiLineDecorationsMap[this.id] = this; - } else { - delete multiLineDecorationsMap[this.id]; - } - } -} - let _INSTANCE_COUNT = 0; /** * Produces 'a'-'z', followed by 'A'-'Z'... followed by 'a'-'z', etc. @@ -120,7 +68,7 @@ function nextInstanceId(): string { return String.fromCharCode(CharCode.A + result - LETTERS_CNT); } -export class TextModelWithDecorations extends TextModelWithMarkers implements editorCommon.ITextModelWithDecorations { +export class TextModelWithDecorations extends TextModelWithTokens implements editorCommon.ITextModelWithDecorations { /** * Used to workaround broken clients that might attempt using a decoration id generated by a different model. @@ -138,10 +86,6 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed protected _tree: IntervalTree; private _treeDecorations: { [decorationId: string]: IntervalNode; }; - private _decorations: { [decorationId: string]: InternalDecoration; }; - private _internalDecorations: { [internalDecorationId: number]: InternalDecoration; }; - private _multiLineDecorationsMap: { [key: string]: InternalDecoration; }; - constructor(rawTextSource: IRawTextSource, creationOptions: editorCommon.ITextModelCreationOptions, languageIdentifier: LanguageIdentifier) { super(rawTextSource, creationOptions, languageIdentifier); @@ -157,17 +101,9 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed this._tree = new IntervalTree(); this._treeDecorations = Object.create(null); - - this._decorations = Object.create(null); - this._internalDecorations = Object.create(null); - this._multiLineDecorationsMap = Object.create(null); } public dispose(): void { - this._decorations = null; - this._internalDecorations = null; - this._multiLineDecorationsMap = null; - this._tree = null; this._treeDecorations = null; @@ -178,10 +114,6 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed super._resetValue(newValue); // Destroy all my decorations - this._decorations = Object.create(null); - this._internalDecorations = Object.create(null); - this._multiLineDecorationsMap = Object.create(null); - this._tree = new IntervalTree(); this._treeDecorations = Object.create(null); } @@ -412,48 +344,9 @@ export class TextModelWithDecorations extends TextModelWithMarkers implements ed * Handle changed markers (i.e. update decorations ranges) */ private _handleTrackedMarkers(markersTracker: MarkersTracker): void { - let changedInternalDecorationIds = markersTracker.getDecorationIds(); - if (changedInternalDecorationIds.length === 0) { - this.emitModelDecorationsChangedEvent(); - return; - } - - changedInternalDecorationIds.sort(); - - let previousInternalDecorationId: number = 0; - for (let i = 0, len = changedInternalDecorationIds.length; i < len; i++) { - let internalDecorationId = changedInternalDecorationIds[i]; - if (internalDecorationId === previousInternalDecorationId) { - continue; - } - previousInternalDecorationId = internalDecorationId; - - let decoration = this._internalDecorations[internalDecorationId]; - if (!decoration) { - // perhaps the decoration was removed in the meantime - continue; - } - - let startMarker = decoration.startMarker.position; - let endMarker = decoration.endMarker.position; - let range = TextModelWithDecorations._createRangeFromMarkers(startMarker, endMarker); - decoration.setRange(this._multiLineDecorationsMap, range); - } - this.emitModelDecorationsChangedEvent(); } - private static _createRangeFromMarkers(startPosition: Position, endPosition: Position): Range { - if (endPosition.isBefore(startPosition)) { - // This tracked range has turned in on itself (end marker before start marker) - // This can happen in extreme editing conditions where lots of text is removed and lots is added - - // Treat it as a collapsed range - return new Range(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column); - } - return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); - } - private _acquireDecorationsTracker(): DecorationsTracker { if (this._currentDecorationsTrackerCnt === 0) { this._currentDecorationsTracker = new DecorationsTracker(); diff --git a/src/vs/editor/common/model/textModelWithMarkers.ts b/src/vs/editor/common/model/textModelWithMarkers.ts deleted file mode 100644 index 6100bfc399924..0000000000000 --- a/src/vs/editor/common/model/textModelWithMarkers.ts +++ /dev/null @@ -1,174 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { IdGenerator } from 'vs/base/common/idGenerator'; -import { Position } from 'vs/editor/common/core/position'; -import { ITextModelWithMarkers, ITextModelCreationOptions } from 'vs/editor/common/editorCommon'; -import { LineMarker } from 'vs/editor/common/model/modelLine'; -import { TextModelWithTokens } from 'vs/editor/common/model/textModelWithTokens'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource'; - -export interface IMarkerIdToMarkerMap { - [key: string]: LineMarker; -} - -export interface INewMarker { - internalDecorationId: number; - position: Position; - stickToPreviousCharacter: boolean; -} - -var _INSTANCE_COUNT = 0; - -export class TextModelWithMarkers extends TextModelWithTokens implements ITextModelWithMarkers { - - private _markerIdGenerator: IdGenerator; - protected _markerIdToMarker: IMarkerIdToMarkerMap; - - constructor(rawTextSource: IRawTextSource, creationOptions: ITextModelCreationOptions, languageIdentifier: LanguageIdentifier) { - super(rawTextSource, creationOptions, languageIdentifier); - this._markerIdGenerator = new IdGenerator((++_INSTANCE_COUNT) + ';'); - this._markerIdToMarker = Object.create(null); - } - - public dispose(): void { - this._markerIdToMarker = null; - super.dispose(); - } - - protected _resetValue(newValue: ITextSource): void { - super._resetValue(newValue); - - // Destroy all my markers - this._markerIdToMarker = Object.create(null); - } - - _addMarker(internalDecorationId: number, lineNumber: number, column: number, stickToPreviousCharacter: boolean): string { - var pos = this.validatePosition(new Position(lineNumber, column)); - - var marker = new LineMarker(this._markerIdGenerator.nextId(), internalDecorationId, pos, stickToPreviousCharacter); - this._markerIdToMarker[marker.id] = marker; - - this._lines[pos.lineNumber - 1].addMarker(marker); - - return marker.id; - } - - protected _addMarkers(newMarkers: INewMarker[]): LineMarker[] { - if (newMarkers.length === 0) { - return []; - } - - let markers: LineMarker[] = []; - for (let i = 0, len = newMarkers.length; i < len; i++) { - let newMarker = newMarkers[i]; - - let marker = new LineMarker(this._markerIdGenerator.nextId(), newMarker.internalDecorationId, newMarker.position, newMarker.stickToPreviousCharacter); - this._markerIdToMarker[marker.id] = marker; - - markers[i] = marker; - } - - let sortedMarkers = markers.slice(0); - sortedMarkers.sort((a, b) => { - return a.position.lineNumber - b.position.lineNumber; - }); - - let currentLineNumber = 0; - let currentMarkers: LineMarker[] = [], currentMarkersLen = 0; - for (let i = 0, len = sortedMarkers.length; i < len; i++) { - let marker = sortedMarkers[i]; - - if (marker.position.lineNumber !== currentLineNumber) { - if (currentLineNumber !== 0) { - this._lines[currentLineNumber - 1].addMarkers(currentMarkers); - } - currentLineNumber = marker.position.lineNumber; - currentMarkers.length = 0; - currentMarkersLen = 0; - } - - currentMarkers[currentMarkersLen++] = marker; - } - this._lines[currentLineNumber - 1].addMarkers(currentMarkers); - - return markers; - } - - _changeMarker(id: string, lineNumber: number, column: number): void { - let marker = this._markerIdToMarker[id]; - if (!marker) { - return; - } - - let newPos = this.validatePosition(new Position(lineNumber, column)); - - if (newPos.lineNumber !== marker.position.lineNumber) { - // Move marker between lines - this._lines[marker.position.lineNumber - 1].removeMarker(marker); - this._lines[newPos.lineNumber - 1].addMarker(marker); - } - - marker.setPosition(newPos); - } - - _changeMarkerStickiness(id: string, newStickToPreviousCharacter: boolean): void { - let marker = this._markerIdToMarker[id]; - if (!marker) { - return; - } - - marker.stickToPreviousCharacter = newStickToPreviousCharacter; - } - - _getMarker(id: string): Position { - let marker = this._markerIdToMarker[id]; - if (!marker) { - return null; - } - - return marker.position; - } - - _getMarkersCount(): number { - return Object.keys(this._markerIdToMarker).length; - } - - _removeMarker(id: string): void { - let marker = this._markerIdToMarker[id]; - if (!marker) { - return; - } - - this._lines[marker.position.lineNumber - 1].removeMarker(marker); - delete this._markerIdToMarker[id]; - } - - protected _removeMarkers(markers: LineMarker[]): void { - markers.sort((a, b) => { - return a.position.lineNumber - b.position.lineNumber; - }); - - let currentLineNumber = 0; - let currentMarkers: { [markerId: string]: boolean; } = null; - for (let i = 0, len = markers.length; i < len; i++) { - let marker = markers[i]; - delete this._markerIdToMarker[marker.id]; - - if (marker.position.lineNumber !== currentLineNumber) { - if (currentLineNumber !== 0) { - this._lines[currentLineNumber - 1].removeMarkers(currentMarkers); - } - currentLineNumber = marker.position.lineNumber; - currentMarkers = Object.create(null); - } - - currentMarkers[marker.id] = true; - } - this._lines[currentLineNumber - 1].removeMarkers(currentMarkers); - } -} diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index 266b6b67f05d0..d04e5269cadea 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; -import { EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; +import { EndOfLineSequence, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; import { EditableTextModel, IValidatedEditOperation } from 'vs/editor/common/model/editableTextModel'; import { MirrorModel } from 'vs/editor/common/model/mirrorModel'; import { assertSyncedModels, testApplyEditsWithSyncedModels } from 'vs/editor/test/common/model/editableTextModelTestUtils'; @@ -1569,7 +1569,6 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { }); let assertMirrorModels = () => { - model._assertLineNumbersOK(); assert.equal(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK'); assert.equal(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK'); }; @@ -1581,257 +1580,3 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { mirrorModel2.dispose(); }); }); - -interface ILightWeightMarker { - id: string; - lineNumber: number; - column: number; - stickToPreviousCharacter: boolean; -} - -suite('EditorModel - EditableTextModel.applyEdits & markers', () => { - - function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, text: string[]): IIdentifiedSingleEditOperation { - return { - identifier: null, - range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), - text: text.join('\n'), - forceMoveMarkers: false - }; - } - - function marker(id: string, lineNumber: number, column: number, stickToPreviousCharacter: boolean): ILightWeightMarker { - return { - id: id, - lineNumber: lineNumber, - column: column, - stickToPreviousCharacter: stickToPreviousCharacter - }; - } - - function toMarkersMap(markers: ILightWeightMarker[]): { [markerId: string]: ILightWeightMarker } { - var result: { [markerId: string]: ILightWeightMarker } = {}; - markers.forEach(m => { - result[m.id] = m; - }); - return result; - } - - function testApplyEditsAndMarkers(text: string[], markers: ILightWeightMarker[], edits: IIdentifiedSingleEditOperation[], changedMarkers: string[], expectedText: string[], expectedMarkers: ILightWeightMarker[]): void { - var textStr = text.join('\n'); - var expectedTextStr = expectedText.join('\n'); - var markersMap = toMarkersMap(markers); - // var expectedMarkersMap = toMarkersMap(expectedMarkers); - var markerId2ModelMarkerId = Object.create(null); - - var model = EditableTextModel.createFromString(textStr); - model.setEOL(EndOfLineSequence.LF); - - // Add markers - markers.forEach((m) => { - let modelMarkerId = model._addMarker(0, m.lineNumber, m.column, m.stickToPreviousCharacter); - markerId2ModelMarkerId[m.id] = modelMarkerId; - }); - - // Apply edits & collect inverse edits - model.applyEdits(edits); - model._assertLineNumbersOK(); - - // Assert edits produced expected result - assert.deepEqual(model.getValue(EndOfLinePreference.LF), expectedTextStr); - - let actualChangedMarkers: string[] = []; - for (let i = 0, len = expectedMarkers.length; i < len; i++) { - let expectedMarker = expectedMarkers[i]; - let initialMarker = markersMap[expectedMarker.id]; - let expectedMarkerModelMarkerId = markerId2ModelMarkerId[expectedMarker.id]; - let actualMarker = model._getMarker(expectedMarkerModelMarkerId); - - if (actualMarker.lineNumber !== initialMarker.lineNumber || actualMarker.column !== initialMarker.column) { - actualChangedMarkers.push(initialMarker.id); - } - - assert.equal(actualMarker.lineNumber, expectedMarker.lineNumber, 'marker lineNumber of marker ' + expectedMarker.id); - assert.equal(actualMarker.column, expectedMarker.column, 'marker column of marker ' + expectedMarker.id); - } - - changedMarkers.sort(); - actualChangedMarkers.sort(); - assert.deepEqual(actualChangedMarkers, changedMarkers, 'changed markers'); - - model.dispose(); - } - - test('no markers changed', () => { - testApplyEditsAndMarkers( - [ - 'Hello world,', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false) - ], - [ - editOp(1, 13, 1, 13, [' how are you?']) - ], - [], - [ - 'Hello world, how are you?', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false) - ] - ); - }); - - test('first line changes', () => { - testApplyEditsAndMarkers( - [ - 'Hello world,', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false) - ], - [ - editOp(1, 7, 1, 12, ['friends']) - ], - [], - [ - 'Hello friends,', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false) - ] - ); - }); - - test('inserting lines', () => { - testApplyEditsAndMarkers( - [ - 'Hello world,', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false) - ], - [ - editOp(1, 7, 1, 12, ['friends']), - editOp(1, 13, 1, 13, ['', 'this is an inserted line', 'and another one. By the way,']) - ], - ['e', 'f', 'g', 'h'], - [ - 'Hello friends,', - 'this is an inserted line', - 'and another one. By the way,', - 'this is a short text', - 'that is used in testing' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 4, 1, false), - marker('f', 4, 16, true), - marker('g', 4, 21, true), - marker('h', 5, 24, false) - ] - ); - }); - - test('replacing a lot', () => { - testApplyEditsAndMarkers( - [ - 'Hello world,', - 'this is a short text', - 'that is used in testing', - 'more lines...', - 'more lines...', - 'more lines...', - 'more lines...' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 2, 1, false), - marker('f', 2, 16, true), - marker('g', 2, 21, true), - marker('h', 3, 24, false), - marker('i', 5, 1, false), - marker('j', 6, 1, false), - marker('k', 7, 14, false), - ], - [ - editOp(1, 7, 1, 12, ['friends']), - editOp(1, 13, 1, 13, ['', 'this is an inserted line', 'and another one. By the way,', 'This is another line']), - editOp(2, 1, 7, 14, ['Some new text here']) - ], - ['e', 'f', 'g', 'h', 'i', 'j', 'k'], - [ - 'Hello friends,', - 'this is an inserted line', - 'and another one. By the way,', - 'This is another line', - 'Some new text here' - ], - [ - marker('a', 1, 1, true), - marker('b', 1, 1, false), - marker('c', 1, 7, false), - marker('d', 1, 12, true), - marker('e', 5, 1, false), - marker('f', 5, 16, true), - marker('g', 5, 19, true), - marker('h', 5, 19, false), - marker('i', 5, 19, false), - marker('j', 5, 19, false), - marker('k', 5, 19, false), - ] - ); - }); -}); diff --git a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts index a901d29fcd326..b0b18723062fb 100644 --- a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts +++ b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts @@ -105,7 +105,6 @@ export function assertSyncedModels(text: string, callback: (model: EditableTextM var assertMirrorModels = () => { assertLineMapping(model, 'model'); - model._assertLineNumbersOK(); assert.equal(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK'); assert.equal(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK'); }; diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 54fd5d1742484..26b004d0a0c4b 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -1094,7 +1094,6 @@ suite('deltaDecorations', () => { assert.equal(initialIds.length, decorations.length, 'returns expected cnt of ids'); assert.equal(initialIds.length, model.getAllDecorations().length, 'does not leak decorations'); assert.equal(initialIds.length, model._getTrackedRangesCount(), 'does not leak tracked ranges'); - assert.equal(0, model._getMarkersCount(), 'does not leak markers'); actualDecorations.sort((a, b) => strcmp(a.id, b.id)); decorations.sort((a, b) => strcmp(a.id, b.id)); assert.deepEqual(actualDecorations, decorations); @@ -1105,7 +1104,6 @@ suite('deltaDecorations', () => { assert.equal(newIds.length, newDecorations.length, 'returns expected cnt of ids'); assert.equal(newIds.length, model.getAllDecorations().length, 'does not leak decorations'); assert.equal(newIds.length, model._getTrackedRangesCount(), 'does not leak tracked ranges'); - assert.equal(0, model._getMarkersCount(), 'does not leak markers'); actualNewDecorations.sort((a, b) => strcmp(a.id, b.id)); newDecorations.sort((a, b) => strcmp(a.id, b.id)); assert.deepEqual(actualDecorations, decorations); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 4b4e91c982417..7874e3fd4fb02 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1641,12 +1641,6 @@ declare module monaco.editor { getWordUntilPosition(position: IPosition): IWordAtPosition; } - /** - * A model that can track markers. - */ - export interface ITextModelWithMarkers extends ITextModel { - } - /** * Describes the behavior of decorations when typing/editing near their edges. * Note: Please do not edit the values, as they very carefully match `DecorationRangeBehavior` @@ -1728,7 +1722,7 @@ declare module monaco.editor { /** * An editable text model. */ - export interface IEditableTextModel extends ITextModelWithMarkers { + export interface IEditableTextModel extends ITextModel { /** * Normalize a string containing whitespace according to indentation rules (converts to spaces or to tabs). */ @@ -1772,7 +1766,7 @@ declare module monaco.editor { /** * A model. */ - export interface IModel extends IReadOnlyModel, IEditableTextModel, ITextModelWithMarkers, ITokenizedModel, ITextModelWithDecorations { + export interface IModel extends IReadOnlyModel, IEditableTextModel, ITokenizedModel, ITextModelWithDecorations { /** * An event emitted when the contents of the model have changed. * @event From d882decfcbf6eac5da54826daaf686cb676b9a0e Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 16:02:04 +0200 Subject: [PATCH 29/64] Delete LineMarker and associated code --- .../editor/common/model/editableTextModel.ts | 63 +- src/vs/editor/common/model/modelLine.ts | 419 +------- .../common/model/textModelWithDecorations.ts | 93 +- .../test/common/commands/sideEditing.test.ts | 20 - .../test/common/model/model.line.test.ts | 910 +----------------- 5 files changed, 125 insertions(+), 1380 deletions(-) diff --git a/src/vs/editor/common/model/editableTextModel.ts b/src/vs/editor/common/model/editableTextModel.ts index 0880fc0824b29..ca0508e2ad180 100644 --- a/src/vs/editor/common/model/editableTextModel.ts +++ b/src/vs/editor/common/model/editableTextModel.ts @@ -7,12 +7,11 @@ import { Range, IRange } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditStack } from 'vs/editor/common/model/editStack'; -import { ILineEdit, LineMarker, MarkersTracker, IModelLine } from 'vs/editor/common/model/modelLine'; +import { ILineEdit, IModelLine } from 'vs/editor/common/model/modelLine'; import { TextModelWithDecorations, ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; import * as strings from 'vs/base/common/strings'; import * as arrays from 'vs/base/common/arrays'; import { Selection } from 'vs/editor/common/core/selection'; -import { Position } from 'vs/editor/common/core/position'; import { IDisposable } from 'vs/base/common/lifecycle'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { ITextSource, IRawTextSource, RawTextSource } from 'vs/editor/common/model/textSource'; @@ -277,15 +276,15 @@ export class EditableTextModel extends TextModelWithDecorations implements edito public applyEdits(rawOperations: editorCommon.IIdentifiedSingleEditOperation[]): editorCommon.IIdentifiedSingleEditOperation[] { try { this._eventEmitter.beginDeferredEmit(); - let markersTracker = this._acquireMarkersTracker(); - return this._applyEdits(markersTracker, rawOperations); + this._acquireMarkersTracker(); + return this._applyEdits(rawOperations); } finally { this._releaseMarkersTracker(); this._eventEmitter.endDeferredEmit(); } } - private _applyEdits(markersTracker: MarkersTracker, rawOperations: editorCommon.IIdentifiedSingleEditOperation[]): editorCommon.IIdentifiedSingleEditOperation[] { + private _applyEdits(rawOperations: editorCommon.IIdentifiedSingleEditOperation[]): editorCommon.IIdentifiedSingleEditOperation[] { if (rawOperations.length === 0) { return []; } @@ -383,7 +382,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito this._mightContainRTL = mightContainRTL; this._mightContainNonBasicASCII = mightContainNonBasicASCII; - this._doApplyEdits(markersTracker, operations); + this._doApplyEdits(operations); this._trimAutoWhitespaceLines = null; if (this._options.trimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) { @@ -470,7 +469,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito return result; } - private _doApplyEdits(markersTracker: MarkersTracker, operations: IValidatedEditOperation[]): void { + private _doApplyEdits(operations: IValidatedEditOperation[]): void { const tabSize = this._options.tabSize; @@ -508,7 +507,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito } this._invalidateLine(currentLineNumber - 1); - this._lines[currentLineNumber - 1].applyEdits(markersTracker, lineEditsQueue.slice(currentLineNumberStart, i), tabSize); + this._lines[currentLineNumber - 1].applyEdits(lineEditsQueue.slice(currentLineNumberStart, i), tabSize); if (this._lineStarts) { // update prefix sum this._lineStarts.changeValue(currentLineNumber - 1, this._lines[currentLineNumber - 1].text.length + this._EOL.length); @@ -522,7 +521,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito } this._invalidateLine(currentLineNumber - 1); - this._lines[currentLineNumber - 1].applyEdits(markersTracker, lineEditsQueue.slice(currentLineNumberStart, lineEditsQueue.length), tabSize); + this._lines[currentLineNumber - 1].applyEdits(lineEditsQueue.slice(currentLineNumberStart, lineEditsQueue.length), tabSize); if (this._lineStarts) { // update prefix sum this._lineStarts.changeValue(currentLineNumber - 1, this._lines[currentLineNumber - 1].text.length + this._EOL.length); @@ -534,10 +533,6 @@ export class EditableTextModel extends TextModelWithDecorations implements edito lineEditsQueue = []; }; - let minTouchedLineNumber = operations[operations.length - 1].range.startLineNumber; - let maxTouchedLineNumber = operations[0].range.endLineNumber + 1; - let totalLinesCountDelta = 0; - for (let i = 0, len = operations.length; i < len; i++) { const op = operations[i]; @@ -561,8 +556,6 @@ export class EditableTextModel extends TextModelWithDecorations implements edito const insertingLinesCnt = (op.lines ? op.lines.length - 1 : 0); const editingLinesCnt = Math.min(deletingLinesCnt, insertingLinesCnt); - totalLinesCountDelta += (insertingLinesCnt - deletingLinesCnt); - // Iterating descending to overlap with previous op // in case there are common lines being edited in both for (let j = editingLinesCnt; j >= 0; j--) { @@ -572,8 +565,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito lineNumber: editLineNumber, startColumn: (editLineNumber === startLineNumber ? startColumn : 1), endColumn: (editLineNumber === endLineNumber ? endColumn : this.getLineMaxColumn(editLineNumber)), - text: (op.lines ? op.lines[j] : ''), - forceMoveMarkers: op.forceMoveMarkers + text: (op.lines ? op.lines[j] : '') }); } @@ -584,23 +576,12 @@ export class EditableTextModel extends TextModelWithDecorations implements edito flushLineEdits(); const spliceStartLineNumber = startLineNumber + editingLinesCnt; - const spliceStartColumn = this.getLineMaxColumn(spliceStartLineNumber); - const endLineRemains = this._lines[endLineNumber - 1].split(markersTracker, endColumn, false, tabSize); + const endLineRemains = this._lines[endLineNumber - 1].split(endColumn, tabSize); this._invalidateLine(spliceStartLineNumber - 1); const spliceCnt = endLineNumber - spliceStartLineNumber; - // Collect all these markers - let markersOnDeletedLines: LineMarker[] = []; - for (let j = 0; j < spliceCnt; j++) { - const deleteLineIndex = spliceStartLineNumber + j; - const deleteLineMarkers = this._lines[deleteLineIndex].getMarkers(); - if (deleteLineMarkers) { - markersOnDeletedLines = markersOnDeletedLines.concat(deleteLineMarkers); - } - } - this._lines.splice(spliceStartLineNumber, spliceCnt); if (this._lineStarts) { // update prefix sum @@ -608,19 +589,12 @@ export class EditableTextModel extends TextModelWithDecorations implements edito } // Reconstruct first line - this._lines[spliceStartLineNumber - 1].append(markersTracker, spliceStartLineNumber, endLineRemains, tabSize); + this._lines[spliceStartLineNumber - 1].append(endLineRemains, tabSize); if (this._lineStarts) { // update prefix sum this._lineStarts.changeValue(spliceStartLineNumber - 1, this._lines[spliceStartLineNumber - 1].text.length + this._EOL.length); } - // Update deleted markers - const deletedMarkersPosition = new Position(spliceStartLineNumber, spliceStartColumn); - for (let j = 0, lenJ = markersOnDeletedLines.length; j < lenJ; j++) { - markersOnDeletedLines[j].updatePosition(markersTracker, deletedMarkersPosition); - } - - this._lines[spliceStartLineNumber - 1].addMarkers(markersOnDeletedLines); rawContentChanges.push( new textModelEvents.ModelRawLineChanged(spliceStartLineNumber, this._lines[spliceStartLineNumber - 1].text) ); @@ -643,7 +617,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito } // Split last line - let leftoverLine = this._lines[spliceLineNumber - 1].split(markersTracker, spliceColumn, op.forceMoveMarkers, tabSize); + let leftoverLine = this._lines[spliceLineNumber - 1].split(spliceColumn, tabSize); if (this._lineStarts) { // update prefix sum this._lineStarts.changeValue(spliceLineNumber - 1, this._lines[spliceLineNumber - 1].text.length + this._EOL.length); @@ -670,7 +644,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito } // Last line - this._lines[startLineNumber + insertingLinesCnt - 1].append(markersTracker, startLineNumber + insertingLinesCnt, leftoverLine, tabSize); + this._lines[startLineNumber + insertingLinesCnt - 1].append(leftoverLine, tabSize); if (this._lineStarts) { // update prefix sum this._lineStarts.changeValue(startLineNumber + insertingLinesCnt - 1, this._lines[startLineNumber + insertingLinesCnt - 1].text.length + this._EOL.length); @@ -695,16 +669,6 @@ export class EditableTextModel extends TextModelWithDecorations implements edito flushLineEdits(); - maxTouchedLineNumber = Math.max(1, Math.min(this.getLineCount(), maxTouchedLineNumber + totalLinesCountDelta)); - if (totalLinesCountDelta !== 0) { - // must update line numbers all the way to the bottom - maxTouchedLineNumber = this.getLineCount(); - } - - for (let lineNumber = minTouchedLineNumber; lineNumber <= maxTouchedLineNumber; lineNumber++) { - this._lines[lineNumber - 1].updateLineNumber(markersTracker, lineNumber); - } - if (rawContentChanges.length !== 0 || contentChanges.length !== 0) { this._increaseVersionId(); @@ -726,7 +690,6 @@ export class EditableTextModel extends TextModelWithDecorations implements edito this._eventEmitter.emit(textModelEvents.TextModelEventType.ModelContentChanged, e); } - // this._assertLineNumbersOK(); this._resetIndentRanges(); } diff --git a/src/vs/editor/common/model/modelLine.ts b/src/vs/editor/common/model/modelLine.ts index e71219bd6898d..b811f40a73f65 100644 --- a/src/vs/editor/common/model/modelLine.ts +++ b/src/vs/editor/common/model/modelLine.ts @@ -7,94 +7,12 @@ import { IState, FontStyle, StandardTokenType, MetadataConsts, ColorId, LanguageId } from 'vs/editor/common/modes'; import { CharCode } from 'vs/base/common/charCode'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; -import { Position } from 'vs/editor/common/core/position'; import { Constants } from 'vs/editor/common/core/uint'; export interface ILineEdit { startColumn: number; endColumn: number; text: string; - forceMoveMarkers: boolean; -} - -export class LineMarker { - _lineMarkerBrand: void; - - public readonly id: string; - public readonly internalDecorationId: number; - - public stickToPreviousCharacter: boolean; - public position: Position; - - constructor(id: string, internalDecorationId: number, position: Position, stickToPreviousCharacter: boolean) { - this.id = id; - this.internalDecorationId = internalDecorationId; - this.position = position; - this.stickToPreviousCharacter = stickToPreviousCharacter; - } - - public toString(): string { - return '{\'' + this.id + '\';' + this.position.toString() + ',' + this.stickToPreviousCharacter + '}'; - } - - public updateLineNumber(markersTracker: MarkersTracker, lineNumber: number): void { - if (this.position.lineNumber === lineNumber) { - return; - } - markersTracker.addChangedMarker(this); - this.position = new Position(lineNumber, this.position.column); - } - - public updateColumn(markersTracker: MarkersTracker, column: number): void { - if (this.position.column === column) { - return; - } - markersTracker.addChangedMarker(this); - this.position = new Position(this.position.lineNumber, column); - } - - public updatePosition(markersTracker: MarkersTracker, position: Position): void { - if (this.position.lineNumber === position.lineNumber && this.position.column === position.column) { - return; - } - markersTracker.addChangedMarker(this); - this.position = position; - } - - public setPosition(position: Position) { - this.position = position; - } - - - public static compareMarkers(a: LineMarker, b: LineMarker): number { - if (a.position.column === b.position.column) { - return (a.stickToPreviousCharacter ? 0 : 1) - (b.stickToPreviousCharacter ? 0 : 1); - } - return a.position.column - b.position.column; - } -} - -export class MarkersTracker { - _changedDecorationsBrand: void; - - private _changedDecorations: number[]; - private _changedDecorationsLen: number; - - constructor() { - this._changedDecorations = []; - this._changedDecorationsLen = 0; - } - - public addChangedMarker(marker: LineMarker): void { - let internalDecorationId = marker.internalDecorationId; - if (internalDecorationId !== 0) { - this._changedDecorations[this._changedDecorationsLen++] = internalDecorationId; - } - } - - public getDecorationIds(): number[] { - return this._changedDecorations; - } } export interface ITokensAdjuster { @@ -102,27 +20,10 @@ export interface ITokensAdjuster { finish(delta: number, lineTextLength: number): void; } -interface IMarkersAdjuster { - adjustDelta(toColumn: number, delta: number, minimumAllowedColumn: number, moveSemantics: MarkerMoveSemantics): void; - adjustSet(toColumn: number, newColumn: number, moveSemantics: MarkerMoveSemantics): void; - finish(delta: number, lineTextLength: number): void; -} - var NO_OP_TOKENS_ADJUSTER: ITokensAdjuster = { adjust: () => { }, finish: () => { } }; -var NO_OP_MARKERS_ADJUSTER: IMarkersAdjuster = { - adjustDelta: () => { }, - adjustSet: () => { }, - finish: () => { } -}; - -const enum MarkerMoveSemantics { - MarkerDefined = 0, - ForceMove = 1, - ForceStay = 2 -} /** * Returns: @@ -156,13 +57,6 @@ function computePlusOneIndentLevel(line: string, tabSize: number): number { export interface IModelLine { readonly text: string; - // --- markers - addMarker(marker: LineMarker): void; - addMarkers(markers: LineMarker[]): void; - removeMarker(marker: LineMarker): void; - removeMarkers(deleteMarkers: { [markerId: string]: boolean; }): void; - getMarkers(): LineMarker[]; - // --- tokenization resetTokenizationState(): void; isInvalid(): boolean; @@ -177,20 +71,14 @@ export interface IModelLine { getIndentLevel(): number; // --- editing - updateLineNumber(markersTracker: MarkersTracker, newLineNumber: number): void; - applyEdits(markersTracker: MarkersTracker, edits: ILineEdit[], tabSize: number): number; - append(markersTracker: MarkersTracker, myLineNumber: number, other: IModelLine, tabSize: number): void; - split(markersTracker: MarkersTracker, splitColumn: number, forceMoveMarkers: boolean, tabSize: number): IModelLine; + applyEdits(edits: ILineEdit[], tabSize: number): number; + append(other: IModelLine, tabSize: number): void; + split(splitColumn: number, tabSize: number): IModelLine; } export abstract class AbstractModelLine { - private _markers: LineMarker[]; - - constructor(initializeMarkers: boolean) { - if (initializeMarkers) { - this._markers = null; - } + constructor() { } /// @@ -202,121 +90,18 @@ export abstract class AbstractModelLine { /// - // private _printMarkers(): string { - // if (!this._markers) { - // return '[]'; - // } - // if (this._markers.length === 0) { - // return '[]'; - // } - - // var markers = this._markers; - - // var printMarker = (m:LineMarker) => { - // if (m.stickToPreviousCharacter) { - // return '|' + m.position.column; - // } - // return m.position.column + '|'; - // }; - // return '[' + markers.map(printMarker).join(', ') + ']'; - // } - - private _createMarkersAdjuster(markersTracker: MarkersTracker): IMarkersAdjuster { - if (!this._markers) { - return NO_OP_MARKERS_ADJUSTER; - } - if (this._markers.length === 0) { - return NO_OP_MARKERS_ADJUSTER; - } - - this._markers.sort(LineMarker.compareMarkers); - - var markers = this._markers; - var markersLength = markers.length; - var markersIndex = 0; - var marker = markers[markersIndex]; - - // console.log('------------- INITIAL MARKERS: ' + this._printMarkers()); - - let adjustMarkerBeforeColumn = (toColumn: number, moveSemantics: MarkerMoveSemantics) => { - if (marker.position.column < toColumn) { - return true; - } - if (marker.position.column > toColumn) { - return false; - } - if (moveSemantics === MarkerMoveSemantics.ForceMove) { - return false; - } - if (moveSemantics === MarkerMoveSemantics.ForceStay) { - return true; - } - return marker.stickToPreviousCharacter; - }; - - let adjustDelta = (toColumn: number, delta: number, minimumAllowedColumn: number, moveSemantics: MarkerMoveSemantics) => { - // console.log('------------------------------'); - // console.log('adjustDelta called: toColumn: ' + toColumn + ', delta: ' + delta + ', minimumAllowedColumn: ' + minimumAllowedColumn + ', moveSemantics: ' + MarkerMoveSemantics[moveSemantics]); - // console.log('BEFORE::: markersIndex: ' + markersIndex + ' : ' + this._printMarkers()); - - while (markersIndex < markersLength && adjustMarkerBeforeColumn(toColumn, moveSemantics)) { - if (delta !== 0) { - let newColumn = Math.max(minimumAllowedColumn, marker.position.column + delta); - marker.updateColumn(markersTracker, newColumn); - } - - markersIndex++; - if (markersIndex < markersLength) { - marker = markers[markersIndex]; - } - } - - // console.log('AFTER::: markersIndex: ' + markersIndex + ' : ' + this._printMarkers()); - }; - - let adjustSet = (toColumn: number, newColumn: number, moveSemantics: MarkerMoveSemantics) => { - // console.log('------------------------------'); - // console.log('adjustSet called: toColumn: ' + toColumn + ', newColumn: ' + newColumn + ', moveSemantics: ' + MarkerMoveSemantics[moveSemantics]); - // console.log('BEFORE::: markersIndex: ' + markersIndex + ' : ' + this._printMarkers()); - - while (markersIndex < markersLength && adjustMarkerBeforeColumn(toColumn, moveSemantics)) { - marker.updateColumn(markersTracker, newColumn); - - markersIndex++; - if (markersIndex < markersLength) { - marker = markers[markersIndex]; - } - } - - // console.log('AFTER::: markersIndex: ' + markersIndex + ' : ' + this._printMarkers()); - }; - - let finish = (delta: number, lineTextLength: number) => { - adjustDelta(Constants.MAX_SAFE_SMALL_INTEGER, delta, 1, MarkerMoveSemantics.MarkerDefined); - - // console.log('------------- FINAL MARKERS: ' + this._printMarkers()); - }; - - return { - adjustDelta: adjustDelta, - adjustSet: adjustSet, - finish: finish - }; - } - - public applyEdits(markersTracker: MarkersTracker, edits: ILineEdit[], tabSize: number): number { + public applyEdits(edits: ILineEdit[], tabSize: number): number { let deltaColumn = 0; let resultText = this.text; let tokensAdjuster = this._createTokensAdjuster(); - let markersAdjuster = this._createMarkersAdjuster(markersTracker); for (let i = 0, len = edits.length; i < len; i++) { let edit = edits[i]; // console.log(); // console.log('============================='); - // console.log('EDIT #' + i + ' [ ' + edit.startColumn + ' -> ' + edit.endColumn + ' ] : <<<' + edit.text + '>>>, forceMoveMarkers: ' + edit.forceMoveMarkers); + // console.log('EDIT #' + i + ' [ ' + edit.startColumn + ' -> ' + edit.endColumn + ' ] : <<<' + edit.text + '>>>'); // console.log('deltaColumn: ' + deltaColumn); let startColumn = deltaColumn + edit.startColumn; @@ -324,35 +109,28 @@ export abstract class AbstractModelLine { let deletingCnt = endColumn - startColumn; let insertingCnt = edit.text.length; - // Adjust tokens & markers before this edit - // console.log('Adjust tokens & markers before this edit'); + // Adjust tokens before this edit + // console.log('Adjust tokens before this edit'); tokensAdjuster.adjust(edit.startColumn - 1, deltaColumn, 1); - markersAdjuster.adjustDelta(edit.startColumn, deltaColumn, 1, edit.forceMoveMarkers ? MarkerMoveSemantics.ForceMove : (deletingCnt > 0 ? MarkerMoveSemantics.ForceStay : MarkerMoveSemantics.MarkerDefined)); - // Adjust tokens & markers for the common part of this edit + // Adjust tokens for the common part of this edit let commonLength = Math.min(deletingCnt, insertingCnt); if (commonLength > 0) { - // console.log('Adjust tokens & markers for the common part of this edit'); + // console.log('Adjust tokens for the common part of this edit'); tokensAdjuster.adjust(edit.startColumn - 1 + commonLength, deltaColumn, startColumn); - - if (!edit.forceMoveMarkers) { - markersAdjuster.adjustDelta(edit.startColumn + commonLength, deltaColumn, startColumn, edit.forceMoveMarkers ? MarkerMoveSemantics.ForceMove : (deletingCnt > insertingCnt ? MarkerMoveSemantics.ForceStay : MarkerMoveSemantics.MarkerDefined)); - } } // Perform the edit & update `deltaColumn` resultText = resultText.substring(0, startColumn - 1) + edit.text + resultText.substring(endColumn - 1); deltaColumn += insertingCnt - deletingCnt; - // Adjust tokens & markers inside this edit - // console.log('Adjust tokens & markers inside this edit'); + // Adjust tokens inside this edit + // console.log('Adjust tokens inside this edit'); tokensAdjuster.adjust(edit.endColumn, deltaColumn, startColumn); - markersAdjuster.adjustSet(edit.endColumn, startColumn + insertingCnt, edit.forceMoveMarkers ? MarkerMoveSemantics.ForceMove : MarkerMoveSemantics.MarkerDefined); } - // Wrap up tokens & markers; adjust remaining if needed + // Wrap up tokens; adjust remaining if needed tokensAdjuster.finish(deltaColumn, resultText.length); - markersAdjuster.finish(deltaColumn, resultText.length); // Save the resulting text this._setText(resultText, tabSize); @@ -360,157 +138,16 @@ export abstract class AbstractModelLine { return deltaColumn; } - public split(markersTracker: MarkersTracker, splitColumn: number, forceMoveMarkers: boolean, tabSize: number): IModelLine { - // console.log('--> split @ ' + splitColumn + '::: ' + this._printMarkers()); - var myText = this.text.substring(0, splitColumn - 1); - var otherText = this.text.substring(splitColumn - 1); - - var otherMarkers: LineMarker[] = null; - - if (this._markers) { - this._markers.sort(LineMarker.compareMarkers); - for (let i = 0, len = this._markers.length; i < len; i++) { - let marker = this._markers[i]; - - if ( - marker.position.column > splitColumn - || ( - marker.position.column === splitColumn - && ( - forceMoveMarkers - || !marker.stickToPreviousCharacter - ) - ) - ) { - let myMarkers = this._markers.slice(0, i); - otherMarkers = this._markers.slice(i); - this._markers = myMarkers; - break; - } - } - - if (otherMarkers) { - for (let i = 0, len = otherMarkers.length; i < len; i++) { - let marker = otherMarkers[i]; - - marker.updateColumn(markersTracker, marker.position.column - (splitColumn - 1)); - } - } - } + public split(splitColumn: number, tabSize: number): IModelLine { + const myText = this.text.substring(0, splitColumn - 1); + const otherText = this.text.substring(splitColumn - 1); this._setText(myText, tabSize); - - var otherLine = this._createModelLine(otherText, tabSize); - if (otherMarkers) { - otherLine.addMarkers(otherMarkers); - } - return otherLine; + return this._createModelLine(otherText, tabSize); } - public append(markersTracker: MarkersTracker, myLineNumber: number, other: IModelLine, tabSize: number): void { - // console.log('--> append: THIS :: ' + this._printMarkers()); - // console.log('--> append: OTHER :: ' + this._printMarkers()); - let thisTextLength = this.text.length; + public append(other: IModelLine, tabSize: number): void { this._setText(this.text + other.text, tabSize); - - if (other instanceof AbstractModelLine) { - if (other._markers) { - // Other has markers - let otherMarkers = other._markers; - - // Adjust other markers - for (let i = 0, len = otherMarkers.length; i < len; i++) { - let marker = otherMarkers[i]; - - marker.updatePosition(markersTracker, new Position(myLineNumber, marker.position.column + thisTextLength)); - } - - this.addMarkers(otherMarkers); - } - } - } - - public addMarker(marker: LineMarker): void { - if (!this._markers) { - this._markers = [marker]; - } else { - this._markers.push(marker); - } - } - - public addMarkers(markers: LineMarker[]): void { - if (markers.length === 0) { - return; - } - - if (!this._markers) { - this._markers = markers.slice(0); - } else { - this._markers = this._markers.concat(markers); - } - } - - public removeMarker(marker: LineMarker): void { - if (!this._markers) { - return; - } - - let index = this._indexOfMarkerId(marker.id); - if (index < 0) { - return; - } - - if (this._markers.length === 1) { - // was last marker on line - this._markers = null; - } else { - this._markers.splice(index, 1); - } - } - - public removeMarkers(deleteMarkers: { [markerId: string]: boolean; }): void { - if (!this._markers) { - return; - } - for (let i = 0, len = this._markers.length; i < len; i++) { - let marker = this._markers[i]; - - if (deleteMarkers[marker.id]) { - this._markers.splice(i, 1); - len--; - i--; - } - } - if (this._markers.length === 0) { - this._markers = null; - } - } - - public getMarkers(): LineMarker[] { - if (!this._markers) { - return null; - } - return this._markers; - } - - public updateLineNumber(markersTracker: MarkersTracker, newLineNumber: number): void { - if (this._markers) { - let markers = this._markers; - for (let i = 0, len = markers.length; i < len; i++) { - let marker = markers[i]; - marker.updateLineNumber(markersTracker, newLineNumber); - } - } - } - - private _indexOfMarkerId(markerId: string): number { - let markers = this._markers; - for (let i = 0, len = markers.length; i < len; i++) { - if (markers[i].id === markerId) { - return i; - } - } - return undefined; } } @@ -559,7 +196,7 @@ export class ModelLine extends AbstractModelLine implements IModelLine { private _lineTokens: ArrayBuffer; constructor(text: string, tabSize: number) { - super(true); + super(); this._metadata = 0; this._setText(text, tabSize); this._state = null; @@ -570,8 +207,8 @@ export class ModelLine extends AbstractModelLine implements IModelLine { return new ModelLine(text, tabSize); } - public split(markersTracker: MarkersTracker, splitColumn: number, forceMoveMarkers: boolean, tabSize: number): IModelLine { - let result = super.split(markersTracker, splitColumn, forceMoveMarkers, tabSize); + public split(splitColumn: number, tabSize: number): IModelLine { + let result = super.split(splitColumn, tabSize); // Mark overflowing tokens for deletion & delete marked tokens this._deleteMarkedTokens(this._markOverflowingTokensForDeletion(0, this.text.length)); @@ -579,10 +216,10 @@ export class ModelLine extends AbstractModelLine implements IModelLine { return result; } - public append(markersTracker: MarkersTracker, myLineNumber: number, other: IModelLine, tabSize: number): void { + public append(other: IModelLine, tabSize: number): void { let thisTextLength = this.text.length; - super.append(markersTracker, myLineNumber, other, tabSize); + super.append(other, tabSize); if (other instanceof ModelLine) { let otherRawTokens = other._lineTokens; @@ -828,7 +465,7 @@ export class MinimalModelLine extends AbstractModelLine implements IModelLine { } constructor(text: string, tabSize: number) { - super(false); + super(); this._setText(text, tabSize); } @@ -836,12 +473,12 @@ export class MinimalModelLine extends AbstractModelLine implements IModelLine { return new MinimalModelLine(text, tabSize); } - public split(markersTracker: MarkersTracker, splitColumn: number, forceMoveMarkers: boolean, tabSize: number): IModelLine { - return super.split(markersTracker, splitColumn, forceMoveMarkers, tabSize); + public split(splitColumn: number, tabSize: number): IModelLine { + return super.split(splitColumn, tabSize); } - public append(markersTracker: MarkersTracker, myLineNumber: number, other: IModelLine, tabSize: number): void { - super.append(markersTracker, myLineNumber, other, tabSize); + public append(other: IModelLine, tabSize: number): void { + super.append(other, tabSize); } // --- BEGIN STATE diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index de20a2b6aec54..40e9add2c52e1 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -10,7 +10,6 @@ import * as strings from 'vs/base/common/strings'; import { CharCode } from 'vs/base/common/charCode'; import { Range, IRange } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { MarkersTracker } from 'vs/editor/common/model/modelLine'; import { TextModelWithTokens } from 'vs/editor/common/model/textModelWithTokens'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource'; @@ -80,7 +79,6 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi private _currentDecorationsTracker: DecorationsTracker; private _currentDecorationsTrackerCnt: number; - private _currentMarkersTracker: MarkersTracker; private _currentMarkersTrackerCnt: number; protected _tree: IntervalTree; @@ -96,7 +94,6 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi this._currentDecorationsTracker = null; this._currentDecorationsTrackerCnt = 0; - this._currentMarkersTracker = null; this._currentMarkersTrackerCnt = 0; this._tree = new IntervalTree(); @@ -124,6 +121,35 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi // --- END TrackedRanges + private _acquireDecorationsTracker(): DecorationsTracker { + if (this._currentDecorationsTrackerCnt === 0) { + this._currentDecorationsTracker = new DecorationsTracker(); + } + this._currentDecorationsTrackerCnt++; + return this._currentDecorationsTracker; + } + + private _releaseDecorationsTracker(): void { + this._currentDecorationsTrackerCnt--; + if (this._currentDecorationsTrackerCnt === 0) { + let decorationsTracker = this._currentDecorationsTracker; + this._currentDecorationsTracker = null; + this._handleTrackedDecorations(decorationsTracker); + } + } + + private _handleTrackedDecorations(decorationsTracker: DecorationsTracker): void { + if ( + !decorationsTracker.didAddDecorations + && !decorationsTracker.didChangeDecorations + && !decorationsTracker.didRemoveDecorations + ) { + return; + } + + this.emitModelDecorationsChangedEvent(); + } + public changeDecorations(callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T, ownerId: number = 0): T { this._assertNotDisposed(); @@ -323,59 +349,17 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi return this._ensureNodesHaveRanges(result); } - protected _acquireMarkersTracker(): MarkersTracker { - if (this._currentMarkersTrackerCnt === 0) { - this._currentMarkersTracker = new MarkersTracker(); - } + protected _acquireMarkersTracker(): void { this._currentMarkersTrackerCnt++; - return this._currentMarkersTracker; } protected _releaseMarkersTracker(): void { this._currentMarkersTrackerCnt--; if (this._currentMarkersTrackerCnt === 0) { - let markersTracker = this._currentMarkersTracker; - this._currentMarkersTracker = null; - this._handleTrackedMarkers(markersTracker); + this.emitModelDecorationsChangedEvent(); } } - /** - * Handle changed markers (i.e. update decorations ranges) - */ - private _handleTrackedMarkers(markersTracker: MarkersTracker): void { - this.emitModelDecorationsChangedEvent(); - } - - private _acquireDecorationsTracker(): DecorationsTracker { - if (this._currentDecorationsTrackerCnt === 0) { - this._currentDecorationsTracker = new DecorationsTracker(); - } - this._currentDecorationsTrackerCnt++; - return this._currentDecorationsTracker; - } - - private _releaseDecorationsTracker(): void { - this._currentDecorationsTrackerCnt--; - if (this._currentDecorationsTrackerCnt === 0) { - let decorationsTracker = this._currentDecorationsTracker; - this._currentDecorationsTracker = null; - this._handleTrackedDecorations(decorationsTracker); - } - } - - private _handleTrackedDecorations(decorationsTracker: DecorationsTracker): void { - if ( - !decorationsTracker.didAddDecorations - && !decorationsTracker.didChangeDecorations - && !decorationsTracker.didRemoveDecorations - ) { - return; - } - - this.emitModelDecorationsChangedEvent(); - } - private emitModelDecorationsChangedEvent(): void { if (!this._isDisposing) { let e: textModelEvents.IModelDecorationsChangedEvent = {}; @@ -599,21 +583,6 @@ const TRACKED_RANGE_OPTIONS = [ ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingAfter }), ]; -class ModelDeltaDecoration implements editorCommon.IModelDeltaDecoration { - - _modelDeltaDecorationBrand: void; - - index: number; - range: Range; - options: ModelDecorationOptions; - - constructor(index: number, range: Range, options: ModelDecorationOptions) { - this.index = index; - this.range = range; - this.options = options; - } -} - function _normalizeOptions(options: editorCommon.IModelDecorationOptions): ModelDecorationOptions { if (options instanceof ModelDecorationOptions) { return options; diff --git a/src/vs/editor/test/common/commands/sideEditing.test.ts b/src/vs/editor/test/common/commands/sideEditing.test.ts index a5e2100692c2b..ceaa7df8265a5 100644 --- a/src/vs/editor/test/common/commands/sideEditing.test.ts +++ b/src/vs/editor/test/common/commands/sideEditing.test.ts @@ -10,15 +10,12 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; -import { ILineEdit, ModelLine, LineMarker, MarkersTracker } from 'vs/editor/common/model/modelLine'; import { withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; import { Model } from 'vs/editor/common/model/model'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { Cursor } from 'vs/editor/common/controller/cursor'; -const NO_TAB_SIZE = 0; - function testCommand(lines: string[], selections: Selection[], edits: IIdentifiedSingleEditOperation[], expectedLines: string[], expectedSelections: Selection[]): void { withMockCodeEditor(lines, {}, (editor, cursor) => { const model = editor.getModel(); @@ -35,15 +32,6 @@ function testCommand(lines: string[], selections: Selection[], edits: IIdentifie }); } -function testLineEditMarker(text: string, column: number, stickToPreviousCharacter: boolean, edit: ILineEdit, expectedColumn: number): void { - var line = new ModelLine(text, NO_TAB_SIZE); - line.addMarker(new LineMarker('1', 0, new Position(0, column), stickToPreviousCharacter)); - - line.applyEdits(new MarkersTracker(), [edit], NO_TAB_SIZE); - - assert.equal(line.getMarkers()[0].position.column, expectedColumn); -} - suite('Editor Side Editing - collapsed selection', () => { test('replace at selection', () => { @@ -90,14 +78,6 @@ suite('Editor Side Editing - collapsed selection', () => { ); }); - test('ModelLine.applyEdits uses `isReplace`', () => { - testLineEditMarker('something', 1, true, { startColumn: 1, endColumn: 1, text: 'asd', forceMoveMarkers: false }, 1); - testLineEditMarker('something', 1, true, { startColumn: 1, endColumn: 1, text: 'asd', forceMoveMarkers: true }, 4); - - testLineEditMarker('something', 1, false, { startColumn: 1, endColumn: 1, text: 'asd', forceMoveMarkers: false }, 4); - testLineEditMarker('something', 1, false, { startColumn: 1, endColumn: 1, text: 'asd', forceMoveMarkers: true }, 4); - }); - test('insert at selection', () => { testCommand( [ diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 81dbba468d517..a2e75ba4ae938 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -6,9 +6,8 @@ import * as assert from 'assert'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; -import { ModelLine, ILineEdit, LineMarker, MarkersTracker } from 'vs/editor/common/model/modelLine'; +import { ModelLine, ILineEdit } from 'vs/editor/common/model/modelLine'; import { MetadataConsts } from 'vs/editor/common/modes'; -import { Position } from 'vs/editor/common/core/position'; import { ViewLineToken, ViewLineTokenFactory } from 'vs/editor/common/core/viewLineToken'; function assertLineTokens(_actual: LineTokens, _expected: TestToken[]): void { @@ -54,7 +53,7 @@ suite('Editor Model - modelLine.applyEdits text', () => { function testEdits(initial: string, edits: ILineEdit[], expected: string): void { var line = new ModelLine(initial, NO_TAB_SIZE); - line.applyEdits(new MarkersTracker(), edits, NO_TAB_SIZE); + line.applyEdits(edits, NO_TAB_SIZE); assert.equal(line.text, expected); } @@ -62,8 +61,7 @@ suite('Editor Model - modelLine.applyEdits text', () => { return { startColumn: startColumn, endColumn: endColumn, - text: text, - forceMoveMarkers: false + text: text }; } @@ -201,7 +199,7 @@ suite('Editor Model - modelLine.split text', () => { function testLineSplit(initial: string, splitColumn: number, expected1: string, expected2: string): void { var line = new ModelLine(initial, NO_TAB_SIZE); - var newLine = line.split(new MarkersTracker(), splitColumn, false, NO_TAB_SIZE); + var newLine = line.split(splitColumn, NO_TAB_SIZE); assert.equal(line.text, expected1); assert.equal(newLine.text, expected2); } @@ -239,7 +237,7 @@ suite('Editor Model - modelLine.append text', () => { function testLineAppend(a: string, b: string, expected: string): void { var line1 = new ModelLine(a, NO_TAB_SIZE); var line2 = new ModelLine(b, NO_TAB_SIZE); - line1.append(new MarkersTracker(), 1, line2, NO_TAB_SIZE); + line1.append(line2, NO_TAB_SIZE); assert.equal(line1.text, expected); } @@ -301,7 +299,7 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { let line = new ModelLine(initialText, NO_TAB_SIZE); line.setTokens(0, TestToken.toTokens(initialTokens)); - line.applyEdits(new MarkersTracker(), edits, NO_TAB_SIZE); + line.applyEdits(edits, NO_TAB_SIZE); assert.equal(line.text, expectedText); assertLineTokens(line.getTokens(0), expectedTokens); @@ -311,10 +309,10 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { let line = new ModelLine('some text', NO_TAB_SIZE); line.setTokens(0, TestToken.toTokens([new TestToken(0, 1)])); - line.applyEdits(new MarkersTracker(), [{ startColumn: 1, endColumn: 10, text: '', forceMoveMarkers: false }], NO_TAB_SIZE); + line.applyEdits([{ startColumn: 1, endColumn: 10, text: '' }], NO_TAB_SIZE); line.setTokens(0, new Uint32Array(0)); - line.applyEdits(new MarkersTracker(), [{ startColumn: 1, endColumn: 1, text: 'a', forceMoveMarkers: false }], NO_TAB_SIZE); + line.applyEdits([{ startColumn: 1, endColumn: 1, text: 'a' }], NO_TAB_SIZE); assertLineTokens(line.getTokens(0), [new TestToken(0, 1)]); }); @@ -330,7 +328,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', [ @@ -353,7 +350,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 2, endColumn: 2, text: 'x', - forceMoveMarkers: false }], 'axabcd efgh', [ @@ -376,7 +372,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 3, endColumn: 3, text: 'stu', - forceMoveMarkers: false }], 'axstuabcd efgh', [ @@ -399,7 +394,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 10, endColumn: 10, text: '\t', - forceMoveMarkers: false }], 'axstuabcd\t efgh', [ @@ -422,7 +416,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 12, endColumn: 12, text: 'dd', - forceMoveMarkers: false }], 'axstuabcd\t ddefgh', [ @@ -445,7 +438,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 18, endColumn: 18, text: 'xyz', - forceMoveMarkers: false }], 'axstuabcd\t ddefghxyz', [ @@ -468,7 +460,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 1, text: 'x', - forceMoveMarkers: false }], 'xaxstuabcd\t ddefghxyz', [ @@ -491,7 +482,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 22, endColumn: 22, text: 'x', - forceMoveMarkers: false }], 'xaxstuabcd\t ddefghxyzx', [ @@ -514,7 +504,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 2, endColumn: 2, text: '', - forceMoveMarkers: false }], 'xaxstuabcd\t ddefghxyzx', [ @@ -533,7 +522,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'a', [ @@ -554,7 +542,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 4, endColumn: 7, text: '', - forceMoveMarkers: false }], 'abcghij', [ @@ -576,7 +563,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 4, endColumn: 4, text: 'hello', - forceMoveMarkers: false }], 'abchellodefghij', [ @@ -599,7 +585,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 2, text: '', - forceMoveMarkers: false }], 'bcd efgh', [ @@ -622,7 +607,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 2, endColumn: 4, text: '', - forceMoveMarkers: false }], 'ad efgh', [ @@ -645,7 +629,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 5, text: '', - forceMoveMarkers: false }], ' efgh', [ @@ -667,7 +650,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 5, endColumn: 6, text: '', - forceMoveMarkers: false }], 'abcdefgh', [ @@ -689,7 +671,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 5, endColumn: 7, text: '', - forceMoveMarkers: false }], 'abcdfgh', [ @@ -711,7 +692,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 5, endColumn: 10, text: '', - forceMoveMarkers: false }], 'abcd', [ @@ -732,7 +712,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 10, text: '', - forceMoveMarkers: false }], '', [ @@ -753,7 +732,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 1, text: '', - forceMoveMarkers: false }], 'abcd efgh', [ @@ -776,7 +754,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 3, text: '', - forceMoveMarkers: false }], 'cd efgh', [ @@ -799,7 +776,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 5, endColumn: 10, text: '', - forceMoveMarkers: false }], 'abcd', [ @@ -822,7 +798,6 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 6, text: 'Hi', - forceMoveMarkers: false }], 'Hi world, ciao', [ @@ -849,12 +824,10 @@ suite('Editor Model - modelLine.applyEdits text & tokens', () => { startColumn: 1, endColumn: 6, text: 'Hi', - forceMoveMarkers: false }, { startColumn: 8, endColumn: 12, text: 'my friends', - forceMoveMarkers: false }], 'Hi wmy friends, ciao', [ @@ -873,7 +846,7 @@ suite('Editor Model - modelLine.split text & tokens', () => { let line = new ModelLine(initialText, NO_TAB_SIZE); line.setTokens(0, TestToken.toTokens(initialTokens)); - let other = line.split(new MarkersTracker(), splitColumn, false, NO_TAB_SIZE); + let other = line.split(splitColumn, NO_TAB_SIZE); assert.equal(line.text, expectedText1); assert.equal(other.text, expectedText2); @@ -960,7 +933,7 @@ suite('Editor Model - modelLine.append text & tokens', () => { let b = new ModelLine(bText, NO_TAB_SIZE); b.setTokens(0, TestToken.toTokens(bTokens)); - a.append(new MarkersTracker(), 1, b, NO_TAB_SIZE); + a.append(b, NO_TAB_SIZE); assert.equal(a.text, expectedText); assertLineTokens(a.getTokens(0), expectedTokens); @@ -1071,1260 +1044,483 @@ suite('Editor Model - modelLine.append text & tokens', () => { }); }); -interface ILightWeightMarker { - id: string; - lineNumber: number; - column: number; - stickToPreviousCharacter: boolean; -} - -suite('Editor Model - modelLine.applyEdits text & markers', () => { - - function marker(id: number, column: number, stickToPreviousCharacter: boolean): LineMarker { - return new LineMarker(String(id), id, new Position(0, column), stickToPreviousCharacter); - } - - function toLightWeightMarker(marker: LineMarker): ILightWeightMarker { - return { - id: marker.id, - lineNumber: marker.position.lineNumber, - column: marker.position.column, - stickToPreviousCharacter: marker.stickToPreviousCharacter - }; - } +suite('Editor Model - modelLine.applyEdits', () => { - function testLineEditMarkers(initialText: string, initialMarkers: LineMarker[], edits: ILineEdit[], expectedText: string, expectedChangedMarkers: number[], _expectedMarkers: LineMarker[]): void { + function testLineEdit(initialText: string, edits: ILineEdit[], expectedText: string): void { let line = new ModelLine(initialText, NO_TAB_SIZE); - line.addMarkers(initialMarkers); - let changedMarkers = new MarkersTracker(); - line.applyEdits(changedMarkers, edits, NO_TAB_SIZE); + line.applyEdits(edits, NO_TAB_SIZE); assert.equal(line.text, expectedText, 'text'); - - let actualMarkers = line.getMarkers().map(toLightWeightMarker); - let expectedMarkers = _expectedMarkers.map(toLightWeightMarker); - assert.deepEqual(actualMarkers, expectedMarkers, 'markers'); - - let actualChangedMarkers = changedMarkers.getDecorationIds(); - actualChangedMarkers.sort(); - assert.deepEqual(actualChangedMarkers, expectedChangedMarkers, 'changed markers'); } test('insertion: updates markers 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 1, endColumn: 1, text: 'abc', - forceMoveMarkers: false }], 'abcabcd efgh', - [2, 3, 4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 4, false), - marker(3, 5, true), - marker(4, 5, false), - marker(5, 8, true), - marker(6, 8, false), - marker(7, 13, true), - marker(8, 13, false) - ] ); }); test('insertion: updates markers 2', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 2, endColumn: 2, text: 'abc', - forceMoveMarkers: false }], 'aabcbcd efgh', - [4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 5, false), - marker(5, 8, true), - marker(6, 8, false), - marker(7, 13, true), - marker(8, 13, false) - ] ); }); test('insertion: updates markers 3', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 3, endColumn: 3, text: 'abc', - forceMoveMarkers: false }], 'ababccd efgh', - [5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 8, true), - marker(6, 8, false), - marker(7, 13, true), - marker(8, 13, false) - ] ); }); test('insertion: updates markers 4', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 5, endColumn: 5, text: 'abc', - forceMoveMarkers: false }], 'abcdabc efgh', - [6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 8, false), - marker(7, 13, true), - marker(8, 13, false) - ] ); }); test('insertion: updates markers 5', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 10, endColumn: 10, text: 'abc', - forceMoveMarkers: false }], 'abcd efghabc', - [8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 13, false) - ] ); }); test('insertion bis: updates markers 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [2, 3, 4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 2, false), - marker(3, 3, true), - marker(4, 3, false), - marker(5, 6, true), - marker(6, 6, false), - marker(7, 11, true), - marker(8, 11, false) - ] ); }); test('insertion bis: updates markers 2', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 2, endColumn: 2, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 3, false), - marker(5, 6, true), - marker(6, 6, false), - marker(7, 11, true), - marker(8, 11, false) - ] ); }); test('insertion bis: updates markers 3', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 3, endColumn: 3, text: 'a', - forceMoveMarkers: false }], 'abacd efgh', - [5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 6, true), - marker(6, 6, false), - marker(7, 11, true), - marker(8, 11, false) - ] ); }); test('insertion bis: updates markers 4', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 5, endColumn: 5, text: 'a', - forceMoveMarkers: false }], 'abcda efgh', - [6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 6, false), - marker(7, 11, true), - marker(8, 11, false) - ] ); }); test('insertion bis: updates markers 5', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 10, endColumn: 10, text: 'a', - forceMoveMarkers: false }], 'abcd efgha', - [8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 11, false) - ] ); }); test('insertion: does not move marker at column 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [marker(1, 1, true)], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [], - [marker(1, 1, true)] ); }); test('insertion: does move marker at column 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [marker(1, 1, false)], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [1], - [marker(1, 2, false)] ); }); test('insertion: two markers at column 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - ], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [2], - [ - marker(1, 1, true), - marker(2, 2, false) - ] ); }); test('insertion: two markers at column 1 unsorted', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(2, 1, false), - marker(1, 1, true), - ], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }], 'aabcd efgh', - [2], - [ - marker(1, 1, true), - marker(2, 2, false) - ] ); }); test('deletion: updates markers 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 1, endColumn: 2, text: '', - forceMoveMarkers: false }], 'bcd efgh', - [3, 4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 1, true), - marker(4, 1, false), - marker(5, 4, true), - marker(6, 4, false), - marker(7, 9, true), - marker(8, 9, false) - ] ); }); test('deletion: updates markers 2', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 1, endColumn: 4, text: '', - forceMoveMarkers: false }], 'd efgh', - [3, 4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 1, true), - marker(4, 1, false), - marker(5, 2, true), - marker(6, 2, false), - marker(7, 7, true), - marker(8, 7, false) - ] ); }); test('deletion: updates markers 3', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 5, endColumn: 6, text: '', - forceMoveMarkers: false }], 'abcdefgh', - [7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 9, true), - marker(8, 9, false) - ] ); }); test('replace: updates markers 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], [{ startColumn: 1, endColumn: 1, text: 'a', - forceMoveMarkers: false }, { startColumn: 2, endColumn: 3, text: '', - forceMoveMarkers: false }], 'aacd efgh', - [2, 3, 4], - [ - marker(1, 1, true), - marker(2, 2, false), - marker(3, 3, true), - marker(4, 3, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ] ); }); test('delete near markers', () => { - testLineEditMarkers( + testLineEdit( 'abcd', - [ - marker(1, 3, true), - marker(2, 3, false) - ], [{ startColumn: 3, endColumn: 4, text: '', - forceMoveMarkers: false }], 'abd', - [], - [ - marker(1, 3, true), - marker(2, 3, false) - ] ); }); test('replace: updates markers 2', () => { - testLineEditMarkers( + testLineEdit( 'Hello world, how are you', - [ - marker(1, 1, false), - marker(2, 6, true), - marker(3, 14, false), - marker(4, 21, true) - ], [{ startColumn: 1, endColumn: 1, text: ' - ', - forceMoveMarkers: false }, { startColumn: 6, endColumn: 12, text: '', - forceMoveMarkers: false }, { startColumn: 22, endColumn: 25, text: 'things', - forceMoveMarkers: false }], ' - Hello, how are things', - [1, 2, 3, 4], - [ - marker(1, 4, false), - marker(2, 9, true), - marker(3, 11, false), - marker(4, 18, true) - ] ); }); test('sorts markers', () => { - testLineEditMarkers( + testLineEdit( 'Hello world, how are you', - [ - marker(4, 21, true), - marker(2, 6, true), - marker(1, 1, false), - marker(3, 14, false) - ], [{ startColumn: 1, endColumn: 1, text: ' - ', - forceMoveMarkers: false }, { startColumn: 6, endColumn: 12, text: '', - forceMoveMarkers: false }, { startColumn: 22, endColumn: 25, text: 'things', - forceMoveMarkers: false }], ' - Hello, how are things', - [1, 2, 3, 4], - [ - marker(1, 4, false), - marker(2, 9, true), - marker(3, 11, false), - marker(4, 18, true) - ] ); }); test('change text inside markers', () => { - testLineEditMarkers( + testLineEdit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 6, false), - marker(4, 10, true) - ], [{ startColumn: 6, endColumn: 10, text: '1234567', - forceMoveMarkers: false }], 'abcd 1234567', - [], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 6, false), - marker(4, 10, true) - ] ); }); test('inserting is different than replacing for markers part 1', () => { - testLineEditMarkers( + testLineEdit( 'abcd', - [ - marker(1, 2, false) - ], [{ startColumn: 2, endColumn: 2, text: 'INSERT', - forceMoveMarkers: false }], 'aINSERTbcd', - [1], - [ - marker(1, 8, false) - ] ); }); test('inserting is different than replacing for markers part 2', () => { - testLineEditMarkers( + testLineEdit( 'abcd', - [ - marker(1, 2, false) - ], [{ startColumn: 2, endColumn: 3, text: 'REPLACED', - forceMoveMarkers: false }], 'aREPLACEDcd', - [], - [ - marker(1, 2, false) - ] ); }); test('replacing the entire line with more text', () => { - testLineEditMarkers( + testLineEdit( 'this is a short text', - [ - marker(1, 1, false), - marker(2, 16, true), - ], [{ startColumn: 1, endColumn: 21, text: 'Some new text here', - forceMoveMarkers: false }], 'Some new text here', - [], - [ - marker(1, 1, false), - marker(2, 16, true), - ] ); }); test('replacing the entire line with less text', () => { - testLineEditMarkers( + testLineEdit( 'this is a short text', - [ - marker(1, 1, false), - marker(2, 16, true), - ], [{ startColumn: 1, endColumn: 21, text: 'ttt', - forceMoveMarkers: false }], 'ttt', - [2], - [ - marker(1, 1, false), - marker(2, 4, true), - ] ); }); test('replace selection', () => { - testLineEditMarkers( + testLineEdit( 'first', - [ - marker(1, 1, true), - marker(2, 6, false), - ], [{ startColumn: 1, endColumn: 6, text: 'something', - forceMoveMarkers: false }], 'something', - [2], - [ - marker(1, 1, true), - marker(2, 10, false), - ] ); }); }); -suite('Editor Model - modelLine.split text & markers', () => { - - function marker(id: number, column: number, stickToPreviousCharacter: boolean): LineMarker { - return new LineMarker(String(id), id, new Position(0, column), stickToPreviousCharacter); - } - - function toLightWeightMarker(marker: LineMarker): ILightWeightMarker { - return { - id: marker.id, - lineNumber: marker.position.lineNumber, - column: marker.position.column, - stickToPreviousCharacter: marker.stickToPreviousCharacter - }; - } +suite('Editor Model - modelLine.split', () => { - function testLineSplitMarkers(initialText: string, initialMarkers: LineMarker[], splitColumn: number, forceMoveMarkers: boolean, expectedText1: string, expectedText2: string, expectedChangedMarkers: number[], _expectedMarkers1: LineMarker[], _expectedMarkers2: LineMarker[]): void { + function testLineSplit(initialText: string, splitColumn: number, forceMoveMarkers: boolean, expectedText1: string, expectedText2: string): void { let line = new ModelLine(initialText, NO_TAB_SIZE); - line.addMarkers(initialMarkers); - let changedMarkers = new MarkersTracker(); - let otherLine = line.split(changedMarkers, splitColumn, forceMoveMarkers, NO_TAB_SIZE); + let otherLine = line.split(splitColumn, NO_TAB_SIZE); assert.equal(line.text, expectedText1, 'text'); assert.equal(otherLine.text, expectedText2, 'text'); - - let actualMarkers1 = line.getMarkers().map(toLightWeightMarker); - let expectedMarkers1 = _expectedMarkers1.map(toLightWeightMarker); - assert.deepEqual(actualMarkers1, expectedMarkers1, 'markers'); - - let actualMarkers2 = otherLine.getMarkers().map(toLightWeightMarker); - let expectedMarkers2 = _expectedMarkers2.map(toLightWeightMarker); - assert.deepEqual(actualMarkers2, expectedMarkers2, 'markers'); - - let actualChangedMarkers = changedMarkers.getDecorationIds(); - actualChangedMarkers.sort(); - assert.deepEqual(actualChangedMarkers, expectedChangedMarkers, 'changed markers'); } test('split at the beginning', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 1, false, '', 'abcd efgh', - [], - [ - marker(1, 1, true) - ], - [ - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ] ); }); test('split at the beginning 2', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 1, true, '', 'abcd efgh', - [], - [], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ] ); }); test('split at the end', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 10, false, 'abcd efgh', '', - [8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - ], - [ - marker(8, 1, false) - ] ); }); test('split it the middle 1', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 2, false, 'a', 'bcd efgh', - [4, 5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - ], - [ - marker(4, 1, false), - marker(5, 4, true), - marker(6, 4, false), - marker(7, 9, true), - marker(8, 9, false) - ] ); }); test('split it the middle 2', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 3, false, 'ab', 'cd efgh', - [5, 6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - ], - [ - marker(5, 3, true), - marker(6, 3, false), - marker(7, 8, true), - marker(8, 8, false) - ] ); }); test('split it the middle 3', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 5, false, 'abcd', ' efgh', - [6, 7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - ], - [ - marker(6, 1, false), - marker(7, 6, true), - marker(8, 6, false) - ] ); }); test('split it the middle 4', () => { - testLineSplitMarkers( + testLineSplit( 'abcd efgh', - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - marker(7, 10, true), - marker(8, 10, false) - ], 6, false, 'abcd ', 'efgh', - [7, 8], - [ - marker(1, 1, true), - marker(2, 1, false), - marker(3, 2, true), - marker(4, 2, false), - marker(5, 5, true), - marker(6, 5, false), - ], - [ - marker(7, 5, true), - marker(8, 5, false) - ] ); }); }); -suite('Editor Model - modelLine.append text & markers', () => { - - function markerOnFirstLine(id: number, column: number, stickToPreviousCharacter: boolean): LineMarker { - return new LineMarker(String(id), id, new Position(1, column), stickToPreviousCharacter); - } - - function markerOnSecondLine(id: number, column: number, stickToPreviousCharacter: boolean): LineMarker { - return new LineMarker(String(id), id, new Position(2, column), stickToPreviousCharacter); - } - - function toLightWeightMarker(marker: LineMarker): ILightWeightMarker { - return { - id: marker.id, - lineNumber: marker.position.lineNumber, - column: marker.position.column, - stickToPreviousCharacter: marker.stickToPreviousCharacter - }; - } +suite('Editor Model - modelLine.append', () => { - function testLinePrependMarkers(aText: string, aMarkers: LineMarker[], bText: string, bMarkers: LineMarker[], expectedText: string, expectedChangedMarkers: number[], _expectedMarkers: LineMarker[]): void { + function testLinePrependMarkers(aText: string, bText: string, expectedText: string): void { let a = new ModelLine(aText, NO_TAB_SIZE); - a.addMarkers(aMarkers); - let b = new ModelLine(bText, NO_TAB_SIZE); - b.addMarkers(bMarkers); - let changedMarkers = new MarkersTracker(); - a.append(changedMarkers, 1, b, NO_TAB_SIZE); + a.append(b, NO_TAB_SIZE); assert.equal(a.text, expectedText, 'text'); - - let actualMarkers = a.getMarkers().map(toLightWeightMarker); - let expectedMarkers = _expectedMarkers.map(toLightWeightMarker); - assert.deepEqual(actualMarkers, expectedMarkers, 'markers'); - - let actualChangedMarkers = changedMarkers.getDecorationIds(); - actualChangedMarkers.sort(); - assert.deepEqual(actualChangedMarkers, expectedChangedMarkers, 'changed markers'); } test('append to an empty', () => { testLinePrependMarkers( 'abcd efgh', - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false), - markerOnFirstLine(7, 10, true), - markerOnFirstLine(8, 10, false), - ], '', - [ - ], 'abcd efgh', - [], - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false), - markerOnFirstLine(7, 10, true), - markerOnFirstLine(8, 10, false) - ] ); }); test('append an empty', () => { testLinePrependMarkers( '', - [ - ], 'abcd efgh', - [ - markerOnSecondLine(1, 1, true), - markerOnSecondLine(2, 1, false), - markerOnSecondLine(3, 2, true), - markerOnSecondLine(4, 2, false), - markerOnSecondLine(5, 5, true), - markerOnSecondLine(6, 5, false), - markerOnSecondLine(7, 10, true), - markerOnSecondLine(8, 10, false), - ], 'abcd efgh', - [1, 2, 3, 4, 5, 6, 7, 8], - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false), - markerOnFirstLine(7, 10, true), - markerOnFirstLine(8, 10, false) - ] ); }); test('append 1', () => { testLinePrependMarkers( 'abcd', - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false) - ], ' efgh', - [ - markerOnSecondLine(5, 1, true), - markerOnSecondLine(6, 1, false), - markerOnSecondLine(7, 6, true), - markerOnSecondLine(8, 6, false), - ], 'abcd efgh', - [5, 6, 7, 8], - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false), - markerOnFirstLine(7, 10, true), - markerOnFirstLine(8, 10, false) - ] ); }); test('append 2', () => { testLinePrependMarkers( 'abcd e', - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false) - ], 'fgh', - [ - markerOnSecondLine(7, 4, true), - markerOnSecondLine(8, 4, false), - ], 'abcd efgh', - [7, 8], - [ - markerOnFirstLine(1, 1, true), - markerOnFirstLine(2, 1, false), - markerOnFirstLine(3, 2, true), - markerOnFirstLine(4, 2, false), - markerOnFirstLine(5, 5, true), - markerOnFirstLine(6, 5, false), - markerOnFirstLine(7, 10, true), - markerOnFirstLine(8, 10, false) - ] ); }); }); From bf8282b1e6ff643284f387f54077a5d92349431a Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 16:31:33 +0200 Subject: [PATCH 30/64] Simplify TextModelWithDecorations --- src/vs/editor/common/controller/cursor.ts | 4 +- src/vs/editor/common/controller/oneCursor.ts | 4 +- src/vs/editor/common/editorCommon.ts | 2 +- .../editor/common/model/editableTextModel.ts | 14 +- .../common/model/textModelWithDecorations.ts | 257 +++++++----------- 5 files changed, 103 insertions(+), 178 deletions(-) diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 91ea1e17dcb1d..bbe2d1aa6eee3 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -583,7 +583,7 @@ class CommandExecutor { const result = this._innerExecuteCommands(ctx, commands); for (let i = 0, len = ctx.trackedRanges.length; i < len; i++) { - ctx.model._deltaTrackedRange(ctx.trackedRanges[i], null, editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); + ctx.model._setTrackedRange(ctx.trackedRanges[i], null, editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); } return result; @@ -773,7 +773,7 @@ class CommandExecutor { } const l = ctx.trackedRanges.length; - const id = ctx.model._deltaTrackedRange(null, selection, stickiness); + const id = ctx.model._setTrackedRange(null, selection, stickiness); ctx.trackedRanges[l] = id; ctx.trackedRangesDirection[l] = selection.getDirection(); return l.toString(); diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index f4be4b16a7802..fc2690244fb60 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -31,7 +31,7 @@ export class OneCursor { } public dispose(context: CursorContext): void { - this._selTrackedRange = context.model._deltaTrackedRange(this._selTrackedRange, null, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); + this._selTrackedRange = context.model._setTrackedRange(this._selTrackedRange, null, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); } public asCursorState(): CursorState { @@ -101,6 +101,6 @@ export class OneCursor { this.modelState = modelState; this.viewState = viewState; - this._selTrackedRange = context.model._deltaTrackedRange(this._selTrackedRange, this.modelState.selection, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); + this._selTrackedRange = context.model._setTrackedRange(this._selTrackedRange, this.modelState.selection, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges); } } diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 6b46304bbebfb..cc577de02ddc2 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -1024,7 +1024,7 @@ export interface ITextModelWithDecorations { /** * @internal */ - _deltaTrackedRange(id: string, newRange: Range, newStickiness: TrackedRangeStickiness): string; + _setTrackedRange(id: string, newRange: Range, newStickiness: TrackedRangeStickiness): string; } /** diff --git a/src/vs/editor/common/model/editableTextModel.ts b/src/vs/editor/common/model/editableTextModel.ts index ca0508e2ad180..d71d8c9b2abab 100644 --- a/src/vs/editor/common/model/editableTextModel.ts +++ b/src/vs/editor/common/model/editableTextModel.ts @@ -276,10 +276,10 @@ export class EditableTextModel extends TextModelWithDecorations implements edito public applyEdits(rawOperations: editorCommon.IIdentifiedSingleEditOperation[]): editorCommon.IIdentifiedSingleEditOperation[] { try { this._eventEmitter.beginDeferredEmit(); - this._acquireMarkersTracker(); + this._acquireDecorationsTracker(); return this._applyEdits(rawOperations); } finally { - this._releaseMarkersTracker(); + this._releaseDecorationsTracker(); this._eventEmitter.endDeferredEmit(); } } @@ -661,7 +661,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito text: text }); - this._tree.acceptReplace(op.rangeOffset, op.rangeLength, text.length, op.forceMoveMarkers); + this._decorationsTree.acceptReplace(op.rangeOffset, op.rangeLength, text.length, op.forceMoveMarkers); // console.log('AFTER:'); // console.log('<<<\n' + this._lines.map(l => l.text).join('\n') + '\n>>>'); @@ -710,10 +710,10 @@ export class EditableTextModel extends TextModelWithDecorations implements edito public undo(): Selection[] { try { this._eventEmitter.beginDeferredEmit(); - this._acquireMarkersTracker(); + this._acquireDecorationsTracker(); return this._undo(); } finally { - this._releaseMarkersTracker(); + this._releaseDecorationsTracker(); this._eventEmitter.endDeferredEmit(); } } @@ -735,10 +735,10 @@ export class EditableTextModel extends TextModelWithDecorations implements edito public redo(): Selection[] { try { this._eventEmitter.beginDeferredEmit(); - this._acquireMarkersTracker(); + this._acquireDecorationsTracker(); return this._redo(); } finally { - this._releaseMarkersTracker(); + this._releaseDecorationsTracker(); this._eventEmitter.endDeferredEmit(); } } diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 40e9add2c52e1..fe9306fdfc244 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -23,33 +23,6 @@ export const ClassName = { EditorErrorDecoration: 'errorsquiggly' }; -class DecorationsTracker { - - public didAddDecorations: boolean; - public didRemoveDecorations: boolean; - public didChangeDecorations: boolean; - - constructor() { - this.didAddDecorations = false; - this.didRemoveDecorations = false; - this.didChangeDecorations = false; - } - - // --- Build decoration events - - public markDidAddDecorations(): void { - this.didAddDecorations = true; - } - - public markDidRemoveDecorations(): void { - this.didRemoveDecorations = true; - } - - public markDidChangeDecorations(): void { - this.didChangeDecorations = true; - } -} - let _INSTANCE_COUNT = 0; /** * Produces 'a'-'z', followed by 'A'-'Z'... followed by 'a'-'z', etc. @@ -75,34 +48,23 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi */ private readonly _instanceId: string; private _lastDecorationId: number; - - private _currentDecorationsTracker: DecorationsTracker; private _currentDecorationsTrackerCnt: number; - - private _currentMarkersTrackerCnt: number; - - protected _tree: IntervalTree; - private _treeDecorations: { [decorationId: string]: IntervalNode; }; + private _decorations: { [decorationId: string]: IntervalNode; }; + protected _decorationsTree: IntervalTree; constructor(rawTextSource: IRawTextSource, creationOptions: editorCommon.ITextModelCreationOptions, languageIdentifier: LanguageIdentifier) { super(rawTextSource, creationOptions, languageIdentifier); this._instanceId = nextInstanceId(); this._lastDecorationId = 0; - - // Initialize decorations - this._currentDecorationsTracker = null; this._currentDecorationsTrackerCnt = 0; - - this._currentMarkersTrackerCnt = 0; - - this._tree = new IntervalTree(); - this._treeDecorations = Object.create(null); + this._decorations = Object.create(null); + this._decorationsTree = new IntervalTree(); } public dispose(): void { - this._tree = null; - this._treeDecorations = null; + this._decorations = null; + this._decorationsTree = null; super.dispose(); } @@ -111,80 +73,56 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi super._resetValue(newValue); // Destroy all my decorations - this._tree = new IntervalTree(); - this._treeDecorations = Object.create(null); + this._decorations = Object.create(null); + this._decorationsTree = new IntervalTree(); } _getTrackedRangesCount(): number { - return this._tree.count(); + return this._decorationsTree.count(); } // --- END TrackedRanges - private _acquireDecorationsTracker(): DecorationsTracker { - if (this._currentDecorationsTrackerCnt === 0) { - this._currentDecorationsTracker = new DecorationsTracker(); - } + protected _acquireDecorationsTracker(): void { this._currentDecorationsTrackerCnt++; - return this._currentDecorationsTracker; } - private _releaseDecorationsTracker(): void { + protected _releaseDecorationsTracker(): void { this._currentDecorationsTrackerCnt--; if (this._currentDecorationsTrackerCnt === 0) { - let decorationsTracker = this._currentDecorationsTracker; - this._currentDecorationsTracker = null; - this._handleTrackedDecorations(decorationsTracker); + this._emitModelDecorationsChangedEvent(); } } - private _handleTrackedDecorations(decorationsTracker: DecorationsTracker): void { - if ( - !decorationsTracker.didAddDecorations - && !decorationsTracker.didChangeDecorations - && !decorationsTracker.didRemoveDecorations - ) { - return; - } - - this.emitModelDecorationsChangedEvent(); - } - public changeDecorations(callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T, ownerId: number = 0): T { this._assertNotDisposed(); try { this._eventEmitter.beginDeferredEmit(); - let decorationsTracker = this._acquireDecorationsTracker(); - return this._changeDecorations(decorationsTracker, ownerId, callback); + this._acquireDecorationsTracker(); + return this._changeDecorations(ownerId, callback); } finally { this._releaseDecorationsTracker(); this._eventEmitter.endDeferredEmit(); } } - private _changeDecorations(decorationsTracker: DecorationsTracker, ownerId: number, callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T): T { + private _changeDecorations(ownerId: number, callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T): T { let changeAccessor: editorCommon.IModelDecorationsChangeAccessor = { addDecoration: (range: IRange, options: editorCommon.IModelDecorationOptions): string => { - decorationsTracker.markDidAddDecorations(); - decorationsTracker.markDidRemoveDecorations(); - return this._deltaDecorationsImpl2(ownerId, [], [{ range: range, options: options }])[0]; + return this._deltaDecorationsImpl(ownerId, [], [{ range: range, options: options }])[0]; }, changeDecoration: (id: string, newRange: IRange): void => { - this._changeDecorationImpl2(decorationsTracker, id, newRange); + this._changeDecorationImpl(id, newRange); }, changeDecorationOptions: (id: string, options: editorCommon.IModelDecorationOptions) => { - this._changeDecorationOptionsImpl2(decorationsTracker, id, _normalizeOptions(options)); + this._changeDecorationOptionsImpl(id, _normalizeOptions(options)); }, removeDecoration: (id: string): void => { - decorationsTracker.markDidAddDecorations(); - decorationsTracker.markDidRemoveDecorations(); - this._deltaDecorationsImpl2(ownerId, [id], []); + this._deltaDecorationsImpl(ownerId, [id], []); }, deltaDecorations: (oldDecorations: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] => { - decorationsTracker.markDidAddDecorations(); - decorationsTracker.markDidRemoveDecorations(); - return this._deltaDecorationsImpl2(ownerId, oldDecorations, newDecorations); + return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); } }; let result: T = null; @@ -206,17 +144,23 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi if (!oldDecorations) { oldDecorations = []; } - return this.changeDecorations((changeAccessor) => { - return changeAccessor.deltaDecorations(oldDecorations, newDecorations); - }, ownerId); + + try { + this._eventEmitter.beginDeferredEmit(); + this._acquireDecorationsTracker(); + return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); + } finally { + this._releaseDecorationsTracker(); + this._eventEmitter.endDeferredEmit(); + } } _getTrackedRange(id: string): Range { return this.getDecorationRange(id); } - _deltaTrackedRange(id: string, newRange: Range, newStickiness: editorCommon.TrackedRangeStickiness): string { - const node = (id ? this._treeDecorations[id] : null); + _setTrackedRange(id: string, newRange: Range, newStickiness: editorCommon.TrackedRangeStickiness): string { + const node = (id ? this._decorations[id] : null); if (!node) { if (!newRange) { @@ -224,13 +168,13 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi return null; } // node doesn't exist, the request is to set => add the tracked range - return this._deltaDecorationsImpl2(0, [], [{ range: newRange, options: TRACKED_RANGE_OPTIONS[newStickiness] }])[0]; + return this._deltaDecorationsImpl(0, [], [{ range: newRange, options: TRACKED_RANGE_OPTIONS[newStickiness] }])[0]; } if (!newRange) { // node exists, the request is to delete => delete node - this._tree.delete(node); - delete this._treeDecorations[node.id]; + this._decorationsTree.delete(node); + delete this._decorations[node.id]; return null; } @@ -239,51 +183,39 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi this._ensureLineStarts(); const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; - this._tree.delete(node); + this._decorationsTree.delete(node); node.reset(this.getVersionId(), startOffset, endOffset, range); node.setOptions(TRACKED_RANGE_OPTIONS[newStickiness]); - this._tree.insert(node); + this._decorationsTree.insert(node); return node.id; } public removeAllDecorationsWithOwnerId(ownerId: number): void { - const nodes = this._tree.collectNodesFromOwner(ownerId); + const nodes = this._decorationsTree.collectNodesFromOwner(ownerId); for (let i = 0, len = nodes.length; i < len; i++) { const node = nodes[i]; - this._tree.delete(node); - delete this._treeDecorations[node.id]; + this._decorationsTree.delete(node); + delete this._decorations[node.id]; } } public getDecorationOptions(decorationId: string): editorCommon.IModelDecorationOptions { - const node = this._treeDecorations[decorationId]; + const node = this._decorations[decorationId]; if (!node) { return null; } return node.options; } - private _getRangeAt(start: number, end: number): Range { - const startResult = this._lineStarts.getIndexOf(start); - const startLineLength = this._lines[startResult.index].text.length; - const startColumn = Math.min(startResult.remainder + 1, startLineLength + 1); - - const endResult = this._lineStarts.getIndexOf(end); - const endLineLength = this._lines[endResult.index].text.length; - const endColumn = Math.min(endResult.remainder + 1, endLineLength + 1); - - return new Range(startResult.index + 1, startColumn, endResult.index + 1, endColumn); - } - public getDecorationRange(decorationId: string): Range { - const node = this._treeDecorations[decorationId]; + const node = this._decorations[decorationId]; if (!node) { return null; } const versionId = this.getVersionId(); if (node.cachedVersionId !== versionId) { - this._tree.resolveNode(node, versionId); + this._decorationsTree.resolveNode(node, versionId); } if (node.range === null) { this._ensureLineStarts(); @@ -300,30 +232,6 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi return this.getLinesDecorations(lineNumber, lineNumber, ownerId, filterOutValidation); } - private _ensureNodesHaveRanges(nodes: IntervalNode[]): IntervalNode[] { - this._ensureLineStarts(); - - for (let i = 0, len = nodes.length; i < len; i++) { - const node = nodes[i]; - if (node.range === null) { - node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd); - } - } - return nodes; - } - - private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): IntervalNode[] { - this._ensureLineStarts(); - - const startOffset = this._lineStarts.getAccumulatedValue(filterRange.startLineNumber - 2) + filterRange.startColumn - 1; - const endOffset = this._lineStarts.getAccumulatedValue(filterRange.endLineNumber - 2) + filterRange.endColumn - 1; - - const versionId = this.getVersionId(); - const result = this._tree.intervalSearch(startOffset, endOffset, filterOwnerId, filterOutValidation, versionId); - - return this._ensureNodesHaveRanges(result); - } - public getLinesDecorations(_startLineNumber: number, _endLineNumber: number, ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { let lineCount = this.getLineCount(); let startLineNumber = Math.min(lineCount, Math.max(1, _startLineNumber)); @@ -339,40 +247,61 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi public getOverviewRulerDecorations(ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { const versionId = this.getVersionId(); - const result = this._tree.search(ownerId, filterOutValidation, true, versionId); + const result = this._decorationsTree.search(ownerId, filterOutValidation, true, versionId); return this._ensureNodesHaveRanges(result); } public getAllDecorations(ownerId: number = 0, filterOutValidation: boolean = false): editorCommon.IModelDecoration[] { const versionId = this.getVersionId(); - const result = this._tree.search(ownerId, filterOutValidation, false, versionId); + const result = this._decorationsTree.search(ownerId, filterOutValidation, false, versionId); return this._ensureNodesHaveRanges(result); } - protected _acquireMarkersTracker(): void { - this._currentMarkersTrackerCnt++; + private _emitModelDecorationsChangedEvent(): void { + if (!this._isDisposing) { + let e: textModelEvents.IModelDecorationsChangedEvent = {}; + this._eventEmitter.emit(textModelEvents.TextModelEventType.ModelDecorationsChanged, e); + } } - protected _releaseMarkersTracker(): void { - this._currentMarkersTrackerCnt--; - if (this._currentMarkersTrackerCnt === 0) { - this.emitModelDecorationsChangedEvent(); - } + private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): IntervalNode[] { + this._ensureLineStarts(); + + const startOffset = this._lineStarts.getAccumulatedValue(filterRange.startLineNumber - 2) + filterRange.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(filterRange.endLineNumber - 2) + filterRange.endColumn - 1; + + const versionId = this.getVersionId(); + const result = this._decorationsTree.intervalSearch(startOffset, endOffset, filterOwnerId, filterOutValidation, versionId); + + return this._ensureNodesHaveRanges(result); } - private emitModelDecorationsChangedEvent(): void { - if (!this._isDisposing) { - let e: textModelEvents.IModelDecorationsChangedEvent = {}; - this._eventEmitter.emit(textModelEvents.TextModelEventType.ModelDecorationsChanged, e); + private _ensureNodesHaveRanges(nodes: IntervalNode[]): IntervalNode[] { + this._ensureLineStarts(); + + for (let i = 0, len = nodes.length; i < len; i++) { + const node = nodes[i]; + if (node.range === null) { + node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd); + } } + return nodes; } - private _externalDecorationId(internalId: number): string { - return `${this._instanceId};${internalId}`; + private _getRangeAt(start: number, end: number): Range { + const startResult = this._lineStarts.getIndexOf(start); + const startLineLength = this._lines[startResult.index].text.length; + const startColumn = Math.min(startResult.remainder + 1, startLineLength + 1); + + const endResult = this._lineStarts.getIndexOf(end); + const endLineLength = this._lines[endResult.index].text.length; + const endColumn = Math.min(endResult.remainder + 1, endLineLength + 1); + + return new Range(startResult.index + 1, startColumn, endResult.index + 1, endColumn); } - private _changeDecorationImpl2(decorationsTracker: DecorationsTracker, decorationId: string, _range: IRange): void { - const node = this._treeDecorations[decorationId]; + private _changeDecorationImpl(decorationId: string, _range: IRange): void { + const node = this._decorations[decorationId]; if (!node) { return; } @@ -381,25 +310,21 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; - this._tree.delete(node); + this._decorationsTree.delete(node); node.reset(this.getVersionId(), startOffset, endOffset, range); - this._tree.insert(node); - - decorationsTracker.markDidChangeDecorations(); + this._decorationsTree.insert(node); } - private _changeDecorationOptionsImpl2(decorationsTracker: DecorationsTracker, decorationId: string, options: ModelDecorationOptions): void { - const node = this._treeDecorations[decorationId]; + private _changeDecorationOptionsImpl(decorationId: string, options: ModelDecorationOptions): void { + const node = this._decorations[decorationId]; if (!node) { return; } node.setOptions(options); - - decorationsTracker.markDidChangeDecorations(); } - private _deltaDecorationsImpl2(ownerId: number, oldDecorationsIds: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] { + private _deltaDecorationsImpl(ownerId: number, oldDecorationsIds: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] { this._ensureLineStarts(); const versionId = this.getVersionId(); @@ -417,12 +342,12 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi if (oldDecorationIndex < oldDecorationsLen) { // (1) get ourselves an old node do { - node = this._treeDecorations[oldDecorationsIds[oldDecorationIndex++]]; + node = this._decorations[oldDecorationsIds[oldDecorationIndex++]]; } while (!node && oldDecorationIndex < oldDecorationsLen); // (2) remove the node from the tree (if it exists) if (node) { - this._tree.delete(node); + this._decorationsTree.delete(node); } } @@ -430,9 +355,9 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi // (3) create a new node if necessary if (!node) { const internalDecorationId = (++this._lastDecorationId); - const decorationId = this._externalDecorationId(internalDecorationId); + const decorationId = `${this._instanceId};${internalDecorationId}`; node = new IntervalNode(decorationId, 0, 0); - this._treeDecorations[decorationId] = node; + this._decorations[decorationId] = node; } // (4) initialize node @@ -446,14 +371,14 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi node.reset(versionId, startOffset, endOffset, range); node.setOptions(options); - this._tree.insert(node); + this._decorationsTree.insert(node); result[newDecorationIndex] = node.id; newDecorationIndex++; } else { if (node) { - delete this._treeDecorations[node.id]; + delete this._decorations[node.id]; } } } From 1262a9824076a3741efa34d821443623bf5a5f53 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 16:52:15 +0200 Subject: [PATCH 31/64] Simplify usage of lineStarts in model --- .../editor/common/model/editableTextModel.ts | 37 ++++--------------- src/vs/editor/common/model/textModel.ts | 32 +++++++--------- .../common/model/textModelWithDecorations.ts | 8 ---- 3 files changed, 20 insertions(+), 57 deletions(-) diff --git a/src/vs/editor/common/model/editableTextModel.ts b/src/vs/editor/common/model/editableTextModel.ts index d71d8c9b2abab..db5b3639da597 100644 --- a/src/vs/editor/common/model/editableTextModel.ts +++ b/src/vs/editor/common/model/editableTextModel.ts @@ -289,8 +289,6 @@ export class EditableTextModel extends TextModelWithDecorations implements edito return []; } - this._ensureLineStarts(); - let mightContainRTL = this._mightContainRTL; let mightContainNonBasicASCII = this._mightContainNonBasicASCII; let canReduceOperations = true; @@ -508,10 +506,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito this._invalidateLine(currentLineNumber - 1); this._lines[currentLineNumber - 1].applyEdits(lineEditsQueue.slice(currentLineNumberStart, i), tabSize); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(currentLineNumber - 1, this._lines[currentLineNumber - 1].text.length + this._EOL.length); - } + this._lineStarts.changeValue(currentLineNumber - 1, this._lines[currentLineNumber - 1].text.length + this._EOL.length); rawContentChanges.push( new textModelEvents.ModelRawLineChanged(currentLineNumber, this._lines[currentLineNumber - 1].text) ); @@ -522,10 +517,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito this._invalidateLine(currentLineNumber - 1); this._lines[currentLineNumber - 1].applyEdits(lineEditsQueue.slice(currentLineNumberStart, lineEditsQueue.length), tabSize); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(currentLineNumber - 1, this._lines[currentLineNumber - 1].text.length + this._EOL.length); - } + this._lineStarts.changeValue(currentLineNumber - 1, this._lines[currentLineNumber - 1].text.length + this._EOL.length); rawContentChanges.push( new textModelEvents.ModelRawLineChanged(currentLineNumber, this._lines[currentLineNumber - 1].text) ); @@ -583,17 +575,11 @@ export class EditableTextModel extends TextModelWithDecorations implements edito const spliceCnt = endLineNumber - spliceStartLineNumber; this._lines.splice(spliceStartLineNumber, spliceCnt); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.removeValues(spliceStartLineNumber, spliceCnt); - } + this._lineStarts.removeValues(spliceStartLineNumber, spliceCnt); // Reconstruct first line this._lines[spliceStartLineNumber - 1].append(endLineRemains, tabSize); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(spliceStartLineNumber - 1, this._lines[spliceStartLineNumber - 1].text.length + this._EOL.length); - } + this._lineStarts.changeValue(spliceStartLineNumber - 1, this._lines[spliceStartLineNumber - 1].text.length + this._EOL.length); rawContentChanges.push( new textModelEvents.ModelRawLineChanged(spliceStartLineNumber, this._lines[spliceStartLineNumber - 1].text) @@ -618,10 +604,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito // Split last line let leftoverLine = this._lines[spliceLineNumber - 1].split(spliceColumn, tabSize); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(spliceLineNumber - 1, this._lines[spliceLineNumber - 1].text.length + this._EOL.length); - } + this._lineStarts.changeValue(spliceLineNumber - 1, this._lines[spliceLineNumber - 1].text.length + this._EOL.length); rawContentChanges.push( new textModelEvents.ModelRawLineChanged(spliceLineNumber, this._lines[spliceLineNumber - 1].text) ); @@ -638,17 +621,11 @@ export class EditableTextModel extends TextModelWithDecorations implements edito } this._lines = arrays.arrayInsert(this._lines, startLineNumber + editingLinesCnt, newLines); newLinesContent[newLinesContent.length - 1] += leftoverLine.text; - if (this._lineStarts) { - // update prefix sum - this._lineStarts.insertValues(startLineNumber + editingLinesCnt, newLinesLengths); - } + this._lineStarts.insertValues(startLineNumber + editingLinesCnt, newLinesLengths); // Last line this._lines[startLineNumber + insertingLinesCnt - 1].append(leftoverLine, tabSize); - if (this._lineStarts) { - // update prefix sum - this._lineStarts.changeValue(startLineNumber + insertingLinesCnt - 1, this._lines[startLineNumber + insertingLinesCnt - 1].text.length + this._EOL.length); - } + this._lineStarts.changeValue(startLineNumber + insertingLinesCnt - 1, this._lines[startLineNumber + insertingLinesCnt - 1].text.length + this._EOL.length); rawContentChanges.push( new textModelEvents.ModelRawLinesInserted(spliceLineNumber + 1, startLineNumber + insertingLinesCnt, newLinesContent.join('\n')) ); diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index e932ce350c3da..b5eb35f4e52b8 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -19,8 +19,6 @@ import { TextSource, ITextSource, IRawTextSource, RawTextSource } from 'vs/edito import { IDisposable } from 'vs/base/common/lifecycle'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; -const USE_MIMINAL_MODEL_LINE = true; - const LIMIT_FIND_COUNT = 999; export const LONG_LINE_BOUNDARY = 10000; @@ -124,7 +122,7 @@ export class TextModel implements editorCommon.ITextModel { } protected _createModelLine(text: string, tabSize: number): IModelLine { - if (USE_MIMINAL_MODEL_LINE && this._isTooLargeForTokenization) { + if (this._isTooLargeForTokenization) { return new MinimalModelLine(text, tabSize); } return new ModelLine(text, tabSize); @@ -262,22 +260,9 @@ export class TextModel implements editorCommon.ITextModel { return this._alternativeVersionId; } - protected _ensureLineStarts(): void { - if (!this._lineStarts) { - const eolLength = this._EOL.length; - const linesLength = this._lines.length; - const lineStartValues = new Uint32Array(linesLength); - for (let i = 0; i < linesLength; i++) { - lineStartValues[i] = this._lines[i].text.length + eolLength; - } - this._lineStarts = new PrefixSumComputer(lineStartValues); - } - } - public getOffsetAt(rawPosition: IPosition): number { this._assertNotDisposed(); let position = this._validatePosition(rawPosition.lineNumber, rawPosition.column, false); - this._ensureLineStarts(); return this._lineStarts.getAccumulatedValue(position.lineNumber - 2) + position.column - 1; } @@ -286,7 +271,6 @@ export class TextModel implements editorCommon.ITextModel { offset = Math.floor(offset); offset = Math.max(0, offset); - this._ensureLineStarts(); let out = this._lineStarts.getIndexOf(offset); let lineLength = this._lines[out.index].text.length; @@ -541,7 +525,7 @@ export class TextModel implements editorCommon.ITextModel { const endColumn = this.getLineMaxColumn(endLineNumber); this._EOL = newEOL; - this._lineStarts = null; + this._constructLineStarts(); this._increaseVersionId(); this._emitModelRawContentChangedEvent( @@ -796,7 +780,17 @@ export class TextModel implements editorCommon.ITextModel { this._mightContainNonBasicASCII = !textSource.isBasicASCII; this._EOL = textSource.EOL; this._lines = modelLines; - this._lineStarts = null; + this._constructLineStarts(); + } + + private _constructLineStarts(): void { + const eolLength = this._EOL.length; + const linesLength = this._lines.length; + const lineStartValues = new Uint32Array(linesLength); + for (let i = 0; i < linesLength; i++) { + lineStartValues[i] = this._lines[i].text.length + eolLength; + } + this._lineStarts = new PrefixSumComputer(lineStartValues); } private _getEndOfLine(eol: editorCommon.EndOfLinePreference): string { diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index fe9306fdfc244..6a2d9cffcf1c8 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -180,7 +180,6 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi // node exists, the request is to set => change the tracked range and its options const range = this._validateRangeRelaxedNoAllocations(newRange); - this._ensureLineStarts(); const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; this._decorationsTree.delete(node); @@ -218,7 +217,6 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi this._decorationsTree.resolveNode(node, versionId); } if (node.range === null) { - this._ensureLineStarts(); node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd); } return node.range; @@ -265,8 +263,6 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi } private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): IntervalNode[] { - this._ensureLineStarts(); - const startOffset = this._lineStarts.getAccumulatedValue(filterRange.startLineNumber - 2) + filterRange.startColumn - 1; const endOffset = this._lineStarts.getAccumulatedValue(filterRange.endLineNumber - 2) + filterRange.endColumn - 1; @@ -277,8 +273,6 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi } private _ensureNodesHaveRanges(nodes: IntervalNode[]): IntervalNode[] { - this._ensureLineStarts(); - for (let i = 0, len = nodes.length; i < len; i++) { const node = nodes[i]; if (node.range === null) { @@ -305,7 +299,6 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi if (!node) { return; } - this._ensureLineStarts(); const range = this._validateRangeRelaxedNoAllocations(_range); const startOffset = this._lineStarts.getAccumulatedValue(range.startLineNumber - 2) + range.startColumn - 1; const endOffset = this._lineStarts.getAccumulatedValue(range.endLineNumber - 2) + range.endColumn - 1; @@ -325,7 +318,6 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi } private _deltaDecorationsImpl(ownerId: number, oldDecorationsIds: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] { - this._ensureLineStarts(); const versionId = this.getVersionId(); const oldDecorationsLen = oldDecorationsIds.length; From 4616b4ccde2e77d6071d9b39d47915969d99191b Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 17:56:03 +0200 Subject: [PATCH 32/64] Small tweaks --- .../contrib/find/common/findDecorations.ts | 39 +++++++++++-------- .../editor/contrib/find/common/findModel.ts | 2 +- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/contrib/find/common/findDecorations.ts b/src/vs/editor/contrib/find/common/findDecorations.ts index 202e32602faca..4a59a7b7e569d 100644 --- a/src/vs/editor/contrib/find/common/findDecorations.ts +++ b/src/vs/editor/contrib/find/common/findDecorations.ts @@ -68,11 +68,21 @@ export class FindDecorations implements IDisposable { this.setCurrentFindMatch(null); } + private _getDecorationIndex(decorationId: string): number { + const index = this._decorations.indexOf(decorationId); + if (index >= 0) { + return index + 1; + } + return 1; + } + public getCurrentMatchesPosition(desiredRange: Range): number { - for (let i = 0, len = this._decorations.length; i < len; i++) { - let range = this._editor.getModel().getDecorationRange(this._decorations[i]); - if (desiredRange.equalsRange(range)) { - return (i + 1); + let candidates = this._editor.getModel().getDecorationsInRange(desiredRange); + for (let i = 0, len = candidates.length; i < len; i++) { + const candidate = candidates[i]; + const candidateOpts = candidate.options; + if (candidateOpts === FindDecorations._FIND_MATCH_DECORATION || candidateOpts === FindDecorations._CURRENT_FIND_MATCH_DECORATION) { + return this._getDecorationIndex(candidate.id); } } return 1; @@ -95,12 +105,12 @@ export class FindDecorations implements IDisposable { if (this._highlightedDecorationId !== null || newCurrentDecorationId !== null) { this._editor.changeDecorations((changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => { if (this._highlightedDecorationId !== null) { - changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations.createFindMatchDecorationOptions(false)); + changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations._FIND_MATCH_DECORATION); this._highlightedDecorationId = null; } if (newCurrentDecorationId !== null) { this._highlightedDecorationId = newCurrentDecorationId; - changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations.createFindMatchDecorationOptions(true)); + changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations._CURRENT_FIND_MATCH_DECORATION); } if (this._rangeHighlightDecorationId !== null) { changeAccessor.removeDecoration(this._rangeHighlightDecorationId); @@ -121,13 +131,14 @@ export class FindDecorations implements IDisposable { return matchPosition; } - public set(matches: Range[], findScope: Range): void { - let newDecorations: editorCommon.IModelDeltaDecoration[] = matches.map((match) => { - return { - range: match, - options: FindDecorations.createFindMatchDecorationOptions(false) + public set(findMatches: editorCommon.FindMatch[], findScope: Range): void { + let newDecorations: editorCommon.IModelDeltaDecoration[] = new Array(findMatches.length); + for (let i = 0, len = findMatches.length; i < len; i++) { + newDecorations[i] = { + range: findMatches[i].range, + options: FindDecorations._FIND_MATCH_DECORATION }; - }); + } if (findScope) { newDecorations.unshift({ range: findScope, @@ -158,10 +169,6 @@ export class FindDecorations implements IDisposable { return result; } - private static createFindMatchDecorationOptions(isCurrent: boolean): ModelDecorationOptions { - return (isCurrent ? this._CURRENT_FIND_MATCH_DECORATION : this._FIND_MATCH_DECORATION); - } - private static _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'currentFindMatch', diff --git a/src/vs/editor/contrib/find/common/findModel.ts b/src/vs/editor/contrib/find/common/findModel.ts index be3705b47f72e..1816df33d59de 100644 --- a/src/vs/editor/contrib/find/common/findModel.ts +++ b/src/vs/editor/contrib/find/common/findModel.ts @@ -171,7 +171,7 @@ export class FindModelBoundToEditorModel { } let findMatches = this._findMatches(findScope, false, MATCHES_LIMIT); - this._decorations.set(findMatches.map(match => match.range), findScope); + this._decorations.set(findMatches, findScope); this._state.changeMatchInfo( this._decorations.getCurrentMatchesPosition(this._editor.getSelection()), From 65743658c463393bff3b648c4763a544775d44eb Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 17 Oct 2017 17:57:02 +0200 Subject: [PATCH 33/64] Emit a decoration change event only when methods are called on the change accessor --- .../editor/common/model/editableTextModel.ts | 2 +- .../common/model/textModelWithDecorations.ts | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/model/editableTextModel.ts b/src/vs/editor/common/model/editableTextModel.ts index db5b3639da597..77c0bd9f8a844 100644 --- a/src/vs/editor/common/model/editableTextModel.ts +++ b/src/vs/editor/common/model/editableTextModel.ts @@ -638,7 +638,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito text: text }); - this._decorationsTree.acceptReplace(op.rangeOffset, op.rangeLength, text.length, op.forceMoveMarkers); + this._adjustDecorationsForEdit(op.rangeOffset, op.rangeLength, text.length, op.forceMoveMarkers); // console.log('AFTER:'); // console.log('<<<\n' + this._lines.map(l => l.text).join('\n') + '\n>>>'); diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 6a2d9cffcf1c8..5a500265f8dc7 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -49,8 +49,9 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi private readonly _instanceId: string; private _lastDecorationId: number; private _currentDecorationsTrackerCnt: number; + private _currentDecorationsTrackerDidChange: boolean; private _decorations: { [decorationId: string]: IntervalNode; }; - protected _decorationsTree: IntervalTree; + private _decorationsTree: IntervalTree; constructor(rawTextSource: IRawTextSource, creationOptions: editorCommon.ITextModelCreationOptions, languageIdentifier: LanguageIdentifier) { super(rawTextSource, creationOptions, languageIdentifier); @@ -58,6 +59,7 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi this._instanceId = nextInstanceId(); this._lastDecorationId = 0; this._currentDecorationsTrackerCnt = 0; + this._currentDecorationsTrackerDidChange = false; this._decorations = Object.create(null); this._decorationsTree = new IntervalTree(); } @@ -84,16 +86,26 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi // --- END TrackedRanges protected _acquireDecorationsTracker(): void { + if (this._currentDecorationsTrackerCnt === 0) { + this._currentDecorationsTrackerDidChange = false; + } this._currentDecorationsTrackerCnt++; } protected _releaseDecorationsTracker(): void { this._currentDecorationsTrackerCnt--; if (this._currentDecorationsTrackerCnt === 0) { - this._emitModelDecorationsChangedEvent(); + if (this._currentDecorationsTrackerDidChange) { + this._emitModelDecorationsChangedEvent(); + } } } + protected _adjustDecorationsForEdit(offset: number, length: number, textLength: number, forceMoveMarkers: boolean): void { + this._currentDecorationsTrackerDidChange = true; + this._decorationsTree.acceptReplace(offset, length, textLength, forceMoveMarkers); + } + public changeDecorations(callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T, ownerId: number = 0): T { this._assertNotDisposed(); @@ -110,18 +122,23 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi private _changeDecorations(ownerId: number, callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T): T { let changeAccessor: editorCommon.IModelDecorationsChangeAccessor = { addDecoration: (range: IRange, options: editorCommon.IModelDecorationOptions): string => { + this._currentDecorationsTrackerDidChange = true; return this._deltaDecorationsImpl(ownerId, [], [{ range: range, options: options }])[0]; }, changeDecoration: (id: string, newRange: IRange): void => { + this._currentDecorationsTrackerDidChange = true; this._changeDecorationImpl(id, newRange); }, changeDecorationOptions: (id: string, options: editorCommon.IModelDecorationOptions) => { + this._currentDecorationsTrackerDidChange = true; this._changeDecorationOptionsImpl(id, _normalizeOptions(options)); }, removeDecoration: (id: string): void => { + this._currentDecorationsTrackerDidChange = true; this._deltaDecorationsImpl(ownerId, [id], []); }, deltaDecorations: (oldDecorations: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] => { + this._currentDecorationsTrackerDidChange = true; return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); } }; @@ -148,6 +165,7 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi try { this._eventEmitter.beginDeferredEmit(); this._acquireDecorationsTracker(); + this._currentDecorationsTrackerDidChange = true; return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); } finally { this._releaseDecorationsTracker(); From 8b4d883d18ec77cbcbd87a9ec11e19b8f48d6c43 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 18 Oct 2017 16:38:06 +0200 Subject: [PATCH 34/64] Add ModelDecorationOverviewRulerOptions._resolvedColor --- src/vs/editor/common/model/textModelWithDecorations.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 5a500265f8dc7..37be2b0ea1498 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -406,12 +406,14 @@ export class ModelDecorationOverviewRulerOptions implements editorCommon.IModelD readonly darkColor: string | ThemeColor; readonly hcColor: string | ThemeColor; readonly position: editorCommon.OverviewRulerLane; + _resolvedColor: string; constructor(options: editorCommon.IModelDecorationOverviewRulerOptions) { this.color = strings.empty; this.darkColor = strings.empty; this.hcColor = strings.empty; this.position = editorCommon.OverviewRulerLane.Center; + this._resolvedColor = null; if (options && options.color) { this.color = options.color; From fe2841c4312358798a579a6380dd5dfb7d260080 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 18 Oct 2017 16:39:46 +0200 Subject: [PATCH 35/64] Remove ClassName duplication --- src/vs/editor/common/model/intervalTree.ts | 1 - src/vs/editor/common/model/textModelWithDecorations.ts | 6 ------ src/vs/editor/common/services/modelServiceImpl.ts | 2 +- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 9b6970da50878..b279da4b813ee 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -12,7 +12,6 @@ import { IModelDecoration } from 'vs/editor/common/editorCommon'; // The red-black tree is based on the "Introduction to Algorithms" by Cormen, Leiserson and Rivest. // -// TODO@interval!!! export const ClassName = { EditorInfoDecoration: 'infosquiggly', EditorWarningDecoration: 'warningsquiggly', diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 37be2b0ea1498..80c7529764c47 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -17,12 +17,6 @@ import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { IntervalNode, IntervalTree } from 'vs/editor/common/model/intervalTree'; -export const ClassName = { - EditorInfoDecoration: 'infosquiggly', - EditorWarningDecoration: 'warningsquiggly', - EditorErrorDecoration: 'errorsquiggly' -}; - let _INSTANCE_COUNT = 0; /** * Produces 'a'-'z', followed by 'A'-'Z'... followed by 'a'-'z', etc. diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 7eb325a67618a..8b535b818f52b 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -26,7 +26,7 @@ import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { PLAINTEXT_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/modesRegistry'; import { IRawTextSource, TextSource, RawTextSource, ITextSource } from 'vs/editor/common/model/textSource'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; -import { ClassName } from 'vs/editor/common/model/textModelWithDecorations'; +import { ClassName } from 'vs/editor/common/model/intervalTree'; import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { themeColorFromId, ThemeColor } from 'vs/platform/theme/common/themeService'; From 93393b305d648f75c62acc7fefe1fde393eb2082 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 18 Oct 2017 17:09:18 +0200 Subject: [PATCH 36/64] Binary encode node color --- src/vs/editor/common/model/intervalTree.ts | 145 +++++++++++++-------- 1 file changed, 89 insertions(+), 56 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index b279da4b813ee..1002ccfa35f99 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -29,17 +29,48 @@ export const enum TrackedRangeStickiness { GrowsOnlyWhenTypingAfter = 3, } -export const enum NodeColor { - Red, - Black +const enum NodeColor { + Black = 0, + Red = 1, +} + +const enum Constants { + ColorMask = 0b00000001, + ColorMaskInverse = 0b11111110, + ColorOffset = 0, + + IsForValidationMask = 0b00000010, + IsForValidationMaskInverse = 0b11111101, + IsForValidationOffset = 1, + + IsVisitedMask = 0b00000100, + IsVisitedMaskInverse = 0b11111011, + IsVisitedOffset = 2, + + StickinessMask = 0b00011000, + StickinessMaskInverse = 0b11100111, + StickinessOffset = 3 +} + +function getNodeColor(node: IntervalNode): NodeColor { + return ((node.metadata & Constants.ColorMask) >>> Constants.ColorOffset); +} +function setNodeColor(node: IntervalNode, color: NodeColor): void { + node.metadata = ( + (node.metadata & Constants.ColorMaskInverse) | (color << Constants.ColorOffset) + ); } export class IntervalNode implements IModelDecoration { + /** + * contains binary encoded information for color, isForValidation, visited, and stickiness. + */ + public metadata: number; + public parent: IntervalNode; public left: IntervalNode; public right: IntervalNode; - public color: NodeColor; public start: number; public end: number; @@ -60,10 +91,12 @@ export class IntervalNode implements IModelDecoration { public visited: boolean; constructor(id: string, start: number, end: number) { + this.metadata = 0; + this.parent = null; this.left = null; this.right = null; - this.color = NodeColor.Red; + setNodeColor(this, NodeColor.Red); this.start = start; this.end = end; @@ -125,7 +158,7 @@ const SENTINEL: IntervalNode = new IntervalNode(null, 0, 0); SENTINEL.parent = SENTINEL; SENTINEL.left = SENTINEL; SENTINEL.right = SENTINEL; -SENTINEL.color = NodeColor.Black; +setNodeColor(SENTINEL, NodeColor.Black); export class IntervalTree { @@ -213,7 +246,7 @@ export class IntervalTree { } public assertInvariants(): void { - assert(SENTINEL.color === NodeColor.Black); + assert(getNodeColor(SENTINEL) === NodeColor.Black); assert(SENTINEL.parent === SENTINEL); assert(SENTINEL.left === SENTINEL); assert(SENTINEL.right === SENTINEL); @@ -239,7 +272,7 @@ export class IntervalTree { } private _print(n: IntervalNode, indent: string, delta: number, out: string[]): void { - out.push(`${indent}[${n.color === NodeColor.Red ? 'R' : 'B'},${n.delta}, ${n.start}->${n.end}, ${n.maxEnd}] : {${delta + n.start}->${delta + n.end}}, maxEnd: ${n.maxEnd + delta}\n`); + out.push(`${indent}[${getNodeColor(n) === NodeColor.Red ? 'R' : 'B'},${n.delta}, ${n.start}->${n.end}, ${n.maxEnd}] : {${delta + n.start}->${delta + n.end}}, maxEnd: ${n.maxEnd + delta}\n`); if (n.left !== SENTINEL) { this._print(n.left, indent + ' ', delta, out); } else { @@ -731,7 +764,7 @@ function rbTreeInsert(T: IntervalTree, newNode: IntervalNode): IntervalNode { newNode.parent = SENTINEL; newNode.left = SENTINEL; newNode.right = SENTINEL; - newNode.color = NodeColor.Black; + setNodeColor(newNode, NodeColor.Black); T.root = newNode; return T.root; } @@ -742,45 +775,45 @@ function rbTreeInsert(T: IntervalTree, newNode: IntervalNode): IntervalNode { // repair tree let x = newNode; - while (x !== T.root && x.parent.color === NodeColor.Red) { + while (x !== T.root && getNodeColor(x.parent) === NodeColor.Red) { if (x.parent === x.parent.parent.left) { const y = x.parent.parent.right; - if (y.color === NodeColor.Red) { - x.parent.color = NodeColor.Black; - y.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; + if (getNodeColor(y) === NodeColor.Red) { + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(y, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); x = x.parent.parent; } else { if (x === x.parent.right) { x = x.parent; leftRotate(T, x); } - x.parent.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); rightRotate(T, x.parent.parent); } } else { const y = x.parent.parent.left; - if (y.color === NodeColor.Red) { - x.parent.color = NodeColor.Black; - y.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; + if (getNodeColor(y) === NodeColor.Red) { + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(y, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); x = x.parent.parent; } else { if (x === x.parent.left) { x = x.parent; rightRotate(T, x); } - x.parent.color = NodeColor.Black; - x.parent.parent.color = NodeColor.Red; + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(x.parent.parent, NodeColor.Red); leftRotate(T, x.parent.parent); } } } - T.root.color = NodeColor.Black; + setNodeColor(T.root, NodeColor.Black); return newNode; } @@ -823,7 +856,7 @@ function treeInsert(T: IntervalTree, z: IntervalNode): void { z.parent = x; z.left = SENTINEL; z.right = SENTINEL; - z.color = NodeColor.Red; + setNodeColor(z, NodeColor.Red); } //#endregion @@ -867,7 +900,7 @@ function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { if (y === T.root) { T.root = x; - x.color = NodeColor.Black; + setNodeColor(x, NodeColor.Black); z.detach(); resetSentinel(); @@ -876,7 +909,7 @@ function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { return; } - let yWasRed = (y.color === NodeColor.Red); + let yWasRed = (getNodeColor(y) === NodeColor.Red); if (y === y.parent.left) { y.parent.left = x; @@ -897,7 +930,7 @@ function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { y.left = z.left; y.right = z.right; y.parent = z.parent; - y.color = z.color; + setNodeColor(y, getNodeColor(z)); if (z === T.root) { T.root = y; @@ -938,32 +971,32 @@ function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { // RB-DELETE-FIXUP let w: IntervalNode; - while (x !== T.root && x.color === NodeColor.Black) { + while (x !== T.root && getNodeColor(x) === NodeColor.Black) { if (x === x.parent.left) { w = x.parent.right; - if (w.color === NodeColor.Red) { - w.color = NodeColor.Black; - x.parent.color = NodeColor.Red; + if (getNodeColor(w) === NodeColor.Red) { + setNodeColor(w, NodeColor.Black); + setNodeColor(x.parent, NodeColor.Red); leftRotate(T, x.parent); w = x.parent.right; } - if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { - w.color = NodeColor.Red; + if (getNodeColor(w.left) === NodeColor.Black && getNodeColor(w.right) === NodeColor.Black) { + setNodeColor(w, NodeColor.Red); x = x.parent; } else { - if (w.right.color === NodeColor.Black) { - w.left.color = NodeColor.Black; - w.color = NodeColor.Red; + if (getNodeColor(w.right) === NodeColor.Black) { + setNodeColor(w.left, NodeColor.Black); + setNodeColor(w, NodeColor.Red); rightRotate(T, w); w = x.parent.right; } - w.color = x.parent.color; - x.parent.color = NodeColor.Black; - w.right.color = NodeColor.Black; + setNodeColor(w, getNodeColor(x.parent)); + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(w.right, NodeColor.Black); leftRotate(T, x.parent); x = T.root; } @@ -971,35 +1004,35 @@ function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { } else { w = x.parent.left; - if (w.color === NodeColor.Red) { - w.color = NodeColor.Black; - x.parent.color = NodeColor.Red; + if (getNodeColor(w) === NodeColor.Red) { + setNodeColor(w, NodeColor.Black); + setNodeColor(x.parent, NodeColor.Red); rightRotate(T, x.parent); w = x.parent.left; } - if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { - w.color = NodeColor.Red; + if (getNodeColor(w.left) === NodeColor.Black && getNodeColor(w.right) === NodeColor.Black) { + setNodeColor(w, NodeColor.Red); x = x.parent; } else { - if (w.left.color === NodeColor.Black) { - w.right.color = NodeColor.Black; - w.color = NodeColor.Red; + if (getNodeColor(w.left) === NodeColor.Black) { + setNodeColor(w.right, NodeColor.Black); + setNodeColor(w, NodeColor.Red); leftRotate(T, w); w = x.parent.left; } - w.color = x.parent.color; - x.parent.color = NodeColor.Black; - w.left.color = NodeColor.Black; + setNodeColor(w, getNodeColor(x.parent)); + setNodeColor(x.parent, NodeColor.Black); + setNodeColor(w.left, NodeColor.Black); rightRotate(T, x.parent); x = T.root; } } } - x.color = NodeColor.Black; + setNodeColor(x, NodeColor.Black); resetSentinel(); } @@ -1131,7 +1164,7 @@ function depth(n: IntervalNode): number { return 1; } assert(depth(n.left) === depth(n.right)); - return (n.color === NodeColor.Black ? 1 : 0) + depth(n.left); + return (getNodeColor(n) === NodeColor.Black ? 1 : 0) + depth(n.left); } function assertValidNode(n: IntervalNode, delta): void { @@ -1142,9 +1175,9 @@ function assertValidNode(n: IntervalNode, delta): void { let l = n.left; let r = n.right; - if (n.color === NodeColor.Red) { - assert(l.color === NodeColor.Black); - assert(r.color === NodeColor.Black); + if (getNodeColor(n) === NodeColor.Red) { + assert(getNodeColor(l) === NodeColor.Black); + assert(getNodeColor(r) === NodeColor.Black); } let expectedMaxEnd = n.end; @@ -1166,7 +1199,7 @@ function assertValidTree(tree: IntervalTree): void { if (tree.root === SENTINEL) { return; } - assert(tree.root.color === NodeColor.Black); + assert(getNodeColor(tree.root) === NodeColor.Black); assert(depth(tree.root.left) === depth(tree.root.right)); assertValidNode(tree.root, 0); } From cc3ccb280ae29e28a974caf2b57262c8ceca8b3d Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 18 Oct 2017 17:21:41 +0200 Subject: [PATCH 37/64] Binary encode node.visited --- src/vs/editor/common/model/intervalTree.ts | 120 +++++++++++---------- 1 file changed, 63 insertions(+), 57 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 1002ccfa35f99..9a7763406449a 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -39,13 +39,13 @@ const enum Constants { ColorMaskInverse = 0b11111110, ColorOffset = 0, - IsForValidationMask = 0b00000010, - IsForValidationMaskInverse = 0b11111101, - IsForValidationOffset = 1, + IsVisitedMask = 0b00000010, + IsVisitedMaskInverse = 0b11111101, + IsVisitedOffset = 1, - IsVisitedMask = 0b00000100, - IsVisitedMaskInverse = 0b11111011, - IsVisitedOffset = 2, + IsForValidationMask = 0b00000100, + IsForValidationMaskInverse = 0b11111011, + IsForValidationOffset = 2, StickinessMask = 0b00011000, StickinessMaskInverse = 0b11100111, @@ -60,6 +60,14 @@ function setNodeColor(node: IntervalNode, color: NodeColor): void { (node.metadata & Constants.ColorMaskInverse) | (color << Constants.ColorOffset) ); } +function getNodeIsVisited(node: IntervalNode): boolean { + return ((node.metadata & Constants.IsVisitedMask) >>> Constants.IsVisitedOffset) === 1; +} +function setNodeIsVisited(node: IntervalNode, value: boolean): void { + node.metadata = ( + (node.metadata & Constants.IsVisitedMaskInverse) | ((value ? 1 : 0) << Constants.IsVisitedOffset) + ); +} export class IntervalNode implements IModelDecoration { @@ -88,8 +96,6 @@ export class IntervalNode implements IModelDecoration { public cachedAbsoluteEnd: number; public range: Range; - public visited: boolean; - constructor(id: string, start: number, end: number) { this.metadata = 0; @@ -114,7 +120,7 @@ export class IntervalNode implements IModelDecoration { this.cachedAbsoluteEnd = end; this.range = null; - this.visited = false; + setNodeIsVisited(this, false); } public reset(versionId: number, start: number, end: number, range: Range): void { @@ -398,10 +404,10 @@ function searchForEditing(T: IntervalTree, start: number, end: number): Interval let result: IntervalNode[] = []; let resultLen = 0; while (node !== SENTINEL) { - if (node.visited) { + if (getNodeIsVisited(node)) { // going up from this node - node.left.visited = false; - node.right.visited = false; + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); if (node === node.parent.right) { delta -= node.parent.delta; } @@ -409,13 +415,13 @@ function searchForEditing(T: IntervalTree, start: number, end: number): Interval continue; } - if (!node.left.visited) { + if (!getNodeIsVisited(node.left)) { // first time seeing this node nodeMaxEnd = delta + node.maxEnd; if (nodeMaxEnd < start) { // cover case b) from above // there is no need to search this node or its children - node.visited = true; + setNodeIsVisited(node, true); continue; } @@ -431,7 +437,7 @@ function searchForEditing(T: IntervalTree, start: number, end: number): Interval if (nodeStart > end) { // cover case a) from above // there is no need to search this node or its right subtree - node.visited = true; + setNodeIsVisited(node, true); continue; } @@ -440,9 +446,9 @@ function searchForEditing(T: IntervalTree, start: number, end: number): Interval node.setCachedOffsets(nodeStart, nodeEnd, 0); result[resultLen++] = node; } - node.visited = true; + setNodeIsVisited(node, true); - if (node.right !== SENTINEL && !node.right.visited) { + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { // go right delta += node.delta; node = node.right; @@ -451,7 +457,7 @@ function searchForEditing(T: IntervalTree, start: number, end: number): Interval } if (T.root) { - T.root.visited = false; + setNodeIsVisited(T.root, false); } return result; @@ -469,10 +475,10 @@ function noOverlapReplace(T: IntervalTree, start: number, end: number, textLengt let nodeMaxEnd = 0; let nodeStart = 0; while (node !== SENTINEL) { - if (node.visited) { + if (getNodeIsVisited(node)) { // going up from this node - node.left.visited = false; - node.right.visited = false; + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); if (node === node.parent.right) { delta -= node.parent.delta; } @@ -481,13 +487,13 @@ function noOverlapReplace(T: IntervalTree, start: number, end: number, textLengt continue; } - if (!node.left.visited) { + if (!getNodeIsVisited(node.left)) { // first time seeing this node nodeMaxEnd = delta + node.maxEnd; if (nodeMaxEnd < start) { // cover case b) from above // there is no need to search this node or its children - node.visited = true; + setNodeIsVisited(node, true); continue; } @@ -506,13 +512,13 @@ function noOverlapReplace(T: IntervalTree, start: number, end: number, textLengt node.delta += (textLength - (end - start)); // cover case a) from above // there is no need to search this node or its right subtree - node.visited = true; + setNodeIsVisited(node, true); continue; } - node.visited = true; + setNodeIsVisited(node, true); - if (node.right !== SENTINEL && !node.right.visited) { + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { // go right delta += node.delta; node = node.right; @@ -521,7 +527,7 @@ function noOverlapReplace(T: IntervalTree, start: number, end: number, textLengt } if (T.root) { - T.root.visited = false; + setNodeIsVisited(T.root, false); } } @@ -533,15 +539,15 @@ function nodeCount(T: IntervalTree): number { let node = T.root; let count = 0; while (node !== SENTINEL) { - if (node.visited) { + if (getNodeIsVisited(node)) { // going up from this node - node.left.visited = false; - node.right.visited = false; + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); node = node.parent; continue; } - if (node.left !== SENTINEL && !node.left.visited) { + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { // go left node = node.left; continue; @@ -549,9 +555,9 @@ function nodeCount(T: IntervalTree): number { // handle current node count++; - node.visited = true; + setNodeIsVisited(node, true); - if (node.right !== SENTINEL && !node.right.visited) { + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { // go right node = node.right; continue; @@ -559,7 +565,7 @@ function nodeCount(T: IntervalTree): number { } if (T.root) { - T.root.visited = false; + setNodeIsVisited(T.root, false); } return count; @@ -570,15 +576,15 @@ function collectNodesFromOwner(T: IntervalTree, ownerId: number): IntervalNode[] let result: IntervalNode[] = []; let resultLen = 0; while (node !== SENTINEL) { - if (node.visited) { + if (getNodeIsVisited(node)) { // going up from this node - node.left.visited = false; - node.right.visited = false; + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); node = node.parent; continue; } - if (node.left !== SENTINEL && !node.left.visited) { + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { // go left node = node.left; continue; @@ -589,9 +595,9 @@ function collectNodesFromOwner(T: IntervalTree, ownerId: number): IntervalNode[] result[resultLen++] = node; } - node.visited = true; + setNodeIsVisited(node, true); - if (node.right !== SENTINEL && !node.right.visited) { + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { // go right node = node.right; continue; @@ -599,7 +605,7 @@ function collectNodesFromOwner(T: IntervalTree, ownerId: number): IntervalNode[] } if (T.root) { - T.root.visited = false; + setNodeIsVisited(T.root, false); } return result; @@ -613,10 +619,10 @@ function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boo let result: IntervalNode[] = []; let resultLen = 0; while (node !== SENTINEL) { - if (node.visited) { + if (getNodeIsVisited(node)) { // going up from this node - node.left.visited = false; - node.right.visited = false; + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); if (node === node.parent.right) { delta -= node.parent.delta; } @@ -624,7 +630,7 @@ function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boo continue; } - if (node.left !== SENTINEL && !node.left.visited) { + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { // go left node = node.left; continue; @@ -650,9 +656,9 @@ function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boo result[resultLen++] = node; } - node.visited = true; + setNodeIsVisited(node, true); - if (node.right !== SENTINEL && !node.right.visited) { + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { // go right delta += node.delta; node = node.right; @@ -661,7 +667,7 @@ function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boo } if (T.root) { - T.root.visited = false; + setNodeIsVisited(T.root, false); } return result; @@ -683,10 +689,10 @@ function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: num let result: IntervalNode[] = []; let resultLen = 0; while (node !== SENTINEL) { - if (node.visited) { + if (getNodeIsVisited(node)) { // going up from this node - node.left.visited = false; - node.right.visited = false; + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); if (node === node.parent.right) { delta -= node.parent.delta; } @@ -694,13 +700,13 @@ function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: num continue; } - if (!node.left.visited) { + if (!getNodeIsVisited(node.left)) { // first time seeing this node nodeMaxEnd = delta + node.maxEnd; if (nodeMaxEnd < intervalStart) { // cover case b) from above // there is no need to search this node or its children - node.visited = true; + setNodeIsVisited(node, true); continue; } @@ -716,7 +722,7 @@ function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: num if (nodeStart > intervalEnd) { // cover case a) from above // there is no need to search this node or its right subtree - node.visited = true; + setNodeIsVisited(node, true); continue; } @@ -739,9 +745,9 @@ function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: num } } - node.visited = true; + setNodeIsVisited(node, true); - if (node.right !== SENTINEL && !node.right.visited) { + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { // go right delta += node.delta; node = node.right; @@ -750,7 +756,7 @@ function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: num } if (T.root) { - T.root.visited = false; + setNodeIsVisited(T.root, false); } return result; From bc2ff777daa54719a6556561446c633a1c4aa088 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 18 Oct 2017 17:27:38 +0200 Subject: [PATCH 38/64] Binary encode node.isForValidation --- src/vs/editor/common/editorCommon.ts | 4 ---- src/vs/editor/common/model/intervalTree.ts | 19 +++++++++++++------ src/vs/monaco.d.ts | 4 ---- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index cc577de02ddc2..a346ca6ea9d50 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -157,10 +157,6 @@ export interface IModelDecoration { * Options associated with this decoration. */ readonly options: IModelDecorationOptions; - /** - * A flag describing if this is a problem decoration (e.g. warning/error). - */ - readonly isForValidation: boolean; } /** diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 9a7763406449a..2d6e1ce9e09f2 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -68,6 +68,14 @@ function setNodeIsVisited(node: IntervalNode, value: boolean): void { (node.metadata & Constants.IsVisitedMaskInverse) | ((value ? 1 : 0) << Constants.IsVisitedOffset) ); } +function getNodeIsForValidation(node: IntervalNode): boolean { + return ((node.metadata & Constants.IsForValidationMask) >>> Constants.IsForValidationOffset) === 1; +} +function setNodeIsForValidation(node: IntervalNode, value: boolean): void { + node.metadata = ( + (node.metadata & Constants.IsForValidationMaskInverse) | ((value ? 1 : 0) << Constants.IsForValidationOffset) + ); +} export class IntervalNode implements IModelDecoration { @@ -88,7 +96,6 @@ export class IntervalNode implements IModelDecoration { public id: string; public ownerId: number; public options: ModelDecorationOptions; - public isForValidation: boolean; public stickiness: TrackedRangeStickiness; public cachedVersionId: number; @@ -112,7 +119,7 @@ export class IntervalNode implements IModelDecoration { this.id = id; this.ownerId = 0; this.options = null; - this.isForValidation = false; + setNodeIsForValidation(this, false); this.stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; this.cachedVersionId = 0; @@ -135,10 +142,10 @@ export class IntervalNode implements IModelDecoration { public setOptions(options: ModelDecorationOptions) { this.options = options; - this.isForValidation = ( + setNodeIsForValidation(this, ( this.options.className === ClassName.EditorErrorDecoration || this.options.className === ClassName.EditorWarningDecoration - ); + )); this.stickiness = this.options.stickiness; } @@ -646,7 +653,7 @@ function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boo if (filterOwnerId && node.ownerId && node.ownerId !== filterOwnerId) { include = false; } - if (filterOutValidation && node.isForValidation) { + if (filterOutValidation && getNodeIsForValidation(node)) { include = false; } if (overviewRulerOnly && !node.options.overviewRuler.color) { @@ -736,7 +743,7 @@ function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: num if (filterOwnerId && node.ownerId && node.ownerId !== filterOwnerId) { include = false; } - if (filterOutValidation && node.isForValidation) { + if (filterOutValidation && getNodeIsForValidation(node)) { include = false; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 7874e3fd4fb02..776a314bbc977 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1188,10 +1188,6 @@ declare module monaco.editor { * Options associated with this decoration. */ readonly options: IModelDecorationOptions; - /** - * A flag describing if this is a problem decoration (e.g. warning/error). - */ - readonly isForValidation: boolean; } /** From 1d8e120e1c9ff55cdb34c4fc419999be5b0663ba Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 18 Oct 2017 17:37:47 +0200 Subject: [PATCH 39/64] Binary encode node.stickiness --- src/vs/editor/common/model/intervalTree.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 2d6e1ce9e09f2..5c6c31fdc078f 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -76,6 +76,14 @@ function setNodeIsForValidation(node: IntervalNode, value: boolean): void { (node.metadata & Constants.IsForValidationMaskInverse) | ((value ? 1 : 0) << Constants.IsForValidationOffset) ); } +function getNodeStickiness(node: IntervalNode): TrackedRangeStickiness { + return ((node.metadata & Constants.StickinessMask) >>> Constants.StickinessOffset); +} +function setNodeStickiness(node: IntervalNode, stickiness: TrackedRangeStickiness): void { + node.metadata = ( + (node.metadata & Constants.StickinessMaskInverse) | (stickiness << Constants.StickinessOffset) + ); +} export class IntervalNode implements IModelDecoration { @@ -96,7 +104,6 @@ export class IntervalNode implements IModelDecoration { public id: string; public ownerId: number; public options: ModelDecorationOptions; - public stickiness: TrackedRangeStickiness; public cachedVersionId: number; public cachedAbsoluteStart: number; @@ -120,7 +127,7 @@ export class IntervalNode implements IModelDecoration { this.ownerId = 0; this.options = null; setNodeIsForValidation(this, false); - this.stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; + setNodeStickiness(this, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); this.cachedVersionId = 0; this.cachedAbsoluteStart = start; @@ -146,7 +153,7 @@ export class IntervalNode implements IModelDecoration { this.options.className === ClassName.EditorErrorDecoration || this.options.className === ClassName.EditorWarningDecoration )); - this.stickiness = this.options.stickiness; + setNodeStickiness(this, this.options.stickiness); } public setCachedOffsets(absoluteStart: number, absoluteEnd: number, cachedVersionId: number): void { @@ -329,13 +336,14 @@ function adjustMarkerBeforeColumn(markerOffset: number, markerStickToPreviousCha * as when decorations were implemented using two markers. */ function nodeAcceptEdit(node: IntervalNode, start: number, end: number, textLength: number, forceMoveMarkers: boolean): void { + const nodeStickiness = getNodeStickiness(node); const startStickToPreviousCharacter = ( - node.stickiness === TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges - || node.stickiness === TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + nodeStickiness === TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + || nodeStickiness === TrackedRangeStickiness.GrowsOnlyWhenTypingBefore ); const endStickToPreviousCharacter = ( - node.stickiness === TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - || node.stickiness === TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + nodeStickiness === TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + || nodeStickiness === TrackedRangeStickiness.GrowsOnlyWhenTypingBefore ); const deletingCnt = (end - start); From 509eecf0b5c2cd66a18479d66dd0ad42cacd3ba3 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 18 Oct 2017 21:32:12 +0200 Subject: [PATCH 40/64] Update decorations when there is an EOL change --- src/vs/editor/common/model/intervalTree.ts | 39 ++++++++++++++++++- src/vs/editor/common/model/textModel.ts | 9 ++++- .../common/model/textModelWithDecorations.ts | 34 ++++++++++++++++ .../common/model/modelDecorations.test.ts | 30 +++++++++++++- 4 files changed, 108 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 5c6c31fdc078f..d342c8eccf75f 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -88,7 +88,7 @@ function setNodeStickiness(node: IntervalNode, stickiness: TrackedRangeStickines export class IntervalNode implements IModelDecoration { /** - * contains binary encoded information for color, isForValidation, visited, and stickiness. + * contains binary encoded information for color, visited, isForValidation and stickiness. */ public metadata: number; @@ -265,6 +265,10 @@ export class IntervalTree { } } + public recomputeAllMaxEnds(): void { + recomputeAllMaxEnds(this); + } + public assertInvariants(): void { assert(getNodeColor(SENTINEL) === NodeColor.Black); assert(SENTINEL.parent === SENTINEL); @@ -1130,6 +1134,39 @@ function rightRotate(T: IntervalTree, y: IntervalNode): void { //#region max end computation +function recomputeAllMaxEnds(T: IntervalTree): void { + let node = T.root; + while (node !== SENTINEL) { + if (getNodeIsVisited(node)) { + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + node = node.parent; + continue; + } + + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { + // go left + node = node.left; + continue; + } + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + node = node.right; + continue; + } + + // handle current node + recomputeMaxEnd(node); + setNodeIsVisited(node, true); + } + + if (T.root) { + setNodeIsVisited(T.root, false); + } +} + function computeMaxEnd(node: IntervalNode): number { let maxEnd = node.end; if (node.left !== SENTINEL) { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index b5eb35f4e52b8..aa93b0ed106e7 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -510,8 +510,13 @@ export class TextModel implements editorCommon.ITextModel { return this._EOL; } + protected _onBeforeEOLChange(): void { + } + + protected _onAfterEOLChange(): void { + } + public setEOL(eol: editorCommon.EndOfLineSequence): void { - // TODO@interval!!! this._assertNotDisposed(); const newEOL = (eol === editorCommon.EndOfLineSequence.CRLF ? '\r\n' : '\n'); if (this._EOL === newEOL) { @@ -524,9 +529,11 @@ export class TextModel implements editorCommon.ITextModel { const endLineNumber = this.getLineCount(); const endColumn = this.getLineMaxColumn(endLineNumber); + this._onBeforeEOLChange(); this._EOL = newEOL; this._constructLineStarts(); this._increaseVersionId(); + this._onAfterEOLChange(); this._emitModelRawContentChangedEvent( new textModelEvents.ModelRawContentChangedEvent( diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 80c7529764c47..e04cdc485fa31 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -100,6 +100,40 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi this._decorationsTree.acceptReplace(offset, length, textLength, forceMoveMarkers); } + protected _onBeforeEOLChange(): void { + super._onBeforeEOLChange(); + + // Ensure all decorations get their `range` set. + const versionId = this.getVersionId(); + const allDecorations = this._decorationsTree.search(0, false, false, versionId); + this._ensureNodesHaveRanges(allDecorations); + } + + protected _onAfterEOLChange(): void { + super._onAfterEOLChange(); + + // Transform back `range` to offsets + const versionId = this.getVersionId(); + const allDecorations = this._decorationsTree.search(0, false, false, versionId); + for (let i = 0, len = allDecorations.length; i < len; i++) { + const node = allDecorations[i]; + + const delta = node.cachedAbsoluteStart - node.start; + + const startOffset = this._lineStarts.getAccumulatedValue(node.range.startLineNumber - 2) + node.range.startColumn - 1; + const endOffset = this._lineStarts.getAccumulatedValue(node.range.endLineNumber - 2) + node.range.endColumn - 1; + + node.cachedAbsoluteStart = startOffset; + node.cachedAbsoluteEnd = endOffset; + node.cachedVersionId = versionId; + + node.start = startOffset - delta; + node.end = endOffset - delta; + } + + this._decorationsTree.recomputeAllMaxEnds(); + } + public changeDecorations(callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T, ownerId: number = 0): T { this._assertNotDisposed(); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 26b004d0a0c4b..0a7ff4e3d8ffa 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, TrackedRangeStickiness, EndOfLineSequence } from 'vs/editor/common/editorCommon'; import { Model } from 'vs/editor/common/model/model'; // --------- utils @@ -28,7 +28,7 @@ function modelHasDecorations(model: Model, decorations: ILightWeightDecoration2[ }); } modelDecorations.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - assert.deepEqual(modelDecorations, decorations, 'Model decorations'); + assert.deepEqual(modelDecorations, decorations); } function modelHasDecoration(model: Model, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, className: string) { @@ -351,6 +351,32 @@ suite('Editor Model - Model Decorations', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 3, 1))]); modelHasDecoration(thisModel, 1, 1, 2, 1, 'myType'); }); + + test('decorations are updated when changing EOL', () => { + addDecoration(thisModel, 1, 2, 4, 1, 'myType1'); + addDecoration(thisModel, 1, 3, 4, 1, 'myType2'); + addDecoration(thisModel, 1, 4, 4, 1, 'myType3'); + addDecoration(thisModel, 1, 5, 4, 1, 'myType4'); + addDecoration(thisModel, 1, 6, 4, 1, 'myType5'); + addDecoration(thisModel, 1, 7, 4, 1, 'myType6'); + addDecoration(thisModel, 1, 8, 4, 1, 'myType7'); + addDecoration(thisModel, 1, 9, 4, 1, 'myType8'); + addDecoration(thisModel, 1, 10, 4, 1, 'myType9'); + thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'x')]); + thisModel.setEOL(EndOfLineSequence.CRLF); + thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'x')]); + modelHasDecorations(thisModel, [ + { range: new Range(1, 4, 4, 1), className: 'myType1' }, + { range: new Range(1, 5, 4, 1), className: 'myType2' }, + { range: new Range(1, 6, 4, 1), className: 'myType3' }, + { range: new Range(1, 7, 4, 1), className: 'myType4' }, + { range: new Range(1, 8, 4, 1), className: 'myType5' }, + { range: new Range(1, 9, 4, 1), className: 'myType6' }, + { range: new Range(1, 10, 4, 1), className: 'myType7' }, + { range: new Range(1, 11, 4, 1), className: 'myType8' }, + { range: new Range(1, 12, 4, 1), className: 'myType9' }, + ]); + }); }); suite('Decorations and editing', () => { From ba81d4affc7a189a7c32c59d7306c04f250251e8 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 18 Oct 2017 22:27:48 +0200 Subject: [PATCH 41/64] Fix range caching logic in decorations --- src/vs/editor/common/model/intervalTree.ts | 84 ++++++++++--------- .../common/model/textModelWithDecorations.ts | 8 +- .../test/common/sortLinesCommand.test.ts | 4 +- .../common/model/modelDecorations.test.ts | 8 ++ 4 files changed, 57 insertions(+), 47 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index d342c8eccf75f..d45aeef43b6c7 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -157,14 +157,12 @@ export class IntervalNode implements IModelDecoration { } public setCachedOffsets(absoluteStart: number, absoluteEnd: number, cachedVersionId: number): void { - this.cachedVersionId = cachedVersionId; - if (this.cachedAbsoluteStart === absoluteStart && this.cachedAbsoluteEnd === absoluteEnd) { - // no change - return; + if (this.cachedVersionId !== cachedVersionId) { + this.range = null; } + this.cachedVersionId = cachedVersionId; this.cachedAbsoluteStart = absoluteStart; this.cachedAbsoluteEnd = absoluteEnd; - this.range = null; } public detach(): void { @@ -265,8 +263,8 @@ export class IntervalTree { } } - public recomputeAllMaxEnds(): void { - recomputeAllMaxEnds(this); + public collectNodesPostOrder(): IntervalNode[] { + return collectNodesPostOrder(this); } public assertInvariants(): void { @@ -630,6 +628,43 @@ function collectNodesFromOwner(T: IntervalTree, ownerId: number): IntervalNode[] return result; } +function collectNodesPostOrder(T: IntervalTree): IntervalNode[] { + let node = T.root; + let result: IntervalNode[] = []; + let resultLen = 0; + while (node !== SENTINEL) { + if (getNodeIsVisited(node)) { + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + node = node.parent; + continue; + } + + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { + // go left + node = node.left; + continue; + } + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + node = node.right; + continue; + } + + // handle current node + result[resultLen++] = node; + setNodeIsVisited(node, true); + } + + if (T.root) { + setNodeIsVisited(T.root, false); + } + + return result; +} + function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] { let node = T.root; let delta = 0; @@ -1134,39 +1169,6 @@ function rightRotate(T: IntervalTree, y: IntervalNode): void { //#region max end computation -function recomputeAllMaxEnds(T: IntervalTree): void { - let node = T.root; - while (node !== SENTINEL) { - if (getNodeIsVisited(node)) { - // going up from this node - setNodeIsVisited(node.left, false); - setNodeIsVisited(node.right, false); - node = node.parent; - continue; - } - - if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { - // go left - node = node.left; - continue; - } - - if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { - // go right - node = node.right; - continue; - } - - // handle current node - recomputeMaxEnd(node); - setNodeIsVisited(node, true); - } - - if (T.root) { - setNodeIsVisited(T.root, false); - } -} - function computeMaxEnd(node: IntervalNode): number { let maxEnd = node.end; if (node.left !== SENTINEL) { @@ -1184,7 +1186,7 @@ function computeMaxEnd(node: IntervalNode): number { return maxEnd; } -function recomputeMaxEnd(node: IntervalNode): void { +export function recomputeMaxEnd(node: IntervalNode): void { node.maxEnd = computeMaxEnd(node); } diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index e04cdc485fa31..0418026b5060c 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -15,7 +15,7 @@ import { LanguageIdentifier } from 'vs/editor/common/modes'; import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; -import { IntervalNode, IntervalTree } from 'vs/editor/common/model/intervalTree'; +import { IntervalNode, IntervalTree, recomputeMaxEnd } from 'vs/editor/common/model/intervalTree'; let _INSTANCE_COUNT = 0; /** @@ -114,7 +114,7 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi // Transform back `range` to offsets const versionId = this.getVersionId(); - const allDecorations = this._decorationsTree.search(0, false, false, versionId); + const allDecorations = this._decorationsTree.collectNodesPostOrder(); for (let i = 0, len = allDecorations.length; i < len; i++) { const node = allDecorations[i]; @@ -129,9 +129,9 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi node.start = startOffset - delta; node.end = endOffset - delta; - } - this._decorationsTree.recomputeAllMaxEnds(); + recomputeMaxEnd(node); + } } public changeDecorations(callback: (changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => T, ownerId: number = 0): T { diff --git a/src/vs/editor/contrib/linesOperations/test/common/sortLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/common/sortLinesCommand.test.ts index ec19e7bbe0dc5..58aa224ff6cd9 100644 --- a/src/vs/editor/contrib/linesOperations/test/common/sortLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/common/sortLinesCommand.test.ts @@ -77,7 +77,7 @@ suite('Editor Contrib - Sort Lines Command', () => { 'third line', 'fifth' ], - new Selection(3, 3, 4, 2) + new Selection(3, 3, 4, 1) ); }); @@ -119,7 +119,7 @@ suite('Editor Contrib - Sort Lines Command', () => { 'second line', 'third line', ], - new Selection(1, 1, 5, 6) + new Selection(1, 1, 5, 11) ); }); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 0a7ff4e3d8ffa..b4485b014de6e 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -377,6 +377,14 @@ suite('Editor Model - Model Decorations', () => { { range: new Range(1, 12, 4, 1), className: 'myType9' }, ]); }); + + test('an apparently simple edit', () => { + addDecoration(thisModel, 1, 2, 4, 1, 'myType1'); + thisModel.applyEdits([EditOperation.replace(new Range(1, 14, 2, 1), 'x')]); + modelHasDecorations(thisModel, [ + { range: new Range(1, 2, 3, 1), className: 'myType1' }, + ]); + }); }); suite('Decorations and editing', () => { From 16294b41874c5cc12796d033c7824e9c657c0179 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 19 Oct 2017 11:23:20 +0200 Subject: [PATCH 42/64] Do not emit events when deltaDecorations is a no-op --- .../common/model/textModelWithDecorations.ts | 8 ++++++++ .../test/common/model/modelDecorations.test.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 0418026b5060c..45fa84e9003c5 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -166,6 +166,10 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi this._deltaDecorationsImpl(ownerId, [id], []); }, deltaDecorations: (oldDecorations: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] => { + if (oldDecorations.length === 0 && newDecorations.length === 0) { + // nothing to do + return []; + } this._currentDecorationsTrackerDidChange = true; return this._deltaDecorationsImpl(ownerId, oldDecorations, newDecorations); } @@ -189,6 +193,10 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi if (!oldDecorations) { oldDecorations = []; } + if (oldDecorations.length === 0 && newDecorations.length === 0) { + // nothing to do + return []; + } try { this._eventEmitter.beginDeferredEmit(); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index b4485b014de6e..23c02e75b0973 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -249,6 +249,21 @@ suite('Editor Model - Model Decorations', () => { assert.equal(listenerCalled, 1, 'listener called'); }); + test('decorations do not emit event on no-op deltaDecorations', () => { + let listenerCalled = 0; + + thisModel.onDidChangeDecorations((e) => { + listenerCalled++; + }); + + thisModel.deltaDecorations([], []); + thisModel.changeDecorations((accessor) => { + accessor.deltaDecorations([], []); + }); + + assert.equal(listenerCalled, 0, 'listener not called'); + }); + // --------- editing text & effects on decorations test('decorations are updated when inserting one line text before it', () => { From 568f81043611a0de3cb5c704a5b3c5e333d161bd Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 19 Oct 2017 11:40:43 +0200 Subject: [PATCH 43/64] Fix NPE in decorations changed listener --- .../contrib/referenceSearch/browser/referencesWidget.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts index 42aaeb2fce05b..ce7638670eb62 100644 --- a/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/browser/referencesWidget.ts @@ -112,6 +112,10 @@ class DecorationsManager implements IDisposable { this._decorations.forEach((reference, decorationId) => { const newRange = this._editor.getModel().getDecorationRange(decorationId); + if (!newRange) { + return; + } + let ignore = false; if (Range.equalsRange(newRange, reference.range)) { From ea948fba1ccb925cede2ff2998a789ecbfc5d6e9 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 19 Oct 2017 11:45:20 +0200 Subject: [PATCH 44/64] Avoid NPE in removeAllDecorationsWithOwnerId --- src/vs/editor/common/model/textModelWithDecorations.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 45fa84e9003c5..f9ca8d6347559 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -244,6 +244,9 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi } public removeAllDecorationsWithOwnerId(ownerId: number): void { + if (this.isDisposed) { + return; + } const nodes = this._decorationsTree.collectNodesFromOwner(ownerId); for (let i = 0, len = nodes.length; i < len; i++) { const node = nodes[i]; From 4b53050d7522ba05c6c395f4861f97eb8152a903 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 19 Oct 2017 12:08:25 +0200 Subject: [PATCH 45/64] Fix typo --- .../editor/common/model/textModelWithDecorations.ts | 2 +- .../test/common/model/modelDecorations.test.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index f9ca8d6347559..a024b227d6d7b 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -244,7 +244,7 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi } public removeAllDecorationsWithOwnerId(ownerId: number): void { - if (this.isDisposed) { + if (this._isDisposed) { return; } const nodes = this._decorationsTree.collectNodesFromOwner(ownerId); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 23c02e75b0973..ea402ff32c42f 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -400,6 +400,18 @@ suite('Editor Model - Model Decorations', () => { { range: new Range(1, 2, 3, 1), className: 'myType1' }, ]); }); + + test('removeAllDecorationsWithOwnerId can be called after model dispose', () => { + let model = Model.createFromString('asd'); + model.dispose(); + model.removeAllDecorationsWithOwnerId(1); + }); + + test('removeAllDecorationsWithOwnerId works', () => { + thisModel.deltaDecorations([], [{ range: new Range(1, 2, 4, 1), options: { className: 'myType1' } }], 1); + thisModel.removeAllDecorationsWithOwnerId(1); + modelHasNoDecorations(thisModel); + }); }); suite('Decorations and editing', () => { From 76f2d8f815ad41b2939b534d76e803650e90f912 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 19 Oct 2017 12:10:22 +0200 Subject: [PATCH 46/64] Track if decoration is in the overview ruler in IntervalNode.metadata --- src/vs/editor/common/model/intervalTree.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index d45aeef43b6c7..83d15c75a34b1 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -47,9 +47,13 @@ const enum Constants { IsForValidationMaskInverse = 0b11111011, IsForValidationOffset = 2, - StickinessMask = 0b00011000, - StickinessMaskInverse = 0b11100111, - StickinessOffset = 3 + IsInOverviewRulerMask = 0b00001000, + IsInOverviewRulerMaskInverse = 0b11110111, + IsInOverviewRulerOffset = 3, + + StickinessMask = 0b00110000, + StickinessMaskInverse = 0b11001111, + StickinessOffset = 4 } function getNodeColor(node: IntervalNode): NodeColor { @@ -76,6 +80,14 @@ function setNodeIsForValidation(node: IntervalNode, value: boolean): void { (node.metadata & Constants.IsForValidationMaskInverse) | ((value ? 1 : 0) << Constants.IsForValidationOffset) ); } +function getNodeIsInOverviewRuler(node: IntervalNode): boolean { + return ((node.metadata & Constants.IsInOverviewRulerMask) >>> Constants.IsInOverviewRulerOffset) === 1; +} +function setNodeIsInOverviewRuler(node: IntervalNode, value: boolean): void { + node.metadata = ( + (node.metadata & Constants.IsInOverviewRulerMaskInverse) | ((value ? 1 : 0) << Constants.IsInOverviewRulerOffset) + ); +} function getNodeStickiness(node: IntervalNode): TrackedRangeStickiness { return ((node.metadata & Constants.StickinessMask) >>> Constants.StickinessOffset); } @@ -128,6 +140,7 @@ export class IntervalNode implements IModelDecoration { this.options = null; setNodeIsForValidation(this, false); setNodeStickiness(this, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + setNodeIsInOverviewRuler(this, false); this.cachedVersionId = 0; this.cachedAbsoluteStart = start; @@ -154,6 +167,7 @@ export class IntervalNode implements IModelDecoration { || this.options.className === ClassName.EditorWarningDecoration )); setNodeStickiness(this, this.options.stickiness); + setNodeIsInOverviewRuler(this, this.options.overviewRuler.color ? true : false); } public setCachedOffsets(absoluteStart: number, absoluteEnd: number, cachedVersionId: number): void { @@ -703,7 +717,7 @@ function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boo if (filterOutValidation && getNodeIsForValidation(node)) { include = false; } - if (overviewRulerOnly && !node.options.overviewRuler.color) { + if (overviewRulerOnly && !getNodeIsInOverviewRuler(node)) { include = false; } if (include) { From 0d1c1e6296a13629f02bca87c87824efc9f56a68 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 19 Oct 2017 15:14:56 +0200 Subject: [PATCH 47/64] Store decorations in two trees, one with all decorations that appear in the overview ruler, and one with decorations that do not appear in the overview ruler --- src/vs/editor/common/model/intervalTree.ts | 24 ++--- .../common/model/textModelWithDecorations.ts | 101 +++++++++++++++++- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 83d15c75a34b1..fd975e97e419d 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -80,7 +80,7 @@ function setNodeIsForValidation(node: IntervalNode, value: boolean): void { (node.metadata & Constants.IsForValidationMaskInverse) | ((value ? 1 : 0) << Constants.IsForValidationOffset) ); } -function getNodeIsInOverviewRuler(node: IntervalNode): boolean { +export function getNodeIsInOverviewRuler(node: IntervalNode): boolean { return ((node.metadata & Constants.IsInOverviewRulerMask) >>> Constants.IsInOverviewRulerOffset) === 1; } function setNodeIsInOverviewRuler(node: IntervalNode, value: boolean): void { @@ -207,11 +207,11 @@ export class IntervalTree { return intervalSearch(this, start, end, filterOwnerId, filterOutValidation, cachedVersionId); } - public search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] { + public search(filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { if (this.root === SENTINEL) { return []; } - return search(this, filterOwnerId, filterOutValidation, overviewRulerOnly, cachedVersionId); + return search(this, filterOwnerId, filterOutValidation, cachedVersionId); } public count(): number { @@ -228,6 +228,13 @@ export class IntervalTree { return collectNodesFromOwner(this, ownerId); } + /** + * Will not set `cachedAbsoluteStart` nor `cachedAbsoluteEnd` on the returned nodes! + */ + public collectNodesPostOrder(): IntervalNode[] { + return collectNodesPostOrder(this); + } + public insert(node: IntervalNode): void { rbTreeInsert(this, node); } @@ -277,10 +284,6 @@ export class IntervalTree { } } - public collectNodesPostOrder(): IntervalNode[] { - return collectNodesPostOrder(this); - } - public assertInvariants(): void { assert(getNodeColor(SENTINEL) === NodeColor.Black); assert(SENTINEL.parent === SENTINEL); @@ -294,7 +297,7 @@ export class IntervalTree { } public getAllInOrder(): IntervalNode[] { - return search(this, 0, false, false, 0); + return search(this, 0, false, 0); } public print(): void { @@ -679,7 +682,7 @@ function collectNodesPostOrder(T: IntervalTree): IntervalNode[] { return result; } -function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] { +function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { let node = T.root; let delta = 0; let nodeStart = 0; @@ -717,9 +720,6 @@ function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boo if (filterOutValidation && getNodeIsForValidation(node)) { include = false; } - if (overviewRulerOnly && !getNodeIsInOverviewRuler(node)) { - include = false; - } if (include) { result[resultLen++] = node; } diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index a024b227d6d7b..ff79fbfccce32 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -15,7 +15,7 @@ import { LanguageIdentifier } from 'vs/editor/common/modes'; import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; -import { IntervalNode, IntervalTree, recomputeMaxEnd } from 'vs/editor/common/model/intervalTree'; +import { IntervalNode, IntervalTree, recomputeMaxEnd, getNodeIsInOverviewRuler } from 'vs/editor/common/model/intervalTree'; let _INSTANCE_COUNT = 0; /** @@ -45,7 +45,7 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi private _currentDecorationsTrackerCnt: number; private _currentDecorationsTrackerDidChange: boolean; private _decorations: { [decorationId: string]: IntervalNode; }; - private _decorationsTree: IntervalTree; + private _decorationsTree: DecorationsTrees; constructor(rawTextSource: IRawTextSource, creationOptions: editorCommon.ITextModelCreationOptions, languageIdentifier: LanguageIdentifier) { super(rawTextSource, creationOptions, languageIdentifier); @@ -55,7 +55,7 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi this._currentDecorationsTrackerCnt = 0; this._currentDecorationsTrackerDidChange = false; this._decorations = Object.create(null); - this._decorationsTree = new IntervalTree(); + this._decorationsTree = new DecorationsTrees(); } public dispose(): void { @@ -70,7 +70,7 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi // Destroy all my decorations this._decorations = Object.create(null); - this._decorationsTree = new IntervalTree(); + this._decorationsTree = new DecorationsTrees(); } _getTrackedRangesCount(): number { @@ -371,7 +371,17 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi return; } - node.setOptions(options); + const nodeWasInOverviewRuler = (node.options.overviewRuler.color ? true : false); + const nodeIsInOverviewRuler = (options.overviewRuler.color ? true : false); + + if (nodeWasInOverviewRuler !== nodeIsInOverviewRuler) { + // Delete + Insert due to an overview ruler status change + this._decorationsTree.delete(node); + node.setOptions(options); + this._decorationsTree.insert(node); + } else { + node.setOptions(options); + } } private _deltaDecorationsImpl(ownerId: number, oldDecorationsIds: string[], newDecorations: editorCommon.IModelDeltaDecoration[]): string[] { @@ -436,6 +446,87 @@ export class TextModelWithDecorations extends TextModelWithTokens implements edi } } +class DecorationsTrees { + + /** + * This tree holds decorations that do not show up in the overview ruler. + */ + private _decorationsTree0: IntervalTree; + + /** + * This tree holds decorations that show up in the overview ruler. + */ + private _decorationsTree1: IntervalTree; + + constructor() { + this._decorationsTree0 = new IntervalTree(); + this._decorationsTree1 = new IntervalTree(); + } + + public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { + const r0 = this._decorationsTree0.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId); + const r1 = this._decorationsTree1.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId); + return r0.concat(r1); + } + + public search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] { + if (overviewRulerOnly) { + return this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId); + } else { + const r0 = this._decorationsTree0.search(filterOwnerId, filterOutValidation, cachedVersionId); + const r1 = this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId); + return r0.concat(r1); + } + } + + public count(): number { + const c0 = this._decorationsTree0.count(); + const c1 = this._decorationsTree1.count(); + return c0 + c1; + } + + public collectNodesFromOwner(ownerId: number): IntervalNode[] { + const r0 = this._decorationsTree0.collectNodesFromOwner(ownerId); + const r1 = this._decorationsTree1.collectNodesFromOwner(ownerId); + return r0.concat(r1); + } + + public collectNodesPostOrder(): IntervalNode[] { + const r0 = this._decorationsTree0.collectNodesPostOrder(); + const r1 = this._decorationsTree1.collectNodesPostOrder(); + return r0.concat(r1); + } + + public insert(node: IntervalNode): void { + if (getNodeIsInOverviewRuler(node)) { + this._decorationsTree1.insert(node); + } else { + this._decorationsTree0.insert(node); + } + } + + public delete(node: IntervalNode): void { + if (getNodeIsInOverviewRuler(node)) { + this._decorationsTree1.delete(node); + } else { + this._decorationsTree0.delete(node); + } + } + + public resolveNode(node: IntervalNode, cachedVersionId: number): void { + if (getNodeIsInOverviewRuler(node)) { + this._decorationsTree1.resolveNode(node, cachedVersionId); + } else { + this._decorationsTree0.resolveNode(node, cachedVersionId); + } + } + + public acceptReplace(offset: number, length: number, textLength: number, forceMoveMarkers: boolean): void { + this._decorationsTree0.acceptReplace(offset, length, textLength, forceMoveMarkers); + this._decorationsTree1.acceptReplace(offset, length, textLength, forceMoveMarkers); + } +} + function cleanClassName(className: string): string { return className.replace(/[^a-z0-9\-]/gi, ' '); } From caa5593e4afb2a30ce2c17dc923c231000b26574 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 19 Oct 2017 15:35:20 +0200 Subject: [PATCH 48/64] Reimplement the decorations overview ruler --- src/vs/editor/browser/view/viewImpl.ts | 4 +- .../overviewRuler/decorationsOverviewRuler.ts | 267 ---------- .../decorationsOverviewRuler2.ts | 488 ++++++++++++++++++ 3 files changed, 490 insertions(+), 269 deletions(-) delete mode 100644 src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts create mode 100644 src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index 1a53e3d98148e..8e14fe9b1ff70 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -30,7 +30,7 @@ import { Margin } from 'vs/editor/browser/viewParts/margin/margin'; import { LinesDecorationsOverlay } from 'vs/editor/browser/viewParts/linesDecorations/linesDecorations'; import { MarginViewLineDecorationsOverlay } from 'vs/editor/browser/viewParts/marginDecorations/marginDecorations'; import { ViewOverlayWidgets } from 'vs/editor/browser/viewParts/overlayWidgets/overlayWidgets'; -import { DecorationsOverviewRuler } from 'vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler'; +import { DecorationsOverviewRuler2 } from 'vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2'; import { OverviewRuler } from 'vs/editor/browser/viewParts/overviewRuler/overviewRuler'; import { Rulers } from 'vs/editor/browser/viewParts/rulers/rulers'; import { ScrollDecorationViewPart } from 'vs/editor/browser/viewParts/scrollDecoration/scrollDecoration'; @@ -170,7 +170,7 @@ export class View extends ViewEventHandler { this.viewParts.push(this.viewZones); // Decorations overview ruler - let decorationsOverviewRuler = new DecorationsOverviewRuler(this._context); + let decorationsOverviewRuler = new DecorationsOverviewRuler2(this._context); this.viewParts.push(decorationsOverviewRuler); diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts deleted file mode 100644 index 91ebeaf394502..0000000000000 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts +++ /dev/null @@ -1,267 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as editorCommon from 'vs/editor/common/editorCommon'; -import { ViewPart } from 'vs/editor/browser/view/viewPart'; -import { OverviewRulerImpl } from 'vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl'; -import { ViewContext } from 'vs/editor/common/view/viewContext'; -import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; -import { Position } from 'vs/editor/common/core/position'; -import { TokenizationRegistry } from 'vs/editor/common/modes'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; -import { editorOverviewRulerBorder, editorCursorForeground } from 'vs/editor/common/view/editorColorRegistry'; -import { Color } from 'vs/base/common/color'; -import { ThemeColor } from 'vs/platform/theme/common/themeService'; - -export class DecorationsOverviewRuler extends ViewPart { - - static MIN_DECORATION_HEIGHT = 6; - static MAX_DECORATION_HEIGHT = 60; - - private readonly _tokensColorTrackerListener: IDisposable; - - private _overviewRuler: OverviewRulerImpl; - - private _renderBorder: boolean; - private _borderColor: string; - private _cursorColor: string; - - private _shouldUpdateDecorations: boolean; - private _shouldUpdateCursorPosition: boolean; - - private _hideCursor: boolean; - private _cursorPositions: Position[]; - - private _zonesFromDecorations: OverviewRulerZone[]; - private _zonesFromCursors: OverviewRulerZone[]; - - constructor(context: ViewContext) { - super(context); - this._overviewRuler = new OverviewRulerImpl( - 1, - 'decorationsOverviewRuler', - this._context.viewLayout.getScrollHeight(), - this._context.configuration.editor.lineHeight, - this._context.configuration.editor.pixelRatio, - DecorationsOverviewRuler.MIN_DECORATION_HEIGHT, - DecorationsOverviewRuler.MAX_DECORATION_HEIGHT, - (lineNumber: number) => this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber) - ); - this._overviewRuler.setLanesCount(this._context.configuration.editor.viewInfo.overviewRulerLanes, false); - this._overviewRuler.setLayout(this._context.configuration.editor.layoutInfo.overviewRuler, false); - - this._renderBorder = this._context.configuration.editor.viewInfo.overviewRulerBorder; - - this._updateColors(); - - this._updateBackground(false); - this._tokensColorTrackerListener = TokenizationRegistry.onDidChange((e) => { - if (e.changedColorMap) { - this._updateBackground(true); - } - }); - - this._shouldUpdateDecorations = true; - this._zonesFromDecorations = []; - - this._shouldUpdateCursorPosition = true; - this._hideCursor = this._context.configuration.editor.viewInfo.hideCursorInOverviewRuler; - - this._zonesFromCursors = []; - this._cursorPositions = []; - } - - public dispose(): void { - super.dispose(); - this._overviewRuler.dispose(); - this._tokensColorTrackerListener.dispose(); - } - - private _updateBackground(render: boolean): void { - const minimapEnabled = this._context.configuration.editor.viewInfo.minimap.enabled; - this._overviewRuler.setUseBackground((minimapEnabled ? TokenizationRegistry.getDefaultBackground() : null), render); - } - - // ---- begin view event handlers - - public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { - let prevLanesCount = this._overviewRuler.getLanesCount(); - let newLanesCount = this._context.configuration.editor.viewInfo.overviewRulerLanes; - - if (prevLanesCount !== newLanesCount) { - this._overviewRuler.setLanesCount(newLanesCount, false); - } - - if (e.lineHeight) { - this._overviewRuler.setLineHeight(this._context.configuration.editor.lineHeight, false); - } - - if (e.pixelRatio) { - this._overviewRuler.setPixelRatio(this._context.configuration.editor.pixelRatio, false); - } - - if (e.viewInfo) { - this._renderBorder = this._context.configuration.editor.viewInfo.overviewRulerBorder; - this._hideCursor = this._context.configuration.editor.viewInfo.hideCursorInOverviewRuler; - this._shouldUpdateCursorPosition = true; - this._updateBackground(false); - } - - if (e.layoutInfo) { - this._overviewRuler.setLayout(this._context.configuration.editor.layoutInfo.overviewRuler, false); - } - - return true; - } - - public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { - this._shouldUpdateCursorPosition = true; - this._cursorPositions = []; - for (let i = 0, len = e.selections.length; i < len; i++) { - this._cursorPositions[i] = e.selections[i].getPosition(); - } - return true; - } - - public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { - this._shouldUpdateDecorations = true; - return true; - } - - public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { - this._shouldUpdateCursorPosition = true; - this._shouldUpdateDecorations = true; - return true; - } - - public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { - this._overviewRuler.setScrollHeight(e.scrollHeight, false); - return super.onScrollChanged(e) || e.scrollHeightChanged; - } - - public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { - return true; - } - - public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { - this._updateColors(); - this._shouldUpdateDecorations = true; - this._shouldUpdateCursorPosition = true; - return true; - } - - // ---- end view event handlers - - public getDomNode(): HTMLElement { - return this._overviewRuler.getDomNode(); - } - - private _updateColors() { - let borderColor = this._context.theme.getColor(editorOverviewRulerBorder); - this._borderColor = borderColor ? borderColor.toString() : null; - - let cursorColor = this._context.theme.getColor(editorCursorForeground); - this._cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null; - - this._overviewRuler.setThemeType(this._context.theme.type, false); - } - - private _createZonesFromDecorations(): OverviewRulerZone[] { - let decorations = this._context.model.getAllOverviewRulerDecorations(); - let zones: OverviewRulerZone[] = []; - - for (let i = 0, len = decorations.length; i < len; i++) { - let dec = decorations[i]; - let overviewRuler = dec.source.options.overviewRuler; - zones[i] = new OverviewRulerZone( - dec.range.startLineNumber, - dec.range.endLineNumber, - overviewRuler.position, - 0, - this.resolveRulerColor(overviewRuler.color), - this.resolveRulerColor(overviewRuler.darkColor), - this.resolveRulerColor(overviewRuler.hcColor) - ); - } - - return zones; - } - - private resolveRulerColor(color: string | ThemeColor): string { - if (editorCommon.isThemeColor(color)) { - let c = this._context.theme.getColor(color.id) || Color.transparent; - return c.toString(); - } - return color; - } - - private _createZonesFromCursors(): OverviewRulerZone[] { - let zones: OverviewRulerZone[] = []; - - for (let i = 0, len = this._cursorPositions.length; i < len; i++) { - let cursor = this._cursorPositions[i]; - - zones[i] = new OverviewRulerZone( - cursor.lineNumber, - cursor.lineNumber, - editorCommon.OverviewRulerLane.Full, - 2, - this._cursorColor, - this._cursorColor, - this._cursorColor - ); - } - - return zones; - } - - public prepareRender(ctx: RenderingContext): void { - // Nothing to read - } - - public render(ctx: RestrictedRenderingContext): void { - if (this._shouldUpdateDecorations || this._shouldUpdateCursorPosition) { - - if (this._shouldUpdateDecorations) { - this._shouldUpdateDecorations = false; - this._zonesFromDecorations = this._createZonesFromDecorations(); - } - - if (this._shouldUpdateCursorPosition) { - this._shouldUpdateCursorPosition = false; - if (this._hideCursor) { - this._zonesFromCursors = []; - } else { - this._zonesFromCursors = this._createZonesFromCursors(); - } - } - - let allZones: OverviewRulerZone[] = []; - allZones = allZones.concat(this._zonesFromCursors); - allZones = allZones.concat(this._zonesFromDecorations); - - this._overviewRuler.setZones(allZones, false); - } - - let hasRendered = this._overviewRuler.render(false); - - if (hasRendered && this._renderBorder && this._borderColor && this._overviewRuler.getLanesCount() > 0 && (this._zonesFromDecorations.length > 0 || this._zonesFromCursors.length > 0)) { - let ctx2 = this._overviewRuler.getDomNode().getContext('2d'); - ctx2.beginPath(); - ctx2.lineWidth = 1; - ctx2.strokeStyle = this._borderColor; - ctx2.moveTo(0, 0); - ctx2.lineTo(0, this._overviewRuler.getPixelHeight()); - ctx2.stroke(); - - ctx2.moveTo(0, 0); - ctx2.lineTo(this._overviewRuler.getPixelWidth(), 0); - ctx2.stroke(); - } - } -} diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts new file mode 100644 index 0000000000000..3586b00f79904 --- /dev/null +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts @@ -0,0 +1,488 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { ViewPart } from 'vs/editor/browser/view/viewPart'; +import { ViewContext } from 'vs/editor/common/view/viewContext'; +import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; +import { Position } from 'vs/editor/common/core/position'; +import { TokenizationRegistry } from 'vs/editor/common/modes'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import * as viewEvents from 'vs/editor/common/view/viewEvents'; +import { editorOverviewRulerBorder, editorCursorForeground } from 'vs/editor/common/view/editorColorRegistry'; +import { Color } from 'vs/base/common/color'; +import { ITheme, ThemeColor } from 'vs/platform/theme/common/themeService'; +import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; +import { ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModelWithDecorations'; + +class Settings { + + public readonly lineHeight: number; + public readonly pixelRatio: number; + public readonly overviewRulerLanes: number; + + public readonly renderBorder: boolean; + public readonly borderColor: string; + + public readonly hideCursor: boolean; + public readonly cursorColor: string; + + public readonly themeType: 'light' | 'dark' | 'hc'; + public readonly backgroundColor: string; + + public readonly top: number; + public readonly right: number; + public readonly domWidth: number; + public readonly domHeight: number; + public readonly canvasWidth: number; + public readonly canvasHeight: number; + + public readonly x: number[]; + public readonly w: number[]; + + constructor(config: editorCommon.IConfiguration, theme: ITheme) { + this.lineHeight = config.editor.lineHeight; + this.pixelRatio = config.editor.pixelRatio; + this.overviewRulerLanes = config.editor.viewInfo.overviewRulerLanes; + + this.renderBorder = config.editor.viewInfo.overviewRulerBorder; + const borderColor = theme.getColor(editorOverviewRulerBorder); + this.borderColor = borderColor ? borderColor.toString() : null; + + this.hideCursor = config.editor.viewInfo.hideCursorInOverviewRuler; + const cursorColor = theme.getColor(editorCursorForeground); + this.cursorColor = cursorColor ? cursorColor.transparent(0.7).toString() : null; + + this.themeType = theme.type; + + const minimapEnabled = config.editor.viewInfo.minimap.enabled; + const backgroundColor = (minimapEnabled ? TokenizationRegistry.getDefaultBackground() : null); + this.backgroundColor = (backgroundColor ? Color.Format.CSS.formatHex(backgroundColor) : null); + + const position = config.editor.layoutInfo.overviewRuler; + this.top = position.top; + this.right = position.right; + this.domWidth = position.width; + this.domHeight = position.height; + this.canvasWidth = (this.domWidth * this.pixelRatio) | 0; + this.canvasHeight = (this.domHeight * this.pixelRatio) | 0; + + const [x, w] = this._initLanes(1, this.canvasWidth, this.overviewRulerLanes); + this.x = x; + this.w = w; + } + + private _initLanes(canvasLeftOffset: number, canvasWidth: number, laneCount: number): [number[], number[]] { + const remainingWidth = canvasWidth - canvasLeftOffset; + + if (laneCount >= 3) { + const leftWidth = Math.floor(remainingWidth / 3); + const rightWidth = Math.floor(remainingWidth / 3); + const centerWidth = remainingWidth - leftWidth - rightWidth; + const leftOffset = canvasLeftOffset; + const centerOffset = leftOffset + leftWidth; + const rightOffset = leftOffset + leftWidth + centerWidth; + + return [ + [ + 0, + leftOffset, // Left + centerOffset, // Center + leftOffset, // Left | Center + rightOffset, // Right + leftOffset, // Left | Right + centerOffset, // Center | Right + leftOffset, // Left | Center | Right + ], [ + 0, + leftWidth, // Left + centerWidth, // Center + leftWidth + centerWidth, // Left | Center + rightWidth, // Right + leftWidth + centerWidth + rightWidth, // Left | Right + centerWidth + rightWidth, // Center | Right + leftWidth + centerWidth + rightWidth, // Left | Center | Right + ] + ]; + } else if (laneCount === 2) { + const leftWidth = Math.floor(remainingWidth / 2); + const rightWidth = remainingWidth - leftWidth; + const leftOffset = canvasLeftOffset; + const rightOffset = leftOffset + leftWidth; + + return [ + [ + 0, + leftOffset, // Left + leftOffset, // Center + leftOffset, // Left | Center + rightOffset, // Right + leftOffset, // Left | Right + leftOffset, // Center | Right + leftOffset, // Left | Center | Right + ], [ + 0, + leftWidth, // Left + leftWidth, // Center + leftWidth, // Left | Center + rightWidth, // Right + leftWidth + rightWidth, // Left | Right + leftWidth + rightWidth, // Center | Right + leftWidth + rightWidth, // Left | Center | Right + ] + ]; + } else { + const offset = canvasLeftOffset; + const width = remainingWidth; + + return [ + [ + 0, + offset, // Left + offset, // Center + offset, // Left | Center + offset, // Right + offset, // Left | Right + offset, // Center | Right + offset, // Left | Center | Right + ], [ + 0, + width, // Left + width, // Center + width, // Left | Center + width, // Right + width, // Left | Right + width, // Center | Right + width, // Left | Center | Right + ] + ]; + } + } + + public equals(other: Settings): boolean { + return ( + this.lineHeight === other.lineHeight + && this.pixelRatio === other.pixelRatio + && this.overviewRulerLanes === other.overviewRulerLanes + && this.renderBorder === other.renderBorder + && this.borderColor === other.borderColor + && this.hideCursor === other.hideCursor + && this.cursorColor === other.cursorColor + && this.themeType === other.themeType + && this.backgroundColor === other.backgroundColor + && this.top === other.top + && this.right === other.right + && this.domWidth === other.domWidth + && this.domHeight === other.domHeight + && this.canvasWidth === other.canvasWidth + && this.canvasHeight === other.canvasHeight + ); + } +} + +const enum Constants { + MIN_DECORATION_HEIGHT = 6 +} + +const enum OverviewRulerLane { + Left = 1, + Center = 2, + Right = 4, + Full = 7 +} + +export class DecorationsOverviewRuler2 extends ViewPart { + + private readonly _tokensColorTrackerListener: IDisposable; + private readonly _domNode: FastDomNode; + private _settings: Settings; + private _cursorPositions: Position[]; + + constructor(context: ViewContext) { + super(context); + + this._domNode = createFastDomNode(document.createElement('canvas')); + this._domNode.setClassName('decorationsOverviewRuler'); + this._domNode.setPosition('absolute'); + this._domNode.setLayerHinting(true); + + this._settings = null; + this._updateSettings(false); + + this._tokensColorTrackerListener = TokenizationRegistry.onDidChange((e) => { + if (e.changedColorMap) { + this._updateSettings(true); + } + }); + + this._cursorPositions = []; + } + + public dispose(): void { + super.dispose(); + this._tokensColorTrackerListener.dispose(); + } + + private _updateSettings(renderNow: boolean): boolean { + const newSettings = new Settings(this._context.configuration, this._context.theme); + if (this._settings !== null && this._settings.equals(newSettings)) { + // nothing to do + return false; + } + + this._settings = newSettings; + + this._domNode.setTop(this._settings.top); + this._domNode.setRight(this._settings.right); + this._domNode.setWidth(this._settings.domWidth); + this._domNode.setHeight(this._settings.domHeight); + this._domNode.domNode.width = this._settings.canvasWidth; + this._domNode.domNode.height = this._settings.canvasHeight; + + if (renderNow) { + this._render(); + } + + return true; + } + + // ---- begin view event handlers + + public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + return this._updateSettings(false); + } + public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { + this._cursorPositions = []; + for (let i = 0, len = e.selections.length; i < len; i++) { + this._cursorPositions[i] = e.selections[i].getPosition(); + } + return true; + } + public onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { + return true; + } + public onFlushed(e: viewEvents.ViewFlushedEvent): boolean { + return true; + } + public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { + return e.scrollHeightChanged; + } + public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { + return true; + } + public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { + // invalidate color cache + const decorations = this._context.model.getAllOverviewRulerDecorations(); + for (let i = 0, len = decorations.length; i < len; i++) { + const decoration = decorations[i]; + const opts = decoration.source.options.overviewRuler; + opts._resolvedColor = null; + } + return this._updateSettings(false); + } + + // ---- end view event handlers + + public getDomNode(): HTMLElement { + return this._domNode.domNode; + } + + public prepareRender(ctx: RenderingContext): void { + // Nothing to read + } + + public render(editorCtx: RestrictedRenderingContext): void { + this._render(); + } + + private _render(): void { + const canvasWidth = this._settings.canvasWidth; + const canvasHeight = this._settings.canvasHeight; + const lineHeight = this._settings.lineHeight; + const viewLayout = this._context.viewLayout; + const theme = this._context.theme; + const outerHeight = this._context.viewLayout.getScrollHeight(); + const heightRatio = canvasHeight / outerHeight; + const decorations = this._context.model.getAllOverviewRulerDecorations(); + + const minDecorationHeight = (Constants.MIN_DECORATION_HEIGHT * this._settings.pixelRatio) | 0; + const halfMinDecorationHeight = (minDecorationHeight / 2) | 0; + + const canvasCtx = this._domNode.domNode.getContext('2d'); + if (this._settings.backgroundColor === null) { + canvasCtx.clearRect(0, 0, canvasWidth, canvasHeight); + } else { + canvasCtx.fillStyle = this._settings.backgroundColor; + canvasCtx.fillRect(0, 0, canvasWidth, canvasHeight); + } + + const paintQueue = new PaintQueue(canvasCtx, this._settings.x, this._settings.w); + for (let i = 0, len = decorations.length; i < len; i++) { + const decoration = decorations[i]; + const opts = decoration.source.options.overviewRuler; + const lane = opts.position; + if (lane === 0) { + continue; + } + const startLineNumber = decoration.range.startLineNumber; + const endLineNumber = decoration.range.endLineNumber; + const color = resolveColor(opts, theme); + + let y1 = (viewLayout.getVerticalOffsetForLineNumber(startLineNumber) * heightRatio) | 0; + let y2 = ((viewLayout.getVerticalOffsetForLineNumber(endLineNumber) + lineHeight) * heightRatio) | 0; + let height = y2 - y1; + if (height < minDecorationHeight) { + let yCenter = ((y1 + y2) / 2) | 0; + if (yCenter < halfMinDecorationHeight) { + yCenter = halfMinDecorationHeight; + } else if (yCenter + halfMinDecorationHeight > canvasHeight) { + yCenter = canvasHeight - halfMinDecorationHeight; + } + y1 = yCenter - halfMinDecorationHeight; + y2 = yCenter + halfMinDecorationHeight; + } + + paintQueueAccept(paintQueue, color, y1, y2, lane); + } + for (let i = 0, len = paintQueue.req.length; i < len; i++) { + paintQueueFlush(paintQueue, paintQueue.req[i]); + } + + // Draw cursors + if (!this._settings.hideCursor) { + const cursorHeight = (2 * this._settings.pixelRatio) | 0; + const halfCursorHeight = (cursorHeight / 2) | 0; + const x = this._settings.x[OverviewRulerLane.Full]; + const w = this._settings.w[OverviewRulerLane.Full]; + canvasCtx.fillStyle = this._settings.cursorColor; + for (let i = 0, len = this._cursorPositions.length; i < len; i++) { + const cursor = this._cursorPositions[i]; + + let yCenter = (viewLayout.getVerticalOffsetForLineNumber(cursor.lineNumber) * heightRatio) | 0; + if (yCenter < halfCursorHeight) { + yCenter = halfCursorHeight; + } else if (yCenter + halfCursorHeight > canvasHeight) { + yCenter = canvasHeight - halfCursorHeight; + } + const y1 = yCenter - halfCursorHeight; + + canvasCtx.fillRect(x, y1, w, cursorHeight); + } + } + + if (this._settings.renderBorder && this._settings.borderColor && this._settings.overviewRulerLanes > 0) { + canvasCtx.beginPath(); + canvasCtx.lineWidth = 1; + canvasCtx.strokeStyle = this._settings.borderColor; + canvasCtx.moveTo(0, 0); + canvasCtx.lineTo(0, canvasHeight); + canvasCtx.stroke(); + + canvasCtx.moveTo(0, 0); + canvasCtx.lineTo(canvasWidth, 0); + canvasCtx.stroke(); + } + } +} + +class PaintRequest { + public color: string; + public y1: number; + public y2: number; + public lane: number; + + public init(color: string, y1: number, y2: number, lane: number): void { + this.color = color; + this.y1 = y1; + this.y2 = y2; + this.lane = lane; + } +} + +class PaintQueue { + readonly ctx: CanvasRenderingContext2D; + + readonly x: number[]; + readonly w: number[]; + + readonly req: PaintRequest[] = []; + readonly pool: PaintRequest[] = []; + + constructor(ctx: CanvasRenderingContext2D, x: number[], w: number[]) { + this.ctx = ctx; + this.x = x; + this.w = w; + } +} + +function paintQueueAccept(Q: PaintQueue, color: string, y1: number, y2: number, lane: number): void { + let result: PaintRequest = null; + for (let i = 0, len = Q.req.length; i < len; i++) { + const req = Q.req[i]; + if (req.y2 < y1) { + paintQueueFlush(Q, req); + + if (i + 1 === len) { + // last element + Q.req.pop(); + Q.pool.push(req); + break; + } else { + Q.req[i] = Q.req.pop(); + len--; + Q.pool.push(req); + i--; + continue; + } + } + + if (req.lane === lane && req.color === color) { + result = req; + break; + } + } + + if (result !== null) { + // there is already an ongoing request for this color & lane + // => simply merge into it + result.y2 = y2; + return; + } + + if (Q.pool.length > 0) { + result = Q.pool.pop(); + } else { + result = new PaintRequest(); + } + + result.init(color, y1, y2, lane); + Q.req.push(result); +} + +function paintQueueFlush(Q: PaintQueue, req: PaintRequest): void { + Q.ctx.fillStyle = req.color; + Q.ctx.fillRect(Q.x[req.lane], req.y1, Q.w[req.lane], req.y2 - req.y1); +} + +function resolveColor(opts: ModelDecorationOverviewRulerOptions, theme: ITheme): string { + if (!opts._resolvedColor) { + const themeType = theme.type; + const color = (themeType === 'dark' ? opts.darkColor : themeType === 'light' ? opts.color : opts.hcColor); + opts._resolvedColor = resolveRulerColor(color, theme); + } + return opts._resolvedColor; +} + +function resolveRulerColor(color: string | ThemeColor, theme: ITheme): string { + if (typeof color === 'string') { + return color; + } + let c = color ? theme.getColor(color.id) : null; + if (!c) { + c = Color.transparent; + } + return c.toString(); +} From 69952f21a9166a746f3a942b48631e9b0219103b Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 19 Oct 2017 18:19:34 +0200 Subject: [PATCH 49/64] Handle integer overflow around delta in the IntervalTree; also, make it highly unlikely that it would ever happen --- src/vs/editor/common/model/intervalTree.ts | 101 +++++++++++++++++- .../test/common/model/intervalTree.test.ts | 89 +++++++++++---- 2 files changed, 166 insertions(+), 24 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index fd975e97e419d..8e5e123164a13 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -53,7 +53,22 @@ const enum Constants { StickinessMask = 0b00110000, StickinessMaskInverse = 0b11001111, - StickinessOffset = 4 + StickinessOffset = 4, + + /** + * Due to how deletion works (in order to avoid always walking the right subtree of the deleted node), + * the deltas for nodes can grow and shrink dramatically. It has been observed, in practice, that unless + * the deltas are corrected, integer overflow will occur. + * + * The integer overflow occurs when 53 bits are used in the numbers, but we will try to avoid it as + * a node's delta gets below a negative 30 bits number. + * + * MIN SMI (SMall Integer) as defined in v8. + * one bit is lost for boxing/unboxing flag. + * one bit is lost for sign flag. + * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values + */ + MIN_SAFE_DELTA = -(1 << 30), } function getNodeColor(node: IntervalNode): NodeColor { @@ -132,7 +147,8 @@ export class IntervalNode implements IModelDecoration { this.start = start; this.end = end; - this.delta = start; + // FORCE_OVERFLOWING_TEST: this.delta = start; + this.delta = 0; this.maxEnd = end; this.id = id; @@ -195,9 +211,11 @@ setNodeColor(SENTINEL, NodeColor.Black); export class IntervalTree { public root: IntervalNode; + public requestNormalizeDelta: boolean; constructor() { this.root = SENTINEL; + this.requestNormalizeDelta = false; } public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] { @@ -237,10 +255,12 @@ export class IntervalTree { public insert(node: IntervalNode): void { rbTreeInsert(this, node); + this._normalizeDeltaIfNecessary(); } public delete(node: IntervalNode): void { rbTreeDelete(this, node); + this._normalizeDeltaIfNecessary(); } public resolveNode(node: IntervalNode, cachedVersionId: number): void { @@ -269,9 +289,11 @@ export class IntervalTree { const node = nodesOfInterest[i]; rbTreeDelete(this, node); } + this._normalizeDeltaIfNecessary(); // (3) edit all tree nodes except the nodes of interest noOverlapReplace(this, offset, offset + length, textLength); + this._normalizeDeltaIfNecessary(); // (4) edit the nodes of interest and insert them back in the tree for (let i = 0, len = nodesOfInterest.length; i < len; i++) { @@ -282,6 +304,7 @@ export class IntervalTree { node.maxEnd = node.end; rbTreeInsert(this, node); } + this._normalizeDeltaIfNecessary(); } public assertInvariants(): void { @@ -323,8 +346,57 @@ export class IntervalTree { out.push(`${indent} NIL\n`); } } + + private _normalizeDeltaIfNecessary(): void { + if (!this.requestNormalizeDelta) { + return; + } + this.requestNormalizeDelta = false; + normalizeDelta(this); + } } +//#region Delta Normalization +function normalizeDelta(T: IntervalTree): void { + let node = T.root; + let delta = 0; + while (node !== SENTINEL) { + + if (node.left !== SENTINEL && !getNodeIsVisited(node.left)) { + // go left + node = node.left; + continue; + } + + if (node.right !== SENTINEL && !getNodeIsVisited(node.right)) { + // go right + delta += node.delta; + node = node.right; + continue; + } + + // handle current node + node.start = delta + node.start; + node.end = delta + node.end; + node.delta = 0; + recomputeMaxEnd(node); + + setNodeIsVisited(node, true); + + // going up from this node + setNodeIsVisited(node.left, false); + setNodeIsVisited(node.right, false); + if (node === node.parent.right) { + delta -= node.parent.delta; + } + node = node.parent; + } + + if (T.root) { + setNodeIsVisited(T.root, false); + } +} +//#endregion //#region Editing @@ -508,6 +580,7 @@ function noOverlapReplace(T: IntervalTree, start: number, end: number, textLengt let delta = 0; let nodeMaxEnd = 0; let nodeStart = 0; + const editDelta = (textLength - (end - start)); while (node !== SENTINEL) { if (getNodeIsVisited(node)) { // going up from this node @@ -541,9 +614,12 @@ function noOverlapReplace(T: IntervalTree, start: number, end: number, textLengt // handle current node nodeStart = delta + node.start; if (nodeStart > end) { - node.start += (textLength - (end - start)); - node.end += (textLength - (end - start)); - node.delta += (textLength - (end - start)); + node.start += editDelta; + node.end += editDelta; + node.delta += editDelta; + if (node.delta < Constants.MIN_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } // cover case a) from above // there is no need to search this node or its right subtree setNodeIsVisited(node, true); @@ -949,6 +1025,9 @@ function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { // x's delta is no longer influenced by z's delta x.delta += z.delta; + if (x.delta < Constants.MIN_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } x.start += z.delta; x.end += z.delta; @@ -966,10 +1045,16 @@ function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { x.start += y.delta; x.end += y.delta; x.delta += y.delta; + if (x.delta < Constants.MIN_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } y.start += z.delta; y.end += z.delta; y.delta = z.delta; + if (y.delta < Constants.MIN_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } } if (y === T.root) { @@ -1130,6 +1215,9 @@ function leftRotate(T: IntervalTree, x: IntervalNode): void { const y = x.right; // set y. y.delta += x.delta; // y's delta is no longer influenced by x's delta + if (y.delta < Constants.MIN_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } y.start += x.delta; y.end += x.delta; @@ -1157,6 +1245,9 @@ function rightRotate(T: IntervalTree, y: IntervalNode): void { const x = y.left; y.delta -= x.delta; + if (y.delta < Constants.MIN_SAFE_DELTA) { + T.requestNormalizeDelta = true; + } y.start -= x.delta; y.end -= x.delta; diff --git a/src/vs/editor/test/common/model/intervalTree.test.ts b/src/vs/editor/test/common/model/intervalTree.test.ts index f5ab68fcb0c5a..29a5978432e78 100644 --- a/src/vs/editor/test/common/model/intervalTree.test.ts +++ b/src/vs/editor/test/common/model/intervalTree.test.ts @@ -14,6 +14,8 @@ const MIN_INTERVAL_START = 1; const MAX_INTERVAL_END = 100; const MIN_INSERTS = 1; const MAX_INSERTS = 30; +const MIN_CHANGE_CNT = 10; +const MAX_CHANGE_CNT = 20; suite('IntervalTree', () => { @@ -94,6 +96,17 @@ suite('IntervalTree', () => { this._treeNodes[op.id] = null; this._oracleNodes[op.id] = null; + } else if (op.type === 'change') { + + this._tree.delete(this._treeNodes[op.id]); + this._treeNodes[op.id].reset(0, op.begin, op.end, null); + this._tree.insert(this._treeNodes[op.id]); + + this._oracle.delete(this._oracleNodes[op.id]); + this._oracleNodes[op.id].start = op.begin; + this._oracleNodes[op.id].end = op.end; + this._oracle.insert(this._oracleNodes[op.id]); + } else { let actualNodes = this._tree.intervalSearch(op.begin, op.end, 0, false, 0); let actual = actualNodes.map(n => new Interval(n.cachedAbsoluteStart, n.cachedAbsoluteEnd)); @@ -139,13 +152,20 @@ suite('IntervalTree', () => { id: number; } + interface IChangeOperation { + type: 'change'; + id: number; + begin: number; + end: number; + } + interface ISearchOperation { type: 'search'; begin: number; end: number; } - type IOperation = IInsertOperation | IDeleteOperation | ISearchOperation; + type IOperation = IInsertOperation | IDeleteOperation | IChangeOperation | ISearchOperation; function testIntervalTree(ops: IOperation[]): void { let state = new TestState(); @@ -176,35 +196,53 @@ suite('IntervalTree', () => { private _state: TestState = new TestState(); private _insertCnt: number; private _deleteCnt: number; + private _changeCnt: number; constructor() { this._insertCnt = getRandomInt(MIN_INSERTS, MAX_INSERTS); + this._changeCnt = getRandomInt(MIN_CHANGE_CNT, MAX_CHANGE_CNT); this._deleteCnt = 0; } + private _doRandomInsert(): void { + let range = getRandomRange(MIN_INTERVAL_START, MAX_INTERVAL_END); + this._run({ + type: 'insert', + begin: range[0], + end: range[1] + }); + } + + private _doRandomDelete(): void { + let idx = getRandomInt(Math.floor(this._deleteCnt / 2), this._deleteCnt - 1); + this._run({ + type: 'delete', + id: this._state.getExistingNodeId(idx) + }); + } + + private _doRandomChange(): void { + let idx = getRandomInt(0, this._deleteCnt - 1); + let range = getRandomRange(MIN_INTERVAL_START, MAX_INTERVAL_END); + this._run({ + type: 'change', + id: this._state.getExistingNodeId(idx), + begin: range[0], + end: range[1] + }); + } + public run() { - while (this._insertCnt > 0 || this._deleteCnt > 0) { - let type: 'insert' | 'delete'; + while (this._insertCnt > 0 || this._deleteCnt > 0 || this._changeCnt > 0) { if (this._insertCnt > 0) { - type = 'insert'; - } else { - type = 'delete'; - } - if (type === 'insert') { - let range = getRandomRange(MIN_INTERVAL_START, MAX_INTERVAL_END); - this._run({ - type: 'insert', - begin: range[0], - end: range[1] - }); + this._doRandomInsert(); this._insertCnt--; this._deleteCnt++; + } else if (this._changeCnt > 0) { + this._doRandomChange(); + this._changeCnt--; } else { - let idx = getRandomInt(0, this._deleteCnt - 1); - this._run({ - type: 'delete', - id: this._state.getExistingNodeId(idx) - }); + this._doRandomDelete(); this._deleteCnt--; } @@ -401,6 +439,19 @@ suite('IntervalTree', () => { { type: 'search', begin: 65, end: 75 } ]); }); + + test('force delta overflow', () => { + // Search the IntervalNode ctor for FORCE_OVERFLOWING_TEST + // to force that this test leads to a delta normalization + testIntervalTree([ + { type: 'insert', begin: 686081138593427, end: 733009856502260 }, + { type: 'insert', begin: 591031326181669, end: 591031326181672 }, + { type: 'insert', begin: 940037682731896, end: 940037682731903 }, + { type: 'insert', begin: 598413641151120, end: 598413641151128 }, + { type: 'insert', begin: 800564156553344, end: 800564156553351 }, + { type: 'insert', begin: 894198957565481, end: 894198957565491 } + ]); + }); }); // TEST_COUNT = 0; From 30e2d204a0def86f644dc59401b8cefed30c0fbb Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 08:43:45 +0200 Subject: [PATCH 50/64] Move getAllOverviewRulerDecorations to IViewModelLinesCollection --- .../decorationsOverviewRuler2.ts | 50 ++---- .../common/viewModel/splitLinesCollection.ts | 156 +++++++++++++++++- src/vs/editor/common/viewModel/viewModel.ts | 17 +- .../common/viewModel/viewModelDecorations.ts | 11 -- .../editor/common/viewModel/viewModelImpl.ts | 17 +- 5 files changed, 193 insertions(+), 58 deletions(-) diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts index 3586b00f79904..98c23749c304c 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts @@ -14,9 +14,8 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { editorOverviewRulerBorder, editorCursorForeground } from 'vs/editor/common/view/editorColorRegistry'; import { Color } from 'vs/base/common/color'; -import { ITheme, ThemeColor } from 'vs/platform/theme/common/themeService'; +import { ITheme } from 'vs/platform/theme/common/themeService'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModelWithDecorations'; class Settings { @@ -275,12 +274,7 @@ export class DecorationsOverviewRuler2 extends ViewPart { } public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { // invalidate color cache - const decorations = this._context.model.getAllOverviewRulerDecorations(); - for (let i = 0, len = decorations.length; i < len; i++) { - const decoration = decorations[i]; - const opts = decoration.source.options.overviewRuler; - opts._resolvedColor = null; - } + this._context.model.invalidateOverviewRulerColorCache(); return this._updateSettings(false); } @@ -303,10 +297,9 @@ export class DecorationsOverviewRuler2 extends ViewPart { const canvasHeight = this._settings.canvasHeight; const lineHeight = this._settings.lineHeight; const viewLayout = this._context.viewLayout; - const theme = this._context.theme; const outerHeight = this._context.viewLayout.getScrollHeight(); const heightRatio = canvasHeight / outerHeight; - const decorations = this._context.model.getAllOverviewRulerDecorations(); + const decorations = this._context.model.getAllOverviewRulerDecorations(this._context.theme); const minDecorationHeight = (Constants.MIN_DECORATION_HEIGHT * this._settings.pixelRatio) | 0; const halfMinDecorationHeight = (minDecorationHeight / 2) | 0; @@ -322,17 +315,12 @@ export class DecorationsOverviewRuler2 extends ViewPart { const paintQueue = new PaintQueue(canvasCtx, this._settings.x, this._settings.w); for (let i = 0, len = decorations.length; i < len; i++) { const decoration = decorations[i]; - const opts = decoration.source.options.overviewRuler; - const lane = opts.position; - if (lane === 0) { + if (decoration.lane === 0) { continue; } - const startLineNumber = decoration.range.startLineNumber; - const endLineNumber = decoration.range.endLineNumber; - const color = resolveColor(opts, theme); - let y1 = (viewLayout.getVerticalOffsetForLineNumber(startLineNumber) * heightRatio) | 0; - let y2 = ((viewLayout.getVerticalOffsetForLineNumber(endLineNumber) + lineHeight) * heightRatio) | 0; + let y1 = (viewLayout.getVerticalOffsetForLineNumber(decoration.startLineNumber) * heightRatio) | 0; + let y2 = ((viewLayout.getVerticalOffsetForLineNumber(decoration.endLineNumber) + lineHeight) * heightRatio) | 0; let height = y2 - y1; if (height < minDecorationHeight) { let yCenter = ((y1 + y2) / 2) | 0; @@ -345,7 +333,7 @@ export class DecorationsOverviewRuler2 extends ViewPart { y2 = yCenter + halfMinDecorationHeight; } - paintQueueAccept(paintQueue, color, y1, y2, lane); + paintQueueAccept(paintQueue, decoration.color, y1, y2, decoration.lane); } for (let i = 0, len = paintQueue.req.length; i < len; i++) { paintQueueFlush(paintQueue, paintQueue.req[i]); @@ -448,7 +436,9 @@ function paintQueueAccept(Q: PaintQueue, color: string, y1: number, y2: number, if (result !== null) { // there is already an ongoing request for this color & lane // => simply merge into it - result.y2 = y2; + if (y2 > result.y2) { + result.y2 = y2; + } return; } @@ -466,23 +456,3 @@ function paintQueueFlush(Q: PaintQueue, req: PaintRequest): void { Q.ctx.fillStyle = req.color; Q.ctx.fillRect(Q.x[req.lane], req.y1, Q.w[req.lane], req.y2 - req.y1); } - -function resolveColor(opts: ModelDecorationOverviewRulerOptions, theme: ITheme): string { - if (!opts._resolvedColor) { - const themeType = theme.type; - const color = (themeType === 'dark' ? opts.darkColor : themeType === 'light' ? opts.color : opts.hcColor); - opts._resolvedColor = resolveRulerColor(color, theme); - } - return opts._resolvedColor; -} - -function resolveRulerColor(color: string | ThemeColor, theme: ITheme): string { - if (typeof color === 'string') { - return color; - } - let c = color ? theme.getColor(color.id) : null; - if (!c) { - c = Color.transparent; - } - return c.toString(); -} diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 5cf9d2c728a9e..78bc73c82ee3c 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -10,10 +10,12 @@ import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { PrefixSumComputerWithCache } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { ViewLineData, ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; +import { ViewLineData, ICoordinatesConverter, OverviewRulerDecoration } from 'vs/editor/common/viewModel/viewModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; +import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModelWithDecorations'; +import { ThemeColor, ITheme } from 'vs/platform/theme/common/themeService'; +import { Color } from 'vs/base/common/color'; export class OutputPosition { _outputPositionBrand: void; @@ -57,6 +59,7 @@ export interface ISplitLine { getModelColumnOfViewPosition(outputLineIndex: number, outputColumn: number): number; getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position; + getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number; } export interface IViewModelLinesCollection { @@ -82,6 +85,8 @@ export interface IViewModelLinesCollection { getViewLineMaxColumn(viewLineNumber: number): number; getViewLineData(viewLineNumber: number): ViewLineData; getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): ViewLineData[]; + + getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): OverviewRulerDecoration[]; } export class CoordinatesConverter implements ICoordinatesConverter { @@ -650,6 +655,42 @@ export class SplitLinesCollection implements IViewModelLinesCollection { // console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + r.lineNumber + ',' + r); return r; } + + private _getViewLineNumberForModelPosition(inputLineNumber: number, inputColumn: number): number { + 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)); + return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, inputColumn); + } + + // this model line is not visible + while (lineIndex > 0 && !this.lines[lineIndex].isVisible()) { + lineIndex--; + } + if (lineIndex === 0 && !this.lines[lineIndex].isVisible()) { + // Could not reach a real line + return 1; + } + const deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1)); + return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); + } + + public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): OverviewRulerDecoration[] { + const decorations = this.model.getOverviewRulerDecorations(ownerId, filterOutValidation); + const result = new OverviewRulerDecorations(); + for (let i = 0, len = decorations.length; i < len; i++) { + const decoration = decorations[i]; + const opts = decoration.options.overviewRuler; + const lane = opts.position; + const color = resolveColor(opts, theme); + const viewStartLineNumber = this._getViewLineNumberForModelPosition(decoration.range.startLineNumber, decoration.range.startColumn); + const viewEndLineNumber = this._getViewLineNumberForModelPosition(decoration.range.endLineNumber, decoration.range.endColumn); + + result.accept(color, viewStartLineNumber, viewEndLineNumber, lane); + } + return result.result; + } } class VisibleIdentitySplitLine implements ISplitLine { @@ -711,6 +752,10 @@ class VisibleIdentitySplitLine implements ISplitLine { public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position { return new Position(deltaLineNumber, inputColumn); } + + public getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number { + return deltaLineNumber; + } } class InvisibleIdentitySplitLine implements ISplitLine { @@ -761,6 +806,10 @@ class InvisibleIdentitySplitLine implements ISplitLine { public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position { throw new Error('Not supported'); } + + public getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number { + throw new Error('Not supported'); + } } export class SplitLine implements ISplitLine { @@ -914,6 +963,14 @@ export class SplitLine implements ISplitLine { // console.log('in -> out ' + deltaLineNumber + ',' + inputColumn + ' ===> ' + (deltaLineNumber+outputLineIndex) + ',' + outputColumn); return new Position(deltaLineNumber + outputLineIndex, outputColumn); } + + public getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number { + if (!this._isVisible) { + throw new Error('Not supported'); + } + const r = this.positionMapper.getOutputPositionOfInputOffset(inputColumn - 1); + return (deltaLineNumber + r.outputLineIndex); + } } function createSplitLine(linePositionMapperFactory: ILineMapperFactory, text: string, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, isVisible: boolean): ISplitLine { @@ -1093,4 +1150,99 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return result; } + + public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): OverviewRulerDecoration[] { + const decorations = this.model.getOverviewRulerDecorations(ownerId, filterOutValidation); + const result = new OverviewRulerDecorations(); + for (let i = 0, len = decorations.length; i < len; i++) { + const decoration = decorations[i]; + const opts = decoration.options.overviewRuler; + const lane = opts.position; + const color = resolveColor(opts, theme); + const viewStartLineNumber = decoration.range.startLineNumber; + const viewEndLineNumber = decoration.range.endLineNumber; + + result.accept(color, viewStartLineNumber, viewEndLineNumber, lane); + } + result.flushAll(); + return result.result; + } +} + +class OverviewRulerDecorations { + + readonly req: OverviewRulerDecoration[] = []; + readonly result: OverviewRulerDecoration[] = []; + + constructor() { + } + + public accept(color: string, startLineNumber: number, endLineNumber: number, lane: number): void { + let result: OverviewRulerDecoration = null; + for (let i = 0, len = this.req.length; i < len; i++) { + const req = this.req[i]; + if (req.endLineNumber < startLineNumber) { + this._flush(req); + + if (i + 1 === len) { + // last element + this.req.pop(); + break; + } else { + this.req[i] = this.req.pop(); + len--; + i--; + continue; + } + } + + if (req.lane === lane && req.color === color) { + result = req; + break; + } + } + + if (result !== null) { + // there is already an ongoing request for this color & lane + // => simply merge into it + if (endLineNumber > result.endLineNumber) { + result.endLineNumber = endLineNumber; + } + return; + } + + result = new OverviewRulerDecoration(startLineNumber, endLineNumber, lane, color); + this.req.push(result); + } + + public flushAll(): void { + for (let i = 0, len = this.req.length; i < len; i++) { + this._flush(this.req[i]); + } + } + + private _flush(element: OverviewRulerDecoration): void { + this.result.push(element); + } +} + + +function resolveColor(opts: ModelDecorationOverviewRulerOptions, theme: ITheme): string { + if (!opts._resolvedColor) { + const themeType = theme.type; + const color = (themeType === 'dark' ? opts.darkColor : themeType === 'light' ? opts.color : opts.hcColor); + opts._resolvedColor = resolveRulerColor(color, theme); + } + return opts._resolvedColor; +} + +function resolveRulerColor(color: string | ThemeColor, theme: ITheme): string { + if (typeof color === 'string') { + return color; + } + let c = color ? theme.getColor(color.id) : null; + if (!c) { + c = Color.transparent; + } + return c.toString(); } diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index 2890bf9314f88..ac4892852669f 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { INewScrollPosition, IModelDecoration, EndOfLinePreference, IViewState } from 'vs/editor/common/editorCommon'; +import { INewScrollPosition, IModelDecoration, EndOfLinePreference, IViewState, OverviewRulerLane } from 'vs/editor/common/editorCommon'; import { ViewLineToken } from 'vs/editor/common/core/viewLineToken'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -14,6 +14,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { Scrollable, IScrollPosition } from 'vs/base/common/scrollable'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; +import { ITheme } from 'vs/platform/theme/common/themeService'; export interface IViewWhitespaceViewportData { readonly id: number; @@ -138,7 +139,8 @@ export interface IViewModel { getLineMaxColumn(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; - getAllOverviewRulerDecorations(): ViewModelDecoration[]; + getAllOverviewRulerDecorations(theme: ITheme): OverviewRulerDecoration[]; + invalidateOverviewRulerColorCache(): void; getValueInRange(range: Range, eol: EndOfLinePreference): string; getModelLineMaxColumn(modelLineNumber: number): number; @@ -276,6 +278,17 @@ export class ViewModelDecoration { } } +export class OverviewRulerDecoration { + _overviewRulerDecorationBrand: void; + + constructor( + public readonly startLineNumber: number, + public endLineNumber: number, + public readonly lane: OverviewRulerLane, + public readonly color: string + ) { } +} + export class ViewEventsCollector { private _events: ViewEvent[]; diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index be7a324004a40..e1bcdeda1a353 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -88,17 +88,6 @@ export class ViewModelDecorations implements IDisposable { return r; } - public getAllOverviewRulerDecorations(): ViewModelDecoration[] { - let modelDecorations = this.model.getOverviewRulerDecorations(this.editorId, this.configuration.editor.readOnly); - let result: ViewModelDecoration[] = [], resultLen = 0; - for (let i = 0, len = modelDecorations.length; i < len; i++) { - let modelDecoration = modelDecorations[i]; - let viewModelDecoration = this._getOrCreateViewModelDecoration(modelDecoration); - result[resultLen++] = viewModelDecoration; - } - return result; - } - public getDecorationsViewportData(viewRange: Range): IDecorationsViewportData { var cacheIsValid = true; cacheIsValid = cacheIsValid && (this._cachedModelDecorationsResolver !== null); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 68cdaf1ccbcad..8eb47ab129f06 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -12,7 +12,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { TokenizationRegistry, ColorId, LanguageId } from 'vs/editor/common/modes'; import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations'; -import { MinimapLinesRenderingData, ViewLineRenderingData, ViewModelDecoration, IViewModel, ICoordinatesConverter, ViewEventsCollector } from 'vs/editor/common/viewModel/viewModel'; +import { MinimapLinesRenderingData, ViewLineRenderingData, ViewModelDecoration, IViewModel, ICoordinatesConverter, ViewEventsCollector, OverviewRulerDecoration } from 'vs/editor/common/viewModel/viewModel'; import { SplitLinesCollection, IViewModelLinesCollection, IdentityLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { MinimapTokensColorTracker } from 'vs/editor/common/view/minimapCharRenderer'; @@ -22,6 +22,8 @@ import { CharacterHardWrappingLineMapperFactory } from 'vs/editor/common/viewMod import { ViewLayout } from 'vs/editor/common/viewLayout/viewLayout'; import { Color } from 'vs/base/common/color'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ITheme } from 'vs/platform/theme/common/themeService'; +import { ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModelWithDecorations'; const USE_IDENTITY_LINES_COLLECTION = true; @@ -428,8 +430,17 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ); } - public getAllOverviewRulerDecorations(): ViewModelDecoration[] { - return this.decorations.getAllOverviewRulerDecorations(); + public getAllOverviewRulerDecorations(theme: ITheme): OverviewRulerDecoration[] { + return this.lines.getAllOverviewRulerDecorations(this.editorId, this.configuration.editor.readOnly, theme); + } + + public invalidateOverviewRulerColorCache(): void { + const decorations = this.model.getOverviewRulerDecorations(); + for (let i = 0, len = decorations.length; i < len; i++) { + const decoration = decorations[i]; + const opts = decoration.options.overviewRuler; + opts._resolvedColor = null; + } } public getValueInRange(range: Range, eol: editorCommon.EndOfLinePreference): string { From b6d492b4469aa236d7ca087f2587ba19e5775d2f Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 09:18:34 +0200 Subject: [PATCH 51/64] Encode overview ruler decorations in a number array --- .../decorationsOverviewRuler2.ts | 144 +++++------------- .../common/viewModel/splitLinesCollection.ts | 73 +++------ src/vs/editor/common/viewModel/viewModel.ts | 21 ++- .../editor/common/viewModel/viewModelImpl.ts | 4 +- 4 files changed, 79 insertions(+), 163 deletions(-) diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts index 98c23749c304c..a6933438dba32 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts @@ -312,31 +312,51 @@ export class DecorationsOverviewRuler2 extends ViewPart { canvasCtx.fillRect(0, 0, canvasWidth, canvasHeight); } - const paintQueue = new PaintQueue(canvasCtx, this._settings.x, this._settings.w); - for (let i = 0, len = decorations.length; i < len; i++) { - const decoration = decorations[i]; - if (decoration.lane === 0) { - continue; - } + const x = this._settings.x; + const w = this._settings.w; + for (let color in decorations) { + const colorDecorations = decorations[color]; + + canvasCtx.fillStyle = color; + + let prevLane = 0; + let prevY1 = 0; + let prevY2 = 0; + for (let i = 0, len = colorDecorations.length; i < len; i++) { + const lane = colorDecorations[3 * i]; + const startLineNumber = colorDecorations[3 * i + 1]; + const endLineNumber = colorDecorations[3 * i + 2]; + + let y1 = (viewLayout.getVerticalOffsetForLineNumber(startLineNumber) * heightRatio) | 0; + let y2 = ((viewLayout.getVerticalOffsetForLineNumber(endLineNumber) + lineHeight) * heightRatio) | 0; + let height = y2 - y1; + if (height < minDecorationHeight) { + let yCenter = ((y1 + y2) / 2) | 0; + if (yCenter < halfMinDecorationHeight) { + yCenter = halfMinDecorationHeight; + } else if (yCenter + halfMinDecorationHeight > canvasHeight) { + yCenter = canvasHeight - halfMinDecorationHeight; + } + y1 = yCenter - halfMinDecorationHeight; + y2 = yCenter + halfMinDecorationHeight; + } - let y1 = (viewLayout.getVerticalOffsetForLineNumber(decoration.startLineNumber) * heightRatio) | 0; - let y2 = ((viewLayout.getVerticalOffsetForLineNumber(decoration.endLineNumber) + lineHeight) * heightRatio) | 0; - let height = y2 - y1; - if (height < minDecorationHeight) { - let yCenter = ((y1 + y2) / 2) | 0; - if (yCenter < halfMinDecorationHeight) { - yCenter = halfMinDecorationHeight; - } else if (yCenter + halfMinDecorationHeight > canvasHeight) { - yCenter = canvasHeight - halfMinDecorationHeight; + if (y1 > prevY2 || lane !== prevLane) { + // flush prev + if (i !== 0) { + canvasCtx.fillRect(x[prevLane], prevY1, w[prevLane], prevY2 - prevY1); + } + prevLane = lane; + prevY1 = y1; + prevY2 = y2; + } else { + // can merge into prev + if (y2 > prevY2) { + prevY2 = y2; + } } - y1 = yCenter - halfMinDecorationHeight; - y2 = yCenter + halfMinDecorationHeight; } - - paintQueueAccept(paintQueue, decoration.color, y1, y2, decoration.lane); - } - for (let i = 0, len = paintQueue.req.length; i < len; i++) { - paintQueueFlush(paintQueue, paintQueue.req[i]); + canvasCtx.fillRect(x[prevLane], prevY1, w[prevLane], prevY2 - prevY1); } // Draw cursors @@ -376,83 +396,3 @@ export class DecorationsOverviewRuler2 extends ViewPart { } } -class PaintRequest { - public color: string; - public y1: number; - public y2: number; - public lane: number; - - public init(color: string, y1: number, y2: number, lane: number): void { - this.color = color; - this.y1 = y1; - this.y2 = y2; - this.lane = lane; - } -} - -class PaintQueue { - readonly ctx: CanvasRenderingContext2D; - - readonly x: number[]; - readonly w: number[]; - - readonly req: PaintRequest[] = []; - readonly pool: PaintRequest[] = []; - - constructor(ctx: CanvasRenderingContext2D, x: number[], w: number[]) { - this.ctx = ctx; - this.x = x; - this.w = w; - } -} - -function paintQueueAccept(Q: PaintQueue, color: string, y1: number, y2: number, lane: number): void { - let result: PaintRequest = null; - for (let i = 0, len = Q.req.length; i < len; i++) { - const req = Q.req[i]; - if (req.y2 < y1) { - paintQueueFlush(Q, req); - - if (i + 1 === len) { - // last element - Q.req.pop(); - Q.pool.push(req); - break; - } else { - Q.req[i] = Q.req.pop(); - len--; - Q.pool.push(req); - i--; - continue; - } - } - - if (req.lane === lane && req.color === color) { - result = req; - break; - } - } - - if (result !== null) { - // there is already an ongoing request for this color & lane - // => simply merge into it - if (y2 > result.y2) { - result.y2 = y2; - } - return; - } - - if (Q.pool.length > 0) { - result = Q.pool.pop(); - } else { - result = new PaintRequest(); - } - - result.init(color, y1, y2, lane); - Q.req.push(result); -} - -function paintQueueFlush(Q: PaintQueue, req: PaintRequest): void { - Q.ctx.fillStyle = req.color; - Q.ctx.fillRect(Q.x[req.lane], req.y1, Q.w[req.lane], req.y2 - req.y1); -} diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 78bc73c82ee3c..5531f419ef28b 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -10,7 +10,7 @@ import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { PrefixSumComputerWithCache } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { ViewLineData, ICoordinatesConverter, OverviewRulerDecoration } from 'vs/editor/common/viewModel/viewModel'; +import { ViewLineData, ICoordinatesConverter, IOverviewRulerDecorations } from 'vs/editor/common/viewModel/viewModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModelWithDecorations'; @@ -86,7 +86,7 @@ export interface IViewModelLinesCollection { getViewLineData(viewLineNumber: number): ViewLineData; getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): ViewLineData[]; - getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): OverviewRulerDecoration[]; + getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): IOverviewRulerDecorations; } export class CoordinatesConverter implements ICoordinatesConverter { @@ -676,13 +676,16 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return this.lines[lineIndex].getViewLineNumberOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); } - public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): OverviewRulerDecoration[] { + public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): IOverviewRulerDecorations { const decorations = this.model.getOverviewRulerDecorations(ownerId, filterOutValidation); const result = new OverviewRulerDecorations(); for (let i = 0, len = decorations.length; i < len; i++) { const decoration = decorations[i]; const opts = decoration.options.overviewRuler; const lane = opts.position; + if (lane === 0) { + continue; + } const color = resolveColor(opts, theme); const viewStartLineNumber = this._getViewLineNumberForModelPosition(decoration.range.startLineNumber, decoration.range.startColumn); const viewEndLineNumber = this._getViewLineNumberForModelPosition(decoration.range.endLineNumber, decoration.range.endColumn); @@ -1151,79 +1154,53 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return result; } - public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): OverviewRulerDecoration[] { + public getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): IOverviewRulerDecorations { const decorations = this.model.getOverviewRulerDecorations(ownerId, filterOutValidation); const result = new OverviewRulerDecorations(); for (let i = 0, len = decorations.length; i < len; i++) { const decoration = decorations[i]; const opts = decoration.options.overviewRuler; const lane = opts.position; + if (lane === 0) { + continue; + } const color = resolveColor(opts, theme); const viewStartLineNumber = decoration.range.startLineNumber; const viewEndLineNumber = decoration.range.endLineNumber; result.accept(color, viewStartLineNumber, viewEndLineNumber, lane); } - result.flushAll(); return result.result; } } class OverviewRulerDecorations { - readonly req: OverviewRulerDecoration[] = []; - readonly result: OverviewRulerDecoration[] = []; + readonly result: IOverviewRulerDecorations = Object.create(null); constructor() { } public accept(color: string, startLineNumber: number, endLineNumber: number, lane: number): void { - let result: OverviewRulerDecoration = null; - for (let i = 0, len = this.req.length; i < len; i++) { - const req = this.req[i]; - if (req.endLineNumber < startLineNumber) { - this._flush(req); - - if (i + 1 === len) { - // last element - this.req.pop(); - break; - } else { - this.req[i] = this.req.pop(); - len--; - i--; - continue; + let prev = this.result[color]; + + if (prev) { + const prevLane = prev[prev.length - 3]; + const prevEndLineNumber = prev[prev.length - 1]; + if (prevLane === lane && prevEndLineNumber >= startLineNumber) { + // merge into prev + if (endLineNumber > prevEndLineNumber) { + prev[prev.length - 1] = endLineNumber; } + return; } - if (req.lane === lane && req.color === color) { - result = req; - break; - } - } - - if (result !== null) { - // there is already an ongoing request for this color & lane - // => simply merge into it - if (endLineNumber > result.endLineNumber) { - result.endLineNumber = endLineNumber; - } - return; - } - - result = new OverviewRulerDecoration(startLineNumber, endLineNumber, lane, color); - this.req.push(result); - } - - public flushAll(): void { - for (let i = 0, len = this.req.length; i < len; i++) { - this._flush(this.req[i]); + // push + prev.push(lane, startLineNumber, endLineNumber); + } else { + this.result[color] = [lane, startLineNumber, endLineNumber]; } } - - private _flush(element: OverviewRulerDecoration): void { - this.result.push(element); - } } diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index ac4892852669f..cd4ea6a28b8ee 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { INewScrollPosition, IModelDecoration, EndOfLinePreference, IViewState, OverviewRulerLane } from 'vs/editor/common/editorCommon'; +import { INewScrollPosition, IModelDecoration, EndOfLinePreference, IViewState } from 'vs/editor/common/editorCommon'; import { ViewLineToken } from 'vs/editor/common/core/viewLineToken'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -139,7 +139,7 @@ export interface IViewModel { getLineMaxColumn(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; - getAllOverviewRulerDecorations(theme: ITheme): OverviewRulerDecoration[]; + getAllOverviewRulerDecorations(theme: ITheme): IOverviewRulerDecorations; invalidateOverviewRulerColorCache(): void; getValueInRange(range: Range, eol: EndOfLinePreference): string; @@ -278,15 +278,14 @@ export class ViewModelDecoration { } } -export class OverviewRulerDecoration { - _overviewRulerDecorationBrand: void; - - constructor( - public readonly startLineNumber: number, - public endLineNumber: number, - public readonly lane: OverviewRulerLane, - public readonly color: string - ) { } +/** + * Decorations are encoded in a number array using the following scheme: + * - 3*i = lane + * - 3*i+1 = startLineNumber + * - 3*i+2 = endLineNumber + */ +export interface IOverviewRulerDecorations { + [color: string]: number[]; } export class ViewEventsCollector { diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 8eb47ab129f06..d316d894d6216 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -12,7 +12,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { TokenizationRegistry, ColorId, LanguageId } from 'vs/editor/common/modes'; import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations'; -import { MinimapLinesRenderingData, ViewLineRenderingData, ViewModelDecoration, IViewModel, ICoordinatesConverter, ViewEventsCollector, OverviewRulerDecoration } from 'vs/editor/common/viewModel/viewModel'; +import { MinimapLinesRenderingData, ViewLineRenderingData, ViewModelDecoration, IViewModel, ICoordinatesConverter, ViewEventsCollector, IOverviewRulerDecorations } from 'vs/editor/common/viewModel/viewModel'; import { SplitLinesCollection, IViewModelLinesCollection, IdentityLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { MinimapTokensColorTracker } from 'vs/editor/common/view/minimapCharRenderer'; @@ -430,7 +430,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ); } - public getAllOverviewRulerDecorations(theme: ITheme): OverviewRulerDecoration[] { + public getAllOverviewRulerDecorations(theme: ITheme): IOverviewRulerDecorations { return this.lines.getAllOverviewRulerDecorations(this.editorId, this.configuration.editor.readOnly, theme); } From 69ab7b140e8a5a8615edfd63bdc38e14c2d99ffd Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 09:26:01 +0200 Subject: [PATCH 52/64] More aggressive merging of overview ruler decorations --- .../viewParts/overviewRuler/decorationsOverviewRuler2.ts | 4 ++-- src/vs/editor/common/viewModel/splitLinesCollection.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts index a6933438dba32..288c6803f2bb8 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts @@ -341,7 +341,7 @@ export class DecorationsOverviewRuler2 extends ViewPart { y2 = yCenter + halfMinDecorationHeight; } - if (y1 > prevY2 || lane !== prevLane) { + if (y1 > prevY2 + 1 || lane !== prevLane) { // flush prev if (i !== 0) { canvasCtx.fillRect(x[prevLane], prevY1, w[prevLane], prevY2 - prevY1); @@ -350,7 +350,7 @@ export class DecorationsOverviewRuler2 extends ViewPart { prevY1 = y1; prevY2 = y2; } else { - // can merge into prev + // merge into prev if (y2 > prevY2) { prevY2 = y2; } diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 5531f419ef28b..501001f4b9552 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -1187,7 +1187,7 @@ class OverviewRulerDecorations { if (prev) { const prevLane = prev[prev.length - 3]; const prevEndLineNumber = prev[prev.length - 1]; - if (prevLane === lane && prevEndLineNumber >= startLineNumber) { + if (prevLane === lane && prevEndLineNumber + 1 >= startLineNumber) { // merge into prev if (endLineNumber > prevEndLineNumber) { prev[prev.length - 1] = endLineNumber; From 34e74ba1578fe105afc8ca26615f45f9397bdd96 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 09:38:09 +0200 Subject: [PATCH 53/64] Tweak the way find decorations are managed --- .../contrib/find/common/findDecorations.ts | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/vs/editor/contrib/find/common/findDecorations.ts b/src/vs/editor/contrib/find/common/findDecorations.ts index 4a59a7b7e569d..20db13e30ca5e 100644 --- a/src/vs/editor/contrib/find/common/findDecorations.ts +++ b/src/vs/editor/contrib/find/common/findDecorations.ts @@ -132,29 +132,32 @@ export class FindDecorations implements IDisposable { } public set(findMatches: editorCommon.FindMatch[], findScope: Range): void { - let newDecorations: editorCommon.IModelDeltaDecoration[] = new Array(findMatches.length); - for (let i = 0, len = findMatches.length; i < len; i++) { - newDecorations[i] = { - range: findMatches[i].range, - options: FindDecorations._FIND_MATCH_DECORATION - }; - } - if (findScope) { - newDecorations.unshift({ - range: findScope, - options: FindDecorations._FIND_SCOPE_DECORATION - }); - } - let tmpDecorations = this._editor.deltaDecorations(this._allDecorations(), newDecorations); + this._editor.changeDecorations((accessor) => { + // Find matches + let newFindMatchesDecorations: editorCommon.IModelDeltaDecoration[] = new Array(findMatches.length); + for (let i = 0, len = findMatches.length; i < len; i++) { + newFindMatchesDecorations[i] = { + range: findMatches[i].range, + options: FindDecorations._FIND_MATCH_DECORATION + }; + } + this._decorations = accessor.deltaDecorations(this._decorations, newFindMatchesDecorations); - if (findScope) { - this._findScopeDecorationId = tmpDecorations.shift(); - } else { - this._findScopeDecorationId = null; - } - this._decorations = tmpDecorations; - this._rangeHighlightDecorationId = null; - this._highlightedDecorationId = null; + // Range highlight + if (this._rangeHighlightDecorationId) { + accessor.removeDecoration(this._rangeHighlightDecorationId); + this._rangeHighlightDecorationId = null; + } + + // Find scope + if (this._findScopeDecorationId) { + accessor.removeDecoration(this._findScopeDecorationId); + this._findScopeDecorationId = null; + } + if (findScope) { + this._findScopeDecorationId = accessor.addDecoration(findScope, FindDecorations._FIND_SCOPE_DECORATION); + } + }); } private _allDecorations(): string[] { From c01d8c1ac13be3ef3058a9ac7635eb0c2f5f1f7a Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 10:07:37 +0200 Subject: [PATCH 54/64] Approximate find matches in the overview ruler if there are a high number of results --- .../contrib/find/common/findDecorations.ts | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/find/common/findDecorations.ts b/src/vs/editor/contrib/find/common/findDecorations.ts index 20db13e30ca5e..09cb7eb07d06f 100644 --- a/src/vs/editor/contrib/find/common/findDecorations.ts +++ b/src/vs/editor/contrib/find/common/findDecorations.ts @@ -16,6 +16,7 @@ export class FindDecorations implements IDisposable { private _editor: editorCommon.ICommonCodeEditor; private _decorations: string[]; + private _overviewRulerApproximateDecorations: string[]; private _findScopeDecorationId: string; private _rangeHighlightDecorationId: string; private _highlightedDecorationId: string; @@ -24,6 +25,7 @@ export class FindDecorations implements IDisposable { constructor(editor: editorCommon.ICommonCodeEditor) { this._editor = editor; this._decorations = []; + this._overviewRulerApproximateDecorations = []; this._findScopeDecorationId = null; this._rangeHighlightDecorationId = null; this._highlightedDecorationId = null; @@ -35,6 +37,7 @@ export class FindDecorations implements IDisposable { this._editor = null; this._decorations = []; + this._overviewRulerApproximateDecorations = []; this._findScopeDecorationId = null; this._rangeHighlightDecorationId = null; this._highlightedDecorationId = null; @@ -43,6 +46,7 @@ export class FindDecorations implements IDisposable { public reset(): void { this._decorations = []; + this._overviewRulerApproximateDecorations = []; this._findScopeDecorationId = null; this._rangeHighlightDecorationId = null; this._highlightedDecorationId = null; @@ -133,16 +137,59 @@ export class FindDecorations implements IDisposable { public set(findMatches: editorCommon.FindMatch[], findScope: Range): void { this._editor.changeDecorations((accessor) => { + + let findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION; + let newOverviewRulerApproximateDecorations: editorCommon.IModelDeltaDecoration[] = []; + + if (findMatches.length > 1000) { + // we go into a mode where the overview ruler gets "approximate" decorations + // the reason is that the overview ruler paints all the decorations in the file and we don't want to cause freezes + findMatchesOptions = FindDecorations._FIND_MATCH_NO_OVERVIEW_DECORATION; + + // approximate a distance in lines where matches should be merged + const lineCount = this._editor.getModel().getLineCount(); + const height = this._editor.getLayoutInfo().height; + const approxPixelsPerLine = height / lineCount; + const mergeLinesDelta = Math.max(2, Math.ceil(3 / approxPixelsPerLine)); + + // merge decorations as much as possible + let prevStartLineNumber = findMatches[0].range.startLineNumber; + let prevEndLineNumber = findMatches[0].range.endLineNumber; + for (let i = 1, len = findMatches.length; i < len; i++) { + const range = findMatches[i].range; + if (prevEndLineNumber + mergeLinesDelta >= range.startLineNumber) { + if (range.endLineNumber > prevEndLineNumber) { + prevEndLineNumber = range.endLineNumber; + } + } else { + newOverviewRulerApproximateDecorations.push({ + range: new Range(prevStartLineNumber, 1, prevEndLineNumber, 1), + options: FindDecorations._FIND_MATCH_ONLY_OVERVIEW_DECORATION + }); + prevStartLineNumber = range.startLineNumber; + prevEndLineNumber = range.endLineNumber; + } + } + + newOverviewRulerApproximateDecorations.push({ + range: new Range(prevStartLineNumber, 1, prevEndLineNumber, 1), + options: FindDecorations._FIND_MATCH_ONLY_OVERVIEW_DECORATION + }); + } + // Find matches let newFindMatchesDecorations: editorCommon.IModelDeltaDecoration[] = new Array(findMatches.length); for (let i = 0, len = findMatches.length; i < len; i++) { newFindMatchesDecorations[i] = { range: findMatches[i].range, - options: FindDecorations._FIND_MATCH_DECORATION + options: findMatchesOptions }; } this._decorations = accessor.deltaDecorations(this._decorations, newFindMatchesDecorations); + // Overview ruler approximate decorations + this._overviewRulerApproximateDecorations = accessor.deltaDecorations(this._overviewRulerApproximateDecorations, newOverviewRulerApproximateDecorations); + // Range highlight if (this._rangeHighlightDecorationId) { accessor.removeDecoration(this._rangeHighlightDecorationId); @@ -163,6 +210,7 @@ export class FindDecorations implements IDisposable { private _allDecorations(): string[] { let result: string[] = []; result = result.concat(this._decorations); + result = result.concat(this._overviewRulerApproximateDecorations); if (this._findScopeDecorationId) { result.push(this._findScopeDecorationId); } @@ -194,6 +242,21 @@ export class FindDecorations implements IDisposable { } }); + private static _FIND_MATCH_NO_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'findMatch', + showIfCollapsed: true + }); + + private static _FIND_MATCH_ONLY_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + overviewRuler: { + color: themeColorFromId(editorFindMatchHighlight), + darkColor: themeColorFromId(editorFindMatchHighlight), + position: editorCommon.OverviewRulerLane.Center + } + }); + private static _RANGE_HIGHLIGHT_DECORATION = ModelDecorationOptions.register({ stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'rangeHighlight', From 9146ff33522d7f6b1e134b6893ca8cb6bd91f64d Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 11:18:01 +0200 Subject: [PATCH 55/64] Query model for decorations only for visible model lines in the viewport (in contiguos visible model lines batches) --- .../common/viewModel/splitLinesCollection.ts | 43 +++++++++++++++++++ .../common/viewModel/viewModelDecorations.ts | 12 +++--- .../editor/common/viewModel/viewModelImpl.ts | 2 +- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 501001f4b9552..f57572f1e38b8 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -87,6 +87,7 @@ export interface IViewModelLinesCollection { getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): ViewLineData[]; getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: ITheme): IOverviewRulerDecorations; + getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): editorCommon.IModelDecoration[]; } export class CoordinatesConverter implements ICoordinatesConverter { @@ -694,6 +695,44 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } return result.result; } + + public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): editorCommon.IModelDecoration[] { + const modelStart = this.convertViewPositionToModelPosition(range.startLineNumber, range.startColumn); + const modelEnd = this.convertViewPositionToModelPosition(range.endLineNumber, range.endColumn); + + if (modelEnd.lineNumber - modelStart.lineNumber <= range.endLineNumber - range.startLineNumber) { + // most likely there are no hidden lines => fast path + return this.model.getDecorationsInRange(new Range(modelStart.lineNumber, modelStart.column, modelEnd.lineNumber, modelEnd.column), ownerId, filterOutValidation); + } + + let result: editorCommon.IModelDecoration[] = []; + const modelStartLineIndex = modelStart.lineNumber - 1; + const modelEndLineIndex = modelEnd.lineNumber - 1; + + let reqStart: Position = null; + for (let modelLineIndex = modelStartLineIndex; modelLineIndex <= modelEndLineIndex; modelLineIndex++) { + const line = this.lines[modelLineIndex]; + if (line.isVisible()) { + // merge into previous request + if (reqStart === null) { + reqStart = new Position(modelLineIndex + 1, modelLineIndex === modelStartLineIndex ? modelStart.column : 1); + } + } else { + // hit invisible line => flush request + if (reqStart !== null) { + result = result.concat(this.model.getDecorationsInRange(new Range(reqStart.lineNumber, reqStart.column, modelLineIndex + 1, 1))); + reqStart = null; + } + } + } + + if (reqStart !== null) { + result = result.concat(this.model.getDecorationsInRange(new Range(reqStart.lineNumber, reqStart.column, modelEnd.lineNumber, modelEnd.column))); + reqStart = null; + } + + return result; + } } class VisibleIdentitySplitLine implements ISplitLine { @@ -1172,6 +1211,10 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { } return result.result; } + + public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): editorCommon.IModelDecoration[] { + return this.model.getDecorationsInRange(range, ownerId, filterOutValidation); + } } class OverviewRulerDecorations { diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index e1bcdeda1a353..f468105a4bfa6 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -9,6 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { InlineDecoration, ViewModelDecoration, ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; +import { IViewModelLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; export interface IDecorationsViewportData { /** @@ -26,6 +27,7 @@ export class ViewModelDecorations implements IDisposable { private readonly editorId: number; private readonly model: editorCommon.IModel; private readonly configuration: editorCommon.IConfiguration; + private readonly _linesCollection: IViewModelLinesCollection; private readonly _coordinatesConverter: ICoordinatesConverter; private _decorationsCache: { [decorationId: string]: ViewModelDecoration; }; @@ -33,10 +35,11 @@ export class ViewModelDecorations implements IDisposable { private _cachedModelDecorationsResolver: IDecorationsViewportData; private _cachedModelDecorationsResolverViewRange: Range; - constructor(editorId: number, model: editorCommon.IModel, configuration: editorCommon.IConfiguration, coordinatesConverter: ICoordinatesConverter) { + constructor(editorId: number, model: editorCommon.IModel, configuration: editorCommon.IConfiguration, linesCollection: IViewModelLinesCollection, coordinatesConverter: ICoordinatesConverter) { this.editorId = editorId; this.model = model; this.configuration = configuration; + this._linesCollection = linesCollection; this._coordinatesConverter = coordinatesConverter; this._decorationsCache = Object.create(null); this._clearCachedModelDecorationsResolver(); @@ -100,10 +103,9 @@ export class ViewModelDecorations implements IDisposable { } private _getDecorationsViewportData(viewportRange: Range): IDecorationsViewportData { - let viewportModelRange = this._coordinatesConverter.convertViewRangeToModelRange(viewportRange); - let startLineNumber = viewportRange.startLineNumber; - let endLineNumber = viewportRange.endLineNumber; - let modelDecorations = this.model.getDecorationsInRange(viewportModelRange, this.editorId, this.configuration.editor.readOnly); + const modelDecorations = this._linesCollection.getDecorationsInRange(viewportRange, this.editorId, this.configuration.editor.readOnly); + const startLineNumber = viewportRange.startLineNumber; + const endLineNumber = viewportRange.endLineNumber; let decorationsInViewport: ViewModelDecoration[] = [], decorationsInViewportLen = 0; let inlineDecorations: InlineDecoration[][] = []; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index d316d894d6216..25a52e2f2b9e5 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -82,7 +82,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel this._isDisposing = false; this._centeredViewLine = -1; - this.decorations = new ViewModelDecorations(this.editorId, this.model, this.configuration, this.coordinatesConverter); + this.decorations = new ViewModelDecorations(this.editorId, this.model, this.configuration, this.lines, this.coordinatesConverter); this._register(this.model.addBulkListener((events: EmitterEvent[]) => { if (this._isDisposing) { From 819c00594eef41d96182fb92be0ca8c8b94a60aa Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 11:33:14 +0200 Subject: [PATCH 56/64] Simplify ViewModelDecoration --- .../viewParts/decorations/decorations.ts | 16 +++++------ .../viewParts/glyphMargin/glyphMargin.ts | 2 +- .../linesDecorations/linesDecorations.ts | 2 +- .../marginDecorations/marginDecorations.ts | 2 +- src/vs/editor/common/viewModel/viewModel.ts | 12 ++++---- .../common/viewModel/viewModelDecorations.ts | 20 ++++++------- .../viewModel/viewModelDecorations.test.ts | 28 +++++++++---------- 7 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/vs/editor/browser/viewParts/decorations/decorations.ts b/src/vs/editor/browser/viewParts/decorations/decorations.ts index 2cc107d4037d7..dc63ec06edc51 100644 --- a/src/vs/editor/browser/viewParts/decorations/decorations.ts +++ b/src/vs/editor/browser/viewParts/decorations/decorations.ts @@ -78,15 +78,15 @@ export class DecorationsOverlay extends DynamicViewOverlay { let decorations: ViewModelDecoration[] = [], decorationsLen = 0; for (let i = 0, len = _decorations.length; i < len; i++) { let d = _decorations[i]; - if (d.source.options.className) { + if (d.options.className) { decorations[decorationsLen++] = d; } } // Sort decorations for consistent render output decorations = decorations.sort((a, b) => { - let aClassName = a.source.options.className; - let bClassName = b.source.options.className; + let aClassName = a.options.className; + let bClassName = b.options.className; if (aClassName < bClassName) { return -1; @@ -120,13 +120,13 @@ export class DecorationsOverlay extends DynamicViewOverlay { for (let i = 0, lenI = decorations.length; i < lenI; i++) { let d = decorations[i]; - if (!d.source.options.isWholeLine) { + if (!d.options.isWholeLine) { continue; } let decorationOutput = ( '
' @@ -148,12 +148,12 @@ export class DecorationsOverlay extends DynamicViewOverlay { for (let i = 0, lenI = decorations.length; i < lenI; i++) { const d = decorations[i]; - if (d.source.options.isWholeLine) { + if (d.options.isWholeLine) { continue; } - const className = d.source.options.className; - const showIfCollapsed = d.source.options.showIfCollapsed; + const className = d.options.className; + const showIfCollapsed = d.options.showIfCollapsed; let range = d.range; if (showIfCollapsed && range.endColumn === 1 && range.endLineNumber !== range.startLineNumber) { diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 33874c27b5170..757305ebc08d3 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -145,7 +145,7 @@ export class GlyphMarginOverlay extends DedupOverlay { let r: DecorationToRender[] = [], rLen = 0; for (let i = 0, len = decorations.length; i < len; i++) { let d = decorations[i]; - let glyphMarginClassName = d.source.options.glyphMarginClassName; + let glyphMarginClassName = d.options.glyphMarginClassName; if (glyphMarginClassName) { r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, glyphMarginClassName); } diff --git a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts index 3a143124040d2..437677880960b 100644 --- a/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts +++ b/src/vs/editor/browser/viewParts/linesDecorations/linesDecorations.ts @@ -73,7 +73,7 @@ export class LinesDecorationsOverlay extends DedupOverlay { let r: DecorationToRender[] = [], rLen = 0; for (let i = 0, len = decorations.length; i < len; i++) { let d = decorations[i]; - let linesDecorationsClassName = d.source.options.linesDecorationsClassName; + let linesDecorationsClassName = d.options.linesDecorationsClassName; if (linesDecorationsClassName) { r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, linesDecorationsClassName); } diff --git a/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts b/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts index b83c9faa123ce..75ff0a0e3ed95 100644 --- a/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts +++ b/src/vs/editor/browser/viewParts/marginDecorations/marginDecorations.ts @@ -63,7 +63,7 @@ export class MarginViewLineDecorationsOverlay extends DedupOverlay { let r: DecorationToRender[] = [], rLen = 0; for (let i = 0, len = decorations.length; i < len; i++) { let d = decorations[i]; - let marginClassName = d.source.options.marginClassName; + let marginClassName = d.options.marginClassName; if (marginClassName) { r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, marginClassName); } diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index cd4ea6a28b8ee..88787418a97df 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { INewScrollPosition, IModelDecoration, EndOfLinePreference, IViewState } from 'vs/editor/common/editorCommon'; +import { INewScrollPosition, EndOfLinePreference, IViewState, IModelDecorationOptions } from 'vs/editor/common/editorCommon'; import { ViewLineToken } from 'vs/editor/common/core/viewLineToken'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -269,12 +269,12 @@ export class InlineDecoration { export class ViewModelDecoration { _viewModelDecorationBrand: void; - public range: Range; - public readonly source: IModelDecoration; + public readonly range: Range; + public readonly options: IModelDecorationOptions; - constructor(source: IModelDecoration) { - this.range = null; - this.source = source; + constructor(range: Range, options: IModelDecorationOptions) { + this.range = range; + this.options = options; } } diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index f468105a4bfa6..b5c6aa4b5ba01 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -72,21 +72,21 @@ export class ViewModelDecorations implements IDisposable { } private _getOrCreateViewModelDecoration(modelDecoration: editorCommon.IModelDecoration): ViewModelDecoration { - let id = modelDecoration.id; + const id = modelDecoration.id; let r = this._decorationsCache[id]; if (!r) { - r = new ViewModelDecoration(modelDecoration); - this._decorationsCache[id] = r; - } - if (r.range === null) { const modelRange = modelDecoration.range; - if (modelDecoration.options.isWholeLine) { - let start = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.startLineNumber, 1)); - let end = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.endLineNumber, this.model.getLineMaxColumn(modelRange.endLineNumber))); - r.range = new Range(start.lineNumber, start.column, end.lineNumber, end.column); + const options = modelDecoration.options; + let viewRange: Range; + if (options.isWholeLine) { + const start = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.startLineNumber, 1)); + const end = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.endLineNumber, this.model.getLineMaxColumn(modelRange.endLineNumber))); + viewRange = new Range(start.lineNumber, start.column, end.lineNumber, end.column); } else { - r.range = this._coordinatesConverter.convertModelRangeToViewRange(modelRange); + viewRange = this._coordinatesConverter.convertModelRangeToViewRange(modelRange); } + r = new ViewModelDecoration(viewRange, options); + this._decorationsCache[id] = r; } return r; } diff --git a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts index 0f52c5129c333..20bbda92be7e8 100644 --- a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts @@ -91,23 +91,23 @@ suite('ViewModelDecorations', () => { let actualDecorations = viewModel.getDecorationsInViewport( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)) ).map((dec) => { - return dec.source.id; + return dec.options.className; }); assert.deepEqual(actualDecorations, [ - dec2, - dec3, - dec4, - dec5, - dec6, - dec7, - dec8, - dec9, - dec10, - dec11, - dec12, - dec13, - dec14, + 'dec2', + 'dec3', + 'dec4', + 'dec5', + 'dec6', + 'dec7', + 'dec8', + 'dec9', + 'dec10', + 'dec11', + 'dec12', + 'dec13', + 'dec14', ]); let inlineDecorations1 = viewModel.getViewLineRenderingData( From f2d47bcc49f69310f5b27e0419a69409a3cc0e9a Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 13:07:50 +0200 Subject: [PATCH 57/64] Improve message --- src/vs/editor/contrib/find/browser/findWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index 726c2451eeff8..7cb6a00b5f0a7 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -48,7 +48,7 @@ const NLS_REPLACE_INPUT_PLACEHOLDER = nls.localize('placeholder.replace', "Repla const NLS_REPLACE_BTN_LABEL = nls.localize('label.replaceButton', "Replace"); const NLS_REPLACE_ALL_BTN_LABEL = nls.localize('label.replaceAllButton', "Replace All"); const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace mode"); -const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Only the first 999 results are highlighted, but all find operations work on the entire text."); +const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Only the first {0} results are highlighted, but all find operations work on the entire text.", MATCHES_LIMIT); const NLS_MATCHES_LOCATION = nls.localize('label.matchesLocation', "{0} of {1}"); const NLS_NO_RESULTS = nls.localize('label.noResults', "No Results"); From 9073cc1c4d13059ef7b18a91c64cbb41022c3644 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 13:10:03 +0200 Subject: [PATCH 58/64] Bump find limit to 20k given all the perf work around decorations --- src/vs/editor/contrib/find/common/findModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/find/common/findModel.ts b/src/vs/editor/contrib/find/common/findModel.ts index 1816df33d59de..dda6ca2f533bd 100644 --- a/src/vs/editor/contrib/find/common/findModel.ts +++ b/src/vs/editor/contrib/find/common/findModel.ts @@ -67,7 +67,7 @@ export const FIND_IDS = { ShowNextFindTermAction: 'find.history.showNext' }; -export const MATCHES_LIMIT = 999; +export const MATCHES_LIMIT = 19999; export class FindModelBoundToEditorModel { From 93157c291bc55e901c81e05dc1d8e93fad9104a7 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 13:26:22 +0200 Subject: [PATCH 59/64] Avoid flickering by painting the overview ruler with colors in the same order --- .../viewParts/overviewRuler/decorationsOverviewRuler2.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts index 288c6803f2bb8..1ed0e160f44b5 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts @@ -314,7 +314,13 @@ export class DecorationsOverviewRuler2 extends ViewPart { const x = this._settings.x; const w = this._settings.w; - for (let color in decorations) { + // Avoid flickering by always rendering the colors in the same order + // colors that don't use transparency will be sorted last (they start with #) + const colors = Object.keys(decorations); + colors.sort(); + for (let cIndex = 0, cLen = colors.length; cIndex < cLen; cIndex++) { + const color = colors[cIndex]; + const colorDecorations = decorations[color]; canvasCtx.fillStyle = color; From b45c8b857898fee4422a730f840caaa1452e8e93 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 13:27:53 +0200 Subject: [PATCH 60/64] Renames --- src/vs/editor/browser/view/viewImpl.ts | 4 ++-- ...corationsOverviewRuler2.ts => decorationsOverviewRuler.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/vs/editor/browser/viewParts/overviewRuler/{decorationsOverviewRuler2.ts => decorationsOverviewRuler.ts} (99%) diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index 8e14fe9b1ff70..1a53e3d98148e 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -30,7 +30,7 @@ import { Margin } from 'vs/editor/browser/viewParts/margin/margin'; import { LinesDecorationsOverlay } from 'vs/editor/browser/viewParts/linesDecorations/linesDecorations'; import { MarginViewLineDecorationsOverlay } from 'vs/editor/browser/viewParts/marginDecorations/marginDecorations'; import { ViewOverlayWidgets } from 'vs/editor/browser/viewParts/overlayWidgets/overlayWidgets'; -import { DecorationsOverviewRuler2 } from 'vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2'; +import { DecorationsOverviewRuler } from 'vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler'; import { OverviewRuler } from 'vs/editor/browser/viewParts/overviewRuler/overviewRuler'; import { Rulers } from 'vs/editor/browser/viewParts/rulers/rulers'; import { ScrollDecorationViewPart } from 'vs/editor/browser/viewParts/scrollDecoration/scrollDecoration'; @@ -170,7 +170,7 @@ export class View extends ViewEventHandler { this.viewParts.push(this.viewZones); // Decorations overview ruler - let decorationsOverviewRuler = new DecorationsOverviewRuler2(this._context); + let decorationsOverviewRuler = new DecorationsOverviewRuler(this._context); this.viewParts.push(decorationsOverviewRuler); diff --git a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts similarity index 99% rename from src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts rename to src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts index 1ed0e160f44b5..5add770087e43 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler2.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/decorationsOverviewRuler.ts @@ -193,7 +193,7 @@ const enum OverviewRulerLane { Full = 7 } -export class DecorationsOverviewRuler2 extends ViewPart { +export class DecorationsOverviewRuler extends ViewPart { private readonly _tokensColorTrackerListener: IDisposable; private readonly _domNode: FastDomNode; From 3c2db906995a6f982cc1fffcf3ff0ce8f8da49d4 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 13:31:49 +0200 Subject: [PATCH 61/64] More overflow checks --- src/vs/editor/common/model/intervalTree.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 8e5e123164a13..676eaa9a30904 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -69,6 +69,13 @@ const enum Constants { * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values */ MIN_SAFE_DELTA = -(1 << 30), + /** + * MAX SMI (SMall Integer) as defined in v8. + * one bit is lost for boxing/unboxing flag. + * one bit is lost for sign flag. + * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values + */ + MAX_SAFE_DELTA = 1 << 30, } function getNodeColor(node: IntervalNode): NodeColor { @@ -617,7 +624,7 @@ function noOverlapReplace(T: IntervalTree, start: number, end: number, textLengt node.start += editDelta; node.end += editDelta; node.delta += editDelta; - if (node.delta < Constants.MIN_SAFE_DELTA) { + if (node.delta < Constants.MIN_SAFE_DELTA || node.delta > Constants.MAX_SAFE_DELTA) { T.requestNormalizeDelta = true; } // cover case a) from above @@ -1025,7 +1032,7 @@ function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { // x's delta is no longer influenced by z's delta x.delta += z.delta; - if (x.delta < Constants.MIN_SAFE_DELTA) { + if (x.delta < Constants.MIN_SAFE_DELTA || x.delta > Constants.MAX_SAFE_DELTA) { T.requestNormalizeDelta = true; } x.start += z.delta; @@ -1045,14 +1052,14 @@ function rbTreeDelete(T: IntervalTree, z: IntervalNode): void { x.start += y.delta; x.end += y.delta; x.delta += y.delta; - if (x.delta < Constants.MIN_SAFE_DELTA) { + if (x.delta < Constants.MIN_SAFE_DELTA || x.delta > Constants.MAX_SAFE_DELTA) { T.requestNormalizeDelta = true; } y.start += z.delta; y.end += z.delta; y.delta = z.delta; - if (y.delta < Constants.MIN_SAFE_DELTA) { + if (y.delta < Constants.MIN_SAFE_DELTA || y.delta > Constants.MAX_SAFE_DELTA) { T.requestNormalizeDelta = true; } } @@ -1215,7 +1222,7 @@ function leftRotate(T: IntervalTree, x: IntervalNode): void { const y = x.right; // set y. y.delta += x.delta; // y's delta is no longer influenced by x's delta - if (y.delta < Constants.MIN_SAFE_DELTA) { + if (y.delta < Constants.MIN_SAFE_DELTA || y.delta > Constants.MAX_SAFE_DELTA) { T.requestNormalizeDelta = true; } y.start += x.delta; @@ -1245,7 +1252,7 @@ function rightRotate(T: IntervalTree, y: IntervalNode): void { const x = y.left; y.delta -= x.delta; - if (y.delta < Constants.MIN_SAFE_DELTA) { + if (y.delta < Constants.MIN_SAFE_DELTA || y.delta > Constants.MAX_SAFE_DELTA) { T.requestNormalizeDelta = true; } y.start -= x.delta; From a0982eb0713d3fd4a944401453ddfcf80071e6ac Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 13:33:25 +0200 Subject: [PATCH 62/64] Avoid needless checks --- src/vs/editor/common/model/intervalTree.ts | 32 ++++++---------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index 676eaa9a30904..c07fdd33ba225 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -399,9 +399,7 @@ function normalizeDelta(T: IntervalTree): void { node = node.parent; } - if (T.root) { - setNodeIsVisited(T.root, false); - } + setNodeIsVisited(T.root, false); } //#endregion @@ -569,9 +567,7 @@ function searchForEditing(T: IntervalTree, start: number, end: number): Interval } } - if (T.root) { - setNodeIsVisited(T.root, false); - } + setNodeIsVisited(T.root, false); return result; } @@ -643,9 +639,7 @@ function noOverlapReplace(T: IntervalTree, start: number, end: number, textLengt } } - if (T.root) { - setNodeIsVisited(T.root, false); - } + setNodeIsVisited(T.root, false); } //#endregion @@ -681,9 +675,7 @@ function nodeCount(T: IntervalTree): number { } } - if (T.root) { - setNodeIsVisited(T.root, false); - } + setNodeIsVisited(T.root, false); return count; } @@ -721,9 +713,7 @@ function collectNodesFromOwner(T: IntervalTree, ownerId: number): IntervalNode[] } } - if (T.root) { - setNodeIsVisited(T.root, false); - } + setNodeIsVisited(T.root, false); return result; } @@ -758,9 +748,7 @@ function collectNodesPostOrder(T: IntervalTree): IntervalNode[] { setNodeIsVisited(node, true); } - if (T.root) { - setNodeIsVisited(T.root, false); - } + setNodeIsVisited(T.root, false); return result; } @@ -817,9 +805,7 @@ function search(T: IntervalTree, filterOwnerId: number, filterOutValidation: boo } } - if (T.root) { - setNodeIsVisited(T.root, false); - } + setNodeIsVisited(T.root, false); return result; } @@ -906,9 +892,7 @@ function intervalSearch(T: IntervalTree, intervalStart: number, intervalEnd: num } } - if (T.root) { - setNodeIsVisited(T.root, false); - } + setNodeIsVisited(T.root, false); return result; } From 3212c452e4104d08889753a2399b72359f32d00f Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 14:54:30 +0200 Subject: [PATCH 63/64] Increase slow test timeout --- src/vs/editor/test/common/controller/cursor.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/test/common/controller/cursor.test.ts b/src/vs/editor/test/common/controller/cursor.test.ts index e21b52a667a5a..8b8a9bd46c46a 100644 --- a/src/vs/editor/test/common/controller/cursor.test.ts +++ b/src/vs/editor/test/common/controller/cursor.test.ts @@ -1651,7 +1651,8 @@ suite('Editor Controller - Regression tests', () => { }); }); - test('issue #23913: Greater than 1000+ multi cursor typing replacement text appears inverted, lines begin to drop off selection', () => { + test('issue #23913: Greater than 1000+ multi cursor typing replacement text appears inverted, lines begin to drop off selection', function () { + this.timeout(10000); const LINE_CNT = 2000; let text = []; From 057714ea5cd35d22b7c329eb34e4613adcb61f27 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 20 Oct 2017 15:15:34 +0200 Subject: [PATCH 64/64] SelectionHighlighter should not highlight the exact same matches as the find widget --- .../contrib/find/common/findController.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/vs/editor/contrib/find/common/findController.ts b/src/vs/editor/contrib/find/common/findController.ts index 09536d1f5f82e..43ea3992e6854 100644 --- a/src/vs/editor/contrib/find/common/findController.ts +++ b/src/vs/editor/contrib/find/common/findController.ts @@ -1038,6 +1038,23 @@ export class SelectionHighlighter extends Disposable implements editorCommon.IEd } } + // Return early if the find widget shows the exact same matches + if (findState.isRevealed) { + let findStateSearchString = findState.searchString; + if (!caseSensitive) { + findStateSearchString = findStateSearchString.toLowerCase(); + } + + let mySearchString = r.searchText; + if (!caseSensitive) { + mySearchString = mySearchString.toLowerCase(); + } + + if (findStateSearchString === mySearchString && r.matchCase === findState.matchCase && r.wholeWord === findState.wholeWord && !findState.isRegex) { + return null; + } + } + return new SelectionHighlighterState(lastWordUnderCursor, r.searchText, r.matchCase, r.wholeWord ? editor.getConfiguration().wordSeparators : null); }