Skip to content

Commit 880e369

Browse files
In memory text editor improvements (#2526)
Added more tests. Fixed a bug with positionAt using crlf. General cleanup and simplification ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [/] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [/] I have not broken the cheatsheet
1 parent eb5dee6 commit 880e369

File tree

4 files changed

+49
-30
lines changed

4 files changed

+49
-30
lines changed

packages/common/src/ide/inMemoryTextDocument/InMemoryTextDocument.ts

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -59,48 +59,43 @@ export class InMemoryTextDocument implements TextDocument {
5959
lineAt(lineOrPosition: number | Position): TextLine {
6060
const value =
6161
typeof lineOrPosition === "number" ? lineOrPosition : lineOrPosition.line;
62-
const index = Math.min(Math.max(value, 0), this.lineCount - 1);
62+
const index = clamp(value, 0, this.lineCount - 1);
6363
return this._lines[index];
6464
}
6565

6666
offsetAt(position: Position): number {
67-
if (position.isBefore(this._lines[0].range.start)) {
67+
if (position.line < 0) {
6868
return 0;
6969
}
70-
if (position.isAfter(this._lines.at(-1)!.range.end)) {
70+
if (position.line > this._lines.length - 1) {
7171
return this._text.length;
7272
}
7373

74-
let offset = 0;
74+
const line = this._lines[position.line];
7575

76-
for (const line of this._lines) {
77-
if (position.line === line.lineNumber) {
78-
return offset + Math.min(position.character, line.range.end.character);
79-
}
80-
offset += line.text.length + line.eolLength;
81-
}
82-
83-
throw Error(`Couldn't find offset for position ${position}`);
76+
return line.offset + clamp(position.character, 0, line.text.length);
8477
}
8578

8679
positionAt(offset: number): Position {
87-
if (offset < 0) {
88-
return this._lines[0].range.start;
80+
if (offset <= 0) {
81+
return this.range.start;
8982
}
9083
if (offset >= this._text.length) {
91-
return this._lines.at(-1)!.range.end;
84+
return this.range.end;
9285
}
9386

94-
let currentOffset = 0;
87+
const line = this._lines.find(
88+
(line) => offset < line.offset + line.lengthIncludingEol,
89+
);
9590

96-
for (const line of this._lines) {
97-
if (currentOffset + line.text.length >= offset) {
98-
return new Position(line.lineNumber, offset - currentOffset);
99-
}
100-
currentOffset += line.text.length + line.eolLength;
91+
if (line == null) {
92+
throw Error(`Couldn't find line for offset ${offset}`);
10193
}
10294

103-
throw Error(`Couldn't find position for offset ${offset}`);
95+
return new Position(
96+
line.lineNumber,
97+
Math.min(offset - line.offset, line.text.length),
98+
);
10499
}
105100

106101
getText(range?: Range): string {
@@ -122,16 +117,22 @@ export class InMemoryTextDocument implements TextDocument {
122117
function createLines(text: string): InMemoryTextLine[] {
123118
const documentParts = text.split(/(\r?\n)/g);
124119
const result: InMemoryTextLine[] = [];
120+
let offset = 0;
125121

126122
for (let i = 0; i < documentParts.length; i += 2) {
127-
result.push(
128-
new InMemoryTextLine(
129-
result.length,
130-
documentParts[i],
131-
documentParts[i + 1],
132-
),
123+
const line = new InMemoryTextLine(
124+
result.length,
125+
offset,
126+
documentParts[i],
127+
documentParts[i + 1],
133128
);
129+
result.push(line);
130+
offset += line.lengthIncludingEol;
134131
}
135132

136133
return result;
137134
}
135+
136+
function clamp(value: number, min: number, max: number): number {
137+
return Math.min(Math.max(value, min), max);
138+
}

packages/common/src/ide/inMemoryTextDocument/InMemoryTextLine.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ export class InMemoryTextLine implements TextLine {
99
readonly firstNonWhitespaceCharacterIndex: number;
1010
readonly lastNonWhitespaceCharacterIndex: number;
1111
readonly isEmptyOrWhitespace: boolean;
12-
readonly eolLength: number;
12+
readonly lengthIncludingEol: number;
1313

1414
constructor(
1515
public lineNumber: number,
16+
public offset: number,
1617
public text: string,
1718
eol: string | undefined,
1819
) {
1920
this.isEmptyOrWhitespace = /^\s*$/.test(text);
20-
this.eolLength = eol?.length ?? 0;
21+
this.lengthIncludingEol = text.length + (eol?.length ?? 0);
2122
const start = new Position(lineNumber, 0);
2223
const end = new Position(lineNumber, text.length);
2324
const endIncludingLineBreak =

packages/common/src/ide/inMemoryTextDocument/test/InMemoryTextDocument.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ suite("InMemoryTextDocument", () => {
6262
assert.equal(document.offsetAt(new Position(0, 0)), 0);
6363
assert.equal(document.offsetAt(new Position(0, 7)), 7);
6464
assert.equal(document.offsetAt(new Position(0, 100)), 7);
65+
assert.equal(document.offsetAt(new Position(1, -100)), 8);
6566
assert.equal(document.offsetAt(new Position(1, 0)), 8);
6667
assert.equal(document.offsetAt(new Position(1, 2)), 10);
6768
assert.equal(document.offsetAt(new Position(2, 0)), 17);
@@ -74,11 +75,21 @@ suite("InMemoryTextDocument", () => {
7475

7576
assert.equal(document.positionAt(-1), "0:0");
7677
assert.equal(document.positionAt(0).toString(), "0:0");
78+
assert.equal(document.positionAt(6).toString(), "0:6");
7779
assert.equal(document.positionAt(7).toString(), "0:7");
7880
assert.equal(document.positionAt(8).toString(), "1:0");
7981
assert.equal(document.positionAt(10).toString(), "1:2");
8082
assert.equal(document.positionAt(17).toString(), "2:0");
8183
assert.equal(document.positionAt(document.text.length).toString(), "2:0");
8284
assert.equal(document.positionAt(100).toString(), "2:0");
8385
});
86+
87+
test("positionAt CRLF", () => {
88+
const document = createTestDocument("a\r\nb");
89+
90+
assert.equal(document.positionAt(0).toString(), "0:0");
91+
assert.equal(document.positionAt(1).toString(), "0:1");
92+
assert.equal(document.positionAt(2).toString(), "0:1");
93+
assert.equal(document.positionAt(3).toString(), "1:0");
94+
});
8495
});

packages/common/src/ide/inMemoryTextDocument/test/InMemoryTextDocumentEdit.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,23 @@ suite("InMemoryTextDocument.edit", () => {
99
const document = createTestDocument(text);
1010
document.edit([{ range: new Range(0, 0, 0, 5), text: "goodbye" }]);
1111

12+
assert.equal(document.version, 1);
1213
assert.equal(document.text, "goodbye\nworld");
1314
});
1415

1516
test("remove", () => {
1617
const document = createTestDocument(text);
1718
document.edit([{ range: new Range(0, 0, 1, 0), text: "" }]);
1819

20+
assert.equal(document.version, 1);
1921
assert.equal(document.text, "world");
2022
});
2123

2224
test("insert", () => {
2325
const document = createTestDocument(text);
2426
document.edit([{ range: new Range(0, 5, 0, 5), text: "!" }]);
2527

28+
assert.equal(document.version, 1);
2629
assert.equal(document.text, "hello!\nworld");
2730
});
2831

@@ -35,6 +38,7 @@ suite("InMemoryTextDocument.edit", () => {
3538
{ range: new Range(0, 0, 0, 0), text: "ccc" },
3639
]);
3740

41+
assert.equal(document.version, 1);
3842
assert.equal(document.text, "aaabbbccc");
3943

4044
assert.equal(changes[0].range.toString(), "0:0-0:0");
@@ -53,6 +57,7 @@ suite("InMemoryTextDocument.edit", () => {
5357
{ range: new Range(0, 0, 0, 5), text: "goodbye" },
5458
]);
5559

60+
assert.equal(document.version, 1);
5661
assert.equal(document.text, "goodbye!\norld");
5762
});
5863

@@ -63,6 +68,7 @@ suite("InMemoryTextDocument.edit", () => {
6368
{ range: new Range(0, 1, 1, 1), text: "" },
6469
]);
6570

71+
assert.equal(document.version, 1);
6672
assert.equal(document.text, "orld");
6773
assert.equal(changes.length, 1);
6874
assert.equal(changes[0].range.toString(), "0:0-1:1");

0 commit comments

Comments
 (0)