new release script
This commit is contained in:
+16
-3
@@ -224,13 +224,14 @@ struct WorkbenchSettingsView: View {
|
|||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.leading, 190)
|
.padding(.leading, 186)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
|
|
||||||
Text("Examples")
|
Text("Examples")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"500 rows: Faster browsing and lower memory usage.\n" +
|
"500 rows: Faster browsing and lower memory usage.\n" +
|
||||||
@@ -284,10 +285,22 @@ struct WorkbenchSettingsView: View {
|
|||||||
Text("Light").tag("light")
|
Text("Light").tag("light")
|
||||||
Text("Dark").tag("dark")
|
Text("Dark").tag("dark")
|
||||||
}
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
.labelsHidden()
|
.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") {
|
settingsRow("Navigator") {
|
||||||
Toggle(
|
Toggle(
|
||||||
"Automatically expand table folders",
|
"Automatically expand table folders",
|
||||||
@@ -327,7 +340,7 @@ struct WorkbenchSettingsView: View {
|
|||||||
|
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 13))
|
||||||
.frame(width: 170, alignment: .trailing)
|
.frame(width: 170, alignment: .leading)
|
||||||
|
|
||||||
content()
|
content()
|
||||||
|
|
||||||
|
|||||||
@@ -455,27 +455,42 @@ final class DatabaseModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
private func execute(_ statement: String, listingDatabases: Bool) {
|
private func execute(_ statement: String, listingDatabases: Bool) {
|
||||||
guard let profile = selected else { status = "Select a connection"; return }
|
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
|
isRunning = true
|
||||||
if !listingDatabases { queryDurationText = "Running…" }
|
if !listingDatabases { queryDurationText = "Running…" }
|
||||||
let queryStarted = Date()
|
let queryStarted = Date()
|
||||||
status = "Connecting…"
|
status = "Executing…"
|
||||||
let activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
|
|
||||||
let password = PasswordStore.read(profile.id)
|
let password = PasswordStore.read(profile.id)
|
||||||
Task {
|
Task {
|
||||||
|
var activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
|
||||||
do {
|
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 {
|
if listingDatabases {
|
||||||
databases = parsed.rows.compactMap { $0.first }
|
databases = lastResult.rows.compactMap { $0.first }
|
||||||
status = "Connected to \(profile.name)"
|
status = "Connected to \(profile.name)"
|
||||||
} else {
|
} else {
|
||||||
result = parsed
|
result = lastResult
|
||||||
if let id = activeQueryID,
|
if let id = activeQueryID,
|
||||||
let index = queryTabs.firstIndex(where: { $0.id == id }) {
|
let index = queryTabs.firstIndex(where: { $0.id == id }) {
|
||||||
queryTabs[index].sql = sql
|
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)
|
recordQuery(statement, profile: profile, database: activeDB, succeeded: true, detail: status)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -491,9 +506,72 @@ final class DatabaseModel: ObservableObject {
|
|||||||
isRunning = false
|
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
|
// MARK: - SQL Keyword Formatter
|
||||||
|
|
||||||
enum SQLFormatter {
|
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()
|
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