Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add PositionTracker #919

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/PositionTracker.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { PositionTracker } from "./PositionTracker";

describe("PositionTracker", () => {
it("should track positions", () => {
const tracker = new PositionTracker();

tracker.push("asdf\nasdf\nfdsa");
expect(tracker.getPosition(1)).toEqual({ line: 1, column: 2 });
expect(tracker.getPosition(3)).toEqual({ line: 1, column: 4 });
expect(tracker.getPosition(4)).toEqual({ line: 2, column: 1 });
expect(tracker.getPosition(5)).toEqual({ line: 2, column: 2 });

expect(tracker.getPosition(10)).toEqual({ line: 3, column: 2 });
});

it("should track positions across multiple chunks", () => {
const tracker = new PositionTracker();

tracker.push("asdf\nas");
tracker.push("df\nfdsa");
expect(tracker.getPosition(1)).toEqual({ line: 1, column: 2 });
expect(tracker.getPosition(3)).toEqual({ line: 1, column: 4 });
expect(tracker.getPosition(4)).toEqual({ line: 2, column: 1 });
expect(tracker.getPosition(5)).toEqual({ line: 2, column: 2 });
expect(tracker.getPosition(10)).toEqual({ line: 3, column: 2 });
});

it("should throw an error if the index is smaller than the previous one", () => {
const tracker = new PositionTracker();

expect(tracker.getPosition(5)).toEqual({ line: 1, column: 6 });
expect(() => tracker.getPosition(4)).toThrow(
"Indices must monotonically increase"
);
});
});
79 changes: 79 additions & 0 deletions src/PositionTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
export interface Position {
line: number;
column: number;
}

/** Allows getting a line/column position for a given index. */
export class PositionTracker {
/** The last index passed to `getPosition`. */
private lastIndex = 0;
/** The line number at the last index. */
private line = 1;
/** The column of the last index. */
private column = 1;

/** The index of the next newline character in the current buffer. */
private nextNewLine = -1;
/** The buffer currently being processed. */
private currentBuffer = "";
/** The offset of the current buffer in the total input. */
private currentBufferOffset = 0;
/** Queue of buffers that haven't been processed yet. */
private readonly nextBuffers: string[] = [];

/**
* Gets line and column for a given index.
*
* @param index The index to get the position for. Must be greater or equal to the last index passed to `getPosition`.
* @returns The position of the given index.
*/
public getPosition(index: number): Position {
if (index < this.lastIndex) {
throw new Error("Indices must monotonically increase");
}

while (
this.nextNewLine > 0 &&
index >= this.nextNewLine + this.currentBufferOffset
) {
this.column = 1;
this.line++;
this.lastIndex = this.nextNewLine + this.currentBufferOffset;
this.getNextNewLine();
}

this.column += index - this.lastIndex;
this.lastIndex = index;
return { line: this.line, column: this.column };
}

/** Push a new buffer onto the queue. */
public push(chunk: string): void {
// If we don't have a newline, we won't have any next buffers.
if (this.nextNewLine < 0) {
// Replace the current buffer, and get the next newline index.
this.currentBufferOffset += this.currentBuffer.length;
this.currentBuffer = chunk;
this.getNextNewLine();
} else {
this.nextBuffers.push(chunk);
}
}

/** Calculate the next newline index. */
private getNextNewLine(): void {
this.nextNewLine = this.currentBuffer.indexOf(
"\n",
this.nextNewLine + 1
);

if (this.nextNewLine < 0) {
const next = this.nextBuffers.shift();

if (next == null) return;
this.currentBufferOffset += this.currentBuffer.length;
this.currentBuffer = next;
this.getNextNewLine();
}
}
}