Skip to content

Commit 4d299d8

Browse files
authored
(perf) cache lineoffsets when mapping between position/offset (#1311)
1 parent 8cd1495 commit 4d299d8

File tree

6 files changed

+78
-85
lines changed

6 files changed

+78
-85
lines changed

packages/language-server/src/lib/documents/Document.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export class Document extends WritableDocument {
6969
setText(text: string) {
7070
this.content = text;
7171
this.version++;
72+
this.lineOffsets = undefined;
7273
this.updateDocInfo();
7374
}
7475

packages/language-server/src/lib/documents/DocumentBase.ts

Lines changed: 14 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { clamp } from '../../utils';
21
import { Position, TextDocument } from 'vscode-languageserver';
2+
import { getLineOffsets, offsetAt, positionAt } from './utils';
33

44
/**
55
* Represents a textual document.
@@ -25,6 +25,11 @@ export abstract class ReadableDocument implements TextDocument {
2525
*/
2626
public version = 0;
2727

28+
/**
29+
* Should be cleared when there's an update to the text
30+
*/
31+
protected lineOffsets?: number[];
32+
2833
/**
2934
* Get the length of the document's content
3035
*/
@@ -37,74 +42,22 @@ export abstract class ReadableDocument implements TextDocument {
3742
* @param offset The index of the position
3843
*/
3944
positionAt(offset: number): Position {
40-
offset = clamp(offset, 0, this.getTextLength());
41-
42-
const lineOffsets = this.getLineOffsets();
43-
let low = 0;
44-
let high = lineOffsets.length;
45-
if (high === 0) {
46-
return Position.create(0, offset);
47-
}
48-
49-
while (low < high) {
50-
const mid = Math.floor((low + high) / 2);
51-
if (lineOffsets[mid] > offset) {
52-
high = mid;
53-
} else {
54-
low = mid + 1;
55-
}
56-
}
57-
58-
// low is the least x for which the line offset is larger than the current offset
59-
// or array.length if no line offset is larger than the current offset
60-
const line = low - 1;
61-
return Position.create(line, offset - lineOffsets[line]);
45+
return positionAt(offset, this.getText(), this.getLineOffsets());
6246
}
6347

6448
/**
6549
* Get the index of the line and character position
6650
* @param position Line and character position
6751
*/
6852
offsetAt(position: Position): number {
69-
const lineOffsets = this.getLineOffsets();
70-
71-
if (position.line >= lineOffsets.length) {
72-
return this.getTextLength();
73-
} else if (position.line < 0) {
74-
return 0;
75-
}
76-
77-
const lineOffset = lineOffsets[position.line];
78-
const nextLineOffset =
79-
position.line + 1 < lineOffsets.length
80-
? lineOffsets[position.line + 1]
81-
: this.getTextLength();
82-
83-
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
53+
return offsetAt(position, this.getText(), this.getLineOffsets());
8454
}
8555

8656
private getLineOffsets() {
87-
const lineOffsets = [];
88-
const text = this.getText();
89-
let isLineStart = true;
90-
91-
for (let i = 0; i < text.length; i++) {
92-
if (isLineStart) {
93-
lineOffsets.push(i);
94-
isLineStart = false;
95-
}
96-
const ch = text.charAt(i);
97-
isLineStart = ch === '\r' || ch === '\n';
98-
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
99-
i++;
100-
}
101-
}
102-
103-
if (isLineStart && text.length > 0) {
104-
lineOffsets.push(text.length);
57+
if (!this.lineOffsets) {
58+
this.lineOffsets = getLineOffsets(this.getText());
10559
}
106-
107-
return lineOffsets;
60+
return this.lineOffsets;
10861
}
10962

11063
/**
@@ -126,7 +79,8 @@ export abstract class ReadableDocument implements TextDocument {
12679
*/
12780
export abstract class WritableDocument extends ReadableDocument {
12881
/**
129-
* Set the text content of the document
82+
* Set the text content of the document.
83+
* Implementers should set `lineOffsets` to `undefined` here.
13084
* @param text The new text content
13185
*/
13286
abstract setText(text: string): void;
@@ -138,6 +92,7 @@ export abstract class WritableDocument extends ReadableDocument {
13892
* @param end End offset of the new text
13993
*/
14094
update(text: string, start: number, end: number): void {
95+
this.lineOffsets = undefined;
14196
const content = this.getText();
14297
this.setText(content.slice(0, start) + text + content.slice(end));
14398
}

packages/language-server/src/lib/documents/DocumentMapper.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
TextEdit,
1414
InsertReplaceEdit
1515
} from 'vscode-languageserver';
16-
import { TagInformation, offsetAt, positionAt } from './utils';
16+
import { TagInformation, offsetAt, positionAt, getLineOffsets } from './utils';
1717
import { SourceMapConsumer } from 'source-map';
1818
import { Logger } from '../../logger';
1919

@@ -90,28 +90,35 @@ export class IdentityMapper implements DocumentMapper {
9090
* Maps positions in a fragment relative to a parent.
9191
*/
9292
export class FragmentMapper implements DocumentMapper {
93+
private lineOffsetsOriginal = getLineOffsets(this.originalText);
94+
private lineOffsetsGenerated = getLineOffsets(this.tagInfo.content);
95+
9396
constructor(
9497
private originalText: string,
9598
private tagInfo: TagInformation,
9699
private url: string
97100
) {}
98101

99102
getOriginalPosition(generatedPosition: Position): Position {
100-
const parentOffset = this.offsetInParent(offsetAt(generatedPosition, this.tagInfo.content));
101-
return positionAt(parentOffset, this.originalText);
103+
const parentOffset = this.offsetInParent(
104+
offsetAt(generatedPosition, this.tagInfo.content, this.lineOffsetsGenerated)
105+
);
106+
return positionAt(parentOffset, this.originalText, this.lineOffsetsOriginal);
102107
}
103108

104109
private offsetInParent(offset: number): number {
105110
return this.tagInfo.start + offset;
106111
}
107112

108113
getGeneratedPosition(originalPosition: Position): Position {
109-
const fragmentOffset = offsetAt(originalPosition, this.originalText) - this.tagInfo.start;
110-
return positionAt(fragmentOffset, this.tagInfo.content);
114+
const fragmentOffset =
115+
offsetAt(originalPosition, this.originalText, this.lineOffsetsOriginal) -
116+
this.tagInfo.start;
117+
return positionAt(fragmentOffset, this.tagInfo.content, this.lineOffsetsGenerated);
111118
}
112119

113120
isInGenerated(pos: Position): boolean {
114-
const offset = offsetAt(pos, this.originalText);
121+
const offset = offsetAt(pos, this.originalText, this.lineOffsetsOriginal);
115122
return offset >= this.tagInfo.start && offset <= this.tagInfo.end;
116123
}
117124

packages/language-server/src/lib/documents/utils.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -172,23 +172,31 @@ export function extractTemplateTag(source: string, html?: HTMLDocument): TagInfo
172172
* Get the line and character based on the offset
173173
* @param offset The index of the position
174174
* @param text The text for which the position should be retrived
175+
* @param lineOffsets number Array with offsets for each line. Computed if not given
175176
*/
176-
export function positionAt(offset: number, text: string): Position {
177+
export function positionAt(
178+
offset: number,
179+
text: string,
180+
lineOffsets = getLineOffsets(text)
181+
): Position {
177182
offset = clamp(offset, 0, text.length);
178183

179-
const lineOffsets = getLineOffsets(text);
180184
let low = 0;
181185
let high = lineOffsets.length;
182186
if (high === 0) {
183187
return Position.create(0, offset);
184188
}
185189

186-
while (low < high) {
190+
while (low <= high) {
187191
const mid = Math.floor((low + high) / 2);
188-
if (lineOffsets[mid] > offset) {
189-
high = mid;
190-
} else {
192+
const lineOffset = lineOffsets[mid];
193+
194+
if (lineOffset === offset) {
195+
return Position.create(mid, 0);
196+
} else if (offset > lineOffset) {
191197
low = mid + 1;
198+
} else {
199+
high = mid - 1;
192200
}
193201
}
194202

@@ -202,10 +210,13 @@ export function positionAt(offset: number, text: string): Position {
202210
* Get the offset of the line and character position
203211
* @param position Line and character position
204212
* @param text The text for which the offset should be retrived
213+
* @param lineOffsets number Array with offsets for each line. Computed if not given
205214
*/
206-
export function offsetAt(position: Position, text: string): number {
207-
const lineOffsets = getLineOffsets(text);
208-
215+
export function offsetAt(
216+
position: Position,
217+
text: string,
218+
lineOffsets = getLineOffsets(text)
219+
): number {
209220
if (position.line >= lineOffsets.length) {
210221
return text.length;
211222
} else if (position.line < 0) {
@@ -219,7 +230,7 @@ export function offsetAt(position: Position, text: string): number {
219230
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
220231
}
221232

222-
function getLineOffsets(text: string) {
233+
export function getLineOffsets(text: string) {
223234
const lineOffsets = [];
224235
let isLineStart = true;
225236

packages/language-server/src/plugins/svelte/features/getCodeActions/getQuickfixes.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import {
1111
TextDocumentEdit,
1212
TextEdit
1313
} from 'vscode-languageserver';
14-
import { mapObjWithRangeToOriginal, offsetAt, positionAt } from '../../../../lib/documents';
14+
import {
15+
getLineOffsets,
16+
mapObjWithRangeToOriginal,
17+
offsetAt,
18+
positionAt
19+
} from '../../../../lib/documents';
1520
import { getIndent, pathToUrl } from '../../../../utils';
1621
import { SvelteDocument } from '../../SvelteDocument';
1722
import ts from 'typescript';
@@ -89,26 +94,28 @@ async function getSvelteIgnoreEdit(svelteDoc: SvelteDocument, ast: Ast, diagnost
8994
} = diagnostic;
9095
const transpiled = await svelteDoc.getTranspiled();
9196
const content = transpiled.getText();
97+
const lineOffsets = getLineOffsets(content);
9298
const { html } = ast;
9399
const generatedStart = transpiled.getGeneratedPosition(start);
94100
const generatedEnd = transpiled.getGeneratedPosition(end);
95101

96-
const diagnosticStartOffset = offsetAt(generatedStart, transpiled.getText());
97-
const diagnosticEndOffset = offsetAt(generatedEnd, transpiled.getText());
102+
const diagnosticStartOffset = offsetAt(generatedStart, content, lineOffsets);
103+
const diagnosticEndOffset = offsetAt(generatedEnd, content, lineOffsets);
98104
const offsetRange: ts.TextRange = {
99105
pos: diagnosticStartOffset,
100106
end: diagnosticEndOffset
101107
};
102108

103109
const node = findTagForRange(html, offsetRange);
104110

105-
const nodeStartPosition = positionAt(node.start, content);
111+
const nodeStartPosition = positionAt(node.start, content, lineOffsets);
106112
const nodeLineStart = offsetAt(
107113
{
108114
line: nodeStartPosition.line,
109115
character: 0
110116
},
111-
transpiled.getText()
117+
content,
118+
lineOffsets
112119
);
113120
const afterStartLineStart = content.slice(nodeLineStart);
114121
const indent = getIndent(afterStartLineStart);

packages/language-server/src/plugins/typescript/DocumentSnapshot.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
offsetAt,
1111
positionAt,
1212
TagInformation,
13-
isInTag
13+
isInTag,
14+
getLineOffsets
1415
} from '../../lib/documents';
1516
import { pathToUrl } from '../../utils';
1617
import { ConsumerDocumentMapper } from './DocumentMapper';
@@ -313,6 +314,7 @@ export class JSOrTSDocumentSnapshot
313314
{
314315
scriptKind = getScriptKindFromFileName(this.filePath);
315316
scriptInfo = null;
317+
private lineOffsets?: number[];
316318

317319
constructor(public version: number, public readonly filePath: string, private text: string) {
318320
super(pathToUrl(filePath));
@@ -335,11 +337,11 @@ export class JSOrTSDocumentSnapshot
335337
}
336338

337339
positionAt(offset: number) {
338-
return positionAt(offset, this.text);
340+
return positionAt(offset, this.text, this.getLineOffsets());
339341
}
340342

341343
offsetAt(position: Position): number {
342-
return offsetAt(position, this.text);
344+
return offsetAt(position, this.text, this.getLineOffsets());
343345
}
344346

345347
async getFragment() {
@@ -365,6 +367,14 @@ export class JSOrTSDocumentSnapshot
365367
}
366368

367369
this.version++;
370+
this.lineOffsets = undefined;
371+
}
372+
373+
private getLineOffsets() {
374+
if (!this.lineOffsets) {
375+
this.lineOffsets = getLineOffsets(this.text);
376+
}
377+
return this.lineOffsets;
368378
}
369379
}
370380

@@ -373,6 +383,8 @@ export class JSOrTSDocumentSnapshot
373383
* to generated snapshot positions and vice versa.
374384
*/
375385
export class SvelteSnapshotFragment implements SnapshotFragment {
386+
private lineOffsets = getLineOffsets(this.text);
387+
376388
constructor(
377389
private readonly mapper: DocumentMapper,
378390
public readonly text: string,
@@ -409,11 +421,11 @@ export class SvelteSnapshotFragment implements SnapshotFragment {
409421
}
410422

411423
positionAt(offset: number) {
412-
return positionAt(offset, this.text);
424+
return positionAt(offset, this.text, this.lineOffsets);
413425
}
414426

415427
offsetAt(position: Position) {
416-
return offsetAt(position, this.text);
428+
return offsetAt(position, this.text, this.lineOffsets);
417429
}
418430

419431
/**

0 commit comments

Comments
 (0)