2022-08-28 21:12:25 +02:00
|
|
|
|
2022-09-08 23:32:25 +02:00
|
|
|
import { describe } from "yargs";
|
2022-09-09 20:02:35 +02:00
|
|
|
import { TypeKind, type Type, type TArrow, TRecord } from "./checker";
|
2022-08-31 13:29:56 +02:00
|
|
|
import { Syntax, SyntaxKind, TextFile, TextPosition, TextRange, Token } from "./cst";
|
2022-09-09 20:18:51 +02:00
|
|
|
import { countDigits, IndentWriter } from "./util";
|
2022-08-31 13:29:56 +02:00
|
|
|
|
|
|
|
const ANSI_RESET = "\u001b[0m"
|
|
|
|
const ANSI_BOLD = "\u001b[1m"
|
|
|
|
const ANSI_UNDERLINE = "\u001b[4m"
|
|
|
|
const ANSI_REVERSED = "\u001b[7m"
|
|
|
|
|
|
|
|
const ANSI_FG_BLACK = "\u001b[30m"
|
|
|
|
const ANSI_FG_RED = "\u001b[31m"
|
|
|
|
const ANSI_FG_GREEN = "\u001b[32m"
|
|
|
|
const ANSI_FG_YELLOW = "\u001b[33m"
|
|
|
|
const ANSI_FG_BLUE = "\u001b[34m"
|
|
|
|
const ANSI_FG_CYAN = "\u001b[35m"
|
|
|
|
const ANSI_FG_MAGENTA = "\u001b[36m"
|
|
|
|
const ANSI_FG_WHITE = "\u001b[37m"
|
|
|
|
|
|
|
|
const ANSI_BG_BLACK = "\u001b[40m"
|
|
|
|
const ANSI_BG_RED = "\u001b[41m"
|
|
|
|
const ANSI_BG_GREEN = "\u001b[42m"
|
|
|
|
const ANSI_BG_YELLOW = "\u001b[43m"
|
|
|
|
const ANSI_BG_BLUE = "\u001b[44m"
|
|
|
|
const ANSI_BG_CYAN = "\u001b[45m"
|
|
|
|
const ANSI_BG_MAGENTA = "\u001b[46m"
|
|
|
|
const ANSI_BG_WHITE = "\u001b[47m"
|
|
|
|
|
2022-09-08 15:58:08 +02:00
|
|
|
const enum Level {
|
|
|
|
Debug,
|
|
|
|
Verbose,
|
|
|
|
Info,
|
|
|
|
Warning,
|
|
|
|
Error,
|
|
|
|
Fatal,
|
|
|
|
}
|
|
|
|
|
2022-08-28 21:12:25 +02:00
|
|
|
export class UnexpectedCharDiagnostic {
|
|
|
|
|
2022-09-08 15:58:08 +02:00
|
|
|
public readonly level = Level.Error;
|
|
|
|
|
2022-08-28 21:12:25 +02:00
|
|
|
public constructor(
|
2022-08-31 13:29:56 +02:00
|
|
|
public file: TextFile,
|
|
|
|
public position: TextPosition,
|
2022-08-28 21:12:25 +02:00
|
|
|
public actual: string,
|
|
|
|
) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-09 20:18:51 +02:00
|
|
|
public format(out: IndentWriter): void {
|
2022-08-31 13:29:56 +02:00
|
|
|
const endPos = this.position.clone();
|
|
|
|
endPos.advance(this.actual);
|
2022-09-09 20:18:51 +02:00
|
|
|
out.write(ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET);
|
|
|
|
out.write(`unexpeced character sequence '${this.actual}'.\n\n`);
|
|
|
|
out.write(printExcerpt(this.file, new TextRange(this.position, endPos)) + '\n');
|
2022-08-31 13:29:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-01 20:06:43 +02:00
|
|
|
const DESCRIPTIONS: Partial<Record<SyntaxKind, string>> = {
|
2022-08-31 13:29:56 +02:00
|
|
|
[SyntaxKind.StringLiteral]: 'a string literal',
|
|
|
|
[SyntaxKind.Identifier]: "an identifier",
|
|
|
|
[SyntaxKind.Comma]: "','",
|
|
|
|
[SyntaxKind.Colon]: "':'",
|
|
|
|
[SyntaxKind.Integer]: "an integer",
|
|
|
|
[SyntaxKind.LParen]: "'('",
|
|
|
|
[SyntaxKind.RParen]: "')'",
|
|
|
|
[SyntaxKind.LBrace]: "'{'",
|
|
|
|
[SyntaxKind.RBrace]: "'}'",
|
|
|
|
[SyntaxKind.LBracket]: "'['",
|
|
|
|
[SyntaxKind.RBracket]: "']'",
|
2022-09-10 14:11:04 +02:00
|
|
|
[SyntaxKind.IdentifierAlt]: 'an identifier starting with an uppercase letter',
|
2022-08-31 13:29:56 +02:00
|
|
|
[SyntaxKind.ConstantExpression]: 'a constant expression',
|
|
|
|
[SyntaxKind.ReferenceExpression]: 'a reference expression',
|
|
|
|
[SyntaxKind.LineFoldEnd]: 'the end of the current line-fold',
|
|
|
|
[SyntaxKind.TupleExpression]: 'a tuple expression such as (1, 2)',
|
|
|
|
[SyntaxKind.ReferenceExpression]: 'a reference to some variable',
|
|
|
|
[SyntaxKind.NestedExpression]: 'an expression nested with parentheses',
|
|
|
|
[SyntaxKind.ConstantExpression]: 'a constant expression such as 1 or "foo"',
|
|
|
|
[SyntaxKind.NamedTupleExpression]: 'a named tuple expression',
|
|
|
|
[SyntaxKind.StructExpression]: 'a struct expression',
|
2022-08-31 13:45:46 +02:00
|
|
|
[SyntaxKind.BlockStart]: 'the start of an indented block',
|
|
|
|
[SyntaxKind.BlockEnd]: 'the end of an indented block',
|
|
|
|
[SyntaxKind.LineFoldEnd]: 'the end of the current line-fold',
|
|
|
|
[SyntaxKind.EndOfFile]: 'end-of-file',
|
2022-08-31 13:29:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function describeSyntaxKind(kind: SyntaxKind): string {
|
|
|
|
const desc = DESCRIPTIONS[kind];
|
|
|
|
if (desc === undefined) {
|
|
|
|
throw new Error(`Could not describe SyntaxKind '${kind}'`);
|
|
|
|
}
|
|
|
|
return desc
|
|
|
|
}
|
|
|
|
|
|
|
|
function describeExpected(expected: SyntaxKind[]) {
|
|
|
|
if (expected.length === 0) {
|
|
|
|
return 'nothing';
|
|
|
|
}
|
|
|
|
let out = describeSyntaxKind(expected[0]);
|
|
|
|
if (expected.length === 1) {
|
2022-08-28 21:12:25 +02:00
|
|
|
return out;
|
|
|
|
}
|
2022-08-31 13:29:56 +02:00
|
|
|
for (let i = 1; i < expected.length-1; i++) {
|
|
|
|
const kind = expected[i];
|
|
|
|
out += ', ' + describeSyntaxKind(kind);
|
|
|
|
}
|
|
|
|
out += ' or ' + describeSyntaxKind(expected[expected.length-1])
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
2022-08-31 13:45:46 +02:00
|
|
|
function describeActual(token: Token): string {
|
|
|
|
switch (token.kind) {
|
|
|
|
case SyntaxKind.BlockStart:
|
|
|
|
case SyntaxKind.BlockEnd:
|
|
|
|
case SyntaxKind.LineFoldEnd:
|
|
|
|
case SyntaxKind.EndOfFile:
|
|
|
|
return describeSyntaxKind(token.kind);
|
|
|
|
default:
|
|
|
|
return `'${token.text}'`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-31 13:29:56 +02:00
|
|
|
export class UnexpectedTokenDiagnostic {
|
|
|
|
|
2022-09-08 15:58:08 +02:00
|
|
|
public readonly level = Level.Error;
|
|
|
|
|
2022-08-31 13:29:56 +02:00
|
|
|
public constructor(
|
|
|
|
public file: TextFile,
|
|
|
|
public actual: Token,
|
|
|
|
public expected: SyntaxKind[],
|
|
|
|
) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-09 20:18:51 +02:00
|
|
|
public format(out: IndentWriter): void {
|
|
|
|
out.write(ANSI_FG_RED + ANSI_BOLD + 'fatal: ' + ANSI_RESET);
|
|
|
|
out.write(`expected ${describeExpected(this.expected)} but got ${describeActual(this.actual)}\n\n`);
|
|
|
|
out.write(printExcerpt(this.file, this.actual.getRange()) + '\n');
|
2022-08-31 13:29:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
export class BindingNotFoudDiagnostic {
|
|
|
|
|
2022-09-08 15:58:08 +02:00
|
|
|
public readonly level = Level.Error;
|
|
|
|
|
2022-08-31 13:29:56 +02:00
|
|
|
public constructor(
|
|
|
|
public name: string,
|
|
|
|
public node: Syntax,
|
|
|
|
) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-09 20:18:51 +02:00
|
|
|
public format(out: IndentWriter): void {
|
|
|
|
out.write(ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET);
|
|
|
|
out.write(`binding '${this.name}' was not found.\n\n`);
|
|
|
|
out.write(printNode(this.node) + '\n');
|
2022-08-31 13:29:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-05 17:25:55 +02:00
|
|
|
export function describeType(type: Type): string {
|
2022-08-31 13:29:56 +02:00
|
|
|
switch (type.kind) {
|
|
|
|
case TypeKind.Con:
|
|
|
|
{
|
|
|
|
let out = type.displayName;
|
|
|
|
for (const argType of type.argTypes) {
|
|
|
|
out += ' ' + describeType(argType);
|
|
|
|
}
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
case TypeKind.Var:
|
|
|
|
return 'a' + type.id;
|
|
|
|
case TypeKind.Arrow:
|
|
|
|
{
|
|
|
|
let out = '(';
|
|
|
|
let first = true;
|
|
|
|
for (const paramType of type.paramTypes) {
|
|
|
|
if (first) first = false;
|
|
|
|
else out += ', ';
|
|
|
|
out += describeType(paramType);
|
|
|
|
}
|
|
|
|
out += ') -> ' + describeType(type.returnType);
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
case TypeKind.Tuple:
|
|
|
|
{
|
|
|
|
let out = '(';
|
|
|
|
let first = true;
|
|
|
|
for (const elementType of type.elementTypes) {
|
|
|
|
if (first) first = false;
|
|
|
|
else out += ', ';
|
|
|
|
out += describeType(elementType);
|
|
|
|
}
|
|
|
|
return out;
|
|
|
|
}
|
2022-09-07 12:45:38 +02:00
|
|
|
case TypeKind.Record:
|
|
|
|
{
|
2022-09-11 11:20:21 +02:00
|
|
|
return type.decl.name.text;
|
|
|
|
// let out = type.decl.name.text + ' { ';
|
|
|
|
// let first = true;
|
|
|
|
// for (const [fieldName, fieldType] of type.fields) {
|
|
|
|
// if (first) first = false;
|
|
|
|
// else out += ', ';
|
|
|
|
// out += fieldName + ': ' + describeType(fieldType);
|
|
|
|
// }
|
|
|
|
// return out + ' }';
|
2022-09-07 12:45:38 +02:00
|
|
|
}
|
|
|
|
case TypeKind.Labeled:
|
|
|
|
{
|
2022-09-11 11:20:21 +02:00
|
|
|
// FIXME may need to include fields that were added during unification
|
2022-09-07 12:45:38 +02:00
|
|
|
return '{ ' + type.name + ': ' + describeType(type.type) + ' }';
|
|
|
|
}
|
2022-09-11 11:20:21 +02:00
|
|
|
case TypeKind.App:
|
|
|
|
{
|
|
|
|
return describeType(type.operatorType) + ' ' + describeType(type.argType);
|
|
|
|
}
|
2022-08-31 13:29:56 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-09 20:02:35 +02:00
|
|
|
function getFirstNodeInTypeChain(type: Type): Syntax | null {
|
|
|
|
let curr = type.next;
|
|
|
|
while (curr !== type && (curr.kind === TypeKind.Var || curr.node === null)) {
|
|
|
|
curr = curr.next;
|
|
|
|
}
|
|
|
|
return curr.node;
|
|
|
|
}
|
|
|
|
|
2022-08-31 13:29:56 +02:00
|
|
|
export class UnificationFailedDiagnostic {
|
|
|
|
|
2022-09-08 15:58:08 +02:00
|
|
|
public readonly level = Level.Error;
|
|
|
|
|
2022-08-31 13:29:56 +02:00
|
|
|
public constructor(
|
|
|
|
public left: Type,
|
|
|
|
public right: Type,
|
2022-09-06 11:56:17 +02:00
|
|
|
public nodes: Syntax[],
|
2022-08-31 13:29:56 +02:00
|
|
|
) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-09 20:18:51 +02:00
|
|
|
public format(out: IndentWriter): void {
|
2022-09-09 20:02:35 +02:00
|
|
|
const leftNode = getFirstNodeInTypeChain(this.left);
|
|
|
|
const rightNode = getFirstNodeInTypeChain(this.right);
|
2022-09-06 11:56:17 +02:00
|
|
|
const node = this.nodes[0];
|
2022-09-09 20:18:51 +02:00
|
|
|
out.write(ANSI_FG_RED + ANSI_BOLD + `error: ` + ANSI_RESET);
|
|
|
|
out.write(`unification of ` + ANSI_FG_GREEN + describeType(this.left) + ANSI_RESET);
|
|
|
|
out.write(' and ' + ANSI_FG_GREEN + describeType(this.right) + ANSI_RESET + ' failed.\n\n');
|
|
|
|
out.write(printNode(node) + '\n');
|
2022-09-06 11:56:17 +02:00
|
|
|
for (let i = 1; i < this.nodes.length; i++) {
|
|
|
|
const node = this.nodes[i];
|
2022-09-09 20:18:51 +02:00
|
|
|
out.write(' ... in an instantiation of the following expression\n\n');
|
|
|
|
out.write(printNode(node, { indentation: i === 0 ? ' ' : ' ' }) + '\n');
|
2022-09-06 11:56:17 +02:00
|
|
|
}
|
2022-09-09 20:02:35 +02:00
|
|
|
if (leftNode !== null) {
|
2022-09-09 20:18:51 +02:00
|
|
|
out.indent();
|
|
|
|
out.write(ANSI_FG_YELLOW + ANSI_BOLD + `info: ` + ANSI_RESET);
|
|
|
|
out.write(`type ` + ANSI_FG_GREEN + describeType(this.left) + ANSI_RESET + ` was inferred from this expression:\n\n`);
|
|
|
|
out.write(printNode(leftNode) + '\n');
|
|
|
|
out.dedent();
|
2022-09-09 20:02:35 +02:00
|
|
|
}
|
|
|
|
if (rightNode !== null) {
|
2022-09-09 20:18:51 +02:00
|
|
|
out.indent();
|
|
|
|
out.write(ANSI_FG_YELLOW + ANSI_BOLD + `info: ` + ANSI_RESET);
|
|
|
|
out.write(`type ` + ANSI_FG_GREEN + describeType(this.right) + ANSI_RESET + ` was inferred from this expression:\n\n`);
|
|
|
|
out.write(printNode(rightNode) + '\n');
|
|
|
|
out.dedent();
|
2022-09-09 20:02:35 +02:00
|
|
|
}
|
2022-08-31 13:29:56 +02:00
|
|
|
}
|
2022-08-28 21:12:25 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-01 20:06:43 +02:00
|
|
|
export class ArityMismatchDiagnostic {
|
|
|
|
|
2022-09-08 15:58:08 +02:00
|
|
|
public readonly level = Level.Error;
|
|
|
|
|
2022-09-01 20:06:43 +02:00
|
|
|
public constructor(
|
|
|
|
public left: TArrow,
|
|
|
|
public right: TArrow,
|
|
|
|
) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-09 20:18:51 +02:00
|
|
|
public format(out: IndentWriter): void {
|
|
|
|
out.write(ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET);
|
|
|
|
out.write(ANSI_FG_GREEN + describeType(this.left) + ANSI_RESET);
|
|
|
|
out.write(` has ${this.left.paramTypes.length} `);
|
|
|
|
out.write(this.left.paramTypes.length === 1 ? 'parameter' : 'parameters');
|
|
|
|
out.write(' while ' + ANSI_FG_GREEN + describeType(this.right) + ANSI_RESET);
|
|
|
|
out.write(` has ${this.right.paramTypes.length}.\n\n`);
|
2022-09-01 20:06:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-08 23:32:25 +02:00
|
|
|
export class FieldMissingDiagnostic {
|
|
|
|
|
|
|
|
public readonly level = Level.Error;
|
|
|
|
|
|
|
|
public constructor(
|
|
|
|
public recordType: TRecord,
|
|
|
|
public fieldName: string,
|
2022-09-11 11:20:21 +02:00
|
|
|
public node: Syntax | null,
|
2022-09-08 23:32:25 +02:00
|
|
|
) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-09 20:18:51 +02:00
|
|
|
public format(out: IndentWriter): void {
|
|
|
|
out.write(ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET);
|
|
|
|
out.write(`field '${this.fieldName}' is missing from `);
|
|
|
|
out.write(describeType(this.recordType) + '\n\n');
|
2022-09-11 11:20:21 +02:00
|
|
|
if (this.node !== null) {
|
|
|
|
out.write(printNode(this.node) + '\n');
|
2022-09-09 20:02:35 +02:00
|
|
|
}
|
2022-09-08 23:32:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
export class FieldDoesNotExistDiagnostic {
|
|
|
|
|
|
|
|
public readonly level = Level.Error;
|
|
|
|
|
|
|
|
public constructor(
|
2022-09-09 20:18:51 +02:00
|
|
|
public recordType: TRecord,
|
|
|
|
public fieldName: string,
|
2022-09-11 11:20:21 +02:00
|
|
|
public node: Syntax | null,
|
2022-09-08 23:32:25 +02:00
|
|
|
) {
|
2022-09-11 11:20:21 +02:00
|
|
|
|
2022-09-08 23:32:25 +02:00
|
|
|
}
|
|
|
|
|
2022-09-09 20:18:51 +02:00
|
|
|
public format(out: IndentWriter): void {
|
|
|
|
out.write(ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET);
|
|
|
|
out.write(`field '${this.fieldName}' does not exist on type `);
|
|
|
|
out.write(describeType(this.recordType) + '\n\n');
|
2022-09-11 11:20:21 +02:00
|
|
|
if (this.node !== null) {
|
|
|
|
out.write(printNode(this.node) + '\n');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
export class KindMismatchDiagnostic {
|
|
|
|
|
|
|
|
public readonly level = Level.Error;
|
|
|
|
|
|
|
|
public constructor(
|
|
|
|
public leftSize: number,
|
|
|
|
public rightSize: number,
|
|
|
|
public node: Syntax | null,
|
|
|
|
) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public format(out: IndentWriter): void {
|
|
|
|
out.write(ANSI_FG_RED + ANSI_BOLD + 'error: ' + ANSI_RESET);
|
|
|
|
out.write(`kind `);
|
|
|
|
for (let i = 0; i < this.leftSize-1; i++) {
|
|
|
|
out.write(`* -> `);
|
|
|
|
}
|
|
|
|
out.write(`* does not match with `);
|
|
|
|
for (let i = 0; i < this.rightSize-1; i++) {
|
|
|
|
out.write(`* -> `);
|
|
|
|
}
|
|
|
|
out.write(`*\n\n`);
|
|
|
|
if (this.node !== null) {
|
|
|
|
out.write(printNode(this.node) + '\n');
|
|
|
|
}
|
2022-09-08 23:32:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-08-28 21:12:25 +02:00
|
|
|
export type Diagnostic
|
2022-08-31 13:29:56 +02:00
|
|
|
= UnexpectedCharDiagnostic
|
|
|
|
| BindingNotFoudDiagnostic
|
|
|
|
| UnificationFailedDiagnostic
|
|
|
|
| UnexpectedTokenDiagnostic
|
2022-09-01 20:06:43 +02:00
|
|
|
| ArityMismatchDiagnostic
|
2022-09-08 23:32:25 +02:00
|
|
|
| FieldMissingDiagnostic
|
|
|
|
| FieldDoesNotExistDiagnostic
|
2022-09-11 11:20:21 +02:00
|
|
|
| KindMismatchDiagnostic
|
2022-08-28 21:12:25 +02:00
|
|
|
|
2022-09-08 15:58:08 +02:00
|
|
|
export interface Diagnostics {
|
|
|
|
add(diagnostic: Diagnostic): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class DiagnosticStore {
|
|
|
|
|
|
|
|
private storage: Diagnostic[] = [];
|
|
|
|
|
|
|
|
public hasError = false;
|
|
|
|
public hasFatal = false;
|
|
|
|
|
|
|
|
public add(diagnostic: Diagnostic): void {
|
|
|
|
this.storage.push(diagnostic);
|
|
|
|
if (diagnostic.level >= Level.Error) {
|
|
|
|
this.hasError = true;
|
|
|
|
}
|
|
|
|
if (diagnostic.level >= Level.Fatal) {
|
|
|
|
this.hasFatal = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public getDiagnostics(): Iterable<Diagnostic> {
|
|
|
|
return this.storage;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2022-08-28 21:12:25 +02:00
|
|
|
|
2022-09-08 15:58:08 +02:00
|
|
|
export class ConsoleDiagnostics {
|
2022-08-28 21:12:25 +02:00
|
|
|
|
|
|
|
public add(diagnostic: Diagnostic): void {
|
2022-09-09 20:18:51 +02:00
|
|
|
const writer = new IndentWriter(process.stderr);
|
|
|
|
diagnostic.format(writer);
|
2022-08-28 21:12:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-08 23:32:25 +02:00
|
|
|
interface PrintExcerptOptions {
|
|
|
|
indentation?: string;
|
|
|
|
extraLineCount?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
function printNode(node: Syntax, options?: PrintExcerptOptions): string {
|
|
|
|
const file = node.getSourceFile().getFile();
|
|
|
|
return printExcerpt(file, node.getRange(), options);
|
|
|
|
}
|
|
|
|
|
2022-08-31 13:29:56 +02:00
|
|
|
function printExcerpt(file: TextFile, span: TextRange, { indentation = ' ', extraLineCount = 2 } = {}): string {
|
|
|
|
let out = '';
|
|
|
|
const content = file.text;
|
|
|
|
const startLine = Math.max(0, span.start.line-1-extraLineCount)
|
|
|
|
const lines = content.split('\n')
|
|
|
|
const endLine = Math.min(lines.length, (span.end !== undefined ? span.end.line : startLine) + extraLineCount)
|
|
|
|
const gutterWidth = Math.max(2, countDigits(endLine+1))
|
|
|
|
for (let i = startLine; i < endLine; i++) {
|
|
|
|
const line = lines[i];
|
|
|
|
let j = firstIndexOfNonEmpty(line);
|
|
|
|
out += indentation + ' ' + ANSI_FG_BLACK + ANSI_BG_WHITE + ' '.repeat(gutterWidth-countDigits(i+1))+(i+1).toString() + ANSI_RESET + ' ' + line + '\n'
|
|
|
|
const gutter = indentation + ' ' + ANSI_FG_BLACK + ANSI_BG_WHITE + ' '.repeat(gutterWidth) + ANSI_RESET + ' '
|
|
|
|
let mark: number;
|
|
|
|
let skip: number;
|
|
|
|
if (i === span.start.line-1 && i === span.end.line-1) {
|
|
|
|
skip = span.start.column-1;
|
|
|
|
mark = span.end.column-span.start.column;
|
|
|
|
} else if (i === span.start.line-1) {
|
|
|
|
skip = span.start.column-1;
|
|
|
|
mark = line.length-span.start.column+1;
|
|
|
|
} else if (i === span.end.line-1) {
|
|
|
|
skip = 0;
|
|
|
|
mark = span.end.column-1;
|
|
|
|
} else if (i > span.start.line-1 && i < span.end.line-1) {
|
|
|
|
skip = 0;
|
|
|
|
mark = line.length;
|
|
|
|
} else {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (j <= skip) {
|
|
|
|
j = 0;
|
|
|
|
}
|
|
|
|
out += gutter + ' '.repeat(j+skip) + ANSI_FG_RED + '~'.repeat(mark-j) + ANSI_RESET + '\n'
|
|
|
|
}
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
function firstIndexOfNonEmpty(str: string) {
|
|
|
|
let j = 0;
|
|
|
|
for (; j < str.length; j++) {
|
|
|
|
const ch = str[j];
|
|
|
|
if (ch !== ' ' && ch !== '\t') {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return j
|
|
|
|
}
|
|
|
|
|