line counts, line iterators, line spans
This commit is contained in:
parent
a439f15c5f
commit
8f1c35b982
1 changed files with 64 additions and 0 deletions
64
src/index.ts
64
src/index.ts
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue