Skip to content

Commit c3b95b0

Browse files
authored
[compiler] Improve snap workflow for debugging errors (facebook#35539)
Much nicer workflow for working through errors in the compiler: * Run `yarn snap -w`, oops there are are errors * Hit 'p' to select a fixture => the suggestions populate with recent failures, sorted alphabetically. No need to copy/paste the name of the fixture you want to focus on! * tab/shift-tab to pick one, hit enter to select that one * ...Focus on fixing that test... * 'p' to re-enter the picker. Snap tracks the last state of each fixture and continues to show all tests that failed on their last run, so you can easily move on to the next one. The currently selected test is highlighted, making it easy to move to the next one. * 'a' at any time to run all tests * 'd' at any time to toggle debug output on/off (while focusing on a single test) --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35539). * facebook#35607 * facebook#35298 * facebook#35596 * facebook#35573 * facebook#35595 * __->__ facebook#35539
1 parent 006ae37 commit c3b95b0

File tree

2 files changed

+186
-14
lines changed

2 files changed

+186
-14
lines changed

compiler/packages/snap/src/runner-watch.ts

Lines changed: 178 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import watcher from '@parcel/watcher';
99
import path from 'path';
1010
import ts from 'typescript';
1111
import {FIXTURES_PATH, PROJECT_ROOT} from './constants';
12-
import {TestFilter} from './fixture-utils';
12+
import {TestFilter, getFixtures} from './fixture-utils';
1313
import {execSync} from 'child_process';
1414

1515
export function watchSrc(
@@ -121,6 +121,12 @@ export type RunnerState = {
121121
// Input mode for interactive pattern entry
122122
inputMode: 'none' | 'pattern';
123123
inputBuffer: string;
124+
// Autocomplete state
125+
allFixtureNames: Array<string>;
126+
matchingFixtures: Array<string>;
127+
selectedIndex: number;
128+
// Track last run status of each fixture (for autocomplete suggestions)
129+
fixtureLastRunStatus: Map<string, 'pass' | 'fail'>;
124130
};
125131

126132
function subscribeFixtures(
@@ -179,46 +185,187 @@ function subscribeTsc(
179185
);
180186
}
181187

188+
/**
189+
* Levenshtein edit distance between two strings
190+
*/
191+
function editDistance(a: string, b: string): number {
192+
const m = a.length;
193+
const n = b.length;
194+
195+
// Create a 2D array for memoization
196+
const dp: number[][] = Array.from({length: m + 1}, () =>
197+
Array(n + 1).fill(0),
198+
);
199+
200+
// Base cases
201+
for (let i = 0; i <= m; i++) dp[i][0] = i;
202+
for (let j = 0; j <= n; j++) dp[0][j] = j;
203+
204+
// Fill in the rest
205+
for (let i = 1; i <= m; i++) {
206+
for (let j = 1; j <= n; j++) {
207+
if (a[i - 1] === b[j - 1]) {
208+
dp[i][j] = dp[i - 1][j - 1];
209+
} else {
210+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
211+
}
212+
}
213+
}
214+
215+
return dp[m][n];
216+
}
217+
218+
function filterFixtures(
219+
allNames: Array<string>,
220+
pattern: string,
221+
): Array<string> {
222+
if (pattern === '') {
223+
return allNames;
224+
}
225+
const lowerPattern = pattern.toLowerCase();
226+
const matches = allNames.filter(name =>
227+
name.toLowerCase().includes(lowerPattern),
228+
);
229+
// Sort by edit distance (lower = better match)
230+
matches.sort((a, b) => {
231+
const distA = editDistance(lowerPattern, a.toLowerCase());
232+
const distB = editDistance(lowerPattern, b.toLowerCase());
233+
return distA - distB;
234+
});
235+
return matches;
236+
}
237+
238+
const MAX_DISPLAY = 15;
239+
240+
function renderAutocomplete(state: RunnerState): void {
241+
// Clear terminal
242+
console.log('\u001Bc');
243+
244+
// Show current input
245+
console.log(`Pattern: ${state.inputBuffer}`);
246+
console.log('');
247+
248+
// Get current filter pattern if active
249+
const currentFilterPattern =
250+
state.mode.filter && state.filter ? state.filter.paths[0] : null;
251+
252+
// Show matching fixtures (limit to MAX_DISPLAY)
253+
const toShow = state.matchingFixtures.slice(0, MAX_DISPLAY);
254+
255+
toShow.forEach((name, i) => {
256+
const isSelected = i === state.selectedIndex;
257+
const matchesCurrentFilter =
258+
currentFilterPattern != null &&
259+
name.toLowerCase().includes(currentFilterPattern.toLowerCase());
260+
261+
let prefix: string;
262+
if (isSelected) {
263+
prefix = '> ';
264+
} else if (matchesCurrentFilter) {
265+
prefix = '* ';
266+
} else {
267+
prefix = ' ';
268+
}
269+
console.log(`${prefix}${name}`);
270+
});
271+
272+
if (state.matchingFixtures.length > MAX_DISPLAY) {
273+
console.log(
274+
` ... and ${state.matchingFixtures.length - MAX_DISPLAY} more`,
275+
);
276+
}
277+
278+
console.log('');
279+
console.log('↑/↓/Tab navigate | Enter select | Esc cancel');
280+
}
281+
182282
function subscribeKeyEvents(
183283
state: RunnerState,
184284
onChange: (state: RunnerState) => void,
185285
) {
186286
process.stdin.on('keypress', async (str, key) => {
187-
// Handle input mode (pattern entry)
287+
// Handle input mode (pattern entry with autocomplete)
188288
if (state.inputMode !== 'none') {
189289
if (key.name === 'return') {
190-
// Enter pressed - process input
191-
const pattern = state.inputBuffer.trim();
290+
// Enter pressed - use selected fixture or typed text
291+
let pattern: string;
292+
if (
293+
state.selectedIndex >= 0 &&
294+
state.selectedIndex < state.matchingFixtures.length
295+
) {
296+
pattern = state.matchingFixtures[state.selectedIndex];
297+
} else {
298+
pattern = state.inputBuffer.trim();
299+
}
300+
192301
state.inputMode = 'none';
193302
state.inputBuffer = '';
194-
process.stdout.write('\n');
303+
state.allFixtureNames = [];
304+
state.matchingFixtures = [];
305+
state.selectedIndex = -1;
195306

196307
if (pattern !== '') {
197-
// Set the pattern as filter
198308
state.filter = {paths: [pattern]};
199309
state.mode.filter = true;
200310
state.mode.action = RunnerAction.Test;
201311
onChange(state);
202312
}
203-
// If empty, just exit input mode without changes
204313
return;
205314
} else if (key.name === 'escape') {
206315
// Cancel input mode
207316
state.inputMode = 'none';
208317
state.inputBuffer = '';
209-
process.stdout.write(' (cancelled)\n');
318+
state.allFixtureNames = [];
319+
state.matchingFixtures = [];
320+
state.selectedIndex = -1;
321+
// Redraw normal UI
322+
onChange(state);
323+
return;
324+
} else if (key.name === 'up' || (key.name === 'tab' && key.shift)) {
325+
// Navigate up in autocomplete list
326+
if (state.matchingFixtures.length > 0) {
327+
if (state.selectedIndex <= 0) {
328+
state.selectedIndex =
329+
Math.min(state.matchingFixtures.length, MAX_DISPLAY) - 1;
330+
} else {
331+
state.selectedIndex--;
332+
}
333+
renderAutocomplete(state);
334+
}
335+
return;
336+
} else if (key.name === 'down' || (key.name === 'tab' && !key.shift)) {
337+
// Navigate down in autocomplete list
338+
if (state.matchingFixtures.length > 0) {
339+
const maxIndex =
340+
Math.min(state.matchingFixtures.length, MAX_DISPLAY) - 1;
341+
if (state.selectedIndex >= maxIndex) {
342+
state.selectedIndex = 0;
343+
} else {
344+
state.selectedIndex++;
345+
}
346+
renderAutocomplete(state);
347+
}
210348
return;
211349
} else if (key.name === 'backspace') {
212350
if (state.inputBuffer.length > 0) {
213351
state.inputBuffer = state.inputBuffer.slice(0, -1);
214-
// Erase character: backspace, space, backspace
215-
process.stdout.write('\b \b');
352+
state.matchingFixtures = filterFixtures(
353+
state.allFixtureNames,
354+
state.inputBuffer,
355+
);
356+
state.selectedIndex = -1;
357+
renderAutocomplete(state);
216358
}
217359
return;
218360
} else if (str && !key.ctrl && !key.meta) {
219-
// Regular character - accumulate and echo
361+
// Regular character - accumulate, filter, and render
220362
state.inputBuffer += str;
221-
process.stdout.write(str);
363+
state.matchingFixtures = filterFixtures(
364+
state.allFixtureNames,
365+
state.inputBuffer,
366+
);
367+
state.selectedIndex = -1;
368+
renderAutocomplete(state);
222369
return;
223370
}
224371
return; // Ignore other keys in input mode
@@ -240,10 +387,23 @@ function subscribeKeyEvents(
240387
state.debug = !state.debug;
241388
state.mode.action = RunnerAction.Test;
242389
} else if (key.name === 'p') {
243-
// p => enter pattern input mode
390+
// p => enter pattern input mode with autocomplete
244391
state.inputMode = 'pattern';
245392
state.inputBuffer = '';
246-
process.stdout.write('Pattern: ');
393+
394+
// Load all fixtures for autocomplete
395+
const fixtures = await getFixtures(null);
396+
state.allFixtureNames = Array.from(fixtures.keys()).sort();
397+
// Show failed fixtures first when no pattern entered
398+
const failedFixtures = Array.from(state.fixtureLastRunStatus.entries())
399+
.filter(([_, status]) => status === 'fail')
400+
.map(([name]) => name)
401+
.sort();
402+
state.matchingFixtures =
403+
failedFixtures.length > 0 ? failedFixtures : state.allFixtureNames;
404+
state.selectedIndex = -1;
405+
406+
renderAutocomplete(state);
247407
return; // Don't trigger onChange yet
248408
} else {
249409
// any other key re-runs tests
@@ -279,6 +439,10 @@ export async function makeWatchRunner(
279439
debug: debugMode,
280440
inputMode: 'none',
281441
inputBuffer: '',
442+
allFixtureNames: [],
443+
matchingFixtures: [],
444+
selectedIndex: -1,
445+
fixtureLastRunStatus: new Map(),
282446
};
283447

284448
subscribeTsc(state, onChange);

compiler/packages/snap/src/runner.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ async function onChange(
142142
true, // requireSingleFixture in watch mode
143143
);
144144
const end = performance.now();
145+
146+
// Track fixture status for autocomplete suggestions
147+
for (const [basename, result] of results) {
148+
const failed =
149+
result.actual !== result.expected || result.unexpectedError != null;
150+
state.fixtureLastRunStatus.set(basename, failed ? 'fail' : 'pass');
151+
}
152+
145153
if (mode.action === RunnerAction.Update) {
146154
update(results);
147155
state.lastUpdate = end;

0 commit comments

Comments
 (0)