From fef2935ae5798a251d7b6d7bdf46a1b5264f1594 Mon Sep 17 00:00:00 2001 From: jess Date: Mon, 6 Apr 2026 17:02:24 -0700 Subject: [PATCH 1/2] fix table rendering: preserve layout size, skip raw glyphs, draw background --- src/EditorView.swift | 122 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 106 insertions(+), 16 deletions(-) diff --git a/src/EditorView.swift b/src/EditorView.swift index 200ff5d..00314f6 100644 --- a/src/EditorView.swift +++ b/src/EditorView.swift @@ -36,8 +36,10 @@ class MarkdownLayoutManager: NSLayoutManager { drawBlockquoteBorder(glyphRange: glyphRange, origin: origin, container: textContainer) case .horizontalRule: drawHorizontalRule(glyphRange: glyphRange, origin: origin, container: textContainer) - case .checkbox, .tableBlock: + case .checkbox: break + case .tableBlock: + drawTableBackground(glyphRange: glyphRange, origin: origin, container: textContainer) } } } @@ -59,6 +61,8 @@ class MarkdownLayoutManager: NSLayoutManager { drawCheckbox(checked: checked, glyphRange: glyphRange, origin: origin, container: textContainer) case .horizontalRule: skipRanges.append(glyphRange) + case .tableBlock: + skipRanges.append(glyphRange) default: break } @@ -152,6 +156,17 @@ class MarkdownLayoutManager: NSLayoutManager { } } + private func drawTableBackground(glyphRange: NSRange, origin: NSPoint, container: NSTextContainer) { + var rect = boundingRect(forGlyphRange: glyphRange, in: container) + rect.origin.x = origin.x + 4 + rect.origin.y += origin.y + rect.size.width = container.containerSize.width - 8 + + let path = NSBezierPath(roundedRect: rect, xRadius: 4, yRadius: 4) + Theme.current.base.setFill() + path.fill() + } + private func drawTableBorders(glyphRange: NSRange, columns: Int, origin: NSPoint, container: NSTextContainer) { guard columns > 0 else { return } var rect = boundingRect(forGlyphRange: glyphRange, in: container) @@ -2434,8 +2449,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) { - textStorage.addAttribute(.foregroundColor, value: palette.base, range: lineRange) - textStorage.addAttribute(.font, value: NSFont.systemFont(ofSize: 0.01), range: lineRange) + textStorage.addAttribute(.foregroundColor, value: NSColor.clear, range: lineRange) + textStorage.addAttribute(.font, value: baseFont, range: lineRange) } // MARK: - Lists and Horizontal Rules @@ -2753,7 +2768,11 @@ private func resolveLocalImagePath(_ rawPath: String) -> String? { class LineNumberTextView: NSTextView { static let gutterWidth: CGFloat = 50 - var evalResults: [Int: EvalEntry] = [:] + static let evalLeftMargin: CGFloat = 80 + + var evalResults: [Int: EvalEntry] = [:] { + didSet { applyEvalSpacing() } + } override var textContainerOrigin: NSPoint { return NSPoint(x: LineNumberTextView.gutterWidth, y: textContainerInset.height) @@ -2857,9 +2876,9 @@ class LineNumberTextView: NSTextView { if let entry = evalResults[lineNumber - 1] { switch entry.format { case .table: - drawTableResult(entry.result, at: y, origin: origin, resultAttrs: resultAttrs) + drawTableResult(entry.result, lineRect: lineRect, origin: origin, resultAttrs: resultAttrs) case .tree: - drawTreeResult(entry.result, at: y, origin: origin, resultAttrs: resultAttrs) + drawTreeResult(entry.result, lineRect: lineRect, origin: origin, resultAttrs: resultAttrs) case .inline: let resultStr = NSAttributedString(string: "\u{2192} \(entry.result)", attributes: resultAttrs) let size = resultStr.size() @@ -2875,13 +2894,13 @@ class LineNumberTextView: NSTextView { // MARK: - Table/Tree Rendering - private func drawTableResult(_ json: String, at y: CGFloat, origin: NSPoint, resultAttrs: [NSAttributedString.Key: Any]) { + private func drawTableResult(_ json: String, lineRect: NSRect, origin: NSPoint, resultAttrs: [NSAttributedString.Key: Any]) { guard let data = json.data(using: .utf8), let parsed = try? JSONSerialization.jsonObject(with: data), let rows = parsed as? [[Any]] else { let fallback = NSAttributedString(string: "\u{2192} \(json.prefix(40))", attributes: resultAttrs) let size = fallback.size() - fallback.draw(at: NSPoint(x: visibleRect.maxX - size.width - 8, y: y)) + fallback.draw(at: NSPoint(x: visibleRect.maxX - size.width - 8, y: lineRect.origin.y + origin.y)) return } @@ -2922,9 +2941,8 @@ class LineNumberTextView: NSTextView { let tableWidth = colWidths.reduce(0, +) + cellPad * CGFloat(colCount + 1) + CGFloat(colCount - 1) let tableHeight = rowHeight * CGFloat(stringRows.count) + CGFloat(stringRows.count + 1) - let rightEdge = visibleRect.maxX - let tableX = rightEdge - tableWidth - 12 - let tableY = y + let tableX = LineNumberTextView.evalLeftMargin + let tableY = lineRect.origin.y + origin.y + lineRect.height + 4 let tableRect = NSRect(x: tableX, y: tableY, width: tableWidth, height: tableHeight) palette.mantle.setFill() @@ -2956,12 +2974,12 @@ class LineNumberTextView: NSTextView { } } - private func drawTreeResult(_ json: String, at y: CGFloat, origin: NSPoint, resultAttrs: [NSAttributedString.Key: Any]) { + private func drawTreeResult(_ json: String, lineRect: NSRect, origin: NSPoint, resultAttrs: [NSAttributedString.Key: Any]) { guard let data = json.data(using: .utf8), let root = try? JSONSerialization.jsonObject(with: data) else { let fallback = NSAttributedString(string: "\u{2192} \(json.prefix(40))", attributes: resultAttrs) let size = fallback.size() - fallback.draw(at: NSPoint(x: visibleRect.maxX - size.width - 8, y: y)) + fallback.draw(at: NSPoint(x: visibleRect.maxX - size.width - 8, y: lineRect.origin.y + origin.y)) return } @@ -3010,9 +3028,8 @@ class LineNumberTextView: NSTextView { let treeHeight = lineHeight * CGFloat(lines.count) + 4 let treeWidth = maxWidth + 16 - let rightEdge = visibleRect.maxX - let treeX = rightEdge - treeWidth - 8 - let treeY = y + let treeX = LineNumberTextView.evalLeftMargin + let treeY = lineRect.origin.y + origin.y + lineRect.height + 4 let treeRect = NSRect(x: treeX, y: treeY, width: treeWidth, height: treeHeight) palette.mantle.setFill() @@ -3032,6 +3049,79 @@ class LineNumberTextView: NSTextView { } } + // MARK: - Eval Spacing + + private func applyEvalSpacing() { + guard let ts = textStorage else { return } + let text = ts.string as NSString + guard text.length > 0 else { return } + + ts.beginEditing() + + var lineStart = 0 + var lineNum = 0 + while lineStart < text.length { + let lineRange = text.lineRange(for: NSRange(location: lineStart, length: 0)) + if let entry = evalResults[lineNum] { + let spacing: CGFloat + switch entry.format { + case .tree: + spacing = evalTreeHeight(entry.result) + 8 + case .table: + spacing = evalTableHeight(entry.result) + 8 + case .inline: + spacing = 0 + } + if spacing > 0 { + let para = NSMutableParagraphStyle() + if let existing = ts.attribute(.paragraphStyle, at: lineRange.location, effectiveRange: nil) as? NSParagraphStyle { + para.setParagraphStyle(existing) + } + para.paragraphSpacing = spacing + ts.addAttribute(.paragraphStyle, value: para, range: lineRange) + } + } + lineNum += 1 + lineStart = NSMaxRange(lineRange) + } + + ts.endEditing() + } + + private func evalTreeHeight(_ json: String) -> CGFloat { + guard let data = json.data(using: .utf8), + let root = try? JSONSerialization.jsonObject(with: data) else { return 0 } + let font = Theme.gutterFont + let lineHeight = font.pointSize + 4 + var count = 0 + func walk(_ node: Any) { + if let arr = node as? [Any] { + for item in arr { + count += 1 + if item is [Any] { walk(item) } + } + } else { + count += 1 + } + } + if root is [Any] { + count = 1 + walk(root) + } else { + count = 1 + } + return lineHeight * CGFloat(count) + 4 + } + + private func evalTableHeight(_ json: String) -> CGFloat { + guard let data = json.data(using: .utf8), + let parsed = try? JSONSerialization.jsonObject(with: data), + let rows = parsed as? [[Any]] else { return 0 } + let font = Theme.gutterFont + let rowHeight = font.pointSize + 6 + return rowHeight * CGFloat(rows.count) + CGFloat(rows.count + 1) + } + // MARK: - Paste override func paste(_ sender: Any?) { From c326903e849c8e5751ca48d7bb8be8bf04c32509 Mon Sep 17 00:00:00 2001 From: jess Date: Mon, 6 Apr 2026 17:10:58 -0700 Subject: [PATCH 2/2] reapply eval spacing after syntax highlighting in all code paths --- src/EditorView.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/EditorView.swift b/src/EditorView.swift index 00314f6..f03a91c 100644 --- a/src/EditorView.swift +++ b/src/EditorView.swift @@ -1233,6 +1233,7 @@ struct EditorTextView: NSViewRepresentable { applySyntaxHighlighting(to: ts, format: fileFormat) ts.endEditing() } + textView.applyEvalSpacing() textView.typingAttributes = [ .font: Theme.editorFont, .foregroundColor: Theme.current.text @@ -1310,6 +1311,7 @@ struct EditorTextView: NSViewRepresentable { ts.beginEditing() applySyntaxHighlighting(to: ts, format: parent.fileFormat) ts.endEditing() + (tv as? LineNumberTextView)?.applyEvalSpacing() tv.needsDisplay = true }) observers.append(NotificationCenter.default.addObserver( @@ -1348,6 +1350,7 @@ struct EditorTextView: NSViewRepresentable { ts.beginEditing() applySyntaxHighlighting(to: ts, format: parent.fileFormat) ts.endEditing() + (tv as? LineNumberTextView)?.applyEvalSpacing() tv.typingAttributes = [ .font: Theme.editorFont, .foregroundColor: Theme.current.text @@ -1451,6 +1454,7 @@ struct EditorTextView: NSViewRepresentable { applySyntaxHighlighting(to: ts, format: format) ts.endEditing() } + (tv as? LineNumberTextView)?.applyEvalSpacing() tv.selectedRanges = sel tv.needsDisplay = true } @@ -1636,6 +1640,7 @@ struct EditorTextView: NSViewRepresentable { ts.replaceCharacters(in: range, with: newMarkdown) applySyntaxHighlighting(to: ts, format: parent.fileFormat) ts.endEditing() + (tv as? LineNumberTextView)?.applyEvalSpacing() tv.selectedRanges = sel parent.text = tv.string updateBlockRanges(for: tv) @@ -3051,7 +3056,7 @@ class LineNumberTextView: NSTextView { // MARK: - Eval Spacing - private func applyEvalSpacing() { + func applyEvalSpacing() { guard let ts = textStorage else { return } let text = ts.string as NSString guard text.length > 0 else { return }