Skip to content

feat: add Quick Switcher with native SwiftUI sheet#278

Merged
datlechin merged 4 commits intomainfrom
refactor/quick-switcher-native-sheet
Mar 11, 2026
Merged

feat: add Quick Switcher with native SwiftUI sheet#278
datlechin merged 4 commits intomainfrom
refactor/quick-switcher-native-sheet

Conversation

@datlechin
Copy link
Owner

@datlechin datlechin commented Mar 11, 2026

Summary

  • Implements Quick Switcher (Cmd+P) for fast fuzzy-search navigation to tables, views, databases, schemas, and query history
  • Uses native SwiftUI .sheet(item:) via ActiveSheet enum, matching existing dialog patterns (DatabaseSwitcherSheet, ExportDialog, ImportDialog)
  • Native List(selection:) with .listStyle(.sidebar), section grouping by kind, proper footer with Cancel/Open buttons
  • Fixes missing .refreshData notification for MySQL/MariaDB/ClickHouse database switching (sidebar didn't refresh after switch)
  • Adds docs/development/notification-refactor.md tracking plan for replacing NotificationCenter with proper Swift patterns

New files

  • QuickSwitcherView.swiftQuickSwitcherSheet (pure SwiftUI, no NSPanel)
  • QuickSwitcherItem.swift — data model
  • QuickSwitcherViewModel.swift — search, fuzzy filtering, keyboard navigation
  • FuzzyMatcher.swift — scoring algorithm for search ranking
  • MainContentCoordinator+QuickSwitcher.swift — selection handler (open table/view/database/schema/history)
  • FuzzyMatcherTests.swift + QuickSwitcherViewModelTests.swift — test coverage

Test plan

  • Build succeeds
  • All QuickSwitcherViewModelTests pass (11/11)
  • All FuzzyMatcherTests pass
  • Visual: Cmd+P opens native sheet at top of window
  • Keyboard: arrow keys, Return to select, Escape to dismiss
  • Fuzzy search filters results correctly
  • Selecting a database switches and refreshes sidebar (MySQL/MariaDB/ClickHouse fix)
  • Double-click on row opens item

Summary by CodeRabbit

  • New Features

    • Quick Switcher palette to jump to tables, views, databases, schemas, and recent queries; supports fuzzy search, grouped/flat results, keyboard navigation, double-click/open, and a command to open it.
    • New fuzzy-matching scorer to rank results by match quality, position, boundaries, consecutive matches, case, and length.
  • Bug Fixes

    • Triggers UI/data refresh after switching databases or schemas.
  • Tests

    • Added tests for fuzzy matching and Quick Switcher behavior.
  • Documentation

    • Added Quick Switcher docs and Cmd+P shortcut; localized docs include duplicated sections.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c445145-850a-4ab0-ac35-9af76374017e

📥 Commits

Reviewing files that changed from the base of the PR and between 896916f and fb647b7.

📒 Files selected for processing (9)
  • TablePro/Core/Utilities/UI/FuzzyMatcher.swift
  • TablePro/Models/UI/QuickSwitcherItem.swift
  • TablePro/ViewModels/QuickSwitcherViewModel.swift
  • TablePro/Views/QuickSwitcher/QuickSwitcherView.swift
  • TableProTests/Utilities/FuzzyMatcherTests.swift
  • TableProTests/ViewModels/QuickSwitcherViewModelTests.swift
  • docs/features/keyboard-shortcuts.mdx
  • docs/vi/features/keyboard-shortcuts.mdx
  • docs/zh/features/keyboard-shortcuts.mdx

📝 Walkthrough

Walkthrough

Adds a Quick Switcher feature: a Unicode-safe fuzzy matcher, QuickSwitcher data model and ViewModel with debounced filtering and navigation, a SwiftUI QuickSwitcherSheet (keyboard + double-click support), coordinator selection handling and command to present it, refresh notifications after some schema loads, and unit tests and docs.

Changes

Cohort / File(s) Summary
Fuzzy Matching Utility
TablePro/Core/Utilities/UI/FuzzyMatcher.swift
New FuzzyMatcher.score(query:candidate:) implementing Unicode-aware fuzzy scoring with full-query requirement, consecutive/word-boundary/case/position/length bonuses; empty query→1, empty candidate→0.
Quick Switcher Data Model
TablePro/Models/UI/QuickSwitcherItem.swift
Adds QuickSwitcherItemKind enum and QuickSwitcherItem struct (id, name, kind, subtitle, score) with iconName and kindLabel computed properties.
Quick Switcher ViewModel
TablePro/ViewModels/QuickSwitcherViewModel.swift
Adds @MainActor @Observable`` QuickSwitcherViewModel handling loadItems(...), debounced filtering using fuzzy scoring, grouping/sorting by kind, selection tracking, keyboard navigation, and result capping.
Quick Switcher UI
TablePro/Views/QuickSwitcher/QuickSwitcherView.swift
Implements QuickSwitcherSheet SwiftUI view: header, search toolbar, loading/empty states, grouped vs. ranked lists, keyboard navigation, double-click overlay, and onSelect callback.
Coordinator & Commands
TablePro/Views/Main/.../MainContentCoordinator.swift, .../MainContentCoordinator+QuickSwitcher.swift, .../MainContentCommandActions.swift
Adds ActiveSheet.case quickSwitcher, openQuickSwitcher() command, and handleQuickSwitcherSelection(_:) which routes to open table/view, async switch database/schema, or post load-query notification.
View & Navigation Updates
TablePro/Views/Main/MainContentView.swift, TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift
Presents QuickSwitcherSheet when active; posts .refreshData notification after schema loads for MySQL/MariaDB/ClickHouse and PostgreSQL branches.
Tests
TableProTests/Utilities/FuzzyMatcherTests.swift, TableProTests/ViewModels/QuickSwitcherViewModelTests.swift
Adds unit tests for fuzzy matcher (boundaries, case, emoji, performance) and ViewModel (filtering, debounce, cap, grouping, selection/navigation).
Docs
docs/features/keyboard-shortcuts.mdx, docs/vi/features/keyboard-shortcuts.mdx, docs/zh/features/keyboard-shortcuts.mdx
Adds Quick Switcher documentation and keybinding (Cmd+P). Note: Vietnamese and Chinese docs include duplicated Quick Switcher blocks.

Sequence Diagram

sequenceDiagram
    actor User
    participant View as QuickSwitcherSheet
    participant ViewModel as QuickSwitcherViewModel
    participant Fuzzy as FuzzyMatcher
    participant Coordinator as MainContentCoordinator

    User->>View: Open quick switcher
    View->>ViewModel: loadItems(schemaProvider, connectionId, databaseType)
    ViewModel->>ViewModel: fetch tables/views/databases/schemas/history
    ViewModel-->>View: publish allItems

    User->>View: Type search query
    View->>ViewModel: update searchText
    ViewModel->>ViewModel: debounce & applyFilter()
    ViewModel->>Fuzzy: score(query, candidate)
    Fuzzy-->>ViewModel: scores
    ViewModel->>View: publish filteredItems (sorted, capped)

    User->>View: Navigate (Up/Down)
    View->>ViewModel: moveUp()/moveDown()
    ViewModel-->>View: update selectedItem

    User->>View: Press Return / Double-click
    View->>Coordinator: handleQuickSwitcherSelection(item)
    alt item is table/view/systemTable
        Coordinator->>Coordinator: openTableTab(...)
    else item is database/schema
        Coordinator->>Coordinator: switchDatabase/switchSchema (async)
    else item is queryHistory
        Coordinator->>Coordinator: post loadQueryIntoEditor notification
    end
    Coordinator-->>View: dismiss sheet
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through letters, quick and spry,

Matched each nibble, leapt up high.
Tables, views, and schemas found,
One soft hop and I'm homeward bound. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature addition: a Quick Switcher component using native SwiftUI with a sheet presentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/quick-switcher-native-sheet

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

Implements Quick Switcher (Cmd+P) for fast navigation to tables, views,
databases, schemas, and query history using fuzzy search. Uses the
existing ActiveSheet enum + .sheet(item:) pattern matching other dialogs.

Also fixes missing .refreshData notification for MySQL/MariaDB/ClickHouse
database switching.
@datlechin datlechin force-pushed the refactor/quick-switcher-native-sheet branch from e1145b4 to 66ebe7e Compare March 11, 2026 16:52
- Fix double Return key handling (remove .keyboardShortcut from button)
- Fix emoji in search blocking all matches (restructure to while loop)
- Remove unused file-scope logger in QuickSwitcher extension
- Add subtitles for view/system table items
- Add test cases for emoji handling, grouped items, nil selection
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (9)
TablePro/Models/UI/QuickSwitcherItem.swift (1)

11-21: Add explicit access control to type declarations.

Per coding guidelines, all declarations should have explicit access control. Both QuickSwitcherItemKind and QuickSwitcherItem need access modifiers.

♻️ Suggested fix
-enum QuickSwitcherItemKind: Hashable, Sendable {
+internal enum QuickSwitcherItemKind: Hashable, Sendable {
     case table
     case view
     case systemTable
     case database
     case schema
     case queryHistory
 }

 /// A single item in the quick switcher results list
-struct QuickSwitcherItem: Identifiable, Hashable {
+internal struct QuickSwitcherItem: Identifiable, Hashable {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Models/UI/QuickSwitcherItem.swift` around lines 11 - 21, The enum
QuickSwitcherItemKind and the struct QuickSwitcherItem lack explicit access
control; add the appropriate access level (e.g.,
public/internal/fileprivate/private) to both declarations by prefixing the
declarations (QuickSwitcherItemKind and QuickSwitcherItem) with the chosen
modifier so they conform to project coding guidelines and make their visibility
explicit.
TablePro/Core/Utilities/UI/FuzzyMatcher.swift (1)

11-15: Add explicit access control per coding guidelines.

The enum FuzzyMatcher and static func score lack explicit access modifiers. As per coding guidelines, all declarations should have explicit access control.

♻️ Suggested fix
-enum FuzzyMatcher {
-    /// Score a candidate string against a search query.
-    /// Returns 0 for no match, higher values indicate better matches.
-    /// Empty query returns 1 (everything matches).
-    static func score(query: String, candidate: String) -> Int {
+internal enum FuzzyMatcher {
+    /// Score a candidate string against a search query.
+    /// Returns 0 for no match, higher values indicate better matches.
+    /// Empty query returns 1 (everything matches).
+    internal static func score(query: String, candidate: String) -> Int {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Core/Utilities/UI/FuzzyMatcher.swift` around lines 11 - 15, Add
explicit access control to the enum and its method by changing "enum
FuzzyMatcher" to "internal enum FuzzyMatcher" and "static func score(query:
String, candidate: String) -> Int" to "internal static func score(query: String,
candidate: String) -> Int" (or use your project's chosen access level if
different) so both the FuzzyMatcher type and the score(...) method have explicit
access modifiers.
TablePro/Views/Main/Extensions/MainContentCoordinator+QuickSwitcher.swift (1)

11-11: Remove unused logger declaration.

The quickSwitcherLogger is declared but never used in this file. Either add logging for debugging purposes or remove the unused declaration.

♻️ Option 1: Remove unused logger
 import Foundation
-import os
-
-private let quickSwitcherLogger = Logger(subsystem: "com.TablePro", category: "MainContentCoordinator+QuickSwitcher")

 extension MainContentCoordinator {
♻️ Option 2: Add logging for debugging
 extension MainContentCoordinator {
     /// Handle selection from the quick switcher palette
     func handleQuickSwitcherSelection(_ item: QuickSwitcherItem) {
+        quickSwitcherLogger.debug("Quick switcher selected: \(item.name, privacy: .public) (\(String(describing: item.kind), privacy: .public))")
         switch item.kind {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/Main/Extensions/MainContentCoordinator`+QuickSwitcher.swift at
line 11, The file declares a private let quickSwitcherLogger which is never
referenced; remove that unused logger declaration (private let
quickSwitcherLogger = Logger(...)) from
MainContentCoordinator+QuickSwitcher.swift to eliminate the dead symbol, or
alternatively, if you prefer to keep logging, add explicit uses of
quickSwitcherLogger inside the QuickSwitcher-related methods (e.g., any
showQuickSwitcher/hideQuickSwitcher/toggleQuickSwitcher implementations) to log
important state transitions instead of leaving it unused.
TablePro/Views/QuickSwitcher/QuickSwitcherView.swift (4)

67-74: Remove unnecessary Task wrapper.

Both the view and QuickSwitcherViewModel are @MainActor-isolated. The Task { @mainactor in ... } wrapper adds unnecessary overhead since onKeyPress closures execute on the main thread.

♻️ Suggested simplification
         .onKeyPress(.upArrow) {
-            Task { `@MainActor` in viewModel.moveUp() }
+            viewModel.moveUp()
             return .handled
         }
         .onKeyPress(.downArrow) {
-            Task { `@MainActor` in viewModel.moveDown() }
+            viewModel.moveDown()
             return .handled
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/QuickSwitcher/QuickSwitcherView.swift` around lines 67 - 74,
The onKeyPress closures in QuickSwitcherView wrap viewModel.moveUp() and
viewModel.moveDown() in unnecessary Task { `@MainActor` ... } blocks; since the
view and QuickSwitcherViewModel are `@MainActor-isolated` and onKeyPress runs on
the main thread, remove the Task wrappers and call viewModel.moveUp() and
viewModel.moveDown() directly inside their respective onKeyPress closures,
preserving the return .handled.

238-243: Redundant keyboard shortcut.

The .keyboardShortcut(.return, modifiers: []) on the Open button is redundant since .onKeyPress(.return) at line 63 already handles Return key and calls the same openSelectedItem() function. Consider removing the keyboard shortcut to avoid potential double-handling.

♻️ Suggested change
             Button("Open") {
                 openSelectedItem()
             }
             .buttonStyle(.borderedProminent)
             .disabled(viewModel.selectedItem == nil)
-            .keyboardShortcut(.return, modifiers: [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/QuickSwitcher/QuickSwitcherView.swift` around lines 238 - 243,
Remove the redundant explicit keyboard shortcut on the "Open" Button to avoid
double-handling of Return: locate the Button that calls openSelectedItem() (the
one using .buttonStyle(.borderedProminent) and .disabled(viewModel.selectedItem
== nil)) and delete the .keyboardShortcut(.return, modifiers: []) modifier; keep
the existing .onKeyPress(.return) handler (which already calls
openSelectedItem()) so Return continues to work via that centralized handler.

142-174: Use DesignConstants.FontSize for consistency.

The item row uses hardcoded font sizes (14, 13, 11, 10) while the rest of the file uses DesignConstants.FontSize. Consider using the design constants for consistency with the project's styling approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/QuickSwitcher/QuickSwitcherView.swift` around lines 142 - 174,
Replace the hardcoded numeric font sizes inside itemRow(_:) with the project's
DesignConstants.FontSize values used elsewhere in QuickSwitcherView: update the
Image font(.system(size: 14)) and the Texts for name (size 13), subtitle (size
11) and kindLabel (size 10) to use the corresponding DesignConstants.FontSize
constants (matching the intent/weight for icon, primary text, secondary subtitle
and badge) so all calls in itemRow(_ item: QuickSwitcherItem) reference
DesignConstants.FontSize instead of literal numbers.

14-14: Add explicit access modifier to struct.

Per coding guidelines, always use explicit access control. Since QuickSwitcherSheet is used from MainContentView, it should be explicitly internal (or public if cross-module).

-struct QuickSwitcherSheet: View {
+internal struct QuickSwitcherSheet: View {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/QuickSwitcher/QuickSwitcherView.swift` at line 14,
QuickSwitcherSheet currently lacks an explicit access modifier; update the
declaration of the struct QuickSwitcherSheet (used by MainContentView) to have
an explicit access level (e.g., mark it as internal struct QuickSwitcherSheet:
View or public if it must be visible across modules) so it conforms to the
project's access-control guidelines and matches how MainContentView references
it.
TablePro/ViewModels/QuickSwitcherViewModel.swift (2)

138-144: Simplify nil return.

The explicit type annotation nil as QuickSwitcherItem? is unnecessary. compactMap infers the optional type from the closure's return type.

♻️ Suggested simplification
         filteredItems = allItems.compactMap { item in
             let matchScore = FuzzyMatcher.score(query: searchText, candidate: item.name)
-            guard matchScore > 0 else { return nil as QuickSwitcherItem? }
+            guard matchScore > 0 else { return nil }
             var scored = item
             scored.score = matchScore
             return scored
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/ViewModels/QuickSwitcherViewModel.swift` around lines 138 - 144, The
compactMap closure in the filteredItems assignment uses an unnecessary explicit
optional cast when returning nil; in the closure that iterates allItems and
computes matchScore via FuzzyMatcher.score(query: searchText, candidate:
item.name), replace the explicit "nil as QuickSwitcherItem?" return with a
simple "return nil" so compactMap can infer the optional type for
QuickSwitcherItem and keep scored population logic (setting scored.score =
matchScore) intact.

13-28: Add explicit access modifiers.

Per coding guidelines, always use explicit access control. The class and some properties lack explicit modifiers.

♻️ Suggested changes
 `@MainActor` `@Observable`
-final class QuickSwitcherViewModel {
+internal final class QuickSwitcherViewModel {
     private static let logger = Logger(subsystem: "com.TablePro", category: "QuickSwitcherViewModel")

     // MARK: - State

-    var searchText = "" {
+    internal var searchText = "" {
         didSet { updateFilter() }
     }

-    var allItems: [QuickSwitcherItem] = [] {
+    internal var allItems: [QuickSwitcherItem] = [] {
         didSet { applyFilter() }
     }
     private(set) var filteredItems: [QuickSwitcherItem] = []
-    var selectedItemId: String?
-    var isLoading = false
+    internal var selectedItemId: String?
+    internal var isLoading = false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/ViewModels/QuickSwitcherViewModel.swift` around lines 13 - 28, The
class and several properties lack explicit access control; update
QuickSwitcherViewModel to declare an explicit access level (e.g., internal) on
the class and its stored properties: mark the class as "internal" (e.g.,
`@MainActor` `@Observable` internal final class QuickSwitcherViewModel), add
explicit access modifiers for searchText, allItems, selectedItemId, and
isLoading (e.g., internal var searchText, internal var allItems, internal var
selectedItemId, internal var isLoading), and make the existing private(set)
filteredItems declaration explicitly internal with its setter privacy (e.g.,
internal private(set) var filteredItems) while leaving the static logger as
private. Ensure all modifiers are consistent with project conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@TablePro/Core/Utilities/UI/FuzzyMatcher.swift`:
- Around line 11-15: Add explicit access control to the enum and its method by
changing "enum FuzzyMatcher" to "internal enum FuzzyMatcher" and "static func
score(query: String, candidate: String) -> Int" to "internal static func
score(query: String, candidate: String) -> Int" (or use your project's chosen
access level if different) so both the FuzzyMatcher type and the score(...)
method have explicit access modifiers.

In `@TablePro/Models/UI/QuickSwitcherItem.swift`:
- Around line 11-21: The enum QuickSwitcherItemKind and the struct
QuickSwitcherItem lack explicit access control; add the appropriate access level
(e.g., public/internal/fileprivate/private) to both declarations by prefixing
the declarations (QuickSwitcherItemKind and QuickSwitcherItem) with the chosen
modifier so they conform to project coding guidelines and make their visibility
explicit.

In `@TablePro/ViewModels/QuickSwitcherViewModel.swift`:
- Around line 138-144: The compactMap closure in the filteredItems assignment
uses an unnecessary explicit optional cast when returning nil; in the closure
that iterates allItems and computes matchScore via FuzzyMatcher.score(query:
searchText, candidate: item.name), replace the explicit "nil as
QuickSwitcherItem?" return with a simple "return nil" so compactMap can infer
the optional type for QuickSwitcherItem and keep scored population logic
(setting scored.score = matchScore) intact.
- Around line 13-28: The class and several properties lack explicit access
control; update QuickSwitcherViewModel to declare an explicit access level
(e.g., internal) on the class and its stored properties: mark the class as
"internal" (e.g., `@MainActor` `@Observable` internal final class
QuickSwitcherViewModel), add explicit access modifiers for searchText, allItems,
selectedItemId, and isLoading (e.g., internal var searchText, internal var
allItems, internal var selectedItemId, internal var isLoading), and make the
existing private(set) filteredItems declaration explicitly internal with its
setter privacy (e.g., internal private(set) var filteredItems) while leaving the
static logger as private. Ensure all modifiers are consistent with project
conventions.

In `@TablePro/Views/Main/Extensions/MainContentCoordinator`+QuickSwitcher.swift:
- Line 11: The file declares a private let quickSwitcherLogger which is never
referenced; remove that unused logger declaration (private let
quickSwitcherLogger = Logger(...)) from
MainContentCoordinator+QuickSwitcher.swift to eliminate the dead symbol, or
alternatively, if you prefer to keep logging, add explicit uses of
quickSwitcherLogger inside the QuickSwitcher-related methods (e.g., any
showQuickSwitcher/hideQuickSwitcher/toggleQuickSwitcher implementations) to log
important state transitions instead of leaving it unused.

In `@TablePro/Views/QuickSwitcher/QuickSwitcherView.swift`:
- Around line 67-74: The onKeyPress closures in QuickSwitcherView wrap
viewModel.moveUp() and viewModel.moveDown() in unnecessary Task { `@MainActor` ...
} blocks; since the view and QuickSwitcherViewModel are `@MainActor-isolated` and
onKeyPress runs on the main thread, remove the Task wrappers and call
viewModel.moveUp() and viewModel.moveDown() directly inside their respective
onKeyPress closures, preserving the return .handled.
- Around line 238-243: Remove the redundant explicit keyboard shortcut on the
"Open" Button to avoid double-handling of Return: locate the Button that calls
openSelectedItem() (the one using .buttonStyle(.borderedProminent) and
.disabled(viewModel.selectedItem == nil)) and delete the
.keyboardShortcut(.return, modifiers: []) modifier; keep the existing
.onKeyPress(.return) handler (which already calls openSelectedItem()) so Return
continues to work via that centralized handler.
- Around line 142-174: Replace the hardcoded numeric font sizes inside
itemRow(_:) with the project's DesignConstants.FontSize values used elsewhere in
QuickSwitcherView: update the Image font(.system(size: 14)) and the Texts for
name (size 13), subtitle (size 11) and kindLabel (size 10) to use the
corresponding DesignConstants.FontSize constants (matching the intent/weight for
icon, primary text, secondary subtitle and badge) so all calls in itemRow(_
item: QuickSwitcherItem) reference DesignConstants.FontSize instead of literal
numbers.
- Line 14: QuickSwitcherSheet currently lacks an explicit access modifier;
update the declaration of the struct QuickSwitcherSheet (used by
MainContentView) to have an explicit access level (e.g., mark it as internal
struct QuickSwitcherSheet: View or public if it must be visible across modules)
so it conforms to the project's access-control guidelines and matches how
MainContentView references it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3c23948-2c73-4e06-ab82-bbbf42c99809

📥 Commits

Reviewing files that changed from the base of the PR and between 83de4e2 and 66ebe7e.

📒 Files selected for processing (11)
  • TablePro/Core/Utilities/UI/FuzzyMatcher.swift
  • TablePro/Models/UI/QuickSwitcherItem.swift
  • TablePro/ViewModels/QuickSwitcherViewModel.swift
  • TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift
  • TablePro/Views/Main/Extensions/MainContentCoordinator+QuickSwitcher.swift
  • TablePro/Views/Main/MainContentCommandActions.swift
  • TablePro/Views/Main/MainContentCoordinator.swift
  • TablePro/Views/Main/MainContentView.swift
  • TablePro/Views/QuickSwitcher/QuickSwitcherView.swift
  • TableProTests/Utilities/FuzzyMatcherTests.swift
  • TableProTests/ViewModels/QuickSwitcherViewModelTests.swift

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
TablePro/ViewModels/QuickSwitcherViewModel.swift (1)

135-159: Add a deterministic tie-breaker for equal-score and equal-kind items.

Both branches only sort on a single key, so items with the same score or kind can shuffle relative order across refreshes. In this UI that means keyboard selection can jump unexpectedly, and the tests are already depending on same-kind items staying in a stable order.

💡 Example of a stable secondary ordering
     private func applyFilter() {
         if searchText.isEmpty {
             filteredItems = allItems.sorted { a, b in
-                kindSortOrder(a.kind) < kindSortOrder(b.kind)
+                let aKind = kindSortOrder(a.kind)
+                let bKind = kindSortOrder(b.kind)
+                if aKind != bKind {
+                    return aKind < bKind
+                }
+                return a.name.localizedCaseInsensitiveCompare(b.name) == .orderedAscending
             }
             if filteredItems.count > maxResults {
                 filteredItems = Array(filteredItems.prefix(maxResults))
             }
         } else {
             filteredItems = allItems.compactMap { item in
                 let matchScore = FuzzyMatcher.score(query: searchText, candidate: item.name)
                 guard matchScore > 0 else { return nil as QuickSwitcherItem? }
                 var scored = item
                 scored.score = matchScore
                 return scored
             }
-            .sorted { $0.score > $1.score }
+            .sorted { lhs, rhs in
+                if lhs.score != rhs.score {
+                    return lhs.score > rhs.score
+                }
+                let lhsKind = kindSortOrder(lhs.kind)
+                let rhsKind = kindSortOrder(rhs.kind)
+                if lhsKind != rhsKind {
+                    return lhsKind < rhsKind
+                }
+                return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
+            }

             if filteredItems.count > maxResults {
                 filteredItems = Array(filteredItems.prefix(maxResults))
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/ViewModels/QuickSwitcherViewModel.swift` around lines 135 - 159, The
sorting in applyFilter() uses only kindSortOrder(a.kind) (when
searchText.isEmpty) and only score (when filtering), which allows equal-kind or
equal-score items to reorder; update both sort calls in applyFilter() to add a
deterministic secondary tie-breaker such as item.name (or item.id) so items with
equal kind or equal score are consistently ordered—adjust the sorted closures
that reference kindSortOrder and $0.score to compare (kind, name) and (score,
name) respectively, leaving all other logic (FuzzyMatcher.score, filteredItems
trimming, maxResults, and selectedItemId assignment) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@TablePro/Core/Utilities/UI/FuzzyMatcher.swift`:
- Around line 30-44: The current loops in FuzzyMatcher.swift skip UTF-16
surrogate halves by using NSString.character(at:) and UnicodeScalar(...) which
drops non-BMP scalars; change the matching iteration to walk full Unicode
scalars (e.g., query.unicodeScalars and candidate.unicodeScalars or Swift
Characters) instead of using queryNS.character(at:) and
candidateNS.character(at:), update references to queryIndex/candidateIndex to
iterate those scalar collections, and preserve existing logic (consecutiveBonus
reset, advancing indices, and guard behavior) so supplementary-plane characters
(emoji, etc.) participate in matching; also apply the same change to the other
occurrence noted around lines 100-106.

In `@TablePro/ViewModels/QuickSwitcherViewModel.swift`:
- Around line 38-120: The loadItems function can finish out of order and
overwrite newer state; fix by introducing a load token (e.g. a private var
currentLoadToken: UUID?) and at the start of loadItems generate a new UUID and
assign it to currentLoadToken, capture it in a local let token, then before any
final state commits (setting allItems and isLoading) verify that
currentLoadToken == token and only then update state; also ensure you clear or
update currentLoadToken appropriately when a newer load starts or when canceling
to fence stale completions from overwriting allItems/isLoading.

---

Nitpick comments:
In `@TablePro/ViewModels/QuickSwitcherViewModel.swift`:
- Around line 135-159: The sorting in applyFilter() uses only
kindSortOrder(a.kind) (when searchText.isEmpty) and only score (when filtering),
which allows equal-kind or equal-score items to reorder; update both sort calls
in applyFilter() to add a deterministic secondary tie-breaker such as item.name
(or item.id) so items with equal kind or equal score are consistently
ordered—adjust the sorted closures that reference kindSortOrder and $0.score to
compare (kind, name) and (score, name) respectively, leaving all other logic
(FuzzyMatcher.score, filteredItems trimming, maxResults, and selectedItemId
assignment) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6910638-3df9-463d-bd4e-f12fe4004973

📥 Commits

Reviewing files that changed from the base of the PR and between 66ebe7e and 644e769.

📒 Files selected for processing (6)
  • TablePro/Core/Utilities/UI/FuzzyMatcher.swift
  • TablePro/ViewModels/QuickSwitcherViewModel.swift
  • TablePro/Views/Main/Extensions/MainContentCoordinator+QuickSwitcher.swift
  • TablePro/Views/QuickSwitcher/QuickSwitcherView.swift
  • TableProTests/Utilities/FuzzyMatcherTests.swift
  • TableProTests/ViewModels/QuickSwitcherViewModelTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • TablePro/Views/Main/Extensions/MainContentCoordinator+QuickSwitcher.swift
  • TablePro/Views/QuickSwitcher/QuickSwitcherView.swift

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/features/keyboard-shortcuts.mdx`:
- Line 267: The heading "## Quick Switcher" should be changed to sentence case;
update the markdown heading text in docs/features/keyboard-shortcuts.mdx by
renaming the heading string from "## Quick Switcher" to "## Quick switcher"
(locate the header line with that exact text and replace it to match the docs
style rule).
- Around line 267-279: Add the new Quick Switcher shortcut (Cmd+P) to the
summary tables so the documentation is consistent: update the "Essential
Shortcuts" table and the "Cheat Sheet" section to include an entry "Quick
Switcher | `Cmd+P`" (or similar wording consistent with the existing table rows)
and ensure any description text references the Quick Switcher heading; look for
the headings "Essential Shortcuts" and "Cheat Sheet" in
docs/features/keyboard-shortcuts.mdx and add the Cmd+P row in the same format as
other shortcut entries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b67f6723-dc4b-4ccb-bfb4-dc757a1ac467

📥 Commits

Reviewing files that changed from the base of the PR and between 644e769 and 896916f.

📒 Files selected for processing (3)
  • docs/features/keyboard-shortcuts.mdx
  • docs/vi/features/keyboard-shortcuts.mdx
  • docs/zh/features/keyboard-shortcuts.mdx
✅ Files skipped from review due to trivial changes (2)
  • docs/zh/features/keyboard-shortcuts.mdx
  • docs/vi/features/keyboard-shortcuts.mdx

- Add explicit access control to all Quick Switcher types
- Rewrite FuzzyMatcher to use unicodeScalars for proper supplementary-plane Unicode support
- Add load token to prevent stale loadItems completions from overwriting newer state
- Add stable sort tie-breaker (name) for deterministic ordering
- Remove unnecessary Task wrappers and redundant .keyboardShortcut on Open button
- Use DesignConstants for font/icon sizes in QuickSwitcherView
- Add Cmd+P to Essential Shortcuts and Cheat Sheet in docs
- Use sentence case for Quick Switcher heading in docs
@datlechin datlechin merged commit f24289f into main Mar 11, 2026
1 of 2 checks passed
@datlechin datlechin deleted the refactor/quick-switcher-native-sheet branch March 11, 2026 17: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.

1 participant