content-width tables, add-column button, column resize handles
This commit is contained in:
parent
405772ee6b
commit
3e52866825
|
|
@ -358,6 +358,125 @@ class FlippedBlockView: NSView {
|
||||||
override var isFlipped: Bool { true }
|
override var isFlipped: Bool { true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - TableBlockView
|
||||||
|
|
||||||
|
class TableBlockView: FlippedBlockView {
|
||||||
|
|
||||||
|
weak var tableBlock: TableBlock?
|
||||||
|
private var trackingArea: NSTrackingArea?
|
||||||
|
private var addColumnButton: NSButton?
|
||||||
|
|
||||||
|
override var isFlipped: Bool { true }
|
||||||
|
|
||||||
|
private enum DragMode { case none, column(Int) }
|
||||||
|
private var dragMode: DragMode = .none
|
||||||
|
private var dragStartX: CGFloat = 0
|
||||||
|
private var dragStartWidth: CGFloat = 0
|
||||||
|
|
||||||
|
private let dividerHitZone: CGFloat = 4
|
||||||
|
|
||||||
|
func setupTracking() {
|
||||||
|
if let old = trackingArea { removeTrackingArea(old) }
|
||||||
|
let area = NSTrackingArea(
|
||||||
|
rect: bounds,
|
||||||
|
options: [.mouseMoved, .mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect],
|
||||||
|
owner: self,
|
||||||
|
userInfo: nil
|
||||||
|
)
|
||||||
|
addTrackingArea(area)
|
||||||
|
trackingArea = area
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAddButton() {
|
||||||
|
addColumnButton?.removeFromSuperview()
|
||||||
|
guard let tb = tableBlock else { return }
|
||||||
|
let gridW = tb.totalGridWidth
|
||||||
|
let th = tb.totalGridHeight
|
||||||
|
let btn = NSButton(frame: NSRect(x: gridW + 2, y: 0, width: 20, height: min(th, 28)))
|
||||||
|
btn.title = "+"
|
||||||
|
btn.bezelStyle = .inline
|
||||||
|
btn.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||||
|
btn.isBordered = false
|
||||||
|
btn.wantsLayer = true
|
||||||
|
btn.layer?.backgroundColor = Theme.current.surface0.cgColor
|
||||||
|
btn.layer?.cornerRadius = 3
|
||||||
|
btn.contentTintColor = Theme.current.overlay2
|
||||||
|
btn.target = self
|
||||||
|
btn.action = #selector(addColumnClicked)
|
||||||
|
btn.alphaValue = 0
|
||||||
|
addSubview(btn)
|
||||||
|
addColumnButton = btn
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func addColumnClicked() {
|
||||||
|
tableBlock?.addColumn()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseEntered(with event: NSEvent) {
|
||||||
|
NSAnimationContext.runAnimationGroup { ctx in
|
||||||
|
ctx.duration = 0.15
|
||||||
|
addColumnButton?.animator().alphaValue = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseExited(with event: NSEvent) {
|
||||||
|
NSCursor.arrow.set()
|
||||||
|
NSAnimationContext.runAnimationGroup { ctx in
|
||||||
|
ctx.duration = 0.15
|
||||||
|
addColumnButton?.animator().alphaValue = 0
|
||||||
|
}
|
||||||
|
dragMode = .none
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseMoved(with event: NSEvent) {
|
||||||
|
let pt = convert(event.locationInWindow, from: nil)
|
||||||
|
if dividerColumn(at: pt) != nil {
|
||||||
|
NSCursor.resizeLeftRight.set()
|
||||||
|
} else {
|
||||||
|
NSCursor.arrow.set()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseDown(with event: NSEvent) {
|
||||||
|
let pt = convert(event.locationInWindow, from: nil)
|
||||||
|
if let col = dividerColumn(at: pt), let tb = tableBlock {
|
||||||
|
dragMode = .column(col)
|
||||||
|
dragStartX = pt.x
|
||||||
|
dragStartWidth = tb.columnWidths[col]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dragMode = .none
|
||||||
|
super.mouseDown(with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseDragged(with event: NSEvent) {
|
||||||
|
let pt = convert(event.locationInWindow, from: nil)
|
||||||
|
switch dragMode {
|
||||||
|
case .column(let col):
|
||||||
|
guard let tb = tableBlock else { return }
|
||||||
|
let delta = pt.x - dragStartX
|
||||||
|
let newWidth = max(tb.minColWidth, min(dragStartWidth + delta, tb.maxColWidth))
|
||||||
|
tb.setColumnWidth(col, to: newWidth)
|
||||||
|
case .none:
|
||||||
|
super.mouseDragged(with: event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseUp(with event: NSEvent) {
|
||||||
|
dragMode = .none
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dividerColumn(at pt: NSPoint) -> Int? {
|
||||||
|
guard let tb = tableBlock else { return nil }
|
||||||
|
let colCount = tb.table.headers.count
|
||||||
|
for i in 1...colCount {
|
||||||
|
let divX = tb.columnX(for: i) - 1
|
||||||
|
if abs(pt.x - divX) <= dividerHitZone { return i - 1 }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - TableBlock
|
// MARK: - TableBlock
|
||||||
|
|
||||||
class TableBlock: NSObject, CompositorBlock, NSTextFieldDelegate {
|
class TableBlock: NSObject, CompositorBlock, NSTextFieldDelegate {
|
||||||
|
|
@ -366,13 +485,15 @@ class TableBlock: NSObject, CompositorBlock, NSTextFieldDelegate {
|
||||||
var sourceRange: NSRange
|
var sourceRange: NSRange
|
||||||
var onTableChanged: ((ParsedTable, NSRange) -> Void)?
|
var onTableChanged: ((ParsedTable, NSRange) -> Void)?
|
||||||
|
|
||||||
private let containerView: NSView
|
private let containerView: TableBlockView
|
||||||
private var cellFields: [[NSTextField]] = []
|
fileprivate var cellFields: [[NSTextField]] = []
|
||||||
private var columnWidths: [CGFloat] = []
|
fileprivate var columnWidths: [CGFloat] = []
|
||||||
|
private var customWidths: [Int: CGFloat] = [:]
|
||||||
private var rowHeights: [CGFloat] = []
|
private var rowHeights: [CGFloat] = []
|
||||||
private var gridWidth: CGFloat = 0
|
|
||||||
|
|
||||||
private let minColWidth: CGFloat = 40
|
let minColWidth: CGFloat = 60
|
||||||
|
let maxColWidth: CGFloat = 300
|
||||||
|
private let cellPadding: CGFloat = 16
|
||||||
private let defaultHeaderHeight: CGFloat = 28
|
private let defaultHeaderHeight: CGFloat = 28
|
||||||
private let defaultCellHeight: CGFloat = 26
|
private let defaultCellHeight: CGFloat = 26
|
||||||
|
|
||||||
|
|
@ -382,49 +503,92 @@ class TableBlock: NSObject, CompositorBlock, NSTextFieldDelegate {
|
||||||
rowHeights.reduce(0, +) + 2
|
rowHeights.reduce(0, +) + 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var totalGridWidth: CGFloat {
|
||||||
|
columnX(for: table.headers.count) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalGridHeight: CGFloat {
|
||||||
|
rowHeights.reduce(0, +)
|
||||||
|
}
|
||||||
|
|
||||||
init(table: ParsedTable, width: CGFloat) {
|
init(table: ParsedTable, width: CGFloat) {
|
||||||
self.table = table
|
self.table = table
|
||||||
self.sourceRange = table.sourceRange
|
self.sourceRange = table.sourceRange
|
||||||
self.gridWidth = width
|
containerView = TableBlockView(frame: .zero)
|
||||||
containerView = FlippedBlockView(frame: .zero)
|
|
||||||
containerView.wantsLayer = true
|
containerView.wantsLayer = true
|
||||||
containerView.layer?.backgroundColor = Theme.current.base.cgColor
|
containerView.layer?.backgroundColor = Theme.current.base.cgColor
|
||||||
containerView.layer?.cornerRadius = 4
|
containerView.layer?.cornerRadius = 4
|
||||||
super.init()
|
super.init()
|
||||||
initSizes(width: width)
|
containerView.tableBlock = self
|
||||||
|
initSizes()
|
||||||
buildGrid()
|
buildGrid()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func initSizes(width: CGFloat) {
|
// MARK: - Sizing
|
||||||
|
|
||||||
|
private func measureColumnWidth(_ col: Int) -> 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 min(max(maxW + cellPadding * 2, minColWidth), maxColWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initSizes() {
|
||||||
let colCount = table.headers.count
|
let colCount = table.headers.count
|
||||||
guard colCount > 0 else { return }
|
guard colCount > 0 else { return }
|
||||||
let available = width - CGFloat(colCount + 1)
|
columnWidths = (0..<colCount).map { col in
|
||||||
let colW = available / CGFloat(colCount)
|
customWidths[col] ?? measureColumnWidth(col)
|
||||||
columnWidths = Array(repeating: max(colW, minColWidth), count: colCount)
|
}
|
||||||
rowHeights = [defaultHeaderHeight]
|
rowHeights = [defaultHeaderHeight]
|
||||||
for _ in 0..<table.rows.count {
|
for _ in 0..<table.rows.count {
|
||||||
rowHeights.append(defaultCellHeight)
|
rowHeights.append(defaultCellHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func columnX(for col: Int) -> CGFloat {
|
func columnX(for col: Int) -> CGFloat {
|
||||||
var x: CGFloat = 1
|
var x: CGFloat = 1
|
||||||
for i in 0..<col { x += columnWidths[i] + 1 }
|
for i in 0..<col { x += columnWidths[i] + 1 }
|
||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
|
||||||
func layoutBlock(width: CGFloat) {
|
func setColumnWidth(_ col: Int, to width: CGFloat) {
|
||||||
if abs(gridWidth - width) > 1 {
|
guard col >= 0, col < columnWidths.count else { return }
|
||||||
gridWidth = width
|
columnWidths[col] = width
|
||||||
initSizes(width: width)
|
customWidths[col] = width
|
||||||
buildGrid()
|
buildGrid()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
func layoutBlock(width: CGFloat) {}
|
||||||
|
|
||||||
func becomeActiveBlock() {}
|
func becomeActiveBlock() {}
|
||||||
|
|
||||||
func resignActiveBlock() {}
|
func resignActiveBlock() {}
|
||||||
|
|
||||||
|
// MARK: - Add column
|
||||||
|
|
||||||
|
func addColumn() {
|
||||||
|
table.headers.append("Header")
|
||||||
|
table.alignments.append(.left)
|
||||||
|
for i in 0..<table.rows.count {
|
||||||
|
table.rows[i].append("")
|
||||||
|
}
|
||||||
|
initSizes()
|
||||||
|
buildGrid()
|
||||||
|
onTableChanged?(table, sourceRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Grid
|
||||||
|
|
||||||
private func buildGrid() {
|
private func buildGrid() {
|
||||||
containerView.subviews.forEach { $0.removeFromSuperview() }
|
containerView.subviews.forEach { $0.removeFromSuperview() }
|
||||||
cellFields = []
|
cellFields = []
|
||||||
|
|
@ -432,10 +596,11 @@ class TableBlock: NSObject, CompositorBlock, NSTextFieldDelegate {
|
||||||
let colCount = table.headers.count
|
let colCount = table.headers.count
|
||||||
guard colCount > 0 else { return }
|
guard colCount > 0 else { return }
|
||||||
let th = rowHeights.reduce(0, +)
|
let th = rowHeights.reduce(0, +)
|
||||||
let totalGridWidth = columnX(for: colCount) + 1
|
let gridW = totalGridWidth
|
||||||
containerView.frame.size = NSSize(width: max(gridWidth, totalGridWidth), height: th + 2)
|
let addBtnSpace: CGFloat = 24
|
||||||
|
containerView.frame.size = NSSize(width: gridW + addBtnSpace, height: th + 2)
|
||||||
|
|
||||||
let headerBg = NSView(frame: NSRect(x: 0, y: 0, width: totalGridWidth, height: rowHeights[0]))
|
let headerBg = NSView(frame: NSRect(x: 0, y: 0, width: gridW, height: rowHeights[0]))
|
||||||
headerBg.wantsLayer = true
|
headerBg.wantsLayer = true
|
||||||
headerBg.layer?.backgroundColor = Theme.current.surface0.cgColor
|
headerBg.layer?.backgroundColor = Theme.current.surface0.cgColor
|
||||||
containerView.addSubview(headerBg)
|
containerView.addSubview(headerBg)
|
||||||
|
|
@ -467,7 +632,6 @@ class TableBlock: NSObject, CompositorBlock, NSTextFieldDelegate {
|
||||||
yOffset += h
|
yOffset += h
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical dividers
|
|
||||||
for i in 1..<colCount {
|
for i in 1..<colCount {
|
||||||
let x = columnX(for: i) - 1
|
let x = columnX(for: i) - 1
|
||||||
let line = NSView(frame: NSRect(x: x, y: 0, width: 1, height: th))
|
let line = NSView(frame: NSRect(x: x, y: 0, width: 1, height: th))
|
||||||
|
|
@ -476,15 +640,17 @@ class TableBlock: NSObject, CompositorBlock, NSTextFieldDelegate {
|
||||||
containerView.addSubview(line)
|
containerView.addSubview(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal dividers
|
|
||||||
var divY: CGFloat = rowHeights[0]
|
var divY: CGFloat = rowHeights[0]
|
||||||
for i in 1..<rowHeights.count {
|
for i in 1..<rowHeights.count {
|
||||||
let line = NSView(frame: NSRect(x: 0, y: divY, width: totalGridWidth, height: 1))
|
let line = NSView(frame: NSRect(x: 0, y: divY, width: gridW, height: 1))
|
||||||
line.wantsLayer = true
|
line.wantsLayer = true
|
||||||
line.layer?.backgroundColor = Theme.current.surface2.cgColor
|
line.layer?.backgroundColor = Theme.current.surface2.cgColor
|
||||||
containerView.addSubview(line)
|
containerView.addSubview(line)
|
||||||
divY += rowHeights[i]
|
divY += rowHeights[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
containerView.setupTracking()
|
||||||
|
containerView.updateAddButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeCell(text: String, frame: NSRect, isHeader: Bool, row: Int, col: Int) -> NSTextField {
|
private func makeCell(text: String, frame: NSRect, isHeader: Bool, row: Int, col: Int) -> NSTextField {
|
||||||
|
|
@ -534,11 +700,28 @@ class TableBlock: NSObject, CompositorBlock, NSTextFieldDelegate {
|
||||||
|
|
||||||
if let movement = obj.userInfo?["NSTextMovement"] as? Int, movement == NSTabTextMovement {
|
if let movement = obj.userInfo?["NSTextMovement"] as? Int, movement == NSTabTextMovement {
|
||||||
let nextCol = col + 1
|
let nextCol = col + 1
|
||||||
let nextRow = row + (nextCol >= table.headers.count ? 1 : 0)
|
if nextCol >= table.headers.count {
|
||||||
let actualCol = nextCol % table.headers.count
|
let isLastRow = (row == table.rows.count - 1) || (row == -1 && table.rows.isEmpty)
|
||||||
let fieldRow = nextRow + 1
|
if isLastRow {
|
||||||
if fieldRow < cellFields.count, actualCol < cellFields[fieldRow].count {
|
addColumn()
|
||||||
containerView.window?.makeFirstResponder(cellFields[fieldRow][actualCol])
|
let fieldRow = row + 1
|
||||||
|
if fieldRow < cellFields.count {
|
||||||
|
let newCol = table.headers.count - 1
|
||||||
|
if newCol < cellFields[fieldRow].count {
|
||||||
|
containerView.window?.makeFirstResponder(cellFields[fieldRow][newCol])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let fieldRow = row + 2
|
||||||
|
if fieldRow < cellFields.count, !cellFields[fieldRow].isEmpty {
|
||||||
|
containerView.window?.makeFirstResponder(cellFields[fieldRow][0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let fieldRow = row + 1
|
||||||
|
if fieldRow < cellFields.count, nextCol < cellFields[fieldRow].count {
|
||||||
|
containerView.window?.makeFirstResponder(cellFields[fieldRow][nextCol])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1106,8 +1289,7 @@ struct CompositorRepresentable: NSViewRepresentable {
|
||||||
switch block.kind {
|
switch block.kind {
|
||||||
case .tableBlock:
|
case .tableBlock:
|
||||||
guard let parsed = parseMarkdownTable(from: text, range: block.range) else { continue }
|
guard let parsed = parseMarkdownTable(from: text, range: block.range) else { continue }
|
||||||
let tableWidth = tc.containerSize.width - 8
|
let tb = TableBlock(table: parsed, width: 0)
|
||||||
let tb = TableBlock(table: parsed, width: tableWidth)
|
|
||||||
tb.onTableChanged = { [weak self] updatedTable, range in
|
tb.onTableChanged = { [weak self] updatedTable, range in
|
||||||
self?.applyTableEdit(updatedTable, sourceRange: range)
|
self?.applyTableEdit(updatedTable, sourceRange: range)
|
||||||
}
|
}
|
||||||
|
|
@ -1164,7 +1346,6 @@ struct CompositorRepresentable: NSViewRepresentable {
|
||||||
for block in lm.blockRanges {
|
for block in lm.blockRanges {
|
||||||
let glyphRange = lm.glyphRange(forCharacterRange: block.range, actualCharacterRange: nil)
|
let glyphRange = lm.glyphRange(forCharacterRange: block.range, actualCharacterRange: nil)
|
||||||
let rect = lm.boundingRect(forGlyphRange: glyphRange, in: tc)
|
let rect = lm.boundingRect(forGlyphRange: glyphRange, in: tc)
|
||||||
let w = tc.containerSize.width - 8
|
|
||||||
let x = origin.x + 4
|
let x = origin.x + 4
|
||||||
|
|
||||||
switch block.kind {
|
switch block.kind {
|
||||||
|
|
@ -1172,14 +1353,15 @@ struct CompositorRepresentable: NSViewRepresentable {
|
||||||
guard tableIdx < tableBlocks.count else { continue }
|
guard tableIdx < tableBlocks.count else { continue }
|
||||||
let tb = tableBlocks[tableIdx]
|
let tb = tableBlocks[tableIdx]
|
||||||
tableIdx += 1
|
tableIdx += 1
|
||||||
tb.layoutBlock(width: w)
|
let tableW = tb.view.frame.width
|
||||||
tb.view.frame = NSRect(x: x, y: rect.origin.y + origin.y, width: w, height: tb.blockHeight)
|
tb.view.frame = NSRect(x: x, y: rect.origin.y + origin.y, width: tableW, height: tb.blockHeight)
|
||||||
tv.addSubview(tb.view)
|
tv.addSubview(tb.view)
|
||||||
|
|
||||||
case .horizontalRule:
|
case .horizontalRule:
|
||||||
guard hrIdx < hrBlocks.count else { continue }
|
guard hrIdx < hrBlocks.count else { continue }
|
||||||
let hb = hrBlocks[hrIdx]
|
let hb = hrBlocks[hrIdx]
|
||||||
hrIdx += 1
|
hrIdx += 1
|
||||||
|
let w = tc.containerSize.width - 8
|
||||||
hb.view.frame = NSRect(x: x, y: rect.origin.y + origin.y, width: w, height: hb.blockHeight)
|
hb.view.frame = NSRect(x: x, y: rect.origin.y + origin.y, width: w, height: hb.blockHeight)
|
||||||
tv.addSubview(hb.view)
|
tv.addSubview(hb.view)
|
||||||
|
|
||||||
|
|
@ -1199,6 +1381,32 @@ struct CompositorRepresentable: NSViewRepresentable {
|
||||||
ts.beginEditing()
|
ts.beginEditing()
|
||||||
ts.replaceCharacters(in: sourceRange, with: newMarkdown)
|
ts.replaceCharacters(in: sourceRange, with: newMarkdown)
|
||||||
applySyntaxHighlighting(to: ts, format: parent.fileFormat)
|
applySyntaxHighlighting(to: ts, format: parent.fileFormat)
|
||||||
|
|
||||||
|
// Collapse new source text and set paragraph spacing for updated table height
|
||||||
|
let newRange = NSRange(location: sourceRange.location, length: (newMarkdown as NSString).length)
|
||||||
|
let blockHeight: CGFloat = 28 + CGFloat(table.rows.count) * 26 + 2
|
||||||
|
let tinyFont = NSFont.systemFont(ofSize: 0.1)
|
||||||
|
ts.addAttribute(.font, value: tinyFont, range: newRange)
|
||||||
|
ts.addAttribute(.foregroundColor, value: NSColor.clear, range: newRange)
|
||||||
|
|
||||||
|
let collapsePara = NSMutableParagraphStyle()
|
||||||
|
collapsePara.minimumLineHeight = 0.1
|
||||||
|
collapsePara.maximumLineHeight = 0.1
|
||||||
|
collapsePara.lineSpacing = 0
|
||||||
|
collapsePara.paragraphSpacing = 0
|
||||||
|
collapsePara.paragraphSpacingBefore = 0
|
||||||
|
ts.addAttribute(.paragraphStyle, value: collapsePara, range: newRange)
|
||||||
|
|
||||||
|
let text = ts.string as NSString
|
||||||
|
let lastLineRange = text.lineRange(for: NSRange(location: max(0, NSMaxRange(newRange) - 1), length: 0))
|
||||||
|
let lastPara = NSMutableParagraphStyle()
|
||||||
|
lastPara.minimumLineHeight = 0.1
|
||||||
|
lastPara.maximumLineHeight = 0.1
|
||||||
|
lastPara.lineSpacing = 0
|
||||||
|
lastPara.paragraphSpacing = blockHeight
|
||||||
|
lastPara.paragraphSpacingBefore = 0
|
||||||
|
ts.addAttribute(.paragraphStyle, value: lastPara, range: lastLineRange)
|
||||||
|
|
||||||
ts.endEditing()
|
ts.endEditing()
|
||||||
(tv as? LineNumberTextView)?.applyEvalSpacing()
|
(tv as? LineNumberTextView)?.applyEvalSpacing()
|
||||||
tv.selectedRanges = sel
|
tv.selectedRanges = sel
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue