Skip to content

Commit 4a8edba

Browse files
shouwang0527claude
authored andcommitted
feat(sidebar): navigate search results with arrow keys and open with Enter (#1280)
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
1 parent ff967c8 commit 4a8edba

3 files changed

Lines changed: 64 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Sidebar search: arrow keys navigate filtered results and Enter opens the selected table, matching DataGrip behavior (#1280)
1213
- Right-click a column header to copy all its values from the loaded rows (#1325)
1314
- Copy as submenu on the row context menu now offers CSV, CSV with Headers, Markdown table, and IN Clause for SQL `WHERE id IN (...)` lookups (#1325)
1415
- Double-click or press Return on a read-only query result cell to open a selectable text viewer in the cell. JSON columns open the JSON viewer in a popover, BLOB columns open the hex viewer. The value is selectable and copyable (#1336)

TablePro/Core/Services/Infrastructure/MainSplitViewController.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
127127
splitView.autosaveName = "com.TablePro.mainSplit"
128128

129129
sidebarContainer = SidebarContainerViewController(rootView: AnyView(buildSidebarView()))
130+
sidebarContainer.onOpenTable = { [weak self] table in
131+
guard let coordinator = self?.sessionState?.coordinator else { return }
132+
let connectionId = coordinator.connectionId
133+
if let preview = WindowLifecycleMonitor.shared.previewWindow(for: connectionId),
134+
let previewCoordinator = MainContentCoordinator.coordinator(for: preview.windowId) {
135+
if previewCoordinator.tabManager.selectedTab?.tableContext.tableName == table.name {
136+
previewCoordinator.promotePreviewTab()
137+
} else {
138+
previewCoordinator.promotePreviewTab()
139+
coordinator.openTableTab(table)
140+
}
141+
} else {
142+
coordinator.promotePreviewTab()
143+
coordinator.openTableTab(table)
144+
}
145+
}
130146
sidebarSplitItem = NSSplitViewItem(sidebarWithViewController: sidebarContainer)
131147
sidebarSplitItem.canCollapse = true
132148
sidebarSplitItem.minimumThickness = 280

TablePro/Core/Services/Infrastructure/SidebarContainerViewController.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ internal final class SidebarContainerViewController: NSViewController {
1414
private var windowState: WindowSidebarState?
1515
private var observationGeneration = 0
1616

17+
var onOpenTable: ((TableInfo) -> Void)?
18+
1719
var rootView: AnyView {
1820
get { hostingController.rootView }
1921
set { hostingController.rootView = newValue }
@@ -120,6 +122,51 @@ extension SidebarContainerViewController: NSSearchFieldDelegate {
120122
writeSearchText("")
121123
}
122124

125+
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
126+
guard let sidebarState, sidebarState.selectedSidebarTab == .tables else { return false }
127+
128+
if commandSelector == #selector(NSResponder.moveUp(_:)) {
129+
navigateSelection(direction: -1)
130+
return true
131+
}
132+
if commandSelector == #selector(NSResponder.moveDown(_:)) {
133+
navigateSelection(direction: 1)
134+
return true
135+
}
136+
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
137+
if let selected = sidebarState.selectedTables.first {
138+
onOpenTable?(selected)
139+
}
140+
return true
141+
}
142+
return false
143+
}
144+
145+
private func navigateSelection(direction: Int) {
146+
guard let sidebarState, let windowState else { return }
147+
let allTables = SchemaService.shared.tables(for: sidebarState.connectionId)
148+
let query = windowState.searchText
149+
let filtered: [TableInfo]
150+
if query.isEmpty {
151+
filtered = allTables
152+
} else {
153+
filtered = allTables.filter { $0.name.localizedCaseInsensitiveContains(query) }
154+
}
155+
guard !filtered.isEmpty else { return }
156+
157+
let currentIndex: Int
158+
if let selected = sidebarState.selectedTables.first,
159+
let idx = filtered.firstIndex(of: selected) {
160+
currentIndex = idx
161+
} else {
162+
currentIndex = direction > 0 ? -1 : filtered.count
163+
}
164+
165+
let nextIndex = currentIndex + direction
166+
guard nextIndex >= 0, nextIndex < filtered.count else { return }
167+
sidebarState.selectedTables = [filtered[nextIndex]]
168+
}
169+
123170
private func writeSearchText(_ text: String) {
124171
guard let sidebarState, let windowState else { return }
125172
switch sidebarState.selectedSidebarTab {

0 commit comments

Comments
 (0)