added typeaheead and introspetion
This commit is contained in:
@@ -197,6 +197,21 @@ final class DatabaseModel: ObservableObject {
|
|||||||
let views: [String]
|
let views: [String]
|
||||||
let columnsByTable: [String: [IntrospectedColumn]]
|
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] = [:]
|
@Published private(set) var introspections: [String: SchemaIntrospection] = [:]
|
||||||
private let introspectionStorageKey = "DBSyn.introspection.snapshots.v2"
|
private let introspectionStorageKey = "DBSyn.introspection.snapshots.v2"
|
||||||
private func persistIntrospections() {
|
private func persistIntrospections() {
|
||||||
@@ -581,6 +596,7 @@ final class DatabaseModel: ObservableObject {
|
|||||||
selectedDatabase = usedSchema
|
selectedDatabase = usedSchema
|
||||||
// Metadata is cached in memory for native SQL completion.
|
// Metadata is cached in memory for native SQL completion.
|
||||||
loadTables(database: usedSchema)
|
loadTables(database: usedSchema)
|
||||||
|
introspect(database: usedSchema)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if listingDatabases {
|
if listingDatabases {
|
||||||
|
|||||||
@@ -972,10 +972,11 @@ struct MainView: View {
|
|||||||
.fill(WB.border)
|
.fill(WB.border)
|
||||||
.frame(width: 1)
|
.frame(width: 1)
|
||||||
|
|
||||||
SQLTextView(text: $model.sql, selectedSQL: $selectedSQL, focusRequest: sqlFocusRequest, completions: model.schemaColumns[model.selectedDatabase] ?? [],
|
SQLTextView(text: $model.sql, selectedSQL: $selectedSQL, focusRequest: sqlFocusRequest,
|
||||||
tables: model.introspection(for: model.selectedDatabase)?.tables ?? model.schemaTables[model.selectedDatabase] ?? [],
|
completions: model.schemaColumns[model.completionDatabase] ?? [],
|
||||||
views: model.introspection(for: model.selectedDatabase)?.views ?? model.schemaViews[model.selectedDatabase] ?? [],
|
tables: model.introspection(for: model.completionDatabase)?.tables ?? model.schemaTables[model.completionDatabase] ?? [],
|
||||||
columnsByTable: model.introspection(for: model.selectedDatabase)?.columnsByTable.mapValues { $0.map(\.name) } ?? [:],
|
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)
|
databases: model.databases)
|
||||||
}
|
}
|
||||||
.background(Color(nsColor: .textBackgroundColor))
|
.background(Color(nsColor: .textBackgroundColor))
|
||||||
@@ -1442,7 +1443,6 @@ struct SQLTextView: NSViewRepresentable {
|
|||||||
editor.backgroundColor = .textBackgroundColor
|
editor.backgroundColor = .textBackgroundColor
|
||||||
editor.textColor = .textColor
|
editor.textColor = .textColor
|
||||||
scroll.backgroundColor = .textBackgroundColor
|
scroll.backgroundColor = .textBackgroundColor
|
||||||
context.coordinator.highlight(editor)
|
|
||||||
if context.coordinator.lastFocusRequest != focusRequest {
|
if context.coordinator.lastFocusRequest != focusRequest {
|
||||||
context.coordinator.lastFocusRequest = focusRequest
|
context.coordinator.lastFocusRequest = focusRequest
|
||||||
DispatchQueue.main.async { [weak editor] in
|
DispatchQueue.main.async { [weak editor] in
|
||||||
@@ -1513,7 +1513,7 @@ struct SQLTextView: NSViewRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func showSuggestions(_ editor: NSTextView) {
|
private func showSuggestions(_ editor: NSTextView) {
|
||||||
guard editor.window?.firstResponder === editor else { dismissSuggestions(); return }
|
guard editor.window != nil else { dismissSuggestions(); return }
|
||||||
let selection = editor.selectedRange()
|
let selection = editor.selectedRange()
|
||||||
guard selection.length == 0, selection.location != NSNotFound else { dismissSuggestions(); return }
|
guard selection.length == 0, selection.location != NSNotFound else { dismissSuggestions(); return }
|
||||||
let source = editor.string as NSString
|
let source = editor.string as NSString
|
||||||
@@ -1546,7 +1546,7 @@ struct SQLTextView: NSViewRepresentable {
|
|||||||
guard !suggestionItems.isEmpty else { dismissSuggestions(); return }
|
guard !suggestionItems.isEmpty else { dismissSuggestions(); return }
|
||||||
dismissPopoverOnly()
|
dismissPopoverOnly()
|
||||||
let popover = NSPopover()
|
let popover = NSPopover()
|
||||||
popover.behavior = .semitransient
|
popover.behavior = .applicationDefined
|
||||||
let items = suggestionItems
|
let items = suggestionItems
|
||||||
let content = NSHostingView(rootView: VStack(alignment: .leading, spacing: 0) {
|
let content = NSHostingView(rootView: VStack(alignment: .leading, spacing: 0) {
|
||||||
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
|
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
|
||||||
@@ -1580,7 +1580,7 @@ struct SQLTextView: NSViewRepresentable {
|
|||||||
if keyMonitor == nil {
|
if keyMonitor == nil {
|
||||||
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self, weak editor] event in
|
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self, weak editor] event in
|
||||||
guard let self, let editor, self.suggestionPopover?.isShown == true,
|
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 {
|
switch event.keyCode {
|
||||||
case 53: self.dismissSuggestions(); return nil // Escape
|
case 53: self.dismissSuggestions(); return nil // Escape
|
||||||
case 36, 48: self.acceptSuggestion(editor); return nil // Return / Tab
|
case 36, 48: self.acceptSuggestion(editor); return nil // Return / Tab
|
||||||
@@ -1678,7 +1678,7 @@ struct SQLTextView: NSViewRepresentable {
|
|||||||
highlight(editor)
|
highlight(editor)
|
||||||
// Do not invoke AppKit's completion panel from textDidChange.
|
// Do not invoke AppKit's completion panel from textDidChange.
|
||||||
// That re-entrant path caused the previous editor freezes/crashes.
|
// 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)
|
scheduleSuggestions(editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user