Better tab support in buildMessageCodeFrame
MODIFY
packages/@romejs/cli-diagnostics/DiagnosticsPrinter.ts
packages/@romejs/cli-diagnostics/DiagnosticsPrinter.ts
@@ -27,7 +27,7 @@ import {
markup,
markupToPlainTextString,
} from '@romejs/string-markup';
-import {toLines} from './utils';
+import {ToLines, toLines} from './utils';
import printAdvice from './printAdvice';
import {default as successBanner} from './banners/success.json';
import {default as errorBanner} from './banners/error.json';
@@ -116,7 +116,7 @@ type FileDependency = ChangeFileDependency | ReferenceFileDependency;
export type DiagnosticsPrinterFileSources = UnknownFilePathMap<{
sourceText: string;
- lines: Array<string>;
+ lines: ToLines;
}>;
export type DiagnosticsPrinterFileMtimes = UnknownFilePathMap<number>;

MODIFY
packages/@romejs/cli-diagnostics/buildMessageCodeFrame.ts
packages/@romejs/cli-diagnostics/buildMessageCodeFrame.ts
@@ -14,7 +14,12 @@ import {
MAX_CODE_FRAME_LINES,
} from './constants';
import {Position} from '@romejs/parser-core';
-import {cleanEquivalentString, joinNoBreak} from './utils';
+import {
+ ToLines,
+ cleanEquivalentString,
+ joinNoBreak,
+ normalizeTabs,
+} from './utils';
import {
Number0,
ob1Coerce0,
@@ -28,14 +33,65 @@ import {
} from '@romejs/ob1';
import {markupToPlainTextString} from '@romejs/string-markup';
+function createPointer(
+ markerMessage: string,
+ line: string,
+ markerStart: Number0,
+ markerEnd: Number0,
+): undefined | string {
+ let result = '';
+
+ let markerSize = ob1Get0(ob1Sub(markerEnd, markerStart));
+
+ // If the range contains tabs then increase the marker size
+ for (let i = ob1Get0(markerStart); i < ob1Get0(markerEnd); i++) {
+ const char = line[i];
+ if (char === '\t') {
+ markerSize++;
+ }
+ }
+
+ const pointerLength: number = Math.max(markerSize, 1);
+
+ // Skip the pointer if it's pointing at the last character
+ let skipPointer = pointerLength === 1 && ob1Get0(markerEnd) >= line.length;
+
+ if (!skipPointer) {
+ // Add indentation, handling hard tabs as two soft spaces
+ for (let i = 0; i < ob1Get0(markerStart); i++) {
+ const char = line[i];
+ if (char === '\t') {
+ // normalizeTabs will be called on this line and this replacement made
+ result += ' ';
+ } else {
+ result += ' ';
+ }
+ }
+
+ // Add pointer
+ result += `<error><emphasis>${'^'.repeat(pointerLength)}</emphasis></error>`;
+ }
+
+ // Add marker
+ if (markerMessage !== '') {
+ result += ` ${markerMessage}`;
+ }
+
+ if (result === '') {
+ return undefined;
+ } else {
+ return result;
+ }
+}
+
export default function buildMessageCodeFrame(
sourceText: string,
- allLines: Array<string>,
+ allLines: ToLines,
start: undefined | Position,
end: undefined | Position,
markerMessage: string,
): string {
- if (start === undefined || end === undefined) {
+ if (allLines.length === 0 || start === undefined || end === undefined) {
if (markerMessage === '') {
return '';
} else {
@@ -44,12 +100,7 @@ export default function buildMessageCodeFrame(
}
const startLineIndex = ob1Coerce1To0(start.line);
-
let endLineIndex = ob1Coerce1To0(end.line);
- let endLineCol = end.column;
-
- let markerOffset = end.column;
- let markerSize: Number0 = ob1Number0;
// Increase the amount of lines we should show for "context"
let contextStartIndex = ob1Coerce0(
@@ -62,56 +113,64 @@ export default function buildMessageCodeFrame(
),
);
+ let maxVisibleLineNo = 0;
+
let formattedLines: Array<
| {
+ pointer: undefined | string;
gutter: string;
line: string;
- lineIndex: Number0;
}
| undefined
> = [];
for (let i = contextStartIndex; i <= contextEndIndex; i = ob1Inc(i)) {
- let line: undefined | string = allLines[ob1Get0(i)];
- if (line === undefined) {
+ let rawLine: undefined | string = allLines.raw[ob1Get0(i)];
+ let highlightLine: undefined | string = allLines.highlighted[ob1Get0(i)];
+ if (highlightLine === undefined || rawLine === undefined) {
continue;
}
// Ensure that the frame doesn't start with whitespace
if (
- line.trim() === '' &&
+ rawLine.trim() === '' &&
formattedLines.length === 0 &&
i !== startLineIndex
) {
continue;
}
+ let pointer: undefined | string;
+
// If this is within the highlighted line range
const shouldHighlight: boolean = i >= startLineIndex && i <= endLineIndex;
if (shouldHighlight) {
- // Range to highlight
- let startCol = ob1Number0;
- let endCol = ob1Coerce0(line.length);
-
+ if (i === startLineIndex && i === endLineIndex) {
+ // Only line in the selection
+ pointer = createPointer(
+ markerMessage,
+ rawLine,
+ start.column,
+ end.column,
+ );
+ } else if (i === startLineIndex) {
// First line in selection
- if (i === startLineIndex) {
- startCol = start.column;
- }
-
+ pointer = createPointer(
+ '',
+ rawLine,
+ start.column,
+ // line could be highlighted
+ ob1Coerce0(rawLine.length),
+ );
+ } else if (i === endLineIndex) {
// Last line in selection
- if (i === endLineIndex) {
- endCol = endLineCol;
-
- // Sometimes the end will be the same as the start if we just want to make a single place
-
- // and not perform highlighting
- if (endCol > startCol) {
- markerSize = ob1Sub(endCol, startCol);
- markerOffset = ob1Sub(markerOffset, markerSize);
- }
+ pointer = createPointer(markerMessage, rawLine, ob1Number0, end.column);
}
}
+ // Replace hard tabs with two spaces
+ highlightLine = normalizeTabs(highlightLine);
+
const lineNo = ob1Coerce0To1(i);
let gutter = `${String(lineNo)}${GUTTER}`;
@@ -122,10 +181,12 @@ export default function buildMessageCodeFrame(
}
formattedLines.push({
+ pointer,
gutter,
- line,
- lineIndex: i,
+ line: highlightLine,
});
+
+ maxVisibleLineNo = ob1Get0(i) + 1;
}
// If we have too many lines in our selection, then collapse them to an ellipsis
@@ -159,17 +220,12 @@ export default function buildMessageCodeFrame(
}
}
- // Don't output a gutter if there's only a single line
- const noGutter = allLines.length === 1;
-
// Calculate max size of gutter, this is the maximum visible line plus the futter length plus the frame indent
const lastLine = formattedLines[formattedLines.length - 1];
if (lastLine === undefined) {
throw new Error('Expected there to be a last line');
}
- // Calculate the max width of the gutter based on the line count
- const maxVisibleLineNo = ob1Get0(lastLine.lineIndex) + 1;
const maxGutterLength =
String(maxVisibleLineNo).length + GUTTER.length + CODE_FRAME_INDENT.length;
@@ -184,27 +240,22 @@ export default function buildMessageCodeFrame(
}
}
- const pointerLength: number = Math.max(ob1Get0(markerSize), 1);
- const pointer: string = `<error><emphasis>${'^'.repeat(pointerLength)}</emphasis></error>`;
- const pointerIndent: string = ' '.repeat(ob1Get0(markerOffset));
- const paddedMarkerMessage = markerMessage === '' ? '' : ` ${markerMessage}`;
-
- // If the marker is just pointing to the first character and we have no message, no point showing it
- const noMarkerLine =
- ob1Get0(markerOffset) === 0 && pointerLength === 1 && markerMessage === '';
-
// Output no gutter with a soft indent if this is true
- if (noGutter) {
- const result = [...allLines];
- if (!noMarkerLine) {
- result.push(`${pointerIndent}${pointer}${paddedMarkerMessage}`);
+ if (formattedLines.length === 1) {
+ const selection = formattedLines[0];
+ if (selection === undefined) {
+ throw new Error(
+ 'Expected a selection? undefined is only valid here as an omitted line signifier',
+ );
}
- return joinNoBreak(result.map((line) => `${CODE_FRAME_INDENT}${line}`));
+
+ const result = [`${CODE_FRAME_INDENT}${selection.line}`];
+ if (selection.pointer !== undefined) {
+ result.push(`${CODE_FRAME_INDENT}${selection.pointer}`);
}
- // Build marker
- const markerGutterIndent: string = ' '.repeat(maxGutterLength - GUTTER.length);
- const markerLine: string = `${markerGutterIndent}<emphasis>${GUTTER}</emphasis>${pointerIndent}${pointer}${paddedMarkerMessage}`;
+ return joinNoBreak(result);
+ }
// Build up the line we display when source lines are omitted
const omittedLine =
@@ -219,18 +270,17 @@ export default function buildMessageCodeFrame(
continue;
}
- const {gutter, line, lineIndex} = selection;
+ const {pointer, gutter, line} = selection;
- if (noGutter) {
- result.push(line);
- } else {
result.push(
`<pad align="right" width="${maxGutterLength}"><emphasis>${gutter}</emphasis></pad>` +
line,
);
- }
- if (lineIndex === endLineIndex && !noMarkerLine) {
- result.push(markerLine);
+
+ if (pointer !== undefined) {
+ result.push(
+ `<pad align="right" width="${maxGutterLength}"><emphasis>${GUTTER}</emphasis></pad>${pointer}`,
+ );
}
}

MODIFY
packages/@romejs/cli-diagnostics/buildPatchCodeFrame.ts
packages/@romejs/cli-diagnostics/buildPatchCodeFrame.ts
@@ -11,7 +11,7 @@ import {
GUTTER,
MAX_PATCH_LINES,
} from './constants';
-import {joinNoBreak, showInvisibles} from './utils';
+import {joinNoBreak, normalizeTabs, showInvisibles} from './utils';
import {Diffs, diffConstants, groupDiffByLines} from '@romejs/string-diff';
import {escapeMarkup, markup, markupTag} from '@romejs/string-markup';
import {DiagnosticAdviceDiff} from '@romejs/diagnostics';
@@ -24,7 +24,7 @@ function formatDiffLine(diffs: Diffs) {
return markupTag('success', escapeMarkup(showInvisibles(text)));
} else {
// type === diffConstants.EQUAL
- return escapeMarkup(text);
+ return escapeMarkup(normalizeTabs(text));
}
}).join('');
}

MODIFY
packages/@romejs/cli-diagnostics/constants.ts
packages/@romejs/cli-diagnostics/constants.ts
export const GUTTER = ' \u2502 ';
export const CODE_FRAME_INDENT = ' ';
export const CODE_FRAME_SELECTED_INDENT = `<error>\></error> `;
-export const FILENAME_INDENT = ' ';
export const MAX_CODE_FRAME_LINES = 8;
export const HALF_MAX_CODE_FRAME_LINES = MAX_CODE_FRAME_LINES / 2;

MODIFY
packages/@romejs/cli-diagnostics/printAdvice.ts
packages/@romejs/cli-diagnostics/printAdvice.ts
@@ -21,7 +21,7 @@ import {
diagnosticLocationToMarkupFilelink,
} from '@romejs/diagnostics';
import {Position} from '@romejs/parser-core';
-import {showInvisibles, toLines} from './utils';
+import {ToLines, showInvisibles, toLines} from './utils';
import buildPatchCodeFrame from './buildPatchCodeFrame';
import buildMessageCodeFrame from './buildMessageCodeFrame';
import {escapeMarkup, markupTag} from '@romejs/string-markup';
@@ -289,7 +289,11 @@ function printFrame(
cleanMarker = markupTag('emphasis', cleanMessage(marker));
}
- let lines: Array<string> = [];
+ let lines: ToLines = {
+ length: 0,
+ raw: [],
+ highlighted: [],
+ };
if (sourceText !== undefined) {
lines = toLines({
path,
@@ -307,7 +311,11 @@ function printFrame(
path.isAbsolute() &&
opts.missingFileSources.has(path.assertAbsolute())
) {
- lines = ['<dim>File does not exist</dim>'];
+ lines = {
+ length: 1,
+ raw: ['File does not exist'],
+ highlighted: ['<dim>File does not exist</dim>'],
+ };
}
if (sourceText === undefined) {

MODIFY
packages/@romejs/cli-diagnostics/utils.ts
packages/@romejs/cli-diagnostics/utils.ts
@@ -9,6 +9,10 @@ import {markupToPlainTextString} from '@romejs/string-markup';
import highlightCode, {AnsiHighlightOptions} from './highlightCode';
import {NEWLINE} from '@romejs/js-parser-utils';
+export function normalizeTabs(str: string): string {
+ return str.replace(/\t/g, ' ');
+}
+
export function showInvisibles(str: string): string {
let ret = '';
for (const cha of str) {
@@ -61,8 +65,25 @@ export function splitLines(src: string): Array<string> {
return src.replace(/\t/g, ' ').split(NEWLINE);
}
-export function toLines(opts: AnsiHighlightOptions): Array<string> {
- const highlighted = highlightCode(opts);
- const lines = splitLines(highlighted);
- return lines;
+export type ToLines = {
+ length: number;
+ raw: Array<string>;
+ highlighted: Array<string>;
+};
+
+export function toLines(opts: AnsiHighlightOptions): ToLines {
+ const raw = splitLines(opts.input);
+ const highlighted = splitLines(highlightCode(opts));
+
+ if (raw.length !== highlighted.length) {
+ throw new Error(
+ `raw and highlighted line count mismatch ${raw.length} !== ${highlighted.length}`,
+ );
+ }
+
+ return {
+ length: raw.length,
+ raw,
+ highlighted,
+ };
}

MODIFY
packages/@romejs/core/test-worker/TestAPI.ts
packages/@romejs/core/test-worker/TestAPI.ts
@@ -10,7 +10,6 @@ import {
DiagnosticAdvice,
createBlessedDiagnosticMessage,
createSingleDiagnosticError,
- deriveDiagnosticFromError,
deriveDiagnosticFromErrorStructure,
descriptions,
getErrorStackAdvice,
@@ -608,7 +607,7 @@ export default class TestAPI implements TestHelper {
if (status === 'UPDATE' && this.options.freezeSnapshots) {
await this.emitDiagnostic(
- deriveDiagnosticFromError(
+ deriveDiagnosticFromErrorStructure(
callError,
{
description: descriptions.SNAPSHOTS.INLINE_FROZEN,
@@ -619,7 +618,7 @@ export default class TestAPI implements TestHelper {
if (status === 'NO_MATCH') {
await this.emitDiagnostic(
- deriveDiagnosticFromError(
+ deriveDiagnosticFromErrorStructure(
callError,
{
description: {
@@ -698,7 +697,7 @@ export default class TestAPI implements TestHelper {
if (existingSnapshot === undefined) {
if (this.options.freezeSnapshots) {
await this.emitDiagnostic(
- deriveDiagnosticFromError(
+ deriveDiagnosticFromErrorStructure(
callError,
{
description: descriptions.SNAPSHOTS.FROZEN,
@@ -748,7 +747,7 @@ export default class TestAPI implements TestHelper {
});
await this.emitDiagnostic(
- deriveDiagnosticFromError(
+ deriveDiagnosticFromErrorStructure(
callError,
{
description: {

MODIFY
packages/@romejs/diagnostics/derive.ts
packages/@romejs/diagnostics/derive.ts
@@ -224,11 +224,17 @@ export function getErrorStackAdvice(
if (cleanStack.startsWith(removeMessage)) {
cleanStack = cleanStack.slice(removeMessage.length);
}
+ cleanStack = cleanStack.trim();
advice.push({
type: 'log',
- category: 'error',
- text: escapeMarkup(cleanStack),
+ category: 'warn',
+ text: 'Raw stack trace is being displayed as we did not receive any frames',
+ });
+
+ advice.push({
+ type: 'list',
+ list: cleanStack.split('\n').map((line) => escapeMarkup(line.trim())),
});
} else {
const adviceFrames = frames.map((frame) => {

MODIFY
packages/@romejs/v8/sourceMapManager.ts
packages/@romejs/v8/sourceMapManager.ts
@@ -78,7 +78,7 @@ function buildStackString(err: Error): string {
parts.push('new');
}
- let name = '<anonymous';
+ let name = '<anonymous>';
if (functionName !== undefined) {
name = functionName;
}

Better tab support in buildMessageCodeFrame