Skip to content

Quick Switcher revamp: floating panel, fuzzy engine, frecency ranking#1644

Merged
datlechin merged 17 commits into
mainfrom
feat/quick-switcher-fuzzy-engine
Jun 12, 2026
Merged

Quick Switcher revamp: floating panel, fuzzy engine, frecency ranking#1644
datlechin merged 17 commits into
mainfrom
feat/quick-switcher-fuzzy-engine

Conversation

@datlechin

@datlechin datlechin commented Jun 10, 2026

Copy link
Copy Markdown
Member

What

Full revamp of the Quick Switcher (Cmd+Shift+O), in 5 commits that each build and test on their own.

Matching engine

  • FuzzyMatcher rewritten as an fzy-style dynamic program with affine gap penalties. It finds the best alignment instead of the first one, returns matched character indices, and treats _ . - / $ and camelCase transitions as word boundaries. Candidates over 1024 chars fall back to the old greedy pass.
  • New QuickSwitcherFrecencyStore: Firefox-style recency buckets (4/14/31/90 days), 10 samples per item, 100 items per connection. Migrates the old MRU list and removes it.
  • Ranking is matchScore x typeWeight x frecency, scored off the main actor behind the existing 40ms debounce.

Surface

  • The switcher is now a floating borderless NSPanel anchored 20% from the top of the window, not a modal sheet. It dismisses on Escape and on losing key focus. ActiveSheet.quickSwitcher is gone.
  • The panel UI matches current Spotlight anatomy, verified against screenshots of the real thing: 640pt wide, 52pt input row with a borderless 22pt text field, 44pt rows with icon containers and 15pt titles, corner radius 13pt (28pt on macOS 26). A native-List variant was tried and reverted; the Spotlight look reads better (commits cbe451a, 999709f).
  • Two-tier selection like Spotlight: default selection is a gray wash; keyboard navigation turns it into the accent fill with white text. The selected row shows an action hint (Open / Switch / Load Query) with a Return keycap. Matched characters render semibold.
  • Single click selects (no delay, via NSApp.currentEvent.clickCount), double click or Return opens, Option+Return opens in a new tab, right-click for actions.
  • Empty search shows a standalone capsule search bar with round scope buttons beside it; results show your recents. Cmd+1..4 or the buttons browse all tables, databases, or queries. While typing, an active scope shows as a capsule badge in the input row.
  • No footer and no loading spinner; the surface grows downward from a fixed top edge.
  • NSGlassEffectView on macOS 26, NSVisualEffectView (.popover) on macOS 14/15.

Corpus and freshness

  • Awaits SQLSchemaProvider.loadSchema when the table cache is empty, so the panel no longer opens with zero tables before autocomplete has warmed up. Concurrent callers coalesce on the provider's in-flight task.
  • Saved queries (SQLFavorite) join the corpus. History items carry the full query in a new payload field, so opening one loads the real query instead of the 100-char preview.

Actions

  • Option+Return opens the table in a new window tab (forceNewWindowTab on openTableTab).
  • Tables already open in a tab get an Open badge, a 1.2x rank boost, and Return focuses the existing tab.
  • Cmd+1..4 switch scope: All, Tables, Databases, Queries.
  • Right-click a row for Open Structure, Copy Name, Copy Query.

Consolidation

  • The sidebar filter (both tree views), database switcher, and connection switcher now use the same fuzzy matcher, so upv finds user_profile_view everywhere. The database switcher also ranks by score.

Out of scope, deliberate

Testing

  • New/updated suites, all green: FuzzyMatcherTests (22), QuickSwitcherFrecencyStoreTests (11), QuickSwitcherViewModelTests (26), QuickSwitcherPanelControllerTests (6), DatabaseSwitcherFilterTests (4), ConnectionSwitcherFilterTests, SidebarViewModelTests.
  • swiftlint lint --strict clean on every touched file (SidebarViewModelTests has pre-existing strict violations, untouched).
  • No UI automation for the panel: the full UI test suite does not run deterministically locally (CI quarantine list). Panel present/dismiss/key handling is covered by the controller unit tests instead.
  • Manual QA still needed on a live session: panel focus, glass on macOS 26, Escape two-stage, Option+Return.

@mintlify

mintlify Bot commented Jun 10, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Jun 10, 2026, 7:55 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1871f30b4c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


init(contentView: NSView) {
super.init(
contentRect: NSRect(origin: .zero, size: contentView.fittingSize),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resize the panel after async content loads

When the quick switcher is opened before its items finish loading, the panel frame is fixed from the initial loading view's fittingSize; after QuickSwitcherPanelView switches to the taller results list there is no code that updates the NSPanel frame, and the borderless panel is not user-resizable. In that common uncached/async-load path, the list/footer can be clipped to the loading-height window until the panel is recreated, so the panel should observe the hosting view's size/preferredContentSize and call setFrame/re-anchor when it changes.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f20daefd1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +17 to +21
let openTableNames = Set(
tabManager.tabs
.filter { $0.tabType == .table }
.compactMap(\.tableContext.tableName)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Key open-tab markers by database/schema

In contexts where tabs from another database or schema can remain open, this collects only bare table names even though tab activation later compares databaseName and schemaName. QuickSwitcherViewModel.loadItems then uses openTableNames.contains(table.name), so a closed users table in the current database/schema is shown and boosted as already “Open” whenever any other users tab exists. Please key this by the same database/schema/table identity used for tab matching.

Useful? React with 👍 / 👎.

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 701f53ac3f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +64 to +66
case #selector(NSResponder.insertNewline(_:)):
parent.onSubmit()
return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle modified Return in the search field

When the documented Option+Return shortcut is pressed while focus is in this NSTextField, AppKit routes it through insertNewlineIgnoringFieldEditor(_:) rather than the plain insertNewline(_:) selector handled here. Because that selector falls through, onSubmit is not called and the selected table is not opened in a new tab; handle the ignoring-field-editor selector alongside insertNewline(_:).

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd7c916eaa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +156 to +157
if viewModel.flatItems.isEmpty {
noResultsRow

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Show loading state before declaring no results

When the panel is opened on an uncached connection and the user starts typing before loadItems completes, flatItems is still empty while isLoading is true, so this branch renders the “No results” row for the query. The old sheet kept showing a loading state during this path, and this can incorrectly tell users that a valid table/query has no matches until the async schema/history load finishes; gate the empty state on !viewModel.isLoading or render a progress row while loading.

Useful? React with 👍 / 👎.

@datlechin datlechin merged commit f767cb5 into main Jun 12, 2026
4 of 5 checks passed
@datlechin datlechin deleted the feat/quick-switcher-fuzzy-engine branch June 12, 2026 03:40
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.

1 participant