137 lines
3.6 KiB
TypeScript
137 lines
3.6 KiB
TypeScript
import { ParsingError } from './argparser.ts';
|
|
import chalk from 'https://deno.land/std@0.85.0/fmt/colors.ts';
|
|
import { AstNode } from './newparser/parser.ts';
|
|
import { padNoAnsi, enumerate } from './utils.ts';
|
|
import { stripAnsi } from './stripAnsi.ts';
|
|
|
|
type HighlightResult = { colorized: string; errorIndex: number };
|
|
|
|
/**
|
|
* Get the input as highlighted keywords to show to the user
|
|
* with the error that was generated from parsing the input.
|
|
*
|
|
* @param nodes AST nodes
|
|
* @param error A parsing error
|
|
*/
|
|
function highlight(
|
|
nodes: AstNode[],
|
|
error: ParsingError
|
|
): HighlightResult | undefined {
|
|
const strings: string[] = [];
|
|
let errorIndex: undefined | number = undefined;
|
|
|
|
function foundError() {
|
|
if (errorIndex !== undefined) return;
|
|
errorIndex = stripAnsi(strings.join(' ')).length;
|
|
}
|
|
|
|
if (error.nodes.length === 0) return;
|
|
|
|
nodes.forEach((node) => {
|
|
if (error.nodes.includes(node)) {
|
|
foundError();
|
|
return strings.push(chalk.red(node.raw));
|
|
} else {
|
|
if (node.type === 'shortOptions') {
|
|
let failed = false;
|
|
let s = '';
|
|
for (const option of node.options) {
|
|
if (error.nodes.includes(option)) {
|
|
s += chalk.red(option.raw);
|
|
failed = true;
|
|
} else {
|
|
s += chalk.dim(option.raw);
|
|
}
|
|
}
|
|
const prefix = failed ? chalk.red(`-`) : chalk.dim('-');
|
|
if (failed) {
|
|
foundError();
|
|
}
|
|
return strings.push(prefix + s);
|
|
}
|
|
|
|
return strings.push(chalk.dim(node.raw));
|
|
}
|
|
});
|
|
|
|
return { colorized: strings.join(' '), errorIndex: errorIndex ?? 0 };
|
|
}
|
|
|
|
/**
|
|
* An error UI
|
|
*
|
|
* @param breadcrumbs The command breadcrumbs to print with the error
|
|
*/
|
|
export function errorBox(
|
|
nodes: AstNode[],
|
|
errors: ParsingError[],
|
|
breadcrumbs: string[]
|
|
): string {
|
|
let withHighlight: { message: string; highlighted?: HighlightResult }[] = [];
|
|
|
|
let errorMessages: string[] = [];
|
|
|
|
for (const error of errors) {
|
|
const highlighted = highlight(nodes, error);
|
|
withHighlight.push({ message: error.message, highlighted });
|
|
}
|
|
|
|
let number = 1;
|
|
const maxNumberWidth = String(withHighlight.length).length;
|
|
|
|
errorMessages.push(
|
|
chalk.red.bold('error: ') +
|
|
'found ' +
|
|
chalk.yellow(withHighlight.length) +
|
|
' error' +
|
|
(withHighlight.length > 1 ? 's' : '')
|
|
);
|
|
errorMessages.push('');
|
|
|
|
withHighlight
|
|
.filter((x) => x.highlighted)
|
|
.forEach((x) => {
|
|
if (!x.highlighted) {
|
|
throw new Error('WELP');
|
|
}
|
|
|
|
const pad = ''.padStart(x.highlighted.errorIndex);
|
|
|
|
errorMessages.push(` ${x.highlighted.colorized}`);
|
|
for (const [index, line] of enumerate(x.message.split('\n'))) {
|
|
const prefix = index === 0 ? chalk.bold('^') : ' ';
|
|
const msg = chalk.red(` ${pad} ${prefix} ${line}`);
|
|
errorMessages.push(msg);
|
|
}
|
|
errorMessages.push('');
|
|
number++;
|
|
});
|
|
|
|
const withNoHighlight = withHighlight.filter((x) => !x.highlighted);
|
|
|
|
if (number > 1) {
|
|
if (withNoHighlight.length === 1) {
|
|
errorMessages.push('Along the following error:');
|
|
} else if (withNoHighlight.length > 1) {
|
|
errorMessages.push('Along the following errors:');
|
|
}
|
|
}
|
|
|
|
withNoHighlight.forEach(({ message }) => {
|
|
const num = chalk.red.bold(
|
|
`${padNoAnsi(number.toString(), maxNumberWidth, 'start')}.`
|
|
);
|
|
errorMessages.push(` ${num} ${chalk.red(message)}`);
|
|
number++;
|
|
});
|
|
|
|
const helpCmd = chalk.yellow(breadcrumbs.join(' ') + ' --help');
|
|
|
|
errorMessages.push('');
|
|
errorMessages.push(
|
|
chalk.red.bold('hint: ') + `for more information, try '${helpCmd}'`
|
|
);
|
|
|
|
return errorMessages.join('\n');
|
|
}
|