line counts, line iterators, line spans

This commit is contained in:
Yura Dupyn 2026-04-06 17:56:04 +02:00
parent a439f15c5f
commit 8f1c35b982

View file

@ -61,6 +61,10 @@ export class SourceText {
return this.chars.length; return this.chars.length;
} }
get lineCount(): number {
return this.lineStarts.length;
}
sliceByCp(start: number, end: number): string { sliceByCp(start: number, end: number): string {
const startRef = this.chars[start]; const startRef = this.chars[start];
// Handle out of bounds gracefully // Handle out of bounds gracefully
@ -74,6 +78,31 @@ export class SourceText {
return this.source.slice(startOff, endOff); return this.source.slice(startOff, endOff);
} }
// Returns a Span for the given line (1-based index).
// If stripNewlines is true, the span will exclude trailing \r\n.
getLineSpan(line: number, stripNewlines = true): Span {
const range = this.getLineRange(line);
let endIdx = range.end;
if (stripNewlines && endIdx > range.start) {
// Look at the character just before endIdx
const lastChar = this.chars[endIdx - 1].char;
if (lastChar === NEW_LINE) {
endIdx--;
if (endIdx > range.start && this.chars[endIdx - 1].char === CARRIAGE_RETURN) {
endIdx--;
}
} else if (lastChar === CARRIAGE_RETURN) {
endIdx--;
}
}
return {
start: this.getLocation(range.start),
end: this.getLocation(endIdx)
};
}
// Converts a linear Code Point Index into a SourceLocation (line, column, index). // Converts a linear Code Point Index into a SourceLocation (line, column, index).
getLocation(index: CodePointIndex): SourceLocation { getLocation(index: CodePointIndex): SourceLocation {
// Does binary search. // Does binary search.
@ -147,6 +176,11 @@ export function sourceText(s: string): SourceText {
return new SourceText(s); return new SourceText(s);
} }
// Creates a Span from two SourceLocations.
export function span(start: SourceLocation, end: SourceLocation): Span {
return { start, end };
}
export class SourceRegion { export class SourceRegion {
constructor( constructor(
public readonly source: SourceText, public readonly source: SourceText,
@ -157,10 +191,40 @@ export class SourceRegion {
return this.span.end.index - this.span.start.index; return this.span.end.index - this.span.start.index;
} }
get lineCount(): number {
return this.span.end.line - this.span.start.line + 1;
}
toString(): string { toString(): string {
return this.source.sliceByCp(this.span.start.index, this.span.end.index); return this.source.sliceByCp(this.span.start.index, this.span.end.index);
} }
// Returns a Span for the given line (1-based index).
getLineSpan(line: number, stripNewlines = true): Span {
if (line < this.span.start.line || line > this.span.end.line) {
throw new Error(`Line ${line} is outside of region lines ${this.span.start.line}-${this.span.end.line}`);
}
return this.source.getLineSpan(line, stripNewlines);
}
// Iterates over all lines that intersect this region.
// Yields a Span for each line.
*lines(stripNewlines = true): IterableIterator<Span> {
const startLine = this.span.start.line;
const endLine = this.span.end.line;
for (let currentLine = startLine; currentLine <= endLine; currentLine++) {
yield this.getLineSpan(currentLine, stripNewlines);
}
}
forEachLine(callback: (span: Span, lineNo: number) => void, stripNewlines = true): void {
let lineNo = this.span.start.line;
for (const lineSpan of this.lines(stripNewlines)) {
callback(lineSpan, lineNo++);
}
}
// Creates a sub-region within this region. // Creates a sub-region within this region.
// Validates that the new span is contained within the current region. // Validates that the new span is contained within the current region.
subRegion(span: Span): SourceRegion { subRegion(span: Span): SourceRegion {