diff --git a/src/EditorView.swift b/src/EditorView.swift index c3e1123..4dc46b0 100644 --- a/src/EditorView.swift +++ b/src/EditorView.swift @@ -537,7 +537,6 @@ struct EditorView: View { EditorTextView(text: $state.documentText, evalResults: state.evalResults, onEvaluate: { state.evaluate() }) - .background(Color(ns: Theme.current.base)) } } @@ -551,20 +550,20 @@ struct EditorTextView: NSViewRepresentable { } func makeNSView(context: Context) -> NSScrollView { - let scrollView = NSScrollView() - scrollView.hasVerticalScroller = true - scrollView.hasHorizontalScroller = false - scrollView.autohidesScrollers = true - scrollView.borderType = .noBorder + let scrollView = NSTextView.scrollableTextView() + let defaultTV = scrollView.documentView as! NSTextView - let textStorage = NSTextStorage() - let layoutManager = MarkdownLayoutManager() - let textContainer = NSTextContainer(containerSize: NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude)) - textContainer.widthTracksTextView = true - layoutManager.addTextContainer(textContainer) - textStorage.addLayoutManager(layoutManager) + // Build LineNumberTextView reusing the default text container + let tc = defaultTV.textContainer! + tc.replaceLayoutManager(MarkdownLayoutManager()) + + let textView = LineNumberTextView(frame: defaultTV.frame, textContainer: tc) + textView.minSize = defaultTV.minSize + textView.maxSize = defaultTV.maxSize + textView.isVerticallyResizable = defaultTV.isVerticallyResizable + textView.isHorizontallyResizable = defaultTV.isHorizontallyResizable + textView.autoresizingMask = defaultTV.autoresizingMask - let textView = LineNumberTextView(frame: .zero, textContainer: textContainer) textView.isEditable = true textView.isSelectable = true textView.allowsUndo = true @@ -585,33 +584,16 @@ struct EditorTextView: NSViewRepresentable { textView.smartInsertDeleteEnabled = false textView.isAutomaticLinkDetectionEnabled = false - textView.autoresizingMask = [.width] - textView.isVerticallyResizable = true - textView.isHorizontallyResizable = false - textView.textContainer?.containerSize = NSSize( - width: scrollView.contentSize.width, - height: CGFloat.greatestFiniteMagnitude - ) + textView.textContainerInset = NSSize(width: 4, height: 8) textView.textContainer?.widthTracksTextView = true - textView.maxSize = NSSize( - width: CGFloat.greatestFiniteMagnitude, - height: CGFloat.greatestFiniteMagnitude - ) - textView.registerForDraggedTypes([.fileURL]) scrollView.documentView = textView - let ruler = LineNumberRulerView(textView: textView) - ruler.evalResults = evalResults - scrollView.verticalRulerView = ruler - scrollView.hasVerticalRuler = true - scrollView.rulersVisible = true - textView.string = text + textView.evalResults = evalResults textView.delegate = context.coordinator context.coordinator.textView = textView - context.coordinator.rulerView = ruler if let ts = textView.textStorage { ts.beginEditing() @@ -652,16 +634,13 @@ struct EditorTextView: NSViewRepresentable { .foregroundColor: Theme.current.text ] - if let ruler = scrollView.verticalRulerView as? LineNumberRulerView { - ruler.evalResults = evalResults - ruler.needsDisplay = true - } + textView.evalResults = evalResults + textView.needsDisplay = true } class Coordinator: NSObject, NSTextViewDelegate { var parent: EditorTextView weak var textView: NSTextView? - weak var rulerView: LineNumberRulerView? private var isUpdatingImages = false private var isUpdatingTables = false private var embeddedTableViews: [MarkdownTableView] = [] @@ -684,7 +663,7 @@ struct EditorTextView: NSViewRepresentable { ] tv.selectedRanges = sel updateBlockRanges(for: tv) - rulerView?.needsDisplay = true + tv.needsDisplay = true DispatchQueue.main.async { [weak self] in self?.updateInlineImages() @@ -1042,6 +1021,7 @@ private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: N if contentRange.length > 0 { let h2Font = NSFont.systemFont(ofSize: 18, weight: .bold) textStorage.addAttribute(.font, value: h2Font, range: contentRange) + textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange) } } return true @@ -1056,6 +1036,7 @@ private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: N if contentRange.length > 0 { let h1Font = NSFont.systemFont(ofSize: 22, weight: .bold) textStorage.addAttribute(.font, value: h1Font, range: contentRange) + textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange) } } return true @@ -1167,6 +1148,7 @@ private func highlightInlineMarkdown(textStorage: NSTextStorage, lineRange: NSRa textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: closeMarker) if contentRange.length > 0 { textStorage.addAttribute(.font, value: font, range: contentRange) + textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange) } } } @@ -1311,6 +1293,7 @@ private func highlightFencedCodeBlocks(textStorage: NSTextStorage, palette: Catp openFence = nil } else { textStorage.addAttribute(.font, value: monoFont, range: lineRange) + textStorage.addAttribute(.foregroundColor, value: palette.text, range: lineRange) } } @@ -1344,6 +1327,7 @@ private func highlightInlineCode(textStorage: NSTextStorage, lineRange: NSRange, textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: closeTick) if contentRange.length > 0 { textStorage.addAttribute(.font, value: monoFont, range: contentRange) + textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange) textStorage.addAttribute(.backgroundColor, value: palette.surface0, range: contentRange) } } @@ -1398,6 +1382,7 @@ private func highlightTableLine(_ trimmed: String, lineRange: NSRange, textStora 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) @@ -1708,12 +1693,80 @@ private func resolveLocalImagePath(_ rawPath: String) -> String? { // MARK: - LineNumberTextView class LineNumberTextView: NSTextView { + static let gutterWidth: CGFloat = 50 + var evalResults: [Int: String] = [:] + + override var textContainerOrigin: NSPoint { + return NSPoint(x: LineNumberTextView.gutterWidth, y: textContainerInset.height) + } + override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) { var widened = rect widened.size.width = 2 super.drawInsertionPoint(in: widened, color: color, turnedOn: flag) } + override func drawBackground(in rect: NSRect) { + super.drawBackground(in: rect) + + let origin = textContainerOrigin + let gutterRect = NSRect(x: 0, y: rect.origin.y, width: LineNumberTextView.gutterWidth, height: rect.height) + Theme.current.mantle.setFill() + gutterRect.fill() + + drawLineNumbers(origin: origin) + } + + private func drawLineNumbers(origin: NSPoint) { + guard let lm = layoutManager, let tc = textContainer else { return } + let palette = Theme.current + let text = string as NSString + guard text.length > 0 else { return } + + var containerVisible = visibleRect + containerVisible.origin.x -= origin.x + containerVisible.origin.y -= origin.y + let visibleGlyphs = lm.glyphRange(forBoundingRect: containerVisible, in: tc) + let visibleChars = lm.characterRange(forGlyphRange: visibleGlyphs, actualGlyphRange: nil) + + var lineNumber = 1 + var idx = 0 + while idx < visibleChars.location { + if text.character(at: idx) == 0x0A { lineNumber += 1 } + idx += 1 + } + + let lineAttrs: [NSAttributedString.Key: Any] = [ + .font: Theme.gutterFont, + .foregroundColor: palette.overlay0 + ] + let resultAttrs: [NSAttributedString.Key: Any] = [ + .font: Theme.gutterFont, + .foregroundColor: palette.teal + ] + + var charIndex = visibleChars.location + while charIndex < NSMaxRange(visibleChars) { + let lineRange = text.lineRange(for: NSRange(location: charIndex, length: 0)) + let lineGlyphRange = lm.glyphRange(forCharacterRange: lineRange, actualCharacterRange: nil) + let lineRect = lm.boundingRect(forGlyphRange: lineGlyphRange, in: tc) + let y = lineRect.origin.y + origin.y + + if let result = evalResults[lineNumber - 1] { + let resultStr = NSAttributedString(string: "\u{2192} \(result)", attributes: resultAttrs) + let size = resultStr.size() + resultStr.draw(at: NSPoint(x: LineNumberTextView.gutterWidth - size.width - 4, y: y)) + } else { + let numStr = NSAttributedString(string: "\(lineNumber)", attributes: lineAttrs) + let size = numStr.size() + numStr.draw(at: NSPoint(x: LineNumberTextView.gutterWidth - size.width - 8, y: y)) + } + + lineNumber += 1 + charIndex = NSMaxRange(lineRange) + } + } + // MARK: - Paste (image from clipboard) override func paste(_ sender: Any?) { @@ -1790,95 +1843,3 @@ class LineNumberTextView: NSTextView { } } -class LineNumberRulerView: NSRulerView { - var evalResults: [Int: String] = [:] - - private weak var editorTextView: NSTextView? - - init(textView: NSTextView) { - self.editorTextView = textView - super.init(scrollView: textView.enclosingScrollView!, orientation: .verticalRuler) - self.clientView = textView - self.ruleThickness = 50 - - NotificationCenter.default.addObserver( - self, - selector: #selector(textDidChange(_:)), - name: NSText.didChangeNotification, - object: textView - ) - } - - required init(coder: NSCoder) { - fatalError() - } - - @objc private func textDidChange(_ notification: Notification) { - needsDisplay = true - } - - override func drawHashMarksAndLabels(in rect: NSRect) { - guard let tv = editorTextView, - let layoutManager = tv.layoutManager, - let textContainer = tv.textContainer else { return } - - let palette = Theme.current - - palette.mantle.setFill() - rect.fill() - - let visibleRect = scrollView!.contentView.bounds - let visibleGlyphRange = layoutManager.glyphRange( - forBoundingRect: visibleRect, in: textContainer - ) - let visibleCharRange = layoutManager.characterRange( - forGlyphRange: visibleGlyphRange, actualGlyphRange: nil - ) - - let text = tv.string as NSString - var lineNumber = 1 - var index = 0 - while index < visibleCharRange.location { - if text.character(at: index) == 0x0A { lineNumber += 1 } - index += 1 - } - - let attrs: [NSAttributedString.Key: Any] = [ - .font: Theme.gutterFont, - .foregroundColor: palette.overlay0 - ] - let resultAttrs: [NSAttributedString.Key: Any] = [ - .font: Theme.gutterFont, - .foregroundColor: palette.teal - ] - - var charIndex = visibleCharRange.location - while charIndex < NSMaxRange(visibleCharRange) { - let lineRange = text.lineRange(for: NSRange(location: charIndex, length: 0)) - let glyphRange = layoutManager.glyphRange(forCharacterRange: lineRange, actualCharacterRange: nil) - var lineRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) - lineRect.origin.y += tv.textContainerInset.height - visibleRect.origin.y - - if let result = evalResults[lineNumber - 1] { - let resultStr = NSAttributedString(string: "→ \(result)", attributes: resultAttrs) - let resultSize = resultStr.size() - let resultPoint = NSPoint( - x: ruleThickness - resultSize.width - 4, - y: lineRect.origin.y - ) - resultStr.draw(at: resultPoint) - } else { - let numStr = NSAttributedString(string: "\(lineNumber)", attributes: attrs) - let size = numStr.size() - let point = NSPoint( - x: ruleThickness - size.width - 8, - y: lineRect.origin.y - ) - numStr.draw(at: point) - } - - lineNumber += 1 - charIndex = NSMaxRange(lineRange) - } - } -}