diff --git a/config/scripts/quick-open-exclusion-benchmark.mjs b/config/scripts/quick-open-exclusion-benchmark.mjs new file mode 100644 index 00000000000..399302c5a2b --- /dev/null +++ b/config/scripts/quick-open-exclusion-benchmark.mjs @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict' +import { performance } from 'node:perf_hooks' +import { build } from 'esbuild' + +const bundled = await build({ + entryPoints: ['src/shared/quick-open-filter.ts'], + bundle: true, + platform: 'node', + format: 'esm', + write: false, + logLevel: 'silent' +}) +const { shouldExcludeQuickOpenRelPath: after } = await import( + `data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].text).toString('base64')}` +) +// Original production predicate, including its exact boundary check. +function before(relPath, prefixes) { + for (const prefix of prefixes) { + if (relPath === prefix) { + return true + } + if (relPath.length > prefix.length && relPath.startsWith(`${prefix}/`)) { + return true + } + } + return false +} +const files = Array.from( + { length: 100000 }, + (_, index) => `src/components/group-${index % 100}/file-${index}.tsx` +) +function run(fn, prefixes) { + let excluded = 0 + for (const file of files) { + excluded += Number(fn(file, prefixes)) + } + return excluded +} +function measure(fn, prefixes) { + run(fn, prefixes) + const samples = [] + for (let index = 0; index < 5; index++) { + const start = performance.now() + run(fn, prefixes) + samples.push(performance.now() - start) + } + return samples.sort((a, b) => a - b)[2] +} +const results = [] +for (const count of [0, 10, 100, 500]) { + const prefixes = Array.from({ length: count }, (_, index) => `nested-worktrees/worktree-${index}`) + assert.equal(run(after, prefixes), run(before, prefixes)) + results.push({ + files: files.length, + exclusions: count, + beforeMs: measure(before, prefixes), + afterMs: measure(after, prefixes) + }) +} +console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2)) diff --git a/src/shared/quick-open-filter.test.ts b/src/shared/quick-open-filter.test.ts index 1d96bc1744f..c481a1e8854 100644 --- a/src/shared/quick-open-filter.test.ts +++ b/src/shared/quick-open-filter.test.ts @@ -102,6 +102,43 @@ describe('buildExcludePathPrefixes', () => { }) describe('shouldExcludeQuickOpenRelPath', () => { + it('matches the original filter across boundary and Unicode path combinations', () => { + const paths = [ + '', + '/', + 'a', + 'a/', + 'a//', + 'ab', + 'a/b', + 'a\\b', + 'A/b', + '界/😀', + '界/😀x', + 'a[1]/x', + 'a./x' + ] + for (const prefix of paths) { + for (const relPath of paths) { + const expected = + relPath === prefix || (relPath.length > prefix.length && relPath.startsWith(`${prefix}/`)) + expect(shouldExcludeQuickOpenRelPath(relPath, [prefix])).toBe(expected) + } + } + }) + + it('preserves normalized Windows and UNC exclusion boundaries', () => { + for (const [root, excluded] of [ + ['C:\\Repo', 'C:\\Repo\\trees\\one'], + ['\\\\server\\share\\repo', '\\\\server\\share\\repo\\trees\\one'] + ]) { + const prefixes = buildExcludePathPrefixes(root, [excluded]) + expect(prefixes).toEqual(['trees/one']) + expect(shouldExcludeQuickOpenRelPath('trees/one/file.ts', prefixes)).toBe(true) + expect(shouldExcludeQuickOpenRelPath('trees/one-more/file.ts', prefixes)).toBe(false) + } + }) + it('matches exact and boundary paths only', () => { expect(shouldExcludeQuickOpenRelPath('packages/app', ['packages/app'])).toBe(true) expect(shouldExcludeQuickOpenRelPath('packages/app/x.ts', ['packages/app'])).toBe(true) diff --git a/src/shared/quick-open-filter.ts b/src/shared/quick-open-filter.ts index 9d5bebd80b3..b7592657a11 100644 --- a/src/shared/quick-open-filter.ts +++ b/src/shared/quick-open-filter.ts @@ -119,7 +119,7 @@ export function shouldExcludeQuickOpenRelPath( if (relPath === prefix) { return true } - if (relPath.length > prefix.length && relPath.startsWith(`${prefix}/`)) { + if (relPath[prefix.length] === '/' && relPath.startsWith(prefix)) { return true } }