From c102bef2a6196595fdd5a2e1e81ac0547c25dc1a Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Sun, 18 Oct 2026 17:28:38 +0100 Subject: [PATCH] added typeaheead and introspetion --- DBSyn/App.swift | 2 +- DBSyn/DatabaseModel.swift | 3 ++- DBSyn/MainView.swift | 16 ++++++++++------ docs/PHASE70-UI-STABILITY.md | 21 +++++++++++++++++++++ 4 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 docs/PHASE70-UI-STABILITY.md diff --git a/DBSyn/App.swift b/DBSyn/App.swift index 3b28257..6684d45 100644 --- a/DBSyn/App.swift +++ b/DBSyn/App.swift @@ -32,7 +32,7 @@ struct DBSynApp: App { NSApplication.shared.orderFrontStandardAboutPanel(options: [ .applicationName: "DBSyn", .applicationIcon: icon ?? NSApplication.shared.applicationIconImage, - .version: "Phase 61", + .version: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0", .credits: NSAttributedString(string: "Native MySQL client for macOS") ]) } diff --git a/DBSyn/DatabaseModel.swift b/DBSyn/DatabaseModel.swift index 102f89b..1e68197 100644 --- a/DBSyn/DatabaseModel.swift +++ b/DBSyn/DatabaseModel.swift @@ -462,7 +462,8 @@ final class DatabaseModel: ObservableObject { columns[row[0], default: []].append(column) } // Ignore results from a connection that has been removed while loading. - guard profiles.contains(where: { $0.id == profile.id }) else { return } + guard profiles.contains(where: { $0.id == profile.id }), + selected?.id == profile.id else { return } let previous = introspections[key] let current = SchemaIntrospection( connectionID: profile.id, database: database, diff --git a/DBSyn/MainView.swift b/DBSyn/MainView.swift index 915c481..8d59d1b 100644 --- a/DBSyn/MainView.swift +++ b/DBSyn/MainView.swift @@ -1553,10 +1553,9 @@ struct SQLTextView: NSViewRepresentable { let selection = editor.selectedRange() editor.string = text - let safeLocation = min( - selection.location, - (text as NSString).length - ) + let safeLocation = selection.location == NSNotFound + ? 0 + : min(selection.location, (text as NSString).length) editor.setSelectedRange( NSRange(location: safeLocation, length: 0) @@ -1579,6 +1578,7 @@ struct SQLTextView: NSViewRepresentable { deinit { pendingCompletion?.cancel() + suggestionPopover?.close() if let keyMonitor { NSEvent.removeMonitor(keyMonitor) } } @@ -1617,7 +1617,10 @@ struct SQLTextView: NSViewRepresentable { let source = editor.string as NSString guard selection.location <= source.length else { dismissSuggestions(); return } let before = source.substring(to: selection.location) - let expression = try! NSRegularExpression(pattern: #"[A-Za-z_][A-Za-z0-9_]*$"#) + guard let expression = try? NSRegularExpression(pattern: #"[A-Za-z_][A-Za-z0-9_]*$"#) else { + dismissSuggestions() + return + } let beforeRange = NSRange(location: 0, length: (before as NSString).length) guard let match = expression.matches(in: before, range: beforeRange).last else { dismissSuggestions(); return } let prefix = (before as NSString).substring(with: match.range) @@ -1678,7 +1681,8 @@ struct SQLTextView: NSViewRepresentable { if keyMonitor == nil { keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self, weak editor] event in guard let self, let editor, self.suggestionPopover?.isShown == true, - editor.window != nil else { return event } + let window = editor.window, window.isKeyWindow, + window.firstResponder === editor else { return event } switch event.keyCode { case 53: self.dismissSuggestions(); return nil // Escape case 36, 48: self.acceptSuggestion(editor); return nil // Return / Tab diff --git a/docs/PHASE70-UI-STABILITY.md b/docs/PHASE70-UI-STABILITY.md new file mode 100644 index 0000000..37a7514 --- /dev/null +++ b/docs/PHASE70-UI-STABILITY.md @@ -0,0 +1,21 @@ +# Phase 70 — UI and stability hardening + +This phase deliberately avoids adding new database-changing operations. + +## Editor stability +- Avoids a forced regular-expression unwrap when preparing suggestions. +- Prevents the SQL editor's local key monitor from consuming Enter, Tab, Escape or arrow keys when another control/window has focus. +- Closes suggestion popovers and removes event monitors when the editor coordinator is released. +- Handles invalid text-selection positions safely when SQL is updated programmatically. + +## Introspection consistency +- Discards introspection results from a connection that is no longer selected, preventing an asynchronous response from applying a different connection's metadata to the active editor. + +## About panel +- Reads the application version from its actual bundle metadata rather than displaying an obsolete phase label. + +## Verification still required on macOS +- Build the Xcode scheme and test SQL editing, completion navigation and keyboard focus changes. +- Switch connections during a long introspection request and confirm stale results are not shown. +- Check System, Light and Dark appearance, and the existing graphs and administration screens. +- Run live MySQL regression tests before treating this as a release candidate.