diff --git a/apps/cli/solutions.ts b/apps/cli/solutions.ts index 66f6561..fe52dae 100644 --- a/apps/cli/solutions.ts +++ b/apps/cli/solutions.ts @@ -9,10 +9,12 @@ * * Every run backfills the db's solutions table with created_at (from the * file's birthtime, mtime when the filesystem has none) so ordering works for - * files that predate the db. Ordering is per command — test lists the most - * recently tested first (never-tested after, by name); submit lists the most - * recently created first. Nothing is ever hidden. Exit 0 of leetcode-cli - * stamps last_tested / last_submitted. + * files that predate the db. Files git reports as modified, added or + * untracked come first, newest edit first — the problem being worked on is + * the focused option, so Enter runs it without a search. The rest are ordered + * per command — test lists the most recently tested first (never-tested + * after, by name); submit lists the most recently created first. Nothing is + * ever hidden. Exit 0 of leetcode-cli stamps last_tested / last_submitted. * * Library: side-effect-free on import — nothing scans the filesystem until * runOnSolution() is called. @@ -29,6 +31,29 @@ function lcNumber(rel: string): number | undefined { return m ? Number(m[1]) : undefined; } +/** + * work/-relative paths of solutions git sees as modified, added or untracked, + * mapped to their mtime. Empty outside a git checkout. + */ +function dirtySolutions(scanned: Set): Map { + const git = Bun.spawnSync( + ["git", "status", "--porcelain", "-z", "--untracked-files=all", "--", WORK_DIR], + { cwd: ROOT }, + ); + const dirty = new Map(); + if (!git.success) return dirty; + const entries = git.stdout.toString().split("\0"); + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]!; + // renames/copies carry their source path as the next entry; skip it + if (/[RC]/.test(entry.slice(0, 2))) i++; + const rel = relative(WORK_DIR, join(ROOT, entry.slice(3))); + // deletions and non-solution files are not in the scan + if (scanned.has(rel)) dirty.set(rel, statSync(join(WORK_DIR, rel)).mtimeMs); + } + return dirty; +} + /** Fuzzy-pick a solution under work/ and run `leetcode ` on it. */ export async function runOnSolution( command: "test" | "submit", @@ -61,7 +86,15 @@ export async function runOnSolution( return (command === "test" ? row?.lastTested : row?.createdAt) ?? undefined; }; + const dirty = dirtySolutions(new Set(scanned)); const files = scanned.sort((a, b) => { + const ma = dirty.get(a); + const mb = dirty.get(b); + if (ma !== mb) { + if (ma === undefined) return 1; + if (mb === undefined) return -1; + return mb - ma; + } const ka = sortKey(a); const kb = sortKey(b); if (ka !== kb) { @@ -91,7 +124,7 @@ export async function runOnSolution( value: f, label: basename(f), hint: parsed - ? `${stage} · ${parsed.difficulty} · ${parsed.category}` + ? `${dirty.has(f) ? "uncommitted · " : ""}${stage} · ${parsed.difficulty} · ${parsed.category}` : f, }; }),