Skip to content

Commit aed43f1

Browse files
committed
fix(editor): favorite keyword autocomplete, Cmd+D shortcut, and selectable Details fields
1 parent c9b8bb6 commit aed43f1

9 files changed

Lines changed: 97 additions & 11 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- Save the current query as a favorite from a star button in the SQL editor toolbar.
13+
- Field names and types in the row Details panel can now be selected and copied.
14+
1015
### Changed
1116

1217
- The database plugin interface is now binary-stable. Adding plugin capabilities in a later release no longer forces installed plugins to be rebuilt; only a breaking interface change does.
18+
- Save as Favorite uses Cmd+D again. The Cmd+Control+D set in 0.47.0 is reserved by macOS for Look Up, so it never fired.
19+
20+
### Fixed
21+
22+
- Favorite keyword suggestions now show in the editor autocomplete when you type the keyword. They were being dropped before reaching the popup.
1323

1424
## [0.47.0] - 2026-06-01
1525

TablePro/Core/Autocomplete/SQLCompletionProvider.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,6 @@ final class SQLCompletionProvider {
111111
) async -> [SQLCompletionItem] {
112112
var items: [SQLCompletionItem] = []
113113

114-
// Check for favorite keyword matches first (highest priority)
115-
if !favoriteKeywords.isEmpty && !context.prefix.isEmpty {
116-
let lowerPrefix = context.prefix.lowercased()
117-
for (keyword, value) in favoriteKeywords where keyword.lowercased().hasPrefix(lowerPrefix) {
118-
items.append(.favorite(keyword: keyword, name: value.name, query: value.query))
119-
}
120-
}
121-
122114
// If we have a dot prefix, we're looking for columns of a specific table
123115
if let dotPrefix = context.dotPrefix {
124116
// Resolve the table name from alias or direct reference
@@ -456,9 +448,20 @@ final class SQLCompletionProvider {
456448
items += await schemaProvider.tableCompletionItems()
457449
}
458450

451+
items += favoriteCompletions(matching: context.prefix)
452+
459453
return items
460454
}
461455

456+
private func favoriteCompletions(matching prefix: String) -> [SQLCompletionItem] {
457+
guard !prefix.isEmpty, !favoriteKeywords.isEmpty else { return [] }
458+
let lowerPrefix = prefix.lowercased()
459+
return favoriteKeywords
460+
.filter { $0.key.lowercased().hasPrefix(lowerPrefix) }
461+
.sorted { $0.key.localizedCaseInsensitiveCompare($1.key) == .orderedAscending }
462+
.map { SQLCompletionItem.favorite(keyword: $0.key, name: $0.value.name, query: $0.value.query) }
463+
}
464+
462465
/// SQL data type keywords (database-aware), with a slight priority boost
463466
/// so they sort before generic constraint keywords in CREATE TABLE context.
464467
/// Uses plugin-provided dialect data when available; falls back to common SQL types.

TablePro/Models/UI/KeyboardShortcutModels.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ struct KeyCombo: Codable, Equatable, Hashable {
401401
KeyCombo(key: "q", command: true, control: true), // Lock Screen
402402
KeyCombo(key: "f", command: true, control: true), // Full Screen
403403
KeyCombo(key: "d", command: true, option: true), // Toggle Dock
404+
KeyCombo(key: "d", command: true, control: true), // Look Up / Define
404405
]
405406

406407
/// Check if this combo is reserved by the system
@@ -535,7 +536,7 @@ struct KeyboardSettings: Codable, Equatable {
535536
.duplicateRow: KeyCombo(key: "d", command: true, shift: true),
536537
.truncateTable: KeyCombo(key: "delete", option: true, isSpecialKey: true),
537538
.previewFKReference: KeyCombo(key: "space", isSpecialKey: true),
538-
.saveAsFavorite: KeyCombo(key: "d", command: true, control: true),
539+
.saveAsFavorite: KeyCombo(key: "d", command: true),
539540

540541
// View
541542
.toggleTableBrowser: KeyCombo(key: "0", command: true),

TablePro/Views/Editor/QueryEditorView.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ struct QueryEditorView: View {
112112
.accessibilityLabel(String(localized: "Format Query"))
113113
.optionalKeyboardShortcut(AppSettingsManager.shared.keyboard.keyboardShortcut(for: .formatQuery))
114114

115+
Button(action: { onSaveAsFavorite?(queryText) }) {
116+
Image(systemName: "star")
117+
.frame(width: 24, height: 24)
118+
}
119+
.buttonStyle(.borderless)
120+
.help(String(localized: "Save as Favorite (⌘D)"))
121+
.accessibilityLabel(String(localized: "Save as Favorite"))
122+
.disabled(!hasQueryText)
123+
115124
Divider()
116125
.frame(height: 16)
117126

TablePro/Views/RightSidebar/EditableFieldView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ internal struct FieldDetailView: View {
119119

120120
TypeBadge(context.columnType.badgeLabel)
121121
}
122+
.textSelection(.enabled)
122123
}
123124

124125
private func editorMinHeight(for kind: FieldEditorKind) -> CGFloat? {

TableProTests/Core/Autocomplete/SQLCompletionProviderTests.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,4 +1049,44 @@ struct SQLCompletionProviderTests {
10491049
let hasStar = items.contains { $0.label == "*" }
10501050
#expect(hasStar, "COUNT( should suggest *")
10511051
}
1052+
1053+
// MARK: - Favorite keyword expansion
1054+
1055+
@Test("Favorite keyword expands at statement start")
1056+
func testFavoriteKeywordExpandsAtStatementStart() async {
1057+
provider.updateFavoriteKeywords(["report": (name: "Daily Report", query: "SELECT * FROM reports")])
1058+
let text = "rep"
1059+
let (items, _) = await provider.getCompletions(text: text, cursorPosition: text.count)
1060+
let favorite = items.first { $0.kind == .favorite }
1061+
#expect(favorite?.label == "report", "Typing the keyword prefix should surface the favorite")
1062+
#expect(favorite?.insertText == "SELECT * FROM reports", "Selecting it inserts the full query")
1063+
#expect(favorite?.detail == "Daily Report", "The favorite name is shown as detail")
1064+
}
1065+
1066+
@Test("Favorite keyword survives clause branches that rebuild candidates")
1067+
func testFavoriteKeywordSurvivesClauseRebuild() async {
1068+
provider.updateFavoriteKeywords(["usr": (name: "Users", query: "SELECT * FROM users")])
1069+
let text = "usr"
1070+
let (items, _) = await provider.getCompletions(text: text, cursorPosition: text.count)
1071+
let hasFavorite = items.contains { $0.kind == .favorite && $0.label == "usr" }
1072+
#expect(hasFavorite, "Favorite must not be discarded by the candidate switch")
1073+
}
1074+
1075+
@Test("Favorite keyword not offered after a dot prefix")
1076+
func testFavoriteKeywordNotOfferedAfterDot() async {
1077+
provider.updateFavoriteKeywords(["col": (name: "Columns", query: "SELECT 1")])
1078+
let text = "users.col"
1079+
let (items, _) = await provider.getCompletions(text: text, cursorPosition: text.count)
1080+
let hasFavorite = items.contains { $0.kind == .favorite }
1081+
#expect(!hasFavorite, "Column completion after a dot must not expand favorites")
1082+
}
1083+
1084+
@Test("Non-matching prefix does not surface favorites")
1085+
func testFavoriteKeywordRequiresPrefixMatch() async {
1086+
provider.updateFavoriteKeywords(["report": (name: "Daily Report", query: "SELECT 1")])
1087+
let text = "SEL"
1088+
let (items, _) = await provider.getCompletions(text: text, cursorPosition: text.count)
1089+
let hasFavorite = items.contains { $0.kind == .favorite }
1090+
#expect(!hasFavorite, "Favorites appear only when the typed token matches their keyword")
1091+
}
10521092
}

TableProTests/Models/KeyboardShortcutTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,26 @@ struct ShortcutActionDefaultsTests {
3030
func cancelQueryDefault() {
3131
#expect(KeyboardSettings.defaultShortcuts[.cancelQuery] == KeyCombo(key: ".", command: true))
3232
}
33+
34+
@Test("Save as Favorite default is Cmd+D")
35+
func saveAsFavoriteDefault() {
36+
#expect(KeyboardSettings.defaultShortcuts[.saveAsFavorite] == KeyCombo(key: "d", command: true))
37+
}
38+
}
39+
40+
@Suite("System reserved shortcuts")
41+
struct SystemReservedShortcutTests {
42+
@Test("Ctrl+Cmd+D is reserved by macOS for Look Up")
43+
func ctrlCmdDIsReserved() {
44+
#expect(KeyCombo(key: "d", command: true, control: true).isSystemReserved)
45+
}
46+
47+
@Test("No default shortcut collides with a system-reserved combo")
48+
func defaultsAvoidSystemReserved() {
49+
for (action, combo) in KeyboardSettings.defaultShortcuts {
50+
#expect(!combo.isSystemReserved, "\(action.rawValue) ships a system-reserved default: \(combo.displayString)")
51+
}
52+
}
3353
}
3454

3555
@Suite("Bare-key validation")

docs/features/favorites.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ Save queries you run often. Organize them in folders, assign keyword shortcuts,
2424

2525
## Creating an SQL Favorite
2626

27-
Three ways to save a favorite:
27+
Ways to save a favorite:
2828

29+
- **From the editor toolbar**: Click the star button above the editor, or press `Cmd+D`
2930
- **From the editor**: Right-click selected SQL > **Save as Favorite**
3031
- **From query history**: Right-click an entry > **Save as Favorite**
3132
- **From the sidebar**: Click **+ New Favorite** in the Favorites tab
@@ -48,7 +49,7 @@ Enter a name, the SQL text, and optionally a keyword and scope.
4849

4950
## Keyword Expansion
5051

51-
Assign a unique keyword to a favorite (e.g., `selall`). Type the keyword in the editor and it appears as an autocomplete suggestion. Select it to insert the full SQL.
52+
Assign a unique keyword to a favorite (e.g., `selall`). Start typing the keyword in the SQL editor and it shows up in the autocomplete popup as a starred suggestion with the favorite's name. Press Tab or Enter to insert the full SQL.
5253

5354
Keywords must be unique across all favorites in the same scope.
5455

docs/features/keyboard-shortcuts.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ TablePro is keyboard-driven. Most actions have shortcuts, and most menu shortcut
4141
| Cancel query | `Cmd+.` | Stop the currently running query |
4242
| Explain query | `Option+Cmd+E` | Show execution plan for query at cursor |
4343
| Format SQL | `Cmd+Shift+L` | Format SQL query |
44+
| Save as Favorite | `Cmd+D` | Save the current query as a favorite |
4445

4546
### Text Editing
4647

0 commit comments

Comments
 (0)