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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- iOS: running a query that returns a very large result no longer crashes the app. The query editor keeps the first rows it loads, stops before memory runs low, and tells you to add LIMIT to fetch more.
- iOS: Safe Mode "Confirm Writes" now prompts before saving a row edit or inserting a row, matching the query editor. Previously grid edits and inserts saved with no confirmation.
- Redshift: schema switching now works, along with the contains, starts with, and ends with filters and table search. All previously failed with a SQL syntax error. (#1439)
- Double-clicking a CSV or TSV file when TablePro is closed now opens the file directly, instead of showing the welcome screen. (#1443)
- Opening a `.sql` file now names the tab after the file instead of showing "SQL Query". (#1220)

## [0.45.0] - 2026-05-26
Expand Down
3 changes: 2 additions & 1 deletion TablePro/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {

func applicationWillFinishLaunching(_ notification: Notification) {
_ = InspectorDocumentController()
guard ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil else { return }
PluginManager.shared.loadPlugins()
}

func application(_ application: NSApplication, open urls: [URL]) {
Expand Down Expand Up @@ -62,7 +64,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
Task { await CloudflareTunnelManager.shared.sweepStalePidsIfNeeded() }

MemoryPressureAdvisor.startMonitoring()
PluginManager.shared.loadPlugins()
UNUserNotificationCenter.current().delegate = self
PluginNotificationService.shared.setUp()
ChatToolBootstrap.register()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ internal final class AppLaunchCoordinator {
for intent in intents {
await LaunchIntentRouter.shared.route(intent)
}
self.dismissWelcomeIfMainWindowVisible()
}
}
}
Expand All @@ -108,12 +109,18 @@ internal final class AppLaunchCoordinator {
for intent in intents {
await LaunchIntentRouter.shared.route(intent)
}
self.dismissWelcomeIfMainWindowVisible()
self.runStartupBehaviorIfNeeded(skipping: intents)
self.phase = .ready
self.finalizeWindowsIfNoVisibleMain(intents: intents)
}
}

private func dismissWelcomeIfMainWindowVisible() {
guard NSApp.windows.contains(where: { Self.isMainWindow($0) && $0.isVisible }) else { return }
WindowOpener.shared.orderOutWelcome()
}

private func runStartupBehaviorIfNeeded(skipping intents: [LaunchIntent]) {
guard intents.isEmpty else { return }

Expand Down
28 changes: 13 additions & 15 deletions TablePro/Core/Services/Infrastructure/LaunchIntentRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal final class LaunchIntentRouter {

case .openInspectorFile(let url):
Self.logger.debug("LaunchIntentRouter.route(.openInspectorFile(\(url.lastPathComponent, privacy: .public)))")
openInspectorDocument(at: url)
try await openInspectorDocument(at: url)

case .importConnection(let exportable):
WelcomeRouter.shared.routeImport(exportable)
Expand Down Expand Up @@ -57,21 +57,19 @@ internal final class LaunchIntentRouter {
}
}

private func openInspectorDocument(at url: URL) {
private func openInspectorDocument(at url: URL) async throws {
Self.logger.debug("LaunchIntentRouter.openInspectorDocument - calling NSDocumentController.shared (\(String(describing: Swift.type(of: NSDocumentController.shared)), privacy: .public)).openDocument for \(url.lastPathComponent, privacy: .public)")
NSDocumentController.shared.openDocument(withContentsOf: url, display: true) { document, alreadyOpen, error in
Self.logger.debug("LaunchIntentRouter.openInspectorDocument completion - document=\(document == nil ? "nil" : "present", privacy: .public) alreadyOpen=\(alreadyOpen, privacy: .public) error=\(error?.localizedDescription ?? "nil", privacy: .public)")
if let error {
Self.logger.error("Failed to open inspector document at \(url.lastPathComponent, privacy: .public): \(error.localizedDescription, privacy: .public)")
AlertHelper.showErrorSheet(
title: String(localized: "Could Not Open File"),
message: error.localizedDescription,
window: NSApp.keyWindow
)
return
}
if document == nil {
Self.logger.warning("NSDocumentController returned no document for \(url.lastPathComponent, privacy: .public)")
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
NSDocumentController.shared.openDocument(withContentsOf: url, display: true) { document, alreadyOpen, error in
Self.logger.debug("LaunchIntentRouter.openInspectorDocument completion - document=\(document == nil ? "nil" : "present", privacy: .public) alreadyOpen=\(alreadyOpen, privacy: .public) error=\(error?.localizedDescription ?? "nil", privacy: .public)")
if let error {
continuation.resume(throwing: error)
return
}
if document == nil {
Self.logger.warning("NSDocumentController returned no document for \(url.lastPathComponent, privacy: .public)")
}
continuation.resume()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// URLClassifierTests.swift
// TableProTests
//

import Foundation
@testable import TablePro
import TableProPluginKit
import Testing

@Suite("URLClassifier file extension routing", .serialized)
@MainActor
struct URLClassifierTests {
private func withInspectorState<T>(
lazy: [String: URL],
active: [String: any DocumentInspectorPlugin] = [:],
body: () throws -> T
) rethrows -> T {
let originalLazy = PluginManager.shared.lazyInspectorFileExtensions
let originalActive = PluginManager.shared.inspectorPlugins
defer {
PluginManager.shared.lazyInspectorFileExtensions = originalLazy
PluginManager.shared.inspectorPlugins = originalActive
}
PluginManager.shared.lazyInspectorFileExtensions = lazy
PluginManager.shared.inspectorPlugins = active
return try body()
}

@Test("CSV routes to openInspectorFile when the extension is registered")
func routesCSVWhenExtensionRegistered() {
let csvURL = URL(fileURLWithPath: "/tmp/sample.csv")
let stubPluginURL = URL(fileURLWithPath: "/tmp/stub.tableplugin")
let intent = withInspectorState(lazy: ["csv": stubPluginURL]) {
URLClassifier.classify(csvURL)
}
guard case .some(.success(.openInspectorFile(let routed))) = intent else {
Issue.record("Expected .openInspectorFile, got \(String(describing: intent))")
return
}
#expect(routed == csvURL)
}

@Test("CSV returns nil when no inspector plugin registers the extension")
func returnsNilWhenExtensionMissing() {
let csvURL = URL(fileURLWithPath: "/tmp/sample.csv")
let intent = withInspectorState(lazy: [:]) {
URLClassifier.classify(csvURL)
}
#expect(intent == nil)
}
}
Loading