diff --git a/src/EditorView.swift b/src/EditorView.swift index 200ff5d..755584d 100644 --- a/src/EditorView.swift +++ b/src/EditorView.swift @@ -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?) {