Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- JSON and PHP tree values use the editor's syntax colors, matching the same value shown as text.
- Theme file format 3, where every color is optional. Themes written for earlier versions are not read and need to be recreated.
- Triggers and ClickHouse Parts in table structure shown as data grids, with multiple selection and Copy.
- Filter and maintenance SQL previews highlighted as SQL in the editor font.
- Result bar items separated by space instead of a dot.
- Filter and maintenance previews highlighted in the editor font, DynamoDB's as JSON.
- Settings > Editor grouped into Display, Gutter and Editing.
- Editor Font and Data Grid Font moved to Settings > Editor and Settings > Data, and kept per Mac.
- Editor font size range of 10 to 24 points everywhere, including zoom.
Expand Down
6 changes: 5 additions & 1 deletion TablePro/Core/Coordinators/FilterCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,11 @@ extension FilterCoordinator: FilterPanelActions {
}

extension FilterCoordinator: FilterSQLPreviewing {
var filterPreviewDatabaseType: DatabaseType {
parent.connection.type
}

func filterPreviewSQL() -> String {
generateFilterPreviewSQL(databaseType: parent.connection.type)
generateFilterPreviewSQL(databaseType: filterPreviewDatabaseType)
}
}
3 changes: 3 additions & 0 deletions TablePro/Models/UI/DataGridConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ struct DataGridConfiguration: Equatable {
/// Why these rows cannot be written back, when they cannot. A grid that silently refuses every
/// keystroke reads as broken, so the reason rides with the configuration and the grid shows it.
var editRefusalMessage: String?

/// Keyed by heading; a column not named here keeps `DataGridMetrics.dataColumnMinWidth`.
var columnMinimumWidths: [String: CGFloat] = [:]
}
45 changes: 35 additions & 10 deletions TablePro/Views/Components/HighlightedSQLTextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ struct HighlightedSQLTextView: NSViewRepresentable {
case fitted(maxLines: Int)
}

internal enum Highlighting: Equatable {
case engine
case json
}

let sql: String
var databaseType: DatabaseType = .mysql
var highlighting: Highlighting = .engine
/// A SwiftUI `.accessibilityIdentifier` lands on the representable's wrapper, not on the text
/// view AppKit publishes, so the only way to name this element is to set it on the text view.
var accessibilityIdentifier: String?
Expand Down Expand Up @@ -103,12 +109,15 @@ struct HighlightedSQLTextView: NSViewRepresentable {
textView.string = sql
}

/// The highlighting bakes a colour per range, so it is applied again whenever the theme or
/// the editor font moves, not only when the query does.
guard !sql.isEmpty, textChanged || fontChanged || context.coordinator.revision != themeRevision else {
/// The highlighting bakes a colour per range, so it is applied again whenever the theme, the
/// editor font or the highlighting moves, not only when the query does.
let highlightingChanged = context.coordinator.highlighting != highlighting
guard !sql.isEmpty,
textChanged || fontChanged || highlightingChanged || context.coordinator.revision != themeRevision else {
return
}
context.coordinator.revision = themeRevision
context.coordinator.highlighting = highlighting
applyHighlighting(to: textView)
}

Expand Down Expand Up @@ -148,6 +157,7 @@ struct HighlightedSQLTextView: NSViewRepresentable {
@MainActor
final class Coordinator {
var revision = -1
var highlighting: Highlighting?
private var themeCancellable: AnyCancellable?

/// An Accent, Highlight or contrast change moves no published value, so nothing would otherwise redraw the
Expand Down Expand Up @@ -242,6 +252,28 @@ struct HighlightedSQLTextView: NSViewRepresentable {
return patterns
}()

/// The JSON editor's colors: keys and values both as strings. Strings go last so a digit or a literal inside one stays string-colored.
private static let jsonPatterns: [(regex: NSRegularExpression, slot: ThemeSlot)] = {
let sources: [(pattern: String, slot: ThemeSlot)] = [
(#"-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b"#, .syntaxNumber),
(#"\b(?:true|false|null)\b"#, .syntaxNull),
(#""(?:[^"\\]|\\.)*""#, .syntaxString),
]
return sources.compactMap { source in
(try? NSRegularExpression(pattern: source.pattern)).map { (regex: $0, slot: source.slot) }
}
}()

private var activePatterns: [(regex: NSRegularExpression, slot: ThemeSlot)] {
if highlighting == .json { return Self.jsonPatterns }
switch PluginManager.shared.editorLanguage(for: databaseType) {
case .javascript:
return Self.mqlPatterns
default:
return Self.syntaxPatterns
}
}

private func applyHighlighting(to textView: NSTextView) {
guard let textStorage = textView.textStorage else { return }
guard textStorage.length > 0 else { return }
Expand All @@ -254,13 +286,6 @@ struct HighlightedSQLTextView: NSViewRepresentable {
textStorage.addAttribute(.font, value: font, range: fullRange)
textStorage.addAttribute(.foregroundColor, value: ThemeEngine.shared.palette[.editorText], range: fullRange)

let activePatterns: [(regex: NSRegularExpression, slot: ThemeSlot)]
switch PluginManager.shared.editorLanguage(for: databaseType) {
case .javascript:
activePatterns = Self.mqlPatterns
default:
activePatterns = Self.syntaxPatterns
}
let text = textStorage.string
let maxHighlightLength = 10_000
let highlightRange: NSRange
Expand Down
1 change: 1 addition & 0 deletions TablePro/Views/Filter/FilterPanelHosting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal extension FilterPanelActions {

@MainActor
internal protocol FilterSQLPreviewing: AnyObject {
var filterPreviewDatabaseType: DatabaseType { get }
func filterPreviewSQL() -> String
}

Expand Down
4 changes: 3 additions & 1 deletion TablePro/Views/Filter/FilterPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ struct FilterPanelView: View {
focusedFilterId = addDefaultFilter(columns: newColumns)
}
.sheet(isPresented: $showSQLSheet) {
SQLPreviewSheet(sql: generatedSQL)
if let sqlPreview = configuration.sqlPreview {
SQLPreviewSheet(sql: generatedSQL, databaseType: sqlPreview.filterPreviewDatabaseType)
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Filter/SQLPreviewSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SwiftUI

struct SQLPreviewSheet: View {
let sql: String
let databaseType: DatabaseType
@Environment(\.dismiss) private var dismiss
@State private var copied = false

Expand All @@ -16,7 +17,7 @@ struct SQLPreviewSheet: View {
.font(.body.weight(.semibold))
.frame(maxWidth: .infinity, alignment: .leading)

HighlightedSQLTextView(sql: sql.isEmpty ? "(no conditions)" : sql, lineLayout: .wrapped)
HighlightedSQLTextView(sql: sql.isEmpty ? "(no conditions)" : sql, databaseType: databaseType, lineLayout: .wrapped)
.frame(maxHeight: 180)
.clipShape(RoundedRectangle(cornerRadius: 6))
.overlay(
Expand Down
20 changes: 17 additions & 3 deletions TablePro/Views/Results/DataGridColumnPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ final class DataGridColumnPool {
firstClickSortDirection: SortDirection,
supportsColumnSort: Bool = true,
supportsValueFilter: Bool = true,
minimumWidths: [String: CGFloat] = [:],
widthCalculator: (String, Int) -> CGFloat
) -> Bool {
attach(to: tableView)
Expand Down Expand Up @@ -117,6 +118,7 @@ final class DataGridColumnPool {
columnType: slot < columnTypes.count ? columnTypes[slot] : nil,
comment: comment,
width: resolvedWidth,
minWidth: minimumWidths[columnName] ?? DataGridMetrics.dataColumnMinWidth,
isEditable: isEditable,
firstClickSortDirection: firstClickSortDirection,
supportsColumnSort: supportsColumnSort,
Expand Down Expand Up @@ -264,6 +266,7 @@ final class DataGridColumnPool {
columnType: ColumnType?,
comment: String?,
width: CGFloat,
minWidth: CGFloat,
isEditable: Bool,
firstClickSortDirection: SortDirection,
supportsColumnSort: Bool = true,
Expand Down Expand Up @@ -295,9 +298,7 @@ final class DataGridColumnPool {
column.headerCell.setAccessibilityLabel(label)
}

if column.width != width {
column.width = width
}
applyWidth(max(width, minWidth), minWidth: minWidth, to: column)
if column.isEditable != isEditable {
column.isEditable = isEditable
}
Expand All @@ -314,6 +315,19 @@ final class DataGridColumnPool {
}
}

/// Raising `minWidth` past `width` moves the width without updating the table's column geometry, so the width goes in first.
private func applyWidth(_ width: CGFloat, minWidth: CGFloat, to column: NSTableColumn) {
if column.minWidth > minWidth {
column.minWidth = minWidth
}
if column.width != width {
column.width = width
}
if column.minWidth != minWidth {
column.minWidth = minWidth
}
}

private func accessibilityLabel(name: String, comment: String?) -> String {
let label = String(format: String(localized: "Column: %@"), name)
guard let comment else { return label }
Expand Down
1 change: 1 addition & 0 deletions TablePro/Views/Results/DataGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ struct DataGridView: NSViewRepresentable {
firstClickSortDirection: coordinator.firstClickSortDirection,
supportsColumnSort: configuration.supportsColumnSort,
supportsValueFilter: configuration.supportsValueFilter,
minimumWidths: configuration.columnMinimumWidths,
widthCalculator: { columnName, slot in
coordinator.automaticColumnWidth(
for: columnName,
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Views/Results/DataGridViewDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ protocol DataGridViewDelegate: AnyObject {
/// A checkbox cell's state, or nil when that row offers no checkbox.
func dataGridCheckboxState(row: Int, column: Int) -> Bool?
func dataGridSetCheckbox(_ isOn: Bool, rows: IndexSet, column: Int)
/// What an assistive client reads for a cell in place of its drawn text, or nil to read the text.
func dataGridAccessibilityText(row displayRow: Int, column: Int) -> String?
/// Nil lets the grid derive the rules from its column types and display formats.
func dataGridSummaryColumnPolicy() -> SelectionSummaryColumnPolicy?
}
Expand All @@ -68,6 +70,7 @@ extension DataGridViewDelegate {
func dataGridMenuOptions(forRow row: Int, columnIndex: Int) -> [GridMenuOption]? { nil }
func dataGridCheckboxState(row: Int, column: Int) -> Bool? { nil }
func dataGridSetCheckbox(_ isOn: Bool, rows: IndexSet, column: Int) {}
func dataGridAccessibilityText(row displayRow: Int, column: Int) -> String? { nil }
func dataGridSummaryColumnPolicy() -> SelectionSummaryColumnPolicy? { nil }
func dataGridDidEditCell(row: Int, column: Int, newValue: String?) {}
func dataGridDeleteRows(_ indices: Set<Int>) {}
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Views/Results/Extensions/DataGridView+Columns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ extension TableViewCoordinator {
let tableRows = tableRowsProvider()
guard let displayRow = displayRow(at: row, in: tableRows),
columnIndex < displayRow.values.count else { return nil }
if let spoken = delegate?.dataGridAccessibilityText(row: row, column: columnIndex) {
return spoken
}
let rawValue = displayRow.values[columnIndex]
let columnType = columnIndex < tableRows.columnTypes.count ? tableRows.columnTypes[columnIndex] : nil
let text = displayValue(
Expand Down
9 changes: 1 addition & 8 deletions TablePro/Views/Results/ResultSetMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@

import SwiftUI

/// The control that chooses which result set the pane shows.
///
/// A pull-down in the status bar's leading zone, beside the view-mode switcher, which is where
/// Postico 2 puts the same control and where Script Editor puts its Description/Result/Log
/// selector. It costs no band of its own, which is the point: the strip it replaces spent 32pt in
/// every query tab, including the overwhelmingly common one with a single result.
///
/// Pin, Unpin, Close and Close Others move across unchanged from the strip's context menu.
/// A pull-down in the status bar rather than a strip of its own, which spent 32pt in every query tab, even with one result.
struct ResultSetMenu: View {
let model: ResultSetMenuModel
/// Spelled out where the bar has room, counted in figures where it does not.
Expand Down
46 changes: 34 additions & 12 deletions TablePro/Views/Sidebar/MaintenanceSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import TableProPluginKit

/// Confirms a maintenance operation and shows the statements that will run.
///
/// The preview is the driver's own SQL, handed in as a closure, and the Execute button sends the same
/// The preview is the driver's own statements, handed in as a closure, and the Execute button sends the same
/// option values back through the same builder. It used to write its own copy of the statement and
/// disagree with it: `REINDEX orders`, which is not valid SQL, where `REINDEX TABLE "orders"` ran.
/// The option controls come from the operation for the same reason, rather than from a switch gated
Expand Down Expand Up @@ -58,17 +58,7 @@ struct MaintenanceSheet: View {
}
}

VStack(alignment: .leading, spacing: 4) {
Text(String(localized: "SQL Preview"))
.font(.caption)
.foregroundStyle(.secondary)
HighlightedSQLTextView(
sql: sqlPreview,
databaseType: databaseType,
lineLayout: .fitted(maxLines: Self.previewMaxLines)
)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
previewSection

Divider()

Expand All @@ -89,6 +79,24 @@ struct MaintenanceSheet: View {
.frame(width: 420)
}

private var previewSection: some View {
let statements = preview(values)
let text = statements.joined(separator: ";\n")
let syntax = Self.previewSyntax(of: statements)
return VStack(alignment: .leading, spacing: 4) {
Text(syntax == .json ? String(localized: "Preview") : String(localized: "SQL Preview"))
.font(.caption)
.foregroundStyle(.secondary)
HighlightedSQLTextView(
sql: text,
databaseType: databaseType,
highlighting: syntax,
lineLayout: .fitted(maxLines: Self.previewMaxLines)
)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
}

private var header: some View {
HStack {
Image(systemName: "wrench.and.screwdriver")
Expand Down Expand Up @@ -147,4 +155,18 @@ struct MaintenanceSheet: View {
}

internal static let previewMaxLines = 8

internal static func previewSyntax(of statements: [String]) -> HighlightedSQLTextView.Highlighting {
guard !statements.isEmpty, statements.allSatisfy(isJSONRequest) else { return .engine }
return .json
}

/// An API driver writes a statement as an action name and its JSON request body, which the SQL patterns barely color.
private static func isJSONRequest(_ statement: String) -> Bool {
let trimmed = statement.trimmingCharacters(in: .whitespacesAndNewlines)
let action = trimmed.prefix { $0.isLetter }
let body = trimmed.dropFirst(action.count).drop { $0.isWhitespace }
guard !action.isEmpty, body.first == "{" else { return false }
return (try? JSONSerialization.jsonObject(with: Data(body.utf8))) is [String: Any]
}
}
8 changes: 5 additions & 3 deletions TablePro/Views/Structure/ClickHousePartsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// ClickHousePartsView.swift
// TablePro
//
// Displays ClickHouse partition/part information from system.parts.
//

import os
import SwiftUI
Expand Down Expand Up @@ -101,6 +99,7 @@ struct ClickHousePartsView: View {
model: grid,
columns: Self.columnTitles,
defaultWidths: Self.columnWidths,
minimumWidths: Self.columnMinimumWidths,
contentKey: hasher.finalize(),
rows: { parts.map(Self.gridRow(for:)) },
databaseType: connection.type,
Expand All @@ -112,6 +111,8 @@ struct ClickHousePartsView: View {
}

internal static let columnWidths: [CGFloat] = [120, 200, 100, 100, 160, 60]
internal static let columnMinimumWidths: [CGFloat] = [80, 100, 60, 60, 100, 50]
internal static let activeColumn = 5

static var columnTitles: [String] {
[
Expand All @@ -134,7 +135,8 @@ struct ClickHousePartsView: View {
ByteSizeFormatting.string(bytes: part.bytesOnDisk),
part.modificationTime,
part.active ? "YES" : "NO",
]
],
spokenValues: [activeColumn: part.active ? String(localized: "Active") : String(localized: "Inactive")]
)
}

Expand Down
Loading
Loading