Skip to content

Commit

Permalink
add TextDocument tests for invalid inputs (#1286)
Browse files Browse the repository at this point in the history
* add TextDocument tests for invalid inputs

* TextDocument.positionAt and offsetAt incorrectly include line endings

* polish

---------

Co-authored-by: Dirk Bäumer <dirkb@microsoft.com>
Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
  • Loading branch information
3 people committed Jul 30, 2024
1 parent 046423e commit 04d7162
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 28 deletions.
22 changes: 20 additions & 2 deletions textDocument/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ class FullTextDocument implements TextDocument {
// low is the least x for which the line offset is larger than the current offset
// or array.length if no line offset is larger than the current offset
const line = low - 1;

offset = this.ensureBeforeEOL(offset, lineOffsets[line]);
return { line, character: offset - lineOffsets[line] };
}

Expand All @@ -308,8 +310,20 @@ class FullTextDocument implements TextDocument {
return 0;
}
const lineOffset = lineOffsets[position.line];
if (position.character <= 0) {
return lineOffset;
}

const nextLineOffset = (position.line + 1 < lineOffsets.length) ? lineOffsets[position.line + 1] : this._content.length;
return Math.max(Math.min(lineOffset + position.character, nextLineOffset), lineOffset);
const offset = Math.min(lineOffset + position.character, nextLineOffset);
return this.ensureBeforeEOL(offset, lineOffset);
}

private ensureBeforeEOL(offset: number, lineOffset: number): number {
while (offset > lineOffset && isEOL(this._content.charCodeAt(offset - 1))) {
offset--;
}
return offset;
}

public get lineCount() {
Expand Down Expand Up @@ -438,7 +452,7 @@ function computeLineOffsets(text: string, isAtLineStart: boolean, textOffset = 0
const result: number[] = isAtLineStart ? [textOffset] : [];
for (let i = 0; i < text.length; i++) {
const ch = text.charCodeAt(i);
if (ch === CharCode.CarriageReturn || ch === CharCode.LineFeed) {
if (isEOL(ch)) {
if (ch === CharCode.CarriageReturn && i + 1 < text.length && text.charCodeAt(i + 1) === CharCode.LineFeed) {
i++;
}
Expand All @@ -448,6 +462,10 @@ function computeLineOffsets(text: string, isAtLineStart: boolean, textOffset = 0
return result;
}

function isEOL(char: number) {
return char === CharCode.CarriageReturn || char === CharCode.LineFeed;
}

function getWellformedRange(range: Range): Range {
const start = range.start;
const end = range.end;
Expand Down
86 changes: 60 additions & 26 deletions textDocument/src/test/textdocument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,23 +63,32 @@ suite('Text Document Lines Model Validator', () => {
});

test('New line characters', () => {
let str = 'ABCDE\rFGHIJ';
assert.equal(newDocument(str).lineCount, 2);
let document = newDocument('ABCDE\rFGHIJ');
assert.equal(document.lineCount, 2);
assert.equal(document.offsetAt(Positions.create(1, 0)), 6);

str = 'ABCDE\nFGHIJ';
assert.equal(newDocument(str).lineCount, 2);
document = newDocument('ABCDE\nFGHIJ');
assert.equal(document.lineCount, 2);
assert.equal(document.offsetAt(Positions.create(1, 0)), 6);

str = 'ABCDE\r\nFGHIJ';
assert.equal(newDocument(str).lineCount, 2);
document = newDocument('ABCDE\r\nFGHIJ');
assert.equal(document.lineCount, 2);
assert.equal(document.offsetAt(Positions.create(1, 0)), 7);

str = 'ABCDE\n\nFGHIJ';
assert.equal(newDocument(str).lineCount, 3);
document = newDocument('ABCDE\n\nFGHIJ');
assert.equal(document.lineCount, 3);
assert.equal(document.offsetAt(Positions.create(1, 0)), 6);
assert.equal(document.offsetAt(Positions.create(2, 0)), 7);

str = 'ABCDE\r\rFGHIJ';
assert.equal(newDocument(str).lineCount, 3);
document = newDocument('ABCDE\r\rFGHIJ');
assert.equal(document.lineCount, 3);
assert.equal(document.offsetAt(Positions.create(1, 0)), 6);
assert.equal(document.offsetAt(Positions.create(2, 0)), 7);

str = 'ABCDE\n\rFGHIJ';
assert.equal(newDocument(str).lineCount, 3);
document = newDocument('ABCDE\n\rFGHIJ');
assert.equal(document.lineCount, 3);
assert.equal(document.offsetAt(Positions.create(1, 0)), 6);
assert.equal(document.offsetAt(Positions.create(2, 0)), 7);
});

test('getText(Range)', () => {
Expand All @@ -94,22 +103,47 @@ suite('Text Document Lines Model Validator', () => {
assert.equal(document.getText(Ranges.create(0, 0, 3, 5)), str);
});

test('Invalid inputs', () => {
const str = 'Hello World';
const document = newDocument(str);
test('Invalid inputs at beginning of file', () => {
let document = newDocument('ABCDE');
assert.equal(document.offsetAt(Positions.create(-1, 0)), 0);
assert.equal(document.offsetAt(Positions.create(0, -1)), 0);
assert.deepEqual(document.positionAt(-1), Positions.create(0, 0));
});

// invalid position
assert.equal(document.offsetAt(Positions.create(0, str.length)), str.length);
assert.equal(document.offsetAt(Positions.create(0, str.length + 3)), str.length);
assert.equal(document.offsetAt(Positions.create(2, 3)), str.length);
assert.equal(document.offsetAt(Positions.create(-1, 3)), 0);
assert.equal(document.offsetAt(Positions.create(0, -3)), 0);
assert.equal(document.offsetAt(Positions.create(1, -3)), str.length);
test('Invalid inputs at end of file', () => {
let str = 'ABCDE\n';
let document = newDocument(str);
assert.equal(document.offsetAt(Positions.create(1, 1)), str.length);
assert.equal(document.offsetAt(Positions.create(2, 0)), str.length);
assert.deepEqual(document.positionAt(str.length), Positions.create(1, 0));
assert.deepEqual(document.positionAt(str.length + 3), Positions.create(1, 0));

str = 'ABCDE';
document = newDocument(str);
assert.equal(document.offsetAt(Positions.create(0, 10)), str.length);
assert.equal(document.offsetAt(Positions.create(1, 1)), str.length);
assert.deepEqual(document.positionAt(str.length), Positions.create(0, 5));
assert.deepEqual(document.positionAt(str.length + 3), Positions.create(0, 5));
});

// invalid offsets
assert.deepEqual(document.positionAt(-1), Positions.create(0, 0));
assert.deepEqual(document.positionAt(str.length), Positions.create(0, str.length));
assert.deepEqual(document.positionAt(str.length + 3), Positions.create(0, str.length));
test('Invalid inputs at beginning of line', () => {
let document = newDocument('A\nB\rC\r\nD');
assert.equal(document.offsetAt(Positions.create(0, -1)), 0);
assert.equal(document.offsetAt(Positions.create(1, -1)), 2);
assert.equal(document.offsetAt(Positions.create(2, -1)), 4);
assert.equal(document.offsetAt(Positions.create(3, -1)), 7);
});

test('Invalid inputs at end of line', () => {
let document = newDocument('A\nB\rC\r\nD');
assert.equal(document.offsetAt(Positions.create(0, 10)), 1);
assert.equal(document.offsetAt(Positions.create(1, 10)), 3);
assert.equal(document.offsetAt(Positions.create(2, 2)), 5); // between \r and \n
assert.equal(document.offsetAt(Positions.create(2, 3)), 5);
assert.equal(document.offsetAt(Positions.create(2, 10)), 5);
assert.equal(document.offsetAt(Positions.create(3, 10)), 8);

assert.deepEqual(document.positionAt(6), Positions.create(2, 1)); // between \r and \n
});
});

Expand Down

0 comments on commit 04d7162

Please sign in to comment.