Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Copy rows writes TSV, HTML table, and plain text to the clipboard for richer paste in spreadsheet apps
- Row drag adds TSV and HTML representations alongside the internal drag type
- AI provider settings allow manually entering a model name when the provider does not return one
- Edit menu has a Find submenu with Find, Find Next (`Cmd+G`), Find Previous (`Cmd+Shift+G`), Use Selection for Find (`Cmd+E`), and Jump to Selection (`Cmd+J`)
- File menu has a New Window item (`Cmd+Ctrl+N`) that opens a fresh editor window for the active connection

### Changed

- `Cmd+N` now opens the New Connection form. Manage Connections moves to the File menu without a default shortcut.
- `Cmd+D` now duplicates the selected row. Save as Favorite moves to `Cmd+Shift+D`.
- Removed default shortcuts that conflicted with system reservations: `Cmd+Y` (Quick Look), `Cmd+Option+Delete` (Empty Trash), `Cmd+Ctrl+C` (Color Picker), `Cmd+L` (URL bar / Add Link).
- Show History moves from `Cmd+Y` to `Cmd+Option+H` to mirror the other panel toggles.
- Truncate Table no longer has a default shortcut; the action stays in the Edit menu and the sidebar context menu.
- Switch Connection no longer has a default shortcut; the menu entry remains and users can rebind in Settings > Keyboard.
- Explain with AI no longer has a default shortcut; the menu entry remains and users can rebind in Settings > Keyboard.
- File menu "Save Changes" renamed to "Save".
- View menu Show/Hide labels now flip based on panel state (Show Sidebar / Hide Sidebar, Show Inspector / Hide Inspector, Show Filters / Hide Filters, Show History / Hide History, Show Results / Hide Results).
- Storage and sync singletons accept dependencies via init for test isolation, matching Apple's URLSession and UserDefaults convention. Production callers using `.shared` are unchanged. `SQLFavoriteStorage` is now an actor so its first access no longer blocks the main thread on SQLite setup.
- Create Database dialog is now driver-driven. Each driver discovers its own valid options (PostgreSQL queries `pg_collation` and `pg_database`, MySQL/MariaDB query `information_schema.character_sets`/`collations`). The hardcoded macOS-flavored locale list is gone. Engines that don't support creation hide the Create button instead of failing on click.
- Introduced TableRows, Row, and Delta value types in TablePro/Models/Query/ as the foundation for the data grid row model rewrite. No callers migrated yet (Phase C.1 of the DataGrid refactor).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,16 @@ public extension TextViewController {
_ = textView.resignFirstResponder()
findViewController?.showFindPanel()
}

func findNextMatch() {
findViewController?.viewModel.moveToNextMatch()
}

func findPreviousMatch() {
findViewController?.viewModel.moveToPreviousMatch()
}

func setFindText(_ text: String) {
findViewController?.viewModel.findText = text
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ private struct HistoryToolbarButton: View {
} label: {
Label("History", systemImage: "clock")
}
.help(String(localized: "Toggle Query History (⌘Y)"))
.help(String(localized: "Toggle Query History (⌥⌘H)"))
}
}

Expand Down
52 changes: 35 additions & 17 deletions TablePro/Models/UI/KeyboardShortcutModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ enum ShortcutCategory: String, Codable, CaseIterable, Identifiable {
/// All customizable keyboard shortcut actions
enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
// File
case newConnection
case newWindow
case manageConnections
case newTab
case openDatabase
Expand Down Expand Up @@ -70,6 +72,11 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case delete
case selectAll
case clearSelection
case find
case findNext
case findPrevious
case useSelectionForFind
case jumpToSelection
case addRow
case duplicateRow
case truncateTable
Expand Down Expand Up @@ -98,14 +105,15 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {

var category: ShortcutCategory {
switch self {
case .manageConnections, .newTab, .openDatabase, .openFile, .switchConnection,
.saveChanges, .saveAs, .previewSQL, .closeTab, .refresh,
case .newConnection, .newWindow, .manageConnections, .newTab, .openDatabase, .openFile,
.switchConnection, .saveChanges, .saveAs, .previewSQL, .closeTab, .refresh,
.executeQuery, .explainQuery, .formatQuery, .export, .importData, .quickSwitcher,
.previousPage, .nextPage, .saveAsFavorite, .openTerminal:
return .file
case .undo, .redo, .cut, .copy, .copyWithHeaders, .copyAsJson, .paste,
.delete, .selectAll, .clearSelection, .addRow,
.duplicateRow, .truncateTable, .previewFKReference:
.delete, .selectAll, .clearSelection,
.find, .findNext, .findPrevious, .useSelectionForFind, .jumpToSelection,
.addRow, .duplicateRow, .truncateTable, .previewFKReference:
return .edit
case .toggleTableBrowser, .toggleInspector, .toggleFilters, .toggleHistory,
.toggleResults, .previousResultTab, .nextResultTab, .closeResultTab:
Expand All @@ -119,13 +127,15 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {

var displayName: String {
switch self {
case .newConnection: return String(localized: "New Connection")
case .newWindow: return String(localized: "New Window")
case .manageConnections: return String(localized: "Manage Connections")
case .executeQuery: return String(localized: "Execute Query")
case .newTab: return String(localized: "New Tab")
case .openDatabase: return String(localized: "Open Database")
case .openFile: return String(localized: "Open File")
case .switchConnection: return String(localized: "Switch Connection")
case .saveChanges: return String(localized: "Save Changes")
case .saveChanges: return String(localized: "Save")
case .saveAs: return String(localized: "Save As")
case .previewSQL: return String(localized: "Preview SQL")
case .closeTab: return String(localized: "Close Tab")
Expand All @@ -148,16 +158,21 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case .delete: return String(localized: "Delete")
case .selectAll: return String(localized: "Select All")
case .clearSelection: return String(localized: "Clear Selection")
case .find: return String(localized: "Find")
case .findNext: return String(localized: "Find Next")
case .findPrevious: return String(localized: "Find Previous")
case .useSelectionForFind: return String(localized: "Use Selection for Find")
case .jumpToSelection: return String(localized: "Jump to Selection")
case .addRow: return String(localized: "Add Row")
case .duplicateRow: return String(localized: "Duplicate Row")
case .truncateTable: return String(localized: "Truncate Table")
case .previewFKReference: return String(localized: "Preview FK Reference")
case .saveAsFavorite: return String(localized: "Save as Favorite")
case .toggleTableBrowser: return String(localized: "Toggle Table Browser")
case .toggleInspector: return String(localized: "Toggle Inspector")
case .toggleFilters: return String(localized: "Toggle Filters")
case .toggleHistory: return String(localized: "Toggle History")
case .toggleResults: return String(localized: "Toggle Results")
case .toggleTableBrowser: return String(localized: "Show Sidebar")
case .toggleInspector: return String(localized: "Show Inspector")
case .toggleFilters: return String(localized: "Show Filters")
case .toggleHistory: return String(localized: "Show History")
case .toggleResults: return String(localized: "Show Results")
case .previousResultTab: return String(localized: "Previous Result")
case .nextResultTab: return String(localized: "Next Result")
case .closeResultTab: return String(localized: "Close Result Tab")
Expand Down Expand Up @@ -457,12 +472,12 @@ struct KeyboardSettings: Codable, Equatable {
/// Default shortcuts — applied when user has no overrides
static let defaultShortcuts: [ShortcutAction: KeyCombo] = [
// File
.manageConnections: KeyCombo(key: "n", command: true),
.newConnection: KeyCombo(key: "n", command: true),
.newWindow: KeyCombo(key: "n", command: true, control: true),
.executeQuery: KeyCombo(key: "return", command: true, isSpecialKey: true),
.newTab: KeyCombo(key: "t", command: true),
.openDatabase: KeyCombo(key: "k", command: true),
.openFile: KeyCombo(key: "o", command: true),
.switchConnection: KeyCombo(key: "c", command: true, control: true),
.saveChanges: KeyCombo(key: "s", command: true),
.saveAs: KeyCombo(key: "s", command: true, shift: true),
.previewSQL: KeyCombo(key: "p", command: true, shift: true),
Expand All @@ -488,17 +503,21 @@ struct KeyboardSettings: Codable, Equatable {
.delete: KeyCombo(key: "delete", command: true, isSpecialKey: true),
.selectAll: KeyCombo(key: "a", command: true),
.clearSelection: KeyCombo(key: "escape", isSpecialKey: true),
.find: KeyCombo(key: "f", command: true),
.findNext: KeyCombo(key: "g", command: true),
.findPrevious: KeyCombo(key: "g", command: true, shift: true),
.useSelectionForFind: KeyCombo(key: "e", command: true),
.jumpToSelection: KeyCombo(key: "j", command: true),
.addRow: KeyCombo(key: "n", command: true, shift: true),
.duplicateRow: KeyCombo(key: "d", command: true, shift: true),
.truncateTable: KeyCombo(key: "delete", option: true, isSpecialKey: true),
.duplicateRow: KeyCombo(key: "d", command: true),
.previewFKReference: KeyCombo(key: "space", isSpecialKey: true),
.saveAsFavorite: KeyCombo(key: "d", command: true),
.saveAsFavorite: KeyCombo(key: "d", command: true, shift: true),

// View
.toggleTableBrowser: KeyCombo(key: "0", command: true),
.toggleInspector: KeyCombo(key: "i", command: true, option: true),
.toggleFilters: KeyCombo(key: "f", command: true, shift: true),
.toggleHistory: KeyCombo(key: "y", command: true),
.toggleHistory: KeyCombo(key: "h", command: true, option: true),
.toggleResults: KeyCombo(key: "r", command: true, option: true),
.previousResultTab: KeyCombo(key: "[", command: true, option: true),
.nextResultTab: KeyCombo(key: "]", command: true, option: true),
Expand All @@ -509,7 +528,6 @@ struct KeyboardSettings: Codable, Equatable {
.showNextTab: KeyCombo(key: "]", command: true, shift: true),

// AI
.aiExplainQuery: KeyCombo(key: "l", command: true),
.aiOptimizeQuery: KeyCombo(key: "l", command: true, option: true),
]
}
Expand Down
82 changes: 69 additions & 13 deletions TablePro/TableProApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ struct AppMenuCommands: Commands {
settingsManager.keyboard.keyboardShortcut(for: action)
}

private func openNewMainWindow() {
let connectionId = actions?.connectionId
?? NSApp.keyWindow.flatMap { WindowLifecycleMonitor.shared.connectionId(forWindow: $0) }
guard let connectionId else {
WelcomeWindowFactory.openOrFront()
return
}
WindowManager.shared.openTab(
payload: EditorTabPayload(connectionId: connectionId, intent: .newEmptyTab)
)
}

/// Prefers the focused scene value; falls back to the coordinator back-reference
/// so Cmd+W still routes through `closeTab()` (with its unsaved-changes dialog)
/// when focus is inside an AppKit subview and `@FocusedValue` has not resolved.
Expand Down Expand Up @@ -195,10 +207,15 @@ struct AppMenuCommands: Commands {

// File menu
CommandGroup(replacing: .newItem) {
Button("Manage Connections") {
NotificationCenter.default.post(name: .newConnection, object: nil)
Button(String(localized: "New Connection...")) {
WindowOpener.shared.openWindow?(id: "connection-form")
}
.optionalKeyboardShortcut(shortcut(for: .manageConnections))
.optionalKeyboardShortcut(shortcut(for: .newConnection))

Button(String(localized: "New Window")) {
openNewMainWindow()
}
.optionalKeyboardShortcut(shortcut(for: .newWindow))
}

CommandGroup(after: .newItem) {
Expand All @@ -213,6 +230,11 @@ struct AppMenuCommands: Commands {
}
.disabled(!(actions?.isConnected ?? false) || actions?.isReadOnly ?? false)

Button(String(localized: "Manage Connections...")) {
NotificationCenter.default.post(name: .openWelcomeWindow, object: nil)
}
.optionalKeyboardShortcut(shortcut(for: .manageConnections))

Button("Open Database...") {
actions?.openDatabaseSwitcher()
}
Expand All @@ -227,7 +249,7 @@ struct AppMenuCommands: Commands {

Divider()

Button("Save Changes") {
Button(String(localized: "Save")) {
actions?.saveChanges()
}
.optionalKeyboardShortcut(shortcut(for: .saveChanges))
Expand Down Expand Up @@ -423,14 +445,38 @@ struct AppMenuCommands: Commands {
// Edit menu - pasteboard commands with FocusedValue support
PasteboardCommands(settingsManager: settingsManager)

// Edit menu - Find + row operations (after pasteboard)
// Edit menu - Find submenu + row operations (after pasteboard)
CommandGroup(after: .pasteboard) {
Divider()

Button(String(localized: "Find...")) {
EditorEventRouter.shared.showFindPanelForKeyWindow()
Menu(String(localized: "Find")) {
Button(String(localized: "Find...")) {
EditorEventRouter.shared.showFindPanelForKeyWindow()
}
.optionalKeyboardShortcut(shortcut(for: .find))

Button(String(localized: "Find Next")) {
EditorEventRouter.shared.findNextForKeyWindow()
}
.optionalKeyboardShortcut(shortcut(for: .findNext))

Button(String(localized: "Find Previous")) {
EditorEventRouter.shared.findPreviousForKeyWindow()
}
.optionalKeyboardShortcut(shortcut(for: .findPrevious))

Divider()

Button(String(localized: "Use Selection for Find")) {
EditorEventRouter.shared.useSelectionForFindForKeyWindow()
}
.optionalKeyboardShortcut(shortcut(for: .useSelectionForFind))

Button(String(localized: "Jump to Selection")) {
EditorEventRouter.shared.jumpToSelectionForKeyWindow()
}
.optionalKeyboardShortcut(shortcut(for: .jumpToSelection))
}
.keyboardShortcut("f", modifiers: .command)

Divider()

Expand Down Expand Up @@ -458,34 +504,44 @@ struct AppMenuCommands: Commands {

// View menu
CommandGroup(after: .sidebar) {
Button(String(localized: "Toggle Sidebar")) {
Button(actions?.isSidebarVisible == true
? String(localized: "Hide Sidebar")
: String(localized: "Show Sidebar")) {
NSApp.sendAction(#selector(NSSplitViewController.toggleSidebar(_:)), to: nil, from: nil)
}
.optionalKeyboardShortcut(shortcut(for: .toggleTableBrowser))

Button("Toggle Inspector") {
Button(actions?.isInspectorVisible == true
? String(localized: "Hide Inspector")
: String(localized: "Show Inspector")) {
actions?.toggleRightSidebar()
}
.optionalKeyboardShortcut(shortcut(for: .toggleInspector))
.disabled(!(actions?.isConnected ?? false))

Divider()

Button("Toggle Filters") {
Button(actions?.isFilterPanelVisible == true
? String(localized: "Hide Filters")
: String(localized: "Show Filters")) {
actions?.toggleFilterPanel()
}
.optionalKeyboardShortcut(shortcut(for: .toggleFilters))
.disabled(!(actions?.isConnected ?? false) || !(actions?.isTableTab ?? false))

Button("Toggle History") {
Button(actions?.isHistoryPanelVisible == true
? String(localized: "Hide History")
: String(localized: "Show History")) {
actions?.toggleHistoryPanel()
}
.optionalKeyboardShortcut(shortcut(for: .toggleHistory))
.disabled(!(actions?.isConnected ?? false))

Divider()

Button("Toggle Results") {
Button(actions?.isResultsVisible == true
? String(localized: "Hide Results")
: String(localized: "Show Results")) {
actions?.toggleResults()
}
.optionalKeyboardShortcut(shortcut(for: .toggleResults))
Expand Down
20 changes: 20 additions & 0 deletions TablePro/Views/Editor/EditorEventRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,26 @@ internal final class EditorEventRouter {
coordinator.showFindPanel()
}

internal func findNextForKeyWindow() {
guard let (coordinator, _) = editor(for: NSApp.keyWindow) else { return }
coordinator.findNextMatch()
}

internal func findPreviousForKeyWindow() {
guard let (coordinator, _) = editor(for: NSApp.keyWindow) else { return }
coordinator.findPreviousMatch()
}

internal func useSelectionForFindForKeyWindow() {
guard let (coordinator, _) = editor(for: NSApp.keyWindow) else { return }
coordinator.useSelectionForFind()
}

internal func jumpToSelectionForKeyWindow() {
guard let (coordinator, _) = editor(for: NSApp.keyWindow) else { return }
coordinator.jumpToSelection()
}

// MARK: - Lookup

private func editor(for window: NSWindow?) -> (SQLEditorCoordinator, TextView)? {
Expand Down
25 changes: 25 additions & 0 deletions TablePro/Views/Editor/SQLEditorCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,31 @@ final class SQLEditorCoordinator: TextViewCoordinator, TextViewDelegate {
controller?.showFindPanel()
}

func findNextMatch() {
controller?.findNextMatch()
}

func findPreviousMatch() {
controller?.findPreviousMatch()
}

func useSelectionForFind() {
guard let controller, let textView = controller.textView else { return }
let range = textView.selectedRange()
guard range.length > 0 else { return }
let selected = (textView.string as NSString).substring(with: range)
guard !selected.isEmpty else { return }
controller.setFindText(selected)
controller.showFindPanel()
}

func jumpToSelection() {
guard let controller, let textView = controller.textView else { return }
let range = textView.selectedRange()
guard range.location != NSNotFound else { return }
textView.scrollToRange(range)
}

// MARK: - CodeEditSourceEditor Workarounds

/// Reorder FindViewController's subviews so the find panel is on top for hit testing.
Expand Down
Loading
Loading