Skip to content

feat(quick-open): support smart separator matching#8757

Open
ferdiangunawan wants to merge 12 commits into
stablyai:mainfrom
ferdiangunawan:feat/quick-open-smart-search
Open

feat(quick-open): support smart separator matching#8757
ferdiangunawan wants to merge 12 commits into
stablyai:mainfrom
ferdiangunawan:feat/quick-open-smart-search

Conversation

@ferdiangunawan

Copy link
Copy Markdown

Summary

Quick Open now treats spaces, underscores, and hyphens as equivalent word boundaries when searching for files.

  • product detail, product_detail, and product-detail can suggest the same matching file.
  • Spaced queries also match snake_case, kebab-case, camelCase, acronym boundaries, and path segments.
  • When both underscore and hyphen variants exist, the exact separator typed by the user ranks first.
  • Prepared search indexes are memoized by file-list identity to keep repeated queries responsive on large repositories.

Screenshots

product detail scr matching product_detail_screen.dart:

Spaced Quick Open query

product-detail matching underscore-named files:

Hyphen Quick Open query

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test
  • pnpm build
  • Added or updated high-quality tests that would catch regressions, or explained why tests were not needed

Focused Quick Open and tab-entry suites pass: 3 files, 45 tests. The full test command was attempted, but this sandbox rejects the local Unix sockets used by unrelated daemon, runtime, relay, and provider tests; the installed Node v25.2.1 also differs from the repository's required Node 24.

The desktop and web build stages completed successfully. The final macOS native stage could not write Swift's Clang module cache outside the workspace (~/.cache/clang) and exited with Operation not permitted.

AI Review Report

  • Reviewed matching correctness for spaces, snake_case, kebab-case, camelCase, acronym boundaries, Unicode case-fold expansion, valid negative scores, and exact-separator ranking.
  • Reviewed macOS, Linux, and Windows behavior: original paths are returned unchanged and no shortcut, shell, Electron, or platform-specific behavior changes.
  • Reviewed local, SSH, and remote runtime compatibility: file listing, opening, IPC, and provider behavior are unchanged; ranking consumes the existing relative file lists.
  • Reviewed supported agents, integrations, and Git providers: none are affected; the matcher is provider-neutral.
  • Reviewed performance: ranking remains bounded to the top 50 results, prepared indexes are memoized for both consumers, and queries avoid allocating transformed strings per file. A synthetic 100k-file list measured about 137 ms for one-time indexing and about 4–23 ms for warm queries.
  • Reviewed UI quality: there is no layout or style change; the screenshots demonstrate the matching and ranking behavior.

Security Audit

  • Query input remains capped at 2 KiB before candidate scanning.
  • No command execution, path mutation, filesystem access, IPC, authentication, secret, dependency, or network behavior is added.
  • Returned file paths remain the original file-list values; normalization is search-only.
  • No follow-up security work was identified.

Notes

  • No version bump is included.
  • Screenshots are hosted on a separate fork branch so binary assets are not part of this PR's source diff.
  • X: https://x.com/fergnw

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 92dd4f07-f8fd-4884-8fc0-356f78e86585

📥 Commits

Reviewing files that changed from the base of the PR and between 9fecdec and 6fefccb.

📒 Files selected for processing (3)
  • src/renderer/src/components/quick-open-search.test.ts
  • src/renderer/src/components/quick-open-search.ts
  • src/renderer/src/components/quick-open-word-boundaries.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/src/components/quick-open-search.test.ts
  • src/renderer/src/components/quick-open-search.ts

📝 Walkthrough

Walkthrough

Quick-open indexing now stores word-start metadata alongside lowercased paths. Query normalization and fuzzy ranking support whitespace, path and identifier separators, word boundaries, Unicode lowercasing, and nullable non-match scores. Tab-entry classification accepts precomputed indexes, and the tab bar memoizes and supplies them. Tests cover the new ranking cases, index metadata, oversized queries, and index reuse.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main quick-open separator-matching change.
Description check ✅ Passed The description follows the template and includes summary, screenshots, testing, AI review, security audit, and notes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2f13a07f-e31d-4bff-b5e3-a8707df31796

📥 Commits

Reviewing files that changed from the base of the PR and between e0edc8e and 9fecdec.

📒 Files selected for processing (5)
  • src/renderer/src/components/quick-open-search.test.ts
  • src/renderer/src/components/quick-open-search.ts
  • src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx
  • src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts
  • src/renderer/src/components/tab-bar/tab-create-entry-classifier.ts

Comment on lines +139 to +169
const wanted = query[qi]
let matched = false
for (let ti = lastMatchIdx + 1; ti < path.length; ti++) {
if (!searchCharactersMatch(path[ti], wanted)) {
continue
}
if (requireWordStart && wordStarts[ti] !== 1) {
continue
}

for (let ti = 0; ti < file.lowerPath.length && qi < query.length; ti++) {
if (file.lowerPath[ti] === query[qi]) {
// Preserve the existing Quick Open score semantics while avoiding
// repeated lowercase work for each candidate on every keystroke.
const gap = lastMatchIdx === -1 ? 0 : ti - lastMatchIdx - 1
score += gap
if (
ti > 0 &&
(file.lowerPath[ti - 1] === '/' ||
file.lowerPath[ti - 1] === '.' ||
file.lowerPath[ti - 1] === '-')
) {
// Why: equivalent separators should match, but the literally typed form
// should rank first when both filename variants exist.
if (path[ti] !== wanted) {
score++
}
// Why: skip path index 0 so a leading `s` in `src/...` stays score 0
// (previous scorer only bonused matches after `/` `.` `-`).
if (ti > 0 && wordStarts[ti] === 1) {
score -= 5
}
lastMatchIdx = ti
qi++
requireWordStart = false
matched = true
break
}
}

if (qi < query.length) {
return -1
if (!matched) {
return null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make typed _/- separators match camelCase boundaries.

These separators currently require a literal _ or -, so product-detail and product_detail cannot match ProductDetail.tsx. Add a zero-width word-start fallback while preserving the literal-separator ranking bonus, plus regression tests for both queries.

Also applies to: 253-262

ferdiangunawan and others added 12 commits July 16, 2026 15:43
Make human word queries work across common filename conventions without regressing large-repository search responsiveness.
Typed `-`/`_` required a literal separator in the path, so
`product-detail` could not find ProductDetail.tsx while the spaced
form could. Separators now bridge zero-width case transitions with a
deterministic rank: literal < swapped separator < case transition.

- extend the filename boost to camelCase names for `-`/`_` queries so
  they rank like spaced queries instead of trailing by ~100 points
- tolerate a trailing separator mid-typing ("product-") instead of
  blanking the result list between keystrokes
…nsions, and spaces

Round-1 review fixes for smart separator matching:
- Re-anchor at segment boundaries (bounded to segments with a first-char
  match) so separator queries reach camelCase names in inner directories
  (user-profile -> user/UserProfile/index.tsx), not just basenames.
- Stop skipping a value separator that matches the query char, so
  extension-bearing queries (product-detail.dart) keep the camelCase
  filename boost instead of losing it to junk.
- Treat a typed '-'/'_' as equivalent to a literal space in a filename,
  making the space/underscore/hyphen equivalence symmetric.
- Add regression tests for all three plus the bare-separator guard.
…g boost

Round-2 review fixes:
- The segment-anchor loop tested the first-char match after consuming the
  '/' boundary, so '/'- or '\'-leading separator queries skipped all
  re-anchoring and dropped nested matches (user/UserProfile/index.tsx).
  Check the first-char match before the boundary; differential-fuzzed
  equivalent to anchoring at every slash (0 divergences / 6k cases incl.
  slash-leading and dot-separated basenames).
- The filename boost ignored '.'/'.' for identifier-separated queries, so
  'product-detail' wrongly boosted a dot-separated 'product.detail.dart'.
  Only spaced queries may bridge '/'/'.'; '-'/'_' keep them distinct.
- Add regression tests for both.
Round-3 review fix: a query space consumes the entire path separator run,
so a literal separator typed right after ("Meeting - 2026", "a _ b", also
space+/ and space+.) had nothing left to match and the file was dropped
entirely — a regression vs the pre-PR literal-substring matcher that also
broke exact-filename paste and blanked results mid-keystroke. Absorb the
following query separators/spaces the consumed run already covers, staying
strict for '/'/'.'. Differential-fuzzed equivalent to naive all-anchor
(0 divergences / 7k cases). Add regression tests.
Round-4 review fix: after a query space consumes a '/' or '.' run,
lastMatchIdx lands on the separator, so the camelCase-bridge detector
scanned from the char after it and misread that separator-induced word
start as a zero-width case transition — letting a typed '-'/'_' cross a
'/'/'.' boundary the spec keeps distinct (e.g. 'user _profile' fuzzy-
matched and boosted user.profile.ts above the real user_my_profile.ts).
Require the transition's prior char to be a non-separator so only genuine
camelCase/acronym boundaries (ProductDetail, APIClient) bridge. Also cover
the spaced extension-boost guard ('foo bar.ts') that lacked a test.
Round-5 review fix: the round-4 guard made the bridge WITNESS strict, but
the consumption loop still landed on the first word start, which is the
separator-induced one when a space consumed a '/'/'.'  run and a deeper
same-letter camelCase decoy authorized the bridge (e.g. 'user -profile'
matched src/user/profilePage.ts across '/', while the snake twin returned
nothing). A zero-width transition can't sit on a just-consumed '/'/'.', so
skip the bridge when lastMatchIdx is on one. Add decoy regression cases.
…guard

The identifier-transition guard's residual behavior (a typed '-'/'_' not
bridging a separator-induced word start, e.g. 'a--b' must not match 'a-b')
had no coverage once the round-5 gate subsumed its '/'/'.'  role. Pin it so
a future simplification can't silently drop the guard.
…parator

Round-7 review fix: the round-5 gate only skipped the camelCase bridge after a
consumed '/' or '.', so a doubled typed separator ('a--b') could still bridge
onto a single-separator name when a deeper same-letter camelCase tail
('x/a-bcBx.ts') authorized it and consumption landed on the '-'-induced word
start. A zero-width transition can't sit right after ANY matched separator, so
widen the gate to !isPathSeparator(path[lastMatchIdx]). Extend the doubled-
separator test with the decoy variant.
@brennanb2025
brennanb2025 force-pushed the feat/quick-open-smart-search branch from 6fefccb to 0e8de17 Compare July 17, 2026 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants