Compare commits
2 commits
9939f6c97f
...
8f1c35b982
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f1c35b982 | ||
|
|
a439f15c5f |
1 changed files with 107 additions and 11 deletions
118
src/index.ts
118
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.
|
||||||
|
|
@ -97,6 +126,22 @@ export class SourceText {
|
||||||
return { index, line, column };
|
return { index, line, column };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creates a SourceRegion from a Span.
|
||||||
|
makeRegion(span: Span): SourceRegion {
|
||||||
|
// Basic validation
|
||||||
|
if (span.start.index < 0 || span.end.index > this.length) {
|
||||||
|
throw new Error(`Span out of bounds: ${span.start.index}-${span.end.index} (length: ${this.length})`);
|
||||||
|
}
|
||||||
|
return new SourceRegion(this, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a SourceRegion covering the entire SourceText.
|
||||||
|
fullRegion(): SourceRegion {
|
||||||
|
const start = this.getLocation(0);
|
||||||
|
const end = this.getLocation(this.length);
|
||||||
|
return this.makeRegion({ start, end });
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the full text of a specific line (1-based index)
|
// Returns the full text of a specific line (1-based index)
|
||||||
getLineText(line: number): string {
|
getLineText(line: number): string {
|
||||||
const lineIndex = line - 1;
|
const lineIndex = line - 1;
|
||||||
|
|
@ -131,18 +176,64 @@ export function sourceText(s: string): SourceText {
|
||||||
return new SourceText(s);
|
return new SourceText(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SourceRegion {
|
// Creates a Span from two SourceLocations.
|
||||||
// TODO
|
export function span(start: SourceLocation, end: SourceLocation): Span {
|
||||||
readonly sourceText: SourceText;
|
return { start, end };
|
||||||
readonly span: Span;
|
|
||||||
|
|
||||||
constructor(sourceText: SourceText, span: Span) {
|
|
||||||
this.sourceText = sourceText;
|
|
||||||
// TODO: What about span validation?
|
|
||||||
this.span = span;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SourceRegion {
|
||||||
|
constructor(
|
||||||
|
public readonly source: SourceText,
|
||||||
|
public readonly span: Span
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.span.end.index - this.span.start.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lineCount(): number {
|
||||||
|
return this.span.end.line - this.span.start.line + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
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.
|
||||||
|
// Validates that the new span is contained within the current region.
|
||||||
|
subRegion(span: Span): SourceRegion {
|
||||||
|
if (span.start.index < this.span.start.index || span.end.index > this.span.end.index) {
|
||||||
|
throw new Error(`Sub-region span ${span.start.index}-${span.end.index} is not within parent region ${this.span.start.index}-${this.span.end.index}`);
|
||||||
|
}
|
||||||
|
return this.source.makeRegion(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type Span = {
|
export type Span = {
|
||||||
start: SourceLocation;
|
start: SourceLocation;
|
||||||
|
|
@ -190,7 +281,12 @@ export type LineView = {
|
||||||
underline: string; // The literal "^^^" string for CLI usage
|
underline: string; // The literal "^^^" string for CLI usage
|
||||||
};
|
};
|
||||||
|
|
||||||
export function renderSpan(text: SourceText, span: Span, contextLines = 1): LineView[] {
|
export function renderRegion(region: SourceRegion, contextLines = 1): LineView[] {
|
||||||
|
return renderSpan(region, region.span, contextLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSpan(region: SourceRegion, span: Span, contextLines = 1): LineView[] {
|
||||||
|
const text = region.source;
|
||||||
const views: LineView[] = [];
|
const views: LineView[] = [];
|
||||||
|
|
||||||
// Determine range of lines to show (including context)
|
// Determine range of lines to show (including context)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue