render eval trees and tables below the eval line with left margin

This commit is contained in:
jess 2026-04-06 17:07:49 -07:00
parent 1ccea45a6f
commit eefca4f05e
1 changed files with 93 additions and 13 deletions

View File

@ -1218,6 +1218,7 @@ struct EditorTextView: NSViewRepresentable {
applySyntaxHighlighting(to: ts, format: fileFormat)
ts.endEditing()
}
textView.applyEvalSpacing()
textView.typingAttributes = [
.font: Theme.editorFont,
.foregroundColor: Theme.current.text
@ -1295,6 +1296,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(
@ -1333,6 +1335,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
@ -1436,6 +1439,7 @@ struct EditorTextView: NSViewRepresentable {
applySyntaxHighlighting(to: ts, format: format)
ts.endEditing()
}
(tv as? LineNumberTextView)?.applyEvalSpacing()
tv.selectedRanges = sel
tv.needsDisplay = true
}
@ -1621,6 +1625,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)
@ -2753,7 +2758,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 +2866,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 +2884,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 +2931,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 +2964,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 +3018,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 +3039,79 @@ class LineNumberTextView: NSTextView {
}
}
// MARK: - Eval Spacing
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?) {