Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Collapsible results panel (`Cmd+Opt+R`), multiple result tabs for multi-statement queries, result pinning
- Inline error banner for query errors

### Changed

- Replace GCD dispatch patterns with Swift structured concurrency

### Fixed

- SQL Server: Unicode characters (Thai, CJK, etc.) in nvarchar/nchar/ntext columns displaying as question marks
Expand Down
39 changes: 18 additions & 21 deletions TablePro/AppDelegate+WindowConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -326,34 +326,31 @@ extension AppDelegate {

isAutoReconnecting = true

DispatchQueue.main.async { [weak self] in
Task { @MainActor [weak self] in
guard let self else { return }
WindowOpener.shared.pendingConnectionId = connection.id
NotificationCenter.default.post(name: .openMainWindow, object: connection.id)

Task { @MainActor in
defer { self.isAutoReconnecting = false }
do {
try await DatabaseManager.shared.connectToSession(connection)

for window in NSApp.windows where self.isWelcomeWindow(window) {
window.close()
}
} catch is CancellationError {
// User cancelled password prompt at startup — return to welcome
for window in NSApp.windows where self.isMainWindow(window) {
window.close()
}
self.openWelcomeWindow()
} catch {
windowLogger.error("Auto-reconnect failed for '\(connection.name)': \(error.localizedDescription)")
defer { self.isAutoReconnecting = false }
do {
try await DatabaseManager.shared.connectToSession(connection)

for window in NSApp.windows where self.isMainWindow(window) {
window.close()
}
for window in NSApp.windows where self.isWelcomeWindow(window) {
window.close()
}
} catch is CancellationError {
for window in NSApp.windows where self.isMainWindow(window) {
window.close()
}
self.openWelcomeWindow()
} catch {
windowLogger.error("Auto-reconnect failed for '\(connection.name)': \(error.localizedDescription)")

self.openWelcomeWindow()
for window in NSApp.windows where self.isMainWindow(window) {
window.close()
}

self.openWelcomeWindow()
}
}
}
Expand Down
7 changes: 1 addition & 6 deletions TablePro/Core/Services/Formatting/SQLFormatterService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,7 @@ struct SQLFormatterService: SQLFormatterProtocol {
}

private static func resolveDialectProvider(for dialect: DatabaseType) -> SQLDialectProvider {
if Thread.isMainThread {
return MainActor.assumeIsolated { SQLDialectFactory.createDialect(for: dialect) }
}
return DispatchQueue.main.sync {
MainActor.assumeIsolated { SQLDialectFactory.createDialect(for: dialect) }
}
SQLDialectFactory.createDialect(for: dialect)
}

// MARK: - Public API
Expand Down
5 changes: 3 additions & 2 deletions TablePro/Core/Services/Query/SQLDialectProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ private struct EmptyDialect: SQLDialectProvider {
// MARK: - Dialect Factory

struct SQLDialectFactory {
@MainActor
static func createDialect(for databaseType: DatabaseType) -> SQLDialectProvider {
if let descriptor = PluginManager.shared.sqlDialect(for: databaseType) {
if let descriptor = PluginMetadataRegistry.shared.snapshot(
forTypeId: databaseType.pluginTypeId
)?.editor.sqlDialect {
return PluginDialectAdapter(descriptor: descriptor)
}
return EmptyDialect()
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/AIChat/AIChatCodeBlockView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ struct AIChatCodeBlockView: View {
Button {
ClipboardService.shared.writeText(code)
isCopied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
Task { @MainActor in
try? await Task.sleep(for: .seconds(1.5))
isCopied = false
}
} label: {
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Components/SQLReviewPopover.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ struct SQLReviewPopover: View {
ClipboardService.shared.writeText(joined)
copied = true

DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
Task { @MainActor in
try? await Task.sleep(for: .seconds(1.5))
copied = false
}
}
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Components/SyncStatusIndicator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ struct SyncStatusIndicator: View {
showActivationSheet = true
default:
UserDefaults.standard.set(SettingsTab.sync.rawValue, forKey: "selectedSettingsTab")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(100))
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
}
}
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Connection/ConnectionExportOptionsSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ struct ConnectionExportOptionsSheet: View {
confirmPassphrase = ""
dismiss()

DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(200))
let panel = NSSavePanel()
panel.allowedContentTypes = [.tableproConnectionShare]
let defaultName = capturedConnections.count == 1
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Connection/WelcomeWindowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ struct WelcomeWindowView: View {
LicenseActivationSheet()
case .importFile(let url):
ConnectionImportSheet(fileURL: url) { count in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(300))
vm.showImportResultAlert(count: count)
}
}
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Editor/EditorEventRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal final class EditorEventRouter {
if textView.window != nil {
installWindowObserver(for: key)
} else {
DispatchQueue.main.async { [weak self] in
Task { [weak self] in
guard let self, self.editors[key]?.windowObserver == nil else { return }
self.installWindowObserver(for: key)
}
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Editor/HistoryPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,8 @@ private extension HistoryPanelView {
deleteEntry(entry)

// After deletion triggers reload, select adjacent entry
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(50))
if let idx = currentIndex, !entries.isEmpty {
let newIndex = min(idx, entries.count - 1)
if newIndex >= 0, newIndex < entries.count {
Expand Down
28 changes: 12 additions & 16 deletions TablePro/Views/Editor/SQLEditorCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ final class SQLEditorCoordinator: TextViewCoordinator {
@ObservationIgnored private var windowKeyObserver: NSObjectProtocol?
/// Debounce work item for frame-change notification to avoid
/// triggering syntax highlight viewport recalculation on every keystroke.
@ObservationIgnored private var frameChangeWorkItem: DispatchWorkItem?
@ObservationIgnored private var frameChangeTask: Task<Void, Never>?
@ObservationIgnored private var wasEditorFocused = false
@ObservationIgnored private var didDestroy = false

Expand Down Expand Up @@ -66,7 +66,7 @@ final class SQLEditorCoordinator: TextViewCoordinator {
if let observer = windowKeyObserver {
NotificationCenter.default.removeObserver(observer)
}
frameChangeWorkItem?.cancel()
frameChangeTask?.cancel()
}

private func cleanupMonitors() {
Expand All @@ -78,8 +78,8 @@ final class SQLEditorCoordinator: TextViewCoordinator {
NotificationCenter.default.removeObserver(observer)
windowKeyObserver = nil
}
frameChangeWorkItem?.cancel()
frameChangeWorkItem = nil
frameChangeTask?.cancel()
frameChangeTask = nil
}

// MARK: - TextViewCoordinator
Expand All @@ -89,7 +89,7 @@ final class SQLEditorCoordinator: TextViewCoordinator {

// Deferred to next run loop because prepareCoordinator runs during
// TextViewController.init, before the view hierarchy is fully loaded.
DispatchQueue.main.async { [weak self] in
Task { [weak self] in
guard let self else { return }
self.fixFindPanelHitTesting(controller: controller)
self.installAIContextMenu(controller: controller)
Expand Down Expand Up @@ -123,24 +123,20 @@ final class SQLEditorCoordinator: TextViewCoordinator {
vimEngine?.invalidateLineCache()

// Notify inline suggestion manager immediately (lightweight)
DispatchQueue.main.async { [weak self] in
Task { [weak self] in
self?.inlineSuggestionManager?.handleTextChange()
self?.vimCursorManager?.updatePosition()
}

// Throttle frame-change notification — during rapid typing, only the
// last notification matters. The highlighter recalculates the visible
// range on each notification, so coalescing saves redundant layout work.
frameChangeWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak controller] in
guard let controller, let textView = controller.textView else { return }
NotificationCenter.default.post(
name: NSView.frameDidChangeNotification,
object: textView
)
frameChangeTask?.cancel()
frameChangeTask = Task { [weak controller] in
try? await Task.sleep(for: .milliseconds(50))
guard !Task.isCancelled, let controller, let textView = controller.textView else { return }
NotificationCenter.default.post(name: NSView.frameDidChangeNotification, object: textView)
}
frameChangeWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: workItem)
}

func textViewDidChangeSelection(controller: TextViewController, newPositions: [CursorPosition]) {
Expand All @@ -155,7 +151,7 @@ final class SQLEditorCoordinator: TextViewCoordinator {
guard let range = newPositions.first?.range, range.location != NSNotFound else { return }

// Defer to next run loop to let EmphasisManager finish its work first.
DispatchQueue.main.async { [weak controller] in
Task { [weak controller] in
controller?.textView.scrollToRange(range)
}
}
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Filter/SQLPreviewSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ struct SQLPreviewSheet: View {
AccessibilityNotification.Announcement(String(localized: "Copied to clipboard")).post()

// Reset after delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
Task { @MainActor in
try? await Task.sleep(for: .seconds(1.5))
copied = false
}
}
Expand Down
8 changes: 4 additions & 4 deletions TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ struct MainEditorContentView: View {
// Update window dirty indicator and toolbar for file-backed tabs
if tabManager.tabs[index].sourceFileURL != nil {
let isDirty = tabManager.tabs[index].isFileDirty
DispatchQueue.main.async {
Task { @MainActor in
if let window = NSApp.keyWindow {
window.isDocumentEdited = isDirty
}
Expand Down Expand Up @@ -456,7 +456,7 @@ struct MainEditorContentView: View {

private func rowProvider(for tab: QueryTab) -> InMemoryRowProvider {
if tab.rowBuffer.isEvicted {
DispatchQueue.main.async { tabProviderCache.removeValue(forKey: tab.id) }
Task { @MainActor in tabProviderCache.removeValue(forKey: tab.id) }
return makeRowProvider(for: tab)
}
if let entry = tabProviderCache[tab.id],
Expand All @@ -467,7 +467,7 @@ struct MainEditorContentView: View {
return entry.provider
}
let provider = makeRowProvider(for: tab)
DispatchQueue.main.async {
Task { @MainActor in
tabProviderCache[tab.id] = RowProviderCacheEntry(
provider: provider,
resultVersion: tab.resultVersion,
Expand Down Expand Up @@ -620,7 +620,7 @@ struct MainEditorContentView: View {
if let index = tabManager.selectedTabIndex {
tabManager.tabs[index].columnLayout = newValue
}
DispatchQueue.main.async {
Task { @MainActor in
coordinator.isUpdatingColumnLayout = false
coordinator.saveColumnLayoutForTable()
}
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Main/MainContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ struct MainContentView: View {
isKeyWindow = true
evictionTask?.cancel()
evictionTask = nil
DispatchQueue.main.async {
Task { @MainActor in
syncSidebarToCurrentTab()
}
// Lazy-load: execute query for restored tabs that skipped auto-execute,
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Results/DataGridCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
) { [weak self] _ in
guard let self else { return }

DispatchQueue.main.async { [weak self] in
Task { @MainActor [weak self] in
guard let self, let tableView = self.tableView else { return }
let settings = AppSettingsManager.shared.dataGrid
let prev = self.lastDataGridSettings
Expand Down
Loading