Skip to content

Commit 49068d8

Browse files
authored
Merge pull request microsoft#165916 from microsoft/hediet/bracket-pair-perf-improvements
Hediet/bracket-pair-perf-improvements
2 parents f1c9243 + 34f4baf commit 49068d8

File tree

13 files changed

+468
-72
lines changed

13 files changed

+468
-72
lines changed

src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { BugIndicatingError } from 'vs/base/common/errors';
67
import { CursorColumns } from 'vs/editor/common/core/cursorColumns';
78
import { BracketKind } from 'vs/editor/common/languages/supports/languageBracketsConfiguration';
89
import { ITextModel } from 'vs/editor/common/model';
@@ -125,7 +126,7 @@ export class PairAstNode extends BaseAstNode {
125126
* Avoid using this property, it allocates an array!
126127
*/
127128
public get children() {
128-
const result = new Array<AstNode>();
129+
const result: AstNode[] = [];
129130
result.push(this.openingBracket);
130131
if (this.child) {
131132
result.push(this.child);
@@ -295,10 +296,19 @@ export abstract class ListAstNode extends BaseAstNode {
295296
return false;
296297
}
297298

299+
if (this.childrenLength === 0) {
300+
// Don't reuse empty lists.
301+
return false;
302+
}
303+
298304
let lastChild: ListAstNode = this;
299-
let lastLength: number;
300-
while (lastChild.kind === AstNodeKind.List && (lastLength = lastChild.childrenLength) > 0) {
301-
lastChild = lastChild.getChild(lastLength! - 1) as ListAstNode;
305+
while (lastChild.kind === AstNodeKind.List) {
306+
const lastLength = lastChild.childrenLength;
307+
if (lastLength === 0) {
308+
// Empty lists should never be contained in other lists.
309+
throw new BugIndicatingError();
310+
}
311+
lastChild = lastChild.getChild(lastLength - 1) as ListAstNode;
302312
}
303313

304314
return lastChild.canBeReused(openBracketIds);
@@ -324,7 +334,7 @@ export abstract class ListAstNode extends BaseAstNode {
324334
}
325335

326336
public flattenLists(): ListAstNode {
327-
const items = new Array<AstNode>();
337+
const items: AstNode[] = [];
328338
for (const c of this.children) {
329339
const normalized = c.flattenLists();
330340
if (normalized.kind === AstNodeKind.List) {

src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export class BeforeEditPositionMapper {
2626
*/
2727
constructor(
2828
edits: readonly TextEditInfo[],
29-
private readonly documentLength: Length,
3029
) {
3130
this.edits = edits.map(edit => TextEditInfoCache.from(edit));
3231
}
@@ -41,12 +40,16 @@ export class BeforeEditPositionMapper {
4140

4241
/**
4342
* @param offset Must be equal to or greater than the last offset this method has been called with.
43+
* Returns null if there is no edit anymore.
4444
*/
45-
getDistanceToNextChange(offset: Length): Length {
45+
getDistanceToNextChange(offset: Length): Length | null {
4646
this.adjustNextEdit(offset);
4747

4848
const nextEdit = this.edits[this.nextEditIdx];
49-
const nextChangeOffset = nextEdit ? this.translateOldToCur(nextEdit.offsetObj) : this.documentLength;
49+
const nextChangeOffset = nextEdit ? this.translateOldToCur(nextEdit.offsetObj) : null;
50+
if (nextChangeOffset === null) {
51+
return null;
52+
}
5053

5154
return lengthDiffNonNegative(offset, nextChangeOffset);
5255
}

src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { FastTokenizer, TextBufferTokenizer } from './tokenizer';
2121
import { BackgroundTokenizationState } from 'vs/editor/common/tokenizationTextModelPart';
2222
import { Position } from 'vs/editor/common/core/position';
2323
import { CallbackIterable } from 'vs/base/common/arrays';
24+
import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos';
2425

2526
export class BracketPairsTree extends Disposable {
2627
private readonly didChangeEmitter = new Emitter<void>();
@@ -45,6 +46,8 @@ export class BracketPairsTree extends Disposable {
4546
}
4647

4748
public readonly onDidChange = this.didChangeEmitter.event;
49+
private queuedTextEditsForInitialAstWithoutTokens: TextEditInfo[] = [];
50+
private queuedTextEdits: TextEditInfo[] = [];
4851

4952
public constructor(
5053
private readonly textModel: TextModel,
@@ -90,7 +93,9 @@ export class BracketPairsTree extends Disposable {
9093
toLength(r.toLineNumber - r.fromLineNumber + 1, 0)
9194
)
9295
);
93-
this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens, false);
96+
97+
this.handleEdits(edits, true);
98+
9499
if (!this.initialAstWithoutTokens) {
95100
this.didChangeEmitter.fire();
96101
}
@@ -106,14 +111,34 @@ export class BracketPairsTree extends Disposable {
106111
);
107112
}).reverse();
108113

109-
this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens, false);
110-
if (this.initialAstWithoutTokens) {
111-
this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(edits, this.initialAstWithoutTokens, false);
114+
this.handleEdits(edits, false);
115+
}
116+
117+
private handleEdits(edits: TextEditInfo[], tokenChange: boolean): void {
118+
// Lazily queue the edits and only apply them when the tree is accessed.
119+
const result = combineTextEditInfos(this.queuedTextEdits, edits);
120+
121+
this.queuedTextEdits = result;
122+
if (this.initialAstWithoutTokens && !tokenChange) {
123+
this.queuedTextEditsForInitialAstWithoutTokens = combineTextEditInfos(this.queuedTextEditsForInitialAstWithoutTokens, edits);
112124
}
113125
}
114126

115127
//#endregion
116128

129+
private flushQueue() {
130+
if (this.queuedTextEdits.length > 0) {
131+
this.astWithTokens = this.parseDocumentFromTextBuffer(this.queuedTextEdits, this.astWithTokens, false);
132+
this.queuedTextEdits = [];
133+
}
134+
if (this.queuedTextEditsForInitialAstWithoutTokens.length > 0) {
135+
if (this.initialAstWithoutTokens) {
136+
this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(this.queuedTextEditsForInitialAstWithoutTokens, this.initialAstWithoutTokens, false);
137+
}
138+
this.queuedTextEditsForInitialAstWithoutTokens = [];
139+
}
140+
}
141+
117142
/**
118143
* @pure (only if isPure = true)
119144
*/
@@ -127,6 +152,8 @@ export class BracketPairsTree extends Disposable {
127152
}
128153

129154
public getBracketsInRange(range: Range): CallbackIterable<BracketInfo> {
155+
this.flushQueue();
156+
130157
const startOffset = toLength(range.startLineNumber - 1, range.startColumn - 1);
131158
const endOffset = toLength(range.endLineNumber - 1, range.endColumn - 1);
132159
return new CallbackIterable(cb => {
@@ -136,6 +163,8 @@ export class BracketPairsTree extends Disposable {
136163
}
137164

138165
public getBracketPairsInRange(range: Range, includeMinIndentation: boolean): CallbackIterable<BracketPairWithMinIndentationInfo> {
166+
this.flushQueue();
167+
139168
const startLength = positionToLength(range.getStartPosition());
140169
const endLength = positionToLength(range.getEndPosition());
141170

@@ -147,11 +176,15 @@ export class BracketPairsTree extends Disposable {
147176
}
148177

149178
public getFirstBracketAfter(position: Position): IFoundBracket | null {
179+
this.flushQueue();
180+
150181
const node = this.initialAstWithoutTokens || this.astWithTokens!;
151182
return getFirstBracketAfter(node, lengthZero, node.length, positionToLength(position));
152183
}
153184

154185
public getFirstBracketBefore(position: Position): IFoundBracket | null {
186+
this.flushQueue();
187+
155188
const node = this.initialAstWithoutTokens || this.astWithTokens!;
156189
return getFirstBracketBefore(node, lengthZero, node.length, positionToLength(position));
157190
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { ArrayQueue } from 'vs/base/common/arrays';
7+
import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper';
8+
import { Length, lengthAdd, lengthDiffNonNegative, lengthEquals, lengthIsZero, lengthLessThanEqual, lengthZero, sumLengths } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length';
9+
10+
export function combineTextEditInfos(textEditInfoFirst: TextEditInfo[], textEditInfoSecond: TextEditInfo[]): TextEditInfo[] {
11+
if (textEditInfoFirst.length === 0) {
12+
return textEditInfoSecond;
13+
}
14+
15+
// s0: State before any edits
16+
const firstMap = new ArrayQueue(toTextMap(textEditInfoFirst));
17+
// s1: State after first edit, but before second edit
18+
const secondMap = toTextMap(textEditInfoSecond);
19+
// s2: State after both edits
20+
21+
// If set, we are in an edit
22+
let remainingS0Length: Length | undefined = undefined;
23+
let remainingS1Length: Length = lengthZero;
24+
25+
/**
26+
* @param s1Length Use undefined for length "infinity"
27+
*/
28+
function readPartialS0Map(s1Length: Length | undefined): TextMapping[] {
29+
const result: TextMapping[] = [];
30+
31+
while (true) {
32+
if ((remainingS0Length !== undefined && !lengthIsZero(remainingS0Length)) || !lengthIsZero(remainingS1Length)) {
33+
let readS1Length: Length;
34+
if (s1Length !== undefined && lengthLessThanEqual(s1Length, remainingS1Length)) {
35+
// remaining satisfies request
36+
readS1Length = s1Length;
37+
remainingS1Length = lengthDiffNonNegative(s1Length, remainingS1Length);
38+
s1Length = lengthZero;
39+
} else {
40+
// Read all of remaining, potentially even more
41+
readS1Length = remainingS1Length;
42+
if (s1Length !== undefined) {
43+
s1Length = lengthDiffNonNegative(remainingS1Length, s1Length);
44+
}
45+
remainingS1Length = lengthZero;
46+
}
47+
48+
if (remainingS0Length === undefined) {
49+
// unchanged area
50+
result.push({
51+
oldLength: readS1Length,
52+
newLength: undefined
53+
});
54+
} else {
55+
// We eagerly consume all of the old length, even if
56+
// we are in an edit and only consume it partially.
57+
result.push({
58+
oldLength: remainingS0Length,
59+
newLength: readS1Length
60+
});
61+
remainingS0Length = lengthZero;
62+
}
63+
}
64+
65+
if (s1Length !== undefined && lengthIsZero(s1Length)) {
66+
break;
67+
}
68+
69+
const item = firstMap.dequeue();
70+
if (!item) {
71+
if (s1Length !== undefined) {
72+
result.push({
73+
oldLength: s1Length,
74+
newLength: undefined,
75+
});
76+
}
77+
break;
78+
}
79+
if (item.newLength === undefined) {
80+
remainingS1Length = item.oldLength;
81+
remainingS0Length = undefined;
82+
} else {
83+
remainingS0Length = item.oldLength;
84+
remainingS1Length = item.newLength;
85+
}
86+
}
87+
88+
return result;
89+
}
90+
91+
const result: TextEditInfo[] = [];
92+
93+
function push(startOffset: Length, endOffset: Length, newLength: Length) {
94+
if (result.length > 0 && lengthEquals(result[result.length - 1].endOffset, startOffset)) {
95+
const lastResult = result[result.length - 1];
96+
result[result.length - 1] = new TextEditInfo(lastResult.startOffset, endOffset, lengthAdd(lastResult.newLength, newLength));
97+
} else {
98+
result.push({ startOffset, endOffset, newLength });
99+
}
100+
}
101+
102+
let s0offset = lengthZero;
103+
for (const s2 of secondMap) {
104+
const s0ToS1Map = readPartialS0Map(s2.oldLength);
105+
if (s2.newLength !== undefined) {
106+
// This is an edit
107+
const s0Length = sumLengths(s0ToS1Map, s => s.oldLength);
108+
const s0EndOffset = lengthAdd(s0offset, s0Length);
109+
push(s0offset, s0EndOffset, s2.newLength);
110+
s0offset = s0EndOffset;
111+
} else {
112+
// We are in an unchanged area
113+
for (const s1 of s0ToS1Map) {
114+
const s0startOffset = s0offset;
115+
s0offset = lengthAdd(s0offset, s1.oldLength);
116+
117+
if (s1.newLength !== undefined) {
118+
push(s0startOffset, s0offset, s1.newLength);
119+
}
120+
}
121+
}
122+
}
123+
124+
const s0ToS1Map = readPartialS0Map(undefined);
125+
for (const s1 of s0ToS1Map) {
126+
const s0startOffset = s0offset;
127+
s0offset = lengthAdd(s0offset, s1.oldLength);
128+
129+
if (s1.newLength !== undefined) {
130+
push(s0startOffset, s0offset, s1.newLength);
131+
}
132+
}
133+
134+
return result;
135+
}
136+
137+
interface TextMapping {
138+
oldLength: Length;
139+
140+
/**
141+
* If set, this mapping represents an edit.
142+
* If not set, this mapping represents an unchanged region (for which the new length equals the old length).
143+
*/
144+
newLength?: Length;
145+
}
146+
147+
function toTextMap(textEditInfos: TextEditInfo[]): TextMapping[] {
148+
const result: TextMapping[] = [];
149+
let lastOffset = lengthZero;
150+
for (const textEditInfo of textEditInfos) {
151+
const spaceLength = lengthDiffNonNegative(lastOffset, textEditInfo.startOffset);
152+
if (!lengthIsZero(spaceLength)) {
153+
result.push({ oldLength: spaceLength });
154+
}
155+
156+
const oldLength = lengthDiffNonNegative(textEditInfo.startOffset, textEditInfo.endOffset);
157+
result.push({ oldLength, newLength: textEditInfo.newLength });
158+
lastOffset = textEditInfo.endOffset;
159+
}
160+
return result;
161+
}

src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/concat23Trees.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function concat(node1: AstNode, node2: AstNode): AstNode {
108108
function append(list: ListAstNode, nodeToAppend: AstNode): AstNode {
109109
list = list.toMutable() as ListAstNode;
110110
let curNode: AstNode = list;
111-
const parents = new Array<ListAstNode>();
111+
const parents: ListAstNode[] = [];
112112
let nodeToAppendOfCorrectHeight: AstNode | undefined;
113113
while (true) {
114114
// assert nodeToInsert.listHeight <= curNode.listHeight
@@ -157,7 +157,7 @@ function append(list: ListAstNode, nodeToAppend: AstNode): AstNode {
157157
function prepend(list: ListAstNode, nodeToAppend: AstNode): AstNode {
158158
list = list.toMutable() as ListAstNode;
159159
let curNode: AstNode = list;
160-
const parents = new Array<ListAstNode>();
160+
const parents: ListAstNode[] = [];
161161
// assert nodeToInsert.listHeight <= curNode.listHeight
162162
while (nodeToAppend.listHeight !== curNode.listHeight) {
163163
// assert 0 <= nodeToInsert.listHeight < curNode.listHeight

src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,12 @@ export function lengthIsZero(length: Length): boolean {
100100
/*
101101
* We have 52 bits available in a JS number.
102102
* We use the upper 26 bits to store the line and the lower 26 bits to store the column.
103-
*
104-
* Set boolean to `true` when debugging, so that debugging is easier.
105103
*/
106-
const factor = /* is debug: */ false ? 100000 : 2 ** 26;
104+
///*
105+
const factor = 2 ** 26;
106+
/*/
107+
const factor = 1000000;
108+
// */
107109

108110
export function toLength(lineCount: number, columnCount: number): Length {
109111
// llllllllllllllllllllllllllcccccccccccccccccccccccccc (52 bits)
@@ -138,9 +140,17 @@ export function lengthGetColumnCountIfZeroLineCount(length: Length): number {
138140
// [10 lines, 5 cols] + [20 lines, 3 cols] = [30 lines, 3 cols]
139141
export function lengthAdd(length1: Length, length2: Length): Length;
140142
export function lengthAdd(l1: any, l2: any): Length {
141-
return ((l2 < factor)
142-
? (l1 + l2) // l2 is the amount of columns (zero line count). Keep the column count from l1.
143-
: (l1 - (l1 % factor) + l2)); // l1 - (l1 % factor) equals toLength(l1.lineCount, 0)
143+
let r = l1 + l2;
144+
if (l2 >= factor) { r = r - (l1 % factor); }
145+
return r;
146+
}
147+
148+
export function sumLengths<T>(items: readonly T[], lengthFn: (item: T) => Length): Length {
149+
return items.reduce((a, b) => lengthAdd(a, lengthFn(b)), lengthZero);
150+
}
151+
152+
export function lengthEquals(length1: Length, length2: Length): boolean {
153+
return length1 === length2;
144154
}
145155

146156
/**

0 commit comments

Comments
 (0)