From bcd33b3fa549353626adc460eea9aee35012b051 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Sun, 18 Oct 2026 16:43:08 +0100 Subject: [PATCH] added typeaheead and introspetion --- DBSyn/DatabaseModel.swift | 16 ++++++++++++++++ DBSyn/MainView.swift | 18 +++++++++--------- docs/PHASE61-10-AUTOSUGGEST.md | 9 +++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 docs/PHASE61-10-AUTOSUGGEST.md diff --git a/DBSyn/DatabaseModel.swift b/DBSyn/DatabaseModel.swift index 9988592..6cca89f 100644 --- a/DBSyn/DatabaseModel.swift +++ b/DBSyn/DatabaseModel.swift @@ -197,6 +197,21 @@ final class DatabaseModel: ObservableObject { let views: [String] let columnsByTable: [String: [IntrospectedColumn]] } + // Resolve USE statements from the editor buffer even before Run is pressed. + // The completion provider must not depend on navigator selection alone. + var completionDatabase: String { + let pattern = #"(?im)^\s*USE\s+`?([A-Za-z_][A-Za-z0-9_]*)`?\s*;?"# + if let regex = try? NSRegularExpression(pattern: pattern) { + let value = sql as NSString + let range = NSRange(location: 0, length: value.length) + if let match = regex.matches(in: sql, range: range).last, + match.numberOfRanges > 1 { + return value.substring(with: match.range(at: 1)) + } + } + return selectedDatabase.isEmpty ? (selected?.database ?? "") : selectedDatabase + } + @Published private(set) var introspections: [String: SchemaIntrospection] = [:] private let introspectionStorageKey = "DBSyn.introspection.snapshots.v2" private func persistIntrospections() { @@ -581,6 +596,7 @@ final class DatabaseModel: ObservableObject { selectedDatabase = usedSchema // Metadata is cached in memory for native SQL completion. loadTables(database: usedSchema) + introspect(database: usedSchema) } } if listingDatabases { diff --git a/DBSyn/MainView.swift b/DBSyn/MainView.swift index a57147c..243739e 100644 --- a/DBSyn/MainView.swift +++ b/DBSyn/MainView.swift @@ -972,10 +972,11 @@ struct MainView: View { .fill(WB.border) .frame(width: 1) - SQLTextView(text: $model.sql, selectedSQL: $selectedSQL, focusRequest: sqlFocusRequest, completions: model.schemaColumns[model.selectedDatabase] ?? [], - tables: model.introspection(for: model.selectedDatabase)?.tables ?? model.schemaTables[model.selectedDatabase] ?? [], - views: model.introspection(for: model.selectedDatabase)?.views ?? model.schemaViews[model.selectedDatabase] ?? [], - columnsByTable: model.introspection(for: model.selectedDatabase)?.columnsByTable.mapValues { $0.map(\.name) } ?? [:], + SQLTextView(text: $model.sql, selectedSQL: $selectedSQL, focusRequest: sqlFocusRequest, + completions: model.schemaColumns[model.completionDatabase] ?? [], + tables: model.introspection(for: model.completionDatabase)?.tables ?? model.schemaTables[model.completionDatabase] ?? [], + views: model.introspection(for: model.completionDatabase)?.views ?? model.schemaViews[model.completionDatabase] ?? [], + columnsByTable: model.introspection(for: model.completionDatabase)?.columnsByTable.mapValues { $0.map(\.name) } ?? [:], databases: model.databases) } .background(Color(nsColor: .textBackgroundColor)) @@ -1442,7 +1443,6 @@ struct SQLTextView: NSViewRepresentable { editor.backgroundColor = .textBackgroundColor editor.textColor = .textColor scroll.backgroundColor = .textBackgroundColor - context.coordinator.highlight(editor) if context.coordinator.lastFocusRequest != focusRequest { context.coordinator.lastFocusRequest = focusRequest DispatchQueue.main.async { [weak editor] in @@ -1513,7 +1513,7 @@ struct SQLTextView: NSViewRepresentable { } private func showSuggestions(_ editor: NSTextView) { - guard editor.window?.firstResponder === editor else { dismissSuggestions(); return } + guard editor.window != nil else { dismissSuggestions(); return } let selection = editor.selectedRange() guard selection.length == 0, selection.location != NSNotFound else { dismissSuggestions(); return } let source = editor.string as NSString @@ -1546,7 +1546,7 @@ struct SQLTextView: NSViewRepresentable { guard !suggestionItems.isEmpty else { dismissSuggestions(); return } dismissPopoverOnly() let popover = NSPopover() - popover.behavior = .semitransient + popover.behavior = .applicationDefined let items = suggestionItems let content = NSHostingView(rootView: VStack(alignment: .leading, spacing: 0) { ForEach(Array(items.enumerated()), id: \.offset) { index, item in @@ -1580,7 +1580,7 @@ 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?.firstResponder === editor else { return event } + editor.window != nil else { return event } switch event.keyCode { case 53: self.dismissSuggestions(); return nil // Escape case 36, 48: self.acceptSuggestion(editor); return nil // Return / Tab @@ -1678,7 +1678,7 @@ struct SQLTextView: NSViewRepresentable { highlight(editor) // Do not invoke AppKit's completion panel from textDidChange. // That re-entrant path caused the previous editor freezes/crashes. - // Completion remains available through the standard macOS Complete command. + // Schedule outside the AppKit edit callback to avoid re-entrant completion. scheduleSuggestions(editor) } diff --git a/docs/PHASE61-10-AUTOSUGGEST.md b/docs/PHASE61-10-AUTOSUGGEST.md new file mode 100644 index 0000000..b49d781 --- /dev/null +++ b/docs/PHASE61-10-AUTOSUGGEST.md @@ -0,0 +1,9 @@ +# Phase 61.10 — SQL autocomplete wiring repair + +- Resolve the active completion database from the most recent `USE schema;` line in the SQL buffer, without requiring the navigator to change or Run to be pressed. +- Feed tables, views and per-table columns from the resolved database's persisted introspection snapshot to the SQL editor. +- Following successful `USE`, load the schema and trigger introspection to populate the cache. +- Avoid requiring `firstResponder === editor` for suggestion display: macOS may make a field editor or popover responder active during the asynchronous callback. +- Use an application-defined popover to avoid semitransient auto-dismissal on focus changes. + +The implementation has not been compiled or tested on macOS. Validate using `USE AAMEDIUK;` followed by `SELECT * FROM ADCU`, and inspect the app's status for introspection errors if suggestions are empty.