Skip to content

Commit 1cbc189

Browse files
Prithvi-Webclaude
andcommitted
fix(map): while a rescan paints the saved index's tree under a query in the highlight box, the map is drawn undimmed and its footer is the map's own line - K03 took the older scan's answer down and left the query with none, and the overlay then judged "ext:mov,mkv" by the bare-word rule, which matches no name: every rectangle was dimmed and the footer read "0 matches" (ninth browser pass M01)
A grammar query with no answer for the tree on screen (queryMode 'grammar', matchedPaths null) now dims nothing and counts nothing: renderSearchOverlay draws no scrim, updateTmStatus writes the map's own line ("N items of 4.0 KB or more drawn · 9.5 GB in this folder · from the saved index") and, drawn as circles or Voronoi cells, renderCellsStatus writes the shapes' line. The box still says the search runs once the scan finishes, and the scan that lands is asked once and its answer draws the highlights and the count, as K03 had it. drawTreemap's painting passes are untouched. treeScanKey runs the page's own query bookkeeping, overlay and both footers through the rescan, as rectangles and as circles. frontendContract's status line fixture now gives its answered grammar query the paths it matched, as the page always does beside a total. Mutants M01-a (overlay rule reverted), b (footer rule reverted), c (shapes' footer rule reverted), d (an answered grammar query treated as waiting), g (the footer treating every grammar query as waiting), f (the match count kept when the query waits), k1 (K03 keeping the older matches), k2 (K03 not saying what the search waits for), r (the landing not asking again), each RED, restored by SHA-256. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c581088 commit 1cbc189

5 files changed

Lines changed: 132 additions & 8 deletions

File tree

‎public/index.html‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14734,7 +14734,13 @@ <h3 id="portableTitle"><span data-icon="disc"></span>Portable session</h3>
1473414734
ctx.setTransform(1, 0, 0, 1, 0, 0);
1473514735
ctx.clearRect(0, 0, tmSearchBuffer.width, tmSearchBuffer.height);
1473614736

14737-
const q = state.treemap.query.trim().toLowerCase();
14737+
// A grammar query with no answer for this tree yet dims nothing: over the
14738+
// saved index's tree while its scan runs (K03 took the older answer down),
14739+
// the bare-word rule below judged "ext:mov,mkv" against every name, dimmed
14740+
// the whole map and counted "0 matches" (ninth browser pass M01). The query
14741+
// waits, the box says so, and its answer draws the highlights.
14742+
const waiting = state.treemap.queryMode === 'grammar' && !state.treemap.matchedPaths;
14743+
const q = waiting ? '' : state.treemap.query.trim().toLowerCase();
1473814744
state.treemap.matches = 0;
1473914745
updateTmStatus();
1474014746
if (!q) return;
@@ -14828,7 +14834,9 @@ <h3 id="portableTitle"><span data-icon="disc"></span>Portable session</h3>
1482814834
}
1482914835

1483014836
function updateTmStatus() {
14831-
const q = state.treemap.query.trim();
14837+
// A grammar query with no answer yet counts nothing: the line is the map's
14838+
// own, not "0 matches" (M01, as renderSearchOverlay draws it).
14839+
const q = state.treemap.queryMode === 'grammar' && !state.treemap.matchedPaths ? '' : state.treemap.query.trim();
1483214840
if (state.treemap.history.active && !q) {
1483314841
const when = formatWhen(state.treemap.history.viewingAt); // the one dialect, memoised
1483414842
$('tmStatus').textContent = `Viewing ${when} · ${formatBytes(state.treemap.rootSize)} — drag the slider to Live to interact`;
@@ -24489,7 +24497,10 @@ <h3 id="portableTitle"><span data-icon="disc"></span>Portable session</h3>
2448924497
function renderCellsStatus() {
2449024498
const st = $('tmStatus');
2449124499
if (!st || !isCells()) return;
24492-
if (state.treemap.query.trim() || state.treemap.history.active || tmPreview.on) return;
24500+
// A grammar query with no answer yet counts nothing, so the line is the
24501+
// shapes' own (M01, as renderSearchOverlay draws it).
24502+
const waiting = state.treemap.queryMode === 'grammar' && !state.treemap.matchedPaths;
24503+
if ((state.treemap.query.trim() && !waiting) || state.treemap.history.active || tmPreview.on) return;
2449324504
const cells = state.treemap.cells;
2449424505
const leaves = cells.reduce((k, c) => k + (c.leaf ? 1 : 0), 0);
2449524506
st.textContent =

‎src/ui/app/050-treemap-view.js‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2207,7 +2207,13 @@ function renderSearchOverlay() {
22072207
ctx.setTransform(1, 0, 0, 1, 0, 0);
22082208
ctx.clearRect(0, 0, tmSearchBuffer.width, tmSearchBuffer.height);
22092209

2210-
const q = state.treemap.query.trim().toLowerCase();
2210+
// A grammar query with no answer for this tree yet dims nothing: over the
2211+
// saved index's tree while its scan runs (K03 took the older answer down),
2212+
// the bare-word rule below judged "ext:mov,mkv" against every name, dimmed
2213+
// the whole map and counted "0 matches" (ninth browser pass M01). The query
2214+
// waits, the box says so, and its answer draws the highlights.
2215+
const waiting = state.treemap.queryMode === 'grammar' && !state.treemap.matchedPaths;
2216+
const q = waiting ? '' : state.treemap.query.trim().toLowerCase();
22112217
state.treemap.matches = 0;
22122218
updateTmStatus();
22132219
if (!q) return;
@@ -2301,7 +2307,9 @@ function renderSearchOverlay() {
23012307
}
23022308

23032309
function updateTmStatus() {
2304-
const q = state.treemap.query.trim();
2310+
// A grammar query with no answer yet counts nothing: the line is the map's
2311+
// own, not "0 matches" (M01, as renderSearchOverlay draws it).
2312+
const q = state.treemap.queryMode === 'grammar' && !state.treemap.matchedPaths ? '' : state.treemap.query.trim();
23052313
if (state.treemap.history.active && !q) {
23062314
const when = formatWhen(state.treemap.history.viewingAt); // the one dialect, memoised
23072315
$('tmStatus').textContent = `Viewing ${when} · ${formatBytes(state.treemap.rootSize)} — drag the slider to Live to interact`;

‎src/ui/app/090-alternate-renderers-the-drawing.js‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -920,7 +920,10 @@ function repaintCells() {
920920
function renderCellsStatus() {
921921
const st = $('tmStatus');
922922
if (!st || !isCells()) return;
923-
if (state.treemap.query.trim() || state.treemap.history.active || tmPreview.on) return;
923+
// A grammar query with no answer yet counts nothing, so the line is the
924+
// shapes' own (M01, as renderSearchOverlay draws it).
925+
const waiting = state.treemap.queryMode === 'grammar' && !state.treemap.matchedPaths;
926+
if ((state.treemap.query.trim() && !waiting) || state.treemap.history.active || tmPreview.on) return;
924927
const cells = state.treemap.cells;
925928
const leaves = cells.reduce((k, c) => k + (c.leaf ? 1 : 0), 0);
926929
st.textContent =

‎tests/frontendContract.test.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3297,11 +3297,14 @@ function runTmStatus(treemap: Record<string, unknown>): string {
32973297
* the answer there — so that sentence must stay exactly as it was.
32983298
*/
32993299
test('a grammar query whose matches outrun the map says so in the status line', () => {
3300-
const partial = runTmStatus({ query: 'size>1gb', queryMode: 'grammar', matches: 1, matchTotal: 2, rootSize: 5 });
3300+
// An answered grammar query carries the paths it matched beside its total, as tmRunGrammarQuery sets them; one with
3301+
// none is still waiting for its answer, and counts nothing (ninth browser pass M01).
3302+
const answered = new Set(['/a/big', '/b/big']);
3303+
const partial = runTmStatus({ query: 'size>1gb', queryMode: 'grammar', matchedPaths: answered, matches: 1, matchTotal: 2, rootSize: 5 });
33013304
assert.match(partial, /1 of 2 matches for “size>1gb”/,
33023305
'the drawn count is named as a share of what the query actually matched');
33033306

3304-
const all = runTmStatus({ query: 'size>1gb', queryMode: 'grammar', matches: 2, matchTotal: 2, rootSize: 5 });
3307+
const all = runTmStatus({ query: 'size>1gb', queryMode: 'grammar', matchedPaths: answered, matches: 2, matchTotal: 2, rootSize: 5 });
33053308
assert.match(all, /^2 matches for “size>1gb”/,
33063309
'nothing to disclose when the map drew them all — no "2 of 2"');
33073310

‎tests/treeScanKey.test.ts‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,105 @@ test('the older scan\'s matches go when a rescan paints the saved index\'s tree
938938
}
939939
});
940940

941+
/**
942+
* The highlight box over a drawn map: the query's bookkeeping, as queryPage lifts it, with the page's own search
943+
* overlay, map footer and shapes' footer drawing what each state says — the scrim laid over each rectangle or shape
944+
* that did not match, and the line under the map. `cells` draws the map as circles (§6.2) instead of rectangles.
945+
*/
946+
function mapUnderQueryPage(state: Record<string, unknown>, answer: (url: string) => unknown, cells = false) {
947+
const $ = stubDom();
948+
const asks: Ask[] = [];
949+
const reasked: string[] = [];
950+
const messages: string[] = [];
951+
/** What the overlay's last draw dimmed on the search buffer: each rectangle or circle laid under the scrim. */
952+
const dimmed: string[] = [];
953+
let style = '';
954+
let path: string[] = [];
955+
const isScrim = (s: string) => /^rgba\(\d+,\d+,\d+,0\.\d+\)$/.test(s);
956+
const ctx = {
957+
setTransform: noop, strokeRect: noop, stroke: noop, closePath: noop, lineTo: noop, moveTo: noop,
958+
// Each draw of the overlay begins by clearing the whole buffer: what it dims is what that draw laid down.
959+
clearRect: (x: number, y: number, w: number, h: number) => { if (x === 0 && y === 0 && w === 200 && h === 100) dimmed.length = 0; },
960+
beginPath: () => { path = []; },
961+
arc: (x: number) => { path.push(`circle at ${x}`); },
962+
fillRect: (x: number) => { if (isScrim(style)) dimmed.push(`rect at ${x}`); },
963+
fill: () => { if (isScrim(style)) dimmed.push(...path); },
964+
set fillStyle(v: string) { style = v; }, get fillStyle() { return style; },
965+
strokeStyle: '', lineWidth: 1,
966+
};
967+
const given: Record<string, unknown> = {
968+
state, $, tmQuerySeq: 0, tmQueryAbort: null, tmQueryDeb: 0, tmLastHits: null, AbortController, clearTimeout: noop,
969+
api: async (url: string, init: { body: string }) => { asks.push({ url, scanId: scanOf(url, init.body) }); return answer(url); },
970+
tmSetQueryMessage: (m: string) => { messages.push(m); }, tmStageable: () => ({ paths: [] }), seedNodes: noop, tmSyncQueryBar: noop,
971+
presentView: noop, formatCount: String, tmUndrawnMessage: () => '',
972+
fxTmPillBeamsSync: noop, loadSavedViews: noop, refreshDock: noop,
973+
// The overlay and the footers over a 200 × 100 map, the search buffer's context recorded.
974+
tmSearchBuffer: { width: 200, height: 100 }, tmBuffer: { width: 200, height: 100 }, tmSearchCtx: ctx, window: { devicePixelRatio: 1 },
975+
document: { documentElement: { dataset: { theme: 'dark' } } }, cssVar: () => '#0A84FF', tmKidsOf: () => new Map(), altCellPath: noop,
976+
isSun: () => false, isCells: () => cells, tmPreview: { on: false }, formatWhen: String,
977+
formatBytes: (n: number) => (n >= 1e9 ? `${(n / 1e9).toFixed(1)} GB` : `${(n / 1e3).toFixed(1)} KB`),
978+
};
979+
const { fns } = page<{ run: (q: string) => Promise<void>; mount: () => void; changed: () => void; paint: () => void }>(
980+
['async function tmRunGrammarQuery(', 'function tmRejudgeQuery(', 'function tmRejudgeQueryWait(', 'function tmNoScanQueryWords(', 'function tmQueryScanChanged(', 'function tmCancelQueries(',
981+
'function tmClearHits(', 'function tmSyncStageButton(', 'function treemapMatch(', 'function renderSearchOverlay(', 'function updateTmStatus(', 'function renderCellsStatus('],
982+
given, `{ run: tmRunGrammarQuery, mount: () => (${pageView('treemap')}).mount(), changed: tmQueryScanChanged,
983+
paint: () => { renderSearchOverlay(); if (isCells()) renderCellsStatus(); } }`);
984+
// A paint lays the map out afresh (loadTreemap → drawTreemap, or drawCells), then draws its overlay and footer over it.
985+
given.loadTreemap = () => fns.paint();
986+
// tmApplyQuery hands a grammar query to tmRunGrammarQuery (the bare-word path is mapColourPop's and indexSearch's).
987+
given.tmApplyQuery = (q: string) => { reasked.push(q); void fns.run(q.trim()); };
988+
return { fns, $, asks, reasked, messages, dimmed };
989+
}
990+
991+
test('while a rescan paints the saved index\'s tree under a query in the highlight box, the map is drawn undimmed and its footer is the map\'s own line — never "0 matches": the query waits for the scan, and the box says so — and the scan that lands is asked once and dims the map again (ninth browser pass M01)', async () => {
992+
// corpus scanned, `ext:mov,mkv` answered (266 matches), then Scan pressed again: the saved index's tree was painted
993+
// while the scan ran, and K03 took the older scan's matches down, leaving the query in the box with no answer. The
994+
// overlay then judged "ext:mov,mkv" by the bare-word rule, which matches no name: every rectangle got the scrim, and
995+
// the footer read "0 matches for “ext:mov,mkv” · 9.5 GB in this folder", with no "· from the saved index".
996+
for (const cells of [false, true]) {
997+
const drawn = cells ? 'as circles' : 'as rectangles';
998+
const nodes = [{ path: '/r/a.mov' }, { path: '/r/b.txt' }];
999+
const a = { path: '/r/a.mov', name: 'a.mov', type: 'file', extension: 'mov', depth: 1 };
1000+
const b = { path: '/r/b.txt', name: 'b.txt', type: 'file', extension: 'txt', depth: 1 };
1001+
const state: Record<string, unknown> = { root: TREE, ...STOPPED, scanningPath: null, view: 'treemap',
1002+
treemap: { rootPath: '/r', query: 'ext:mov,mkv', queryRoot: '/r', queryMode: 'bare', queryScanId: null, matches: 0, matchedPaths: null, matchTotal: null,
1003+
mode: cells ? 'circles' : 'treemap', history: { active: false }, minSize: 4000, rootSize: 9.5e9, nodes, nodesFor: { scanId: 'scan-old', nodes },
1004+
pxRects: [{ n: a, x: 0, y: 0, w: 100, h: 100 }, { n: b, x: 100, y: 0, w: 100, h: 100 }],
1005+
cells: [{ n: { path: '/r' }, kind: 'circle', cx: 100, cy: 50, r: 50 }, { n: a, kind: 'circle', cx: 50, cy: 50, r: 40, leaf: true }, { n: b, kind: 'circle', cx: 150, cy: 50, r: 40, leaf: true }] } };
1006+
const t = state.treemap as Record<string, unknown>;
1007+
const c = mapUnderQueryPage(state, () => ({ hits: [{ path: '/r/a.mov', name: 'a.mov', size: 5e9 }], total: 266 }), cells);
1008+
const footer = () => c.$('tmStatus').textContent;
1009+
const answered = cells ? ['circle at 100', 'circle at 150'] : ['rect at 100']; // b.txt, and in circles the folder around both
1010+
// Control: the older scan's tree, the query answered — what it did not match is dimmed, and the footer counts it.
1011+
await c.fns.run('ext:mov,mkv');
1012+
assert.deepEqual(c.dimmed, answered, `${drawn}: control: the answer dims what it did not match`);
1013+
assert.equal(footer(), '1 of 266 matches for “ext:mov,mkv” · 9.5 GB in this folder', `${drawn}: control: the footer counts the answer`);
1014+
// Scan pressed again: the saved index's tree is painted (TOPIC.scan, null), and shown again as the scan chrome re-enters the view.
1015+
Object.assign(state, INDEX_RUNNING, { scanningPath: '/r' });
1016+
t.nodesFor = { scanId: null, nodes };
1017+
c.fns.changed();
1018+
c.fns.paint();
1019+
for (const when of ['painted', 're-entered']) {
1020+
if (when === 're-entered') c.fns.mount();
1021+
assert.deepEqual([t.queryMode, t.matchedPaths, t.matchTotal], ['grammar', null, null], `${drawn}, ${when}: control: the query waits, the older answer gone (K03)`);
1022+
assert.equal(c.messages.at(-1), 'The search runs on a finished scan — it runs once the scan finishes.', `${drawn}, ${when}: control: the box says the search waits for the scan`);
1023+
assert.deepEqual(c.dimmed, [], `${drawn}, ${when}: nothing is dimmed while the query waits`);
1024+
assert.equal(t.matches, 0, `${drawn}, ${when}: and nothing is counted as a match`);
1025+
assert.equal(footer(), cells ? '2 shapes · 2 without children · 9.5 GB total' : '2 items of 4.0 KB or more drawn · 9.5 GB in this folder · from the saved index',
1026+
`${drawn}, ${when}: the footer is the map's own line, with no match count`);
1027+
}
1028+
// The scan lands (finishScan re-enters the view): the query is asked once, of it — nothing was asked of the running
1029+
// scan — and its answer dims the map again and is counted under it.
1030+
Object.assign(state, LANDED);
1031+
t.nodesFor = { scanId: 'scan-new', nodes };
1032+
c.fns.mount();
1033+
await turns();
1034+
assert.deepEqual([c.reasked, c.asks.map((x) => x.scanId), c.dimmed, footer()],
1035+
[['ext:mov,mkv'], ['scan-old', 'scan-new'], answered, '1 of 266 matches for “ext:mov,mkv” · 9.5 GB in this folder'],
1036+
`${drawn}: the scan that lands is asked the query, once, and its answer dims the map and is counted again`);
1037+
}
1038+
});
1039+
9411040
test('the highlight box\'s query asked of the older scan during a rescan is not installed over the new tree when it answers after the rescan landed — the new scan is asked — and a scan of the same folder landing stops it on the server', async () => {
9421041
const tm = () => ({ rootPath: '/r', query: 'used>1y', queryRoot: '/r', queryMode: 'bare', queryScanId: null, matches: 0, matchedPaths: null });
9431042
let release: (v: unknown) => void = noop;

0 commit comments

Comments
 (0)