diff --git a/src/EditorView.swift b/src/EditorView.swift
index 95e4c8f..eb0fa6f 100644
--- a/src/EditorView.swift
+++ b/src/EditorView.swift
@@ -321,85 +321,153 @@ class MarkdownTableView: NSView, NSTextFieldDelegate {
var onTableChanged: ((ParsedTable) -> Void)?
private var cellFields: [[NSTextField]] = []
- private let cellHeight: CGFloat = 26
- private let cellPadding: CGFloat = 4
- private let headerHeight: CGFloat = 28
+ private var columnWidths: [CGFloat] = []
+ private var rowHeights: [CGFloat] = []
+ private var tableWidth: CGFloat = 0
+
+ private let minColWidth: CGFloat = 40
+ private let minRowHeight: CGFloat = 24
+ private let defaultHeaderHeight: CGFloat = 28
+ private let defaultCellHeight: CGFloat = 26
+ private let dividerHitZone: CGFloat = 6
+
+ private let indicatorRowHeight: CGFloat = 20
+ private let indicatorColWidth: CGFloat = 30
+ private var indicatorsVisible = true
+ private var indicatorContainer: NSView?
+ private var focusMonitor: Any?
+
+ private enum DragMode { case none, column(Int), row(Int) }
+ private var dragMode: DragMode = .none
+ private var dragStartPoint: NSPoint = .zero
+ private var dragStartSize: CGFloat = 0
init(table: ParsedTable, width: CGFloat) {
self.table = table
+ self.tableWidth = width
super.init(frame: .zero)
wantsLayer = true
layer?.backgroundColor = Theme.current.base.cgColor
layer?.cornerRadius = 4
layer?.borderWidth = 1
layer?.borderColor = Theme.current.surface2.cgColor
- buildGrid(width: width)
+ initSizes(width: width)
+ buildGrid()
+ setupTrackingArea()
+ setupFocusMonitoring()
}
required init?(coder: NSCoder) { fatalError() }
- private func buildGrid(width: CGFloat) {
+ deinit {
+ if let monitor = focusMonitor {
+ NotificationCenter.default.removeObserver(monitor)
+ }
+ }
+
+ private func initSizes(width: CGFloat) {
+ let colCount = table.headers.count
+ guard colCount > 0 else { return }
+ let available = width - CGFloat(colCount + 1)
+ let colW = available / CGFloat(colCount)
+ columnWidths = Array(repeating: max(colW, minColWidth), count: colCount)
+
+ rowHeights = []
+ rowHeights.append(defaultHeaderHeight)
+ for _ in 0..
CGFloat {
+ var x: CGFloat = 1
+ for i in 0.. CGFloat {
+ let th = totalHeight
+ var y = th
+ for i in 0...row {
+ y -= rowHeights[i]
+ }
+ return y
+ }
+
+ private func buildGrid() {
subviews.forEach { $0.removeFromSuperview() }
+ indicatorContainer = nil
cellFields = []
let colCount = table.headers.count
guard colCount > 0 else { return }
- let colWidth = (width - CGFloat(colCount + 1)) / CGFloat(colCount)
- let totalRows = 1 + table.rows.count
- let totalHeight = headerHeight + CGFloat(table.rows.count) * cellHeight
+ let th = totalHeight
+ let ox = indicatorColWidth
+ let oy = indicatorRowHeight
+ let gridWidth = columnX(for: colCount) + 1
+ let fullWidth = gridWidth + ox
+ let fullHeight = th + oy
- frame.size = NSSize(width: width, height: totalHeight)
+ frame.size = NSSize(width: fullWidth, height: fullHeight)
- // Header row
- let headerBg = NSView(frame: NSRect(x: 0, y: totalHeight - headerHeight, width: width, height: headerHeight))
+ let headerBg = NSView(frame: NSRect(x: ox, y: th - rowHeights[0] + oy, width: gridWidth, height: rowHeights[0]))
headerBg.wantsLayer = true
headerBg.layer?.backgroundColor = Theme.current.surface0.cgColor
addSubview(headerBg)
var headerFields: [NSTextField] = []
for (col, header) in table.headers.enumerated() {
- let x = CGFloat(col) * colWidth + CGFloat(col + 1)
- let field = makeCell(text: header, frame: NSRect(x: x, y: totalHeight - headerHeight + 2, width: colWidth, height: headerHeight - 4), isHeader: true, row: -1, col: col)
+ let x = columnX(for: col) + ox
+ let h = rowHeights[0]
+ let field = makeCell(text: header, frame: NSRect(x: x, y: th - h + 2 + oy, width: columnWidths[col], height: h - 4), isHeader: true, row: -1, col: col)
addSubview(field)
headerFields.append(field)
}
cellFields.append(headerFields)
- // Data rows
for (rowIdx, row) in table.rows.enumerated() {
var rowFields: [NSTextField] = []
- let y = totalHeight - headerHeight - CGFloat(rowIdx + 1) * cellHeight
+ let y = rowY(for: rowIdx + 1) + oy
+ let h = rowHeights[rowIdx + 1]
for (col, cell) in row.enumerated() where col < colCount {
- let x = CGFloat(col) * colWidth + CGFloat(col + 1)
- let field = makeCell(text: cell, frame: NSRect(x: x, y: y + 2, width: colWidth, height: cellHeight - 4), isHeader: false, row: rowIdx, col: col)
+ let x = columnX(for: col) + ox
+ let field = makeCell(text: cell, frame: NSRect(x: x, y: y + 2, width: columnWidths[col], height: h - 4), isHeader: false, row: rowIdx, col: col)
addSubview(field)
rowFields.append(field)
}
while rowFields.count < colCount {
let col = rowFields.count
- let x = CGFloat(col) * colWidth + CGFloat(col + 1)
- let field = makeCell(text: "", frame: NSRect(x: x, y: y + 2, width: colWidth, height: cellHeight - 4), isHeader: false, row: rowIdx, col: col)
+ let x = columnX(for: col) + ox
+ let field = makeCell(text: "", frame: NSRect(x: x, y: y + 2, width: columnWidths[col], height: h - 4), isHeader: false, row: rowIdx, col: col)
addSubview(field)
rowFields.append(field)
}
cellFields.append(rowFields)
}
- // Grid lines
+ let totalRows = 1 + table.rows.count
for i in 1.. NSTextField {
@@ -426,6 +494,290 @@ class MarkdownTableView: NSView, NSTextFieldDelegate {
return field
}
+ // MARK: - Column letter helper
+
+ private func columnLetter(for index: Int) -> String {
+ var n = index
+ var result = ""
+ repeat {
+ result = String(UnicodeScalar(65 + (n % 26))!) + result
+ n = n / 26 - 1
+ } while n >= 0
+ return result
+ }
+
+ // MARK: - Indicators
+
+ private func buildIndicators() {
+ indicatorContainer?.removeFromSuperview()
+
+ let container = NSView(frame: bounds)
+ container.wantsLayer = true
+ container.alphaValue = indicatorsVisible ? 1 : 0
+ addSubview(container)
+ indicatorContainer = container
+
+ let colCount = table.headers.count
+ let totalRows = 1 + table.rows.count
+ let topY = totalHeight + indicatorRowHeight
+ let indicatorBg = Theme.current.surface0
+
+ let corner = NSView(frame: NSRect(x: 0, y: topY - indicatorRowHeight, width: indicatorColWidth, height: indicatorRowHeight))
+ corner.wantsLayer = true
+ corner.layer?.backgroundColor = indicatorBg.cgColor
+ container.addSubview(corner)
+
+ for col in 0.. Int? {
+ let colCount = table.headers.count
+ for i in 1.. Int? {
+ let totalRows = 1 + table.rows.count
+ for i in 0.. CGFloat {
+ let headerFont = NSFontManager.shared.convert(Theme.editorFont, toHaveTrait: .boldFontMask)
+ let cellFont = Theme.editorFont
+
+ var maxW: CGFloat = 0
+ if col < table.headers.count {
+ let w = (table.headers[col] as NSString).size(withAttributes: [.font: headerFont]).width
+ maxW = max(maxW, w)
+ }
+ for row in table.rows {
+ guard col < row.count else { continue }
+ let w = (row[col] as NSString).size(withAttributes: [.font: cellFont]).width
+ maxW = max(maxW, w)
+ }
+ return max(maxW + 16, minColWidth)
+ }
+
+ private func measureRowHeight(_ row: Int) -> CGFloat {
+ let font: NSFont = row == 0
+ ? NSFontManager.shared.convert(Theme.editorFont, toHaveTrait: .boldFontMask)
+ : Theme.editorFont
+ let cells: [String] = row == 0 ? table.headers : (row - 1 < table.rows.count ? table.rows[row - 1] : [])
+
+ var maxH: CGFloat = 0
+ for (col, text) in cells.enumerated() where col < columnWidths.count {
+ let constrainedSize = NSSize(width: columnWidths[col], height: .greatestFiniteMagnitude)
+ let rect = (text as NSString).boundingRect(
+ with: constrainedSize,
+ options: [.usesLineFragmentOrigin],
+ attributes: [.font: font]
+ )
+ maxH = max(maxH, rect.height)
+ }
+ return max(maxH + 8, minRowHeight)
+ }
+
+ private func autoFitColumn(_ col: Int) {
+ guard col >= 0, col < columnWidths.count else { return }
+ columnWidths[col] = measureColumnWidth(col)
+ buildGrid()
+ }
+
+ private func autoFitRow(_ row: Int) {
+ guard row >= 0, row < rowHeights.count else { return }
+ rowHeights[row] = measureRowHeight(row)
+ buildGrid()
+ }
+
+ // MARK: - Cell editing
+
+ func controlTextDidBeginEditing(_ obj: Notification) {
+ if !indicatorsVisible {
+ showIndicators()
+ }
+ }
+
func controlTextDidEndEditing(_ obj: Notification) {
guard let field = obj.object as? NSTextField else { return }
let tag = field.tag
@@ -464,8 +816,8 @@ class MarkdownTableView: NSView, NSTextFieldDelegate {
table.rows.append(Array(repeating: "", count: table.headers.count))
onTableChanged?(table)
- let width = frame.width
- buildGrid(width: width)
+ rowHeights.append(defaultCellHeight)
+ buildGrid()
needsLayout = true
return true
}
@@ -1530,27 +1882,8 @@ private func highlightFootnoteDefinition(textStorage: NSTextStorage, lineRange:
// MARK: - Tables
private func highlightTableLine(_ trimmed: String, lineRange: NSRange, textStorage: NSTextStorage, palette: CatppuccinPalette, baseFont: NSFont, isHeader: Bool, isSeparator: Bool) {
- let monoFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)
- let boldMono = NSFontManager.shared.convert(monoFont, toHaveTrait: .boldFontMask)
-
- textStorage.addAttribute(.font, value: monoFont, range: lineRange)
- textStorage.addAttribute(.foregroundColor, value: palette.text, range: lineRange)
-
- if isSeparator {
- textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
- return
- }
-
- if isHeader {
- textStorage.addAttribute(.font, value: boldMono, range: lineRange)
- }
-
- // Mute pipe delimiters
- guard let pipeRegex = try? NSRegularExpression(pattern: "\\|") else { return }
- let matches = pipeRegex.matches(in: textStorage.string, range: lineRange)
- for match in matches {
- textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: match.range)
- }
+ textStorage.addAttribute(.foregroundColor, value: palette.base, range: lineRange)
+ textStorage.addAttribute(.font, value: NSFont.systemFont(ofSize: 0.01), range: lineRange)
}
// MARK: - Lists and Horizontal Rules