diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index 5a7e64cca..3131bdf00 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -78,12 +78,20 @@ function resolveToolNames(mode: FffMode): ToolNames { // Cursor store — simple bounded Map for pagination cursors // --------------------------------------------------------------------------- -const cursorCache = new Map(); +// Fuzzy-fallback cursors must resume a fuzzy query: the engine cursor is bound +// to the match stream that produced it, so replaying it against the literal +// query (which matched nothing) would turn every resume into "No matches found". +interface StoredCursor { + cursor: GrepCursor; + fuzzy: boolean; +} + +const cursorCache = new Map(); let cursorCounter = 0; -function storeCursor(cursor: GrepCursor): string { +function storeCursor(cursor: GrepCursor, fuzzy: boolean): string { const id = `fff_c${++cursorCounter}`; - cursorCache.set(id, cursor); + cursorCache.set(id, { cursor, fuzzy }); if (cursorCache.size > 200) { const first = cursorCache.keys().next().value; if (first) cursorCache.delete(first); @@ -91,7 +99,7 @@ function storeCursor(cursor: GrepCursor): string { return id; } -function getCursor(id: string): GrepCursor | undefined { +function getCursor(id: string): StoredCursor | undefined { return cursorCache.get(id); } @@ -897,12 +905,15 @@ export default function fffExtension(pi: ExtensionAPI) { // (case-insensitive when pattern is all lowercase). const smartCase = params.caseSensitive !== true; + const storedCursor = params.cursor ? getCursor(params.cursor) : undefined; + const resumeFuzzy = storedCursor?.fuzzy === true; + const grepResult = picker.grep(query, { - mode, + mode: resumeFuzzy ? "fuzzy" : mode, smartCase, maxMatchesPerFile: pageSize, pageSize, - cursor: (params.cursor ? getCursor(params.cursor) : null) ?? null, + cursor: storedCursor?.cursor ?? null, beforeContext: context, afterContext: context, classifyDefinitions: true, @@ -954,7 +965,9 @@ export default function fffExtension(pi: ExtensionAPI) { notices.push(`Invalid regex: ${result.regexFallbackError}, used literal match`); } if (result.nextCursor) { - notices.push(`Continue with cursor="${storeCursor(result.nextCursor)}"`); + notices.push( + `Continue with cursor="${storeCursor(result.nextCursor, fuzzyNotice !== null || resumeFuzzy)}"`, + ); } if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`; @@ -1186,7 +1199,7 @@ export default function fffExtension(pi: ExtensionAPI) { maxMatchesPerFile: pageSize, pageSize, smartCase: true, - cursor: (params.cursor ? getCursor(params.cursor) : null) ?? null, + cursor: (params.cursor ? getCursor(params.cursor)?.cursor : null) ?? null, beforeContext: context, afterContext: context, }); @@ -1201,7 +1214,7 @@ export default function fffExtension(pi: ExtensionAPI) { notices.push(`${effectiveLimit}+ matches (refine patterns)`); if (result.nextCursor) notices.push( - `More available. cursor="${storeCursor(result.nextCursor)}" to continue`, + `More available. cursor="${storeCursor(result.nextCursor, false)}" to continue`, ); if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;