new release script
This commit is contained in:
+16
-3
@@ -224,13 +224,14 @@ struct WorkbenchSettingsView: View {
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.leading, 190)
|
||||
.padding(.leading, 186)
|
||||
|
||||
Divider()
|
||||
.padding(.vertical, 12)
|
||||
|
||||
Text("Examples")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Text(
|
||||
"500 rows: Faster browsing and lower memory usage.\n" +
|
||||
@@ -284,10 +285,22 @@ struct WorkbenchSettingsView: View {
|
||||
Text("Light").tag("light")
|
||||
Text("Dark").tag("dark")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.labelsHidden()
|
||||
.frame(width: 170)
|
||||
.accessibilityLabel("Application appearance")
|
||||
.frame(width: 260, alignment: .leading)
|
||||
}
|
||||
|
||||
Text(appearance == "system"
|
||||
? "DBSyn follows your Mac’s appearance setting."
|
||||
: appearance == "light"
|
||||
? "DBSyn always uses Light appearance."
|
||||
: "DBSyn always uses Dark appearance.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.leading, 186)
|
||||
|
||||
settingsRow("Navigator") {
|
||||
Toggle(
|
||||
"Automatically expand table folders",
|
||||
@@ -327,7 +340,7 @@ struct WorkbenchSettingsView: View {
|
||||
|
||||
Text(title)
|
||||
.font(.system(size: 13))
|
||||
.frame(width: 170, alignment: .trailing)
|
||||
.frame(width: 170, alignment: .leading)
|
||||
|
||||
content()
|
||||
|
||||
|
||||
@@ -455,27 +455,42 @@ final class DatabaseModel: ObservableObject {
|
||||
}
|
||||
private func execute(_ statement: String, listingDatabases: Bool) {
|
||||
guard let profile = selected else { status = "Select a connection"; return }
|
||||
guard !statement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||
let statements = listingDatabases ? [statement] : SQLStatementSplitter.split(statement)
|
||||
guard !statements.isEmpty else { return }
|
||||
isRunning = true
|
||||
if !listingDatabases { queryDurationText = "Running…" }
|
||||
let queryStarted = Date()
|
||||
status = "Connecting…"
|
||||
let activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
|
||||
status = "Executing…"
|
||||
let password = PasswordStore.read(profile.id)
|
||||
Task {
|
||||
var activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
|
||||
do {
|
||||
let parsed = try await NativeMySQLRunner.run(profile: profile, database: activeDB, password: password, sql: statement)
|
||||
var lastResult = QueryResult()
|
||||
for item in statements {
|
||||
// Each query is sent separately: COM_QUERY accepts one SQL
|
||||
// statement, not an entire semicolon-separated editor buffer.
|
||||
let parsed = try await NativeMySQLRunner.run(
|
||||
profile: profile, database: activeDB, password: password, sql: item
|
||||
)
|
||||
lastResult = parsed
|
||||
if let usedSchema = SQLStatementSplitter.usedDatabase(item) {
|
||||
activeDB = usedSchema
|
||||
selectedDatabase = usedSchema
|
||||
// Metadata is cached in memory for native SQL completion.
|
||||
loadTables(database: usedSchema)
|
||||
}
|
||||
}
|
||||
if listingDatabases {
|
||||
databases = parsed.rows.compactMap { $0.first }
|
||||
databases = lastResult.rows.compactMap { $0.first }
|
||||
status = "Connected to \(profile.name)"
|
||||
} else {
|
||||
result = parsed
|
||||
result = lastResult
|
||||
if let id = activeQueryID,
|
||||
let index = queryTabs.firstIndex(where: { $0.id == id }) {
|
||||
queryTabs[index].sql = sql
|
||||
queryTabs[index].result = parsed
|
||||
queryTabs[index].result = lastResult
|
||||
}
|
||||
status = "Query completed (\(parsed.rows.count) rows)"
|
||||
status = "Executed \(statements.count) statement(s) (\(lastResult.rows.count) rows)"
|
||||
recordQuery(statement, profile: profile, database: activeDB, succeeded: true, detail: status)
|
||||
}
|
||||
} catch {
|
||||
@@ -491,9 +506,72 @@ final class DatabaseModel: ObservableObject {
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Splits editor scripts without splitting semicolons inside quoted values or comments.
|
||||
private enum SQLStatementSplitter {
|
||||
static func split(_ source: String) -> [String] {
|
||||
let chars = Array(source)
|
||||
var output: [String] = []
|
||||
var current = ""
|
||||
var state = 0 // 0 normal, 1 single quote, 2 double quote, 3 identifier, 4 line comment, 5 block comment
|
||||
var i = 0
|
||||
while i < chars.count {
|
||||
let c = chars[i]
|
||||
let next = i + 1 < chars.count ? chars[i + 1] : "\0"
|
||||
if state == 4 {
|
||||
if c == "\n" || c == "\r" { state = 0; current.append(" ") }
|
||||
i += 1; continue
|
||||
}
|
||||
if state == 5 {
|
||||
if c == "*" && next == "/" { state = 0; i += 2; current.append(" ") }
|
||||
else { i += 1 }
|
||||
continue
|
||||
}
|
||||
if state == 0 {
|
||||
if c == "#" || (c == "-" && next == "-" && (i + 2 == chars.count || chars[i + 2].isWhitespace)) {
|
||||
state = 4; i += c == "#" ? 1 : 2; continue
|
||||
}
|
||||
if c == "/" && next == "*" { state = 5; i += 2; continue }
|
||||
if c == "'" { state = 1 }
|
||||
else if c == "\"" { state = 2 }
|
||||
else if c == "`" { state = 3 }
|
||||
else if c == ";" {
|
||||
let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty { output.append(trimmed) }
|
||||
current = ""; i += 1; continue
|
||||
}
|
||||
} else {
|
||||
if c == "\\" && state != 3 && i + 1 < chars.count {
|
||||
current.append(c); current.append(next); i += 2; continue
|
||||
}
|
||||
if (state == 1 && c == "'") || (state == 2 && c == "\"") || (state == 3 && c == "`") {
|
||||
if next == c { current.append(c); current.append(next); i += 2; continue }
|
||||
state = 0
|
||||
}
|
||||
}
|
||||
current.append(c); i += 1
|
||||
}
|
||||
let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty { output.append(trimmed) }
|
||||
return output
|
||||
}
|
||||
|
||||
static func usedDatabase(_ statement: String) -> String? {
|
||||
let pattern = #"(?is)^\s*USE\s+(?:`((?:``|[^`])+)`|([A-Za-z0-9_$]+))\s*$"#
|
||||
guard let regex = try? NSRegularExpression(pattern: pattern),
|
||||
let match = regex.firstMatch(in: statement, range: NSRange(statement.startIndex..., in: statement)) else { return nil }
|
||||
for index in [1, 2] where match.range(at: index).location != NSNotFound {
|
||||
if let range = Range(match.range(at: index), in: statement) {
|
||||
return String(statement[range]).replacingOccurrences(of: "``", with: "`")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQL Keyword Formatter
|
||||
|
||||
enum SQLFormatter {
|
||||
|
||||
@@ -1546,6 +1546,14 @@ struct SQLTextView: NSViewRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
// MySQL # comments are muted, including the entire line.
|
||||
if let comments = try? NSRegularExpression(pattern: #"(?m)^[ \t]*#.*$"#) {
|
||||
for match in comments.matches(in: editor.string, range: range) {
|
||||
storage.addAttribute(.foregroundColor,
|
||||
value: NSColor.secondaryLabelColor,
|
||||
range: match.range)
|
||||
}
|
||||
}
|
||||
storage.endEditing()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Phase 61.2 — Appearance selector
|
||||
|
||||
Settings → General now has a prominent segmented control for System, Light, and Dark. Changes apply immediately and persist through `preferences.appearance`.
|
||||
Reference in New Issue
Block a user