Tab opening ranking (#23435)

* Add literal-basename file matching for tab opening

- Distinguish literal filename matches from fuzzy matches
- Prioritize search for short tokens (1-2 Latin characters)
- Sort literal matches naturally instead of by fuzzy score
- Reserve search option even when literal matches fill results

* Keep literal-basename file matches in tab entry options

Ensure literal-basename file matches appear in tab creation suggestions
as fallback options when exact-basename matches exist, improving file
discovery for users.
This commit is contained in:
Jinjing
2026-09-27 13:50:12 -07:00
committed by GitHub
parent a4b60c2ea3
commit eaa6fb71b0
7 changed files with 352 additions and 42 deletions
@@ -99,6 +99,66 @@ afterEach(() => {
})
describe('TabBarCreateEntry search behavior', () => {
it.each([false, true])('searches weak file matches on Enter (delayed listing: %s)', (delayed) => {
const readyListing = {
files: [
'tests/e2e/terminal-split-activation-latency-main-probe.ts',
'src/main/linear/issue-context-inline-media.test.ts',
'src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-cache-match.ts'
],
loading: false,
loadError: null
}
fileListMock.current = delayed ? { files: [], loading: true, loadError: null } : readyListing
const onOpenEntry = vi.fn().mockResolvedValue(undefined)
renderEntry({ onOpenEntry })
setQuery('asciinema')
if (delayed) {
expect(container.querySelector('[aria-selected="true"]')).toBeNull()
submit()
expect(onOpenEntry).not.toHaveBeenCalled()
fileListMock.current = readyListing
renderEntry({ onOpenEntry })
}
expect(container.querySelector('[role="option"]')?.textContent).toContain('Search Google')
expect(container.querySelector('[aria-selected="true"]')?.textContent).toContain(
'Search Google'
)
submit()
expect(onOpenEntry).toHaveBeenCalledWith(
expect.objectContaining({
classification: { kind: 'search', engine: 'google', query: 'asciinema' }
})
)
})
it.each(['a', 'ab', 'asciinema'])('waits for an exact filename before arming %s', (query) => {
fileListMock.current = { files: [], loading: true, loadError: null }
const onOpenEntry = vi.fn().mockResolvedValue(undefined)
renderEntry({ onOpenEntry })
setQuery(query)
expect(container.querySelector('[aria-selected="true"]')).toBeNull()
submit()
expect(onOpenEntry).not.toHaveBeenCalled()
fileListMock.current = { files: [`docs/${query}`], loading: false, loadError: null }
renderEntry({ onOpenEntry })
expect(container.querySelector('[aria-selected="true"]')?.textContent).toContain('Open file')
submit()
expect(onOpenEntry).toHaveBeenCalledWith(
expect.objectContaining({
classification: {
kind: 'existing-file',
matchKind: 'exact-basename',
relativePath: `docs/${query}`
}
})
)
})
it('tracks the configured provider and submits its exact classification', () => {
const onOpenEntry = vi.fn().mockResolvedValue(undefined)
renderEntry({ onOpenEntry })
@@ -194,9 +194,9 @@ describe('tab create entry classification', () => {
relativePath: 'src/components/Button.tsx'
})
expect(classifyTabEntryQuery('btn', files)).toEqual({
kind: 'existing-file',
matchKind: 'fuzzy',
relativePath: 'src/components/Button.tsx'
kind: 'search',
engine: 'google',
query: 'btn'
})
})
@@ -212,6 +212,18 @@ describe('tab create entry classification', () => {
])
})
it('keeps literal filename matches after an exact basename match', () => {
expect(
getTabEntryOptions('Makefile', readyFiles(['build/Makefile', 'build/Makefile.am'])).map(
(option) => option.classification
)
).toEqual([
{ kind: 'existing-file', matchKind: 'exact-basename', relativePath: 'build/Makefile' },
{ kind: 'search', engine: 'google', query: 'Makefile' },
{ kind: 'existing-file', matchKind: 'literal-basename', relativePath: 'build/Makefile.am' }
])
})
it('prefers creating typed file paths over fuzzy matches', () => {
expect(
getTabEntryOptions('read.md', readyFiles(['README.md'])).map(
@@ -224,7 +236,7 @@ describe('tab create entry classification', () => {
])
})
it('keeps single-token quick-open matches ahead of search, but not phrases', () => {
it('keeps literal filename matches ahead of search, but not phrases', () => {
expect(
getTabEntryOptions('typescript', readyFiles(['docs/typescript-guide.md'])).map(
(option) => option.classification
@@ -232,7 +244,7 @@ describe('tab create entry classification', () => {
).toEqual([
{
kind: 'existing-file',
matchKind: 'fuzzy',
matchKind: 'literal-basename',
relativePath: 'docs/typescript-guide.md'
},
{ kind: 'search', engine: 'google', query: 'typescript' },
@@ -260,7 +272,7 @@ describe('tab create entry classification', () => {
})
// Fuzzy matching is a subsequence scan, so a short token matches broadly.
it('keeps a search slot when fuzzy matches would fill the whole list', () => {
it('puts search ahead of fuzzy filename and path matches', () => {
const files = [
'src/components/Button.tsx',
'src/lib/bootstrap-nav.ts',
@@ -269,15 +281,14 @@ describe('tab create entry classification', () => {
'src/bin/tune.ts'
]
expect(getTabEntryOptions('btn', readyFiles(files)).map((o) => o.classification.kind)).toEqual([
'search',
'existing-file',
'existing-file',
'existing-file',
'search'
'existing-file'
])
// A one-slot list still answers with the file, so Enter keeps quick-open.
expect(
getTabEntryOptions('btn', readyFiles(files), 1).map((o) => o.classification.kind)
).toEqual(['existing-file'])
).toEqual(['search'])
})
it('ranks search before ordinary create-file actions', () => {
@@ -3,7 +3,11 @@ import type { RuntimeFileListState } from '../quick-open-file-list'
import { translate } from '@/i18n/i18n'
import { getTabEntryOmniboxPlaceholder } from './tab-create-entry-copy'
import { DEFAULT_SEARCH_ENGINE, type SearchEngine } from '../../../../shared/browser-url'
import { findExistingFileMatches, isLikelyNewFileIntent } from './tab-create-entry-file-matches'
import {
findExistingFileMatches,
isLikelyNewFileIntent,
type ExistingFileMatch
} from './tab-create-entry-file-matches'
import { parseForcedSearchQuery } from './tab-create-entry-forced-search'
import {
isTabEntryAbsolutePathLike,
@@ -31,11 +35,7 @@ export const TAB_ENTRY_ABSOLUTE_PATH_REMOTE_BLOCKED_MESSAGE =
export type TabEntryClassification =
| { kind: 'empty'; message: string }
| { kind: 'explicit-url'; url: string }
| {
kind: 'existing-file'
matchKind: 'exact-path' | 'exact-basename' | 'fuzzy'
relativePath: string
}
| ExistingFileMatch
| { kind: 'host-url'; url: string }
| { kind: 'search'; engine: SearchEngine; query: string }
| { kind: 'new-file'; relativePath: string }
@@ -223,8 +223,9 @@ export function getTabEntryOptions(
getPreparedQuickOpenFiles(fileList.files),
Math.max(actionLimit, 1)
)
const exactExistingFiles = existingFiles.filter((file) => file.matchKind !== 'fuzzy')
const fuzzyExistingFiles = existingFiles.filter((file) => file.matchKind === 'fuzzy')
const exactExistingFiles = existingFiles.filter(
(file) => file.matchKind === 'exact-path' || file.matchKind === 'exact-basename'
)
if (exactExistingFiles.length > 0) {
const options: TabEntryActionClassification[] = [...exactExistingFiles]
@@ -233,37 +234,41 @@ export function getTabEntryOptions(
} else if (!hostUrl && newFile) {
options.push(search)
}
options.push(...existingFiles.filter((file) => file.matchKind === 'literal-basename'))
return toOptions(options, actionLimit)
}
if (hostUrl?.kind === 'blocked') {
return [blockedOption('invalid-url', hostUrl.message)]
}
if (hostUrl?.kind === 'host-url') {
return toOptions([hostUrl, ...fuzzyExistingFiles], actionLimit)
return toOptions([hostUrl, ...existingFiles], actionLimit)
}
if (pathError || !newFile) {
// Why: an unusable path is still a live quick-open prefix — "src/" cannot be
// created, but it matches real files, and dropping them turns every typed
// separator into an error row mid-keystroke.
return fuzzyExistingFiles.length > 0
? toOptions(fuzzyExistingFiles, actionLimit)
return existingFiles.length > 0
? toOptions(existingFiles, actionLimit)
: [invalidPathOption(pathError)]
}
if (isLikelyNewFileIntent(trimmed)) {
return toOptions([newFile, search, ...fuzzyExistingFiles], actionLimit)
return toOptions([newFile, search, ...existingFiles], actionLimit)
}
// Why no create row: a spaced, extension-less phrase is a web query, and a
// stray arrow/click on "Create file" leaves an empty `release notes` on disk
// that then outranks search as an exact match forever after.
if (/\s/.test(trimmed)) {
return toOptions([search, ...fuzzyExistingFiles], actionLimit)
return toOptions([search, ...existingFiles], actionLimit)
}
// Why: a single token is still a quick-open attempt ("btn" → Button.tsx), so
// only phrases promote web search over fuzzy matches. Fuzzy matching is a
// subsequence scan that fills every slot in a real repo, so hold one back —
// otherwise search silently disappears from the list it should always offer.
// Short Latin tokens match too many filenames to imply file intent; CJK words can be short.
if (/^[\p{Script=Latin}\p{Nd}]{1,2}$/u.test(trimmed.normalize('NFC'))) {
return toOptions([search, ...existingFiles, newFile], actionLimit)
}
const literalFiles = existingFiles.filter((file) => file.matchKind === 'literal-basename')
const fuzzyFiles = existingFiles.filter((file) => file.matchKind === 'fuzzy')
// Reserve a search slot even when literal filename matches fill the list.
return toOptions(
[...fuzzyExistingFiles.slice(0, Math.max(actionLimit - 1, 1)), search, newFile],
[...literalFiles.slice(0, Math.max(actionLimit - 1, 1)), search, ...fuzzyFiles, newFile],
actionLimit
)
}
@@ -2,6 +2,7 @@ import { expect, it, vi } from 'vitest'
import { prepareQuickOpenFiles, rankQuickOpenFiles } from '../quick-open-search'
import { findExistingFileMatches } from './tab-create-entry-file-matches'
import type * as QuickOpenSearch from '../quick-open-search'
import { compareFileNames } from '../../../../shared/file-name-sort'
const ranking = vi.hoisted(() => ({ calls: 0 }))
vi.mock('../quick-open-search', async (importOriginal) => {
@@ -28,6 +29,20 @@ it('skips fuzzy ranking when exact matches fill the requested window', () => {
expect(ranking.calls).toBe(0)
})
it('selects literal filenames from a large inventory without running fuzzy ranking', () => {
const files = prepareQuickOpenFiles([
...Array.from({ length: 100_000 }, (_, index) => `a/s/c/i/i/n/e/m/a/file-${index}.ts`),
'docs/asciinema-10.md',
'docs/asciinema-2.md'
])
ranking.calls = 0
expect(findExistingFileMatches('asciinema', files, 2).map((file) => file.relativePath)).toEqual([
'docs/asciinema-2.md',
'docs/asciinema-10.md'
])
expect(ranking.calls).toBe(0)
})
it('deduplicates exact paths and still fills remaining slots with fuzzy matches', () => {
const files = prepareQuickOpenFiles(['a.md', 'a.md', 'other/a.md', 'abc.md'])
expect(findExistingFileMatches('a.md', files, 4)).toEqual([
@@ -37,14 +52,12 @@ it('deduplicates exact paths and still fills remaining slots with fuzzy matches'
])
})
// Why: skipping the ranker is only sound because a fuzzy hit can never outrank an
// exact one — exact results are concatenated first and dedupe is first-wins, so the
// exact prefix is exactly what a full ranked-then-sliced list would have returned.
// This re-runs the pre-skip pipeline and holds the shipped one to it.
// Sort every literal candidate to check the bounded selection against a full reference.
function rankThenSlice(
query: string,
files: readonly QuickOpenSearch.QuickOpenIndexedFile[],
limit: number
limit: number,
preferLiteralFilenames = false
): { kind: string; matchKind: string; relativePath: string }[] {
const normalized = query.trim().replace(/\\/g, '/')
if (!normalized || limit <= 0) {
@@ -56,6 +69,12 @@ function rankThenSlice(
...files
.filter((f) => f.lowerFilename === lower)
.map((f) => ['exact-basename', f.path] as const),
...(preferLiteralFilenames
? files
.filter((file) => file.lowerFilename.includes(lower))
.sort((a, b) => compareFileNames(a.path, b.path))
.map((file) => ['literal-basename', file.path] as const)
: []),
...rankQuickOpenFiles(normalized, files, limit).map((f) => ['fuzzy', f.path] as const)
]
const seen = new Set<string>()
@@ -65,7 +84,7 @@ function rankThenSlice(
.slice(0, limit)
}
it('returns what rank-then-slice would have returned, including at limit 1', () => {
it('preserves quick-open ordering for explicit file and path queries, including at limit 1', () => {
const corpus = [
['a.md', 'ab.md', 'abc.md', 'deep/nested/a.md'],
['ab.md', 'abc.md', 'deep/nested/a.md'],
@@ -74,7 +93,7 @@ it('returns what rank-then-slice would have returned, including at limit 1', ()
['README.md', 'docs/readme.md', 'readme.mdx'],
['only-fuzzy.md']
]
const queries = ['a.md', 'index.ts', 'readme.md', 'src/index.ts', 'a', 'nope.md']
const queries = ['a.md', 'index.ts', 'readme.md', 'src/index.ts', 'nope.md']
for (const paths of corpus) {
const files = prepareQuickOpenFiles(paths)
for (const query of queries) {
@@ -90,6 +109,46 @@ it('returns what rank-then-slice would have returned, including at limit 1', ()
}
})
it('matches the full literal-first reference for bare tokens across limits and input orders', () => {
const queries = ['a', 'ab', 'abc', 'BTN', 'button', 'asciinema', '工具', 'é']
for (const query of queries) {
const paths = [
...Array.from({ length: 12 }, (_, index) => `${[...query].join('/')}/file-${index}.ts`),
`docs/${query}-10.md`,
`docs/${query}-2.md`,
`docs/${query}-2.md`,
`docs\\${query.toLowerCase()}-guide.md`,
`docs/a${'x'.repeat(120)}-${query.toUpperCase()}.md`,
'unrelated.txt'
]
for (const corpus of [paths, [...paths, query, `docs/${query}`, query]]) {
for (const orderedPaths of [corpus, corpus.toReversed()]) {
const files = prepareQuickOpenFiles(orderedPaths)
for (const limit of [0, 1, 2, 3, 5, 20]) {
expect({
query,
limit,
matches: findExistingFileMatches(` ${query} `, files, limit)
}).toEqual({ query, limit, matches: rankThenSlice(query, files, limit, true) })
}
}
}
}
})
it('does not let duplicate literal candidates consume the result limit', () => {
const files = prepareQuickOpenFiles([
...Array.from({ length: 10 }, () => 'docs/button-1.md'),
'docs/button-2.md'
])
ranking.calls = 0
expect(findExistingFileMatches('button', files, 2)).toEqual([
{ kind: 'existing-file', matchKind: 'literal-basename', relativePath: 'docs/button-1.md' },
{ kind: 'existing-file', matchKind: 'literal-basename', relativePath: 'docs/button-2.md' }
])
expect(ranking.calls).toBe(0)
})
it('takes the exact match at limit 1 without consulting the ranker at all', () => {
const files = prepareQuickOpenFiles(['a.md', 'ab.md', 'abc.md', 'deep/nested/a.md'])
ranking.calls = 0
@@ -1,8 +1,12 @@
import { rankQuickOpenFiles, type QuickOpenIndexedFile } from '../quick-open-search'
import {
QuickOpenPathRanker,
rankQuickOpenFiles,
type QuickOpenIndexedFile
} from '../quick-open-search'
export type ExistingFileMatch = {
kind: 'existing-file'
matchKind: 'exact-path' | 'exact-basename' | 'fuzzy'
matchKind: 'exact-path' | 'exact-basename' | 'literal-basename' | 'fuzzy'
relativePath: string
}
@@ -36,6 +40,27 @@ export function isLikelyNewFileIntent(query: string): boolean {
return hasFilenameExtension(trimmed) || /^\.[^.].*$/.test(trimmed)
}
function findLiteralFilenameMatches(
query: string,
files: readonly QuickOpenIndexedFile[],
limit: number
): ExistingFileMatch[] {
// Natural ordering avoids arbitrary fuzzy scores among literal filename matches.
const ranker = new QuickOpenPathRanker('', limit)
const seen = new Set<string>()
for (const file of files) {
if (file.lowerFilename.includes(query) && !seen.has(file.path)) {
seen.add(file.path)
ranker.consider(file.path)
}
}
return ranker.result().paths.map((relativePath) => ({
kind: 'existing-file',
matchKind: 'literal-basename',
relativePath
}))
}
function dedupeMatches(matches: ExistingFileMatch[]): ExistingFileMatch[] {
const seen = new Set<string>()
return matches.filter((match) => {
@@ -75,11 +100,21 @@ export function findExistingFileMatches(
if (exactMatches.length >= limit) {
return exactMatches.slice(0, limit)
}
const preferFilenames = !isLikelyNewFileIntent(normalizedQuery) && !/\s/.test(normalizedQuery)
const preferredMatches = preferFilenames
? dedupeMatches([
...exactMatches,
...findLiteralFilenameMatches(lowerQuery, indexedFiles, limit)
])
: exactMatches
if (preferredMatches.length >= limit) {
return preferredMatches.slice(0, limit)
}
const fuzzyMatches = rankQuickOpenFiles(normalizedQuery, indexedFiles, limit).map((file) => ({
kind: 'existing-file' as const,
matchKind: 'fuzzy' as const,
relativePath: file.path
}))
return dedupeMatches([...exactMatches, ...fuzzyMatches]).slice(0, limit)
return dedupeMatches([...preferredMatches, ...fuzzyMatches]).slice(0, limit)
}
@@ -2,10 +2,11 @@ import { describe, expect, it } from 'vitest'
import { insertHistoryRowsBelowFileMatches } from './tab-create-entry-history-placement'
import type { BrowserHistoryOmniboxRow } from './tab-create-entry-active-option'
import type { TabEntryOption } from './tab-create-entry-action'
import type { ExistingFileMatch } from './tab-create-entry-file-matches'
function fileOption(
relativePath: string,
matchKind: 'exact-path' | 'exact-basename' | 'fuzzy'
matchKind: ExistingFileMatch['matchKind']
): TabEntryOption {
return {
id: `existing-file:${relativePath}`,
@@ -40,7 +41,7 @@ describe('tab create entry history placement', () => {
const placed = insertHistoryRowsBelowFileMatches(
[
fileOption('README.md', 'exact-basename'),
fileOption('docs/readme-notes.md', 'fuzzy'),
fileOption('docs/readme-notes.md', 'literal-basename'),
newFileOption,
searchOption
],
@@ -53,9 +54,9 @@ describe('tab create entry history placement', () => {
expect(placed[3]).toMatchObject({ option: { id: 'new-file:readme' } })
})
it('stays below a fuzzy match that is the only file row', () => {
it.each(['literal-basename', 'fuzzy'] as const)('stays below a lone %s file row', (matchKind) => {
const placed = insertHistoryRowsBelowFileMatches(
[fileOption('docs/readme-notes.md', 'fuzzy'), searchOption],
[fileOption('docs/readme-notes.md', matchKind), searchOption],
rows
)
@@ -0,0 +1,139 @@
import { describe, expect, it } from 'vitest'
import { classifyTabEntryQuery, getTabEntryOptions } from './tab-create-entry-classifier'
import { prepareQuickOpenFiles } from '../quick-open-search'
import { findExistingFileMatches } from './tab-create-entry-file-matches'
const unrelatedFiles = [
'tests/e2e/terminal-split-activation-latency-main-probe.ts',
'src/main/linear/issue-context-inline-media.test.ts',
'src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-cache-match.ts'
]
const readyFiles = (files: string[]) => ({ files, loading: false, loadError: null })
describe('new-tab file matches versus web search', () => {
it('puts search ahead of scattered path matches for asciinema', () => {
const options = getTabEntryOptions('asciinema', readyFiles(unrelatedFiles))
expect(options.map((option) => option.classification.kind)).toEqual([
'search',
'existing-file',
'existing-file',
'existing-file'
])
expect(classifyTabEntryQuery('asciinema', readyFiles(unrelatedFiles))).toEqual({
kind: 'search',
engine: 'google',
query: 'asciinema'
})
})
it('keeps an actual filename match ahead of search and weak matches below it', () => {
const options = getTabEntryOptions(
'asciinema',
readyFiles([...unrelatedFiles, 'docs/asciinema-guide.md'])
)
expect(options[0].classification).toMatchObject({
kind: 'existing-file',
relativePath: 'docs/asciinema-guide.md'
})
expect(options[1].classification.kind).toBe('search')
expect(options.slice(2).every((option) => option.classification.kind === 'existing-file')).toBe(
true
)
})
it.each([1, 2, 4, 8])('finds a literal filename before limiting to %i actions', (limit) => {
const files = readyFiles([
...Array.from({ length: 10 }, (_, index) => `b/u/t/t/o/n/file-${index}.ts`),
'src/components/Button.tsx'
])
expect(getTabEntryOptions('button', files, limit)[0].classification).toMatchObject({
kind: 'existing-file',
relativePath: 'src/components/Button.tsx'
})
})
it('offers fuzzy filename abbreviations below search', () => {
expect(
getTabEntryOptions('btn', readyFiles(['src/components/Button.tsx'])).map(
(option) => option.classification
)
).toEqual([
{ kind: 'search', engine: 'google', query: 'btn' },
{ kind: 'existing-file', matchKind: 'fuzzy', relativePath: 'src/components/Button.tsx' },
{ kind: 'new-file', relativePath: 'btn' }
])
})
it('finds literal filenames even when their old fuzzy score falls outside the result limit', () => {
const target = `docs/a${'x'.repeat(120)}-asciinema.md`
const files = readyFiles([
...Array.from({ length: 10 }, (_, index) => `a/s/c/i/i/n/e/m/a/file-${index}.ts`),
target
])
expect(getTabEntryOptions('asciinema', files)[0].classification).toMatchObject({
kind: 'existing-file',
relativePath: target
})
})
it.each([
['btn', 'src/components/Button.tsx', false],
['button', 'src/components/Button.tsx', true],
['asciinema', 'docs/ASCIINEMA-guide.md', true],
['asciinema', 'docs\\ASCIINEMA-guide.md', true],
['asciinema', `docs/a${'x'.repeat(120)}-asciinema.md`, true],
['asciinema', unrelatedFiles[0], false],
['asciinema', unrelatedFiles[1], false],
['asciinema', unrelatedFiles[2], false],
['abc', 'alphabet-biology-chemistry.md', false],
['abc', 'a----b----c.md', false],
['btn', 'src/base/tone.ts', false]
])('reports literal filename matching for %s in %s', (query, path, matches) => {
const results = findExistingFileMatches(query, prepareQuickOpenFiles([path]), 1)
expect(results.some((result) => result.matchKind === 'literal-basename')).toBe(matches)
})
it.each(['a', 'AB', 'é', 'e\u0301', '12'])('puts search first for short token %s', (query) => {
const files = readyFiles(
Array.from({ length: 10 }, (_, index) => `docs/${query}-guide-${index}.md`)
)
for (const limit of [1, 2, 4, 8]) {
const options = getTabEntryOptions(query, files, limit)
expect(options[0].classification).toEqual({ kind: 'search', engine: 'google', query })
expect(options.slice(1).map((option) => option.classification)).toEqual(
Array.from({ length: limit - 1 }, (_, index) => ({
kind: 'existing-file',
matchKind: 'literal-basename',
relativePath: `docs/${query}-guide-${index}.md`
}))
)
}
})
it.each(['a', 'ab'])('keeps exact short filename %s ahead of search', (query) => {
for (const relativePath of [query, `docs/${query}`]) {
const files = readyFiles([`${query}-guide.md`, relativePath])
expect(classifyTabEntryQuery(query, files)).toEqual({
kind: 'existing-file',
matchKind: relativePath === query ? 'exact-path' : 'exact-basename',
relativePath
})
}
})
it.each(['文', '工具'])('keeps short CJK filename matches ahead of search for %s', (query) => {
expect(classifyTabEntryQuery(query, readyFiles([`docs/${query}指南.md`]))).toEqual({
kind: 'existing-file',
matchKind: 'literal-basename',
relativePath: `docs/${query}指南.md`
})
})
it('reserves search when literal filenames fill the list', () => {
const files = readyFiles(['button.tsx', 'button.css', 'button.test.ts', 'button.md'])
expect(getTabEntryOptions('button', files).map((option) => option.classification.kind)).toEqual(
['existing-file', 'existing-file', 'existing-file', 'search']
)
expect(classifyTabEntryQuery('button', files).kind).toBe('existing-file')
})
})