| @@ -534,6 +534,7 @@ public final class OrgEditor: NSObject { |
| 534 | 534 | } |
| 535 | 535 | } |
| 536 | 536 | let revealed = caretLine |
| 537 | var scriptExclusions: [Range<Int>] = [] |
| 537 | 538 | for run in Presentation.styles(document.tree, in: range, highlighter: highlighter) { |
| 538 | 539 | guard let clipped = clip(run.range, to: range) else { continue } |
| 539 | 540 | if !showMarkup, hides(run.role), NSIntersectionRange(clipped, revealed).length == 0 { |
| @@ -541,6 +542,48 @@ public final class OrgEditor: NSObject { |
| 541 | 542 | } else { |
| 542 | 543 | theme.apply(run.role, to: storage, range: clipped) |
| 543 | 544 | } |
| 545 | switch run.role { |
| 546 | case .bold, .italic, .underline, .strikeThrough, .verbatim, .code, .link, .linkMarkup, .keyword: scriptExclusions.append(run.range) |
| 547 | default: break |
| 548 | } |
| 549 | } |
| 550 | if !showMarkup, options.prettyEntities { prettify(range, in: storage, revealed: revealed, scriptExclusions: scriptExclusions) } |
| 551 | } |
| 552 | |
| 553 | /// `org-pretty-entities`: entities drawn as their character over the entity's last |
| 554 | /// character (the rest hidden), sub/superscripts raised without their markers. Not on the |
| 555 | /// caret's line, in comments or in blocks; sub/superscripts not in emphasis, links or |
| 556 | /// keywords, as `org-raise-scripts` leaves them. |
| 557 | private func prettify(_ range: Range<Int>, in storage: NSTextStorage, revealed: NSRange, scriptExclusions: [Range<Int>]) { |
| 558 | let text = storage.string as NSString |
| 559 | var location = range.lowerBound |
| 560 | while location < range.upperBound { |
| 561 | let line = text.lineRange(for: NSRange(location: location, length: 0)) |
| 562 | guard line.length > 0 else { break } |
| 563 | location = NSMaxRange(line) |
| 564 | if line.location == revealed.location || shaded.containing(line.location) != nil { continue } |
| 565 | let content = text.substring(with: line) |
| 566 | if content.range(of: "^[ \\t]*#([ +]|$)", options: .regularExpression) != nil { continue } |
| 567 | for run in PrettyEntities.runs(in: content, subSuperscriptsNeedBraces: options.subSuperscriptsNeedBraces) { |
| 568 | switch run { |
| 569 | case .entity(let r, let display): |
| 570 | let start = line.location + r.lowerBound |
| 571 | let last = line.location + r.upperBound - 1 |
| 572 | if last > start { storage.addAttributes(theme.hidden, range: NSRange(location: start, length: last - start)) } |
| 573 | storage.addAttributes([.foregroundColor: NSColor.clear, .orgEntityDisplay: display], range: NSRange(location: last, length: 1)) |
| 574 | case .script(let markers, let body, let superscript): |
| 575 | let bodyStart = line.location + body.lowerBound |
| 576 | if scriptExclusions.contains(where: { $0.contains(bodyStart) }) || body.isEmpty { continue } |
| 577 | for marker in markers { |
| 578 | storage.addAttributes(theme.hidden, range: NSRange(location: line.location + marker.lowerBound, length: marker.count)) |
| 579 | } |
| 580 | let size = theme.font.pointSize |
| 581 | storage.addAttributes([ |
| 582 | .font: NSFont.monospacedSystemFont(ofSize: size * 0.7, weight: .regular), |
| 583 | .baselineOffset: (superscript ? 0.3 : -0.3) * size, |
| 584 | ], range: NSRange(location: bodyStart, length: body.count)) |
| 585 | } |
| 586 | } |
| 544 | 587 | } |
| 545 | 588 | } |
| 546 | 589 | |
| @@ -819,6 +862,9 @@ extension OrgEditor: NSTextLayoutManagerDelegate { |
| 819 | 862 | let offset = manager.offset(from: manager.documentRange.location, to: range.location) |
| 820 | 863 | if folded.contains(offset) { return FoldedHeadingFragment(textElement: textElement, range: range) } |
| 821 | 864 | if shaded.containing(offset) != nil { return ShadedFragment(textElement: textElement, range: range) } |
| 865 | if let paragraph = textElement as? NSTextParagraph, EntityFragment.hasEntities(paragraph.attributedString) { |
| 866 | return EntityFragment(textElement: textElement, range: range) |
| 867 | } |
| 822 | 868 | } |
| 823 | 869 | return NSTextLayoutFragment(textElement: textElement, range: range) |
| 824 | 870 | } |
| @@ -829,7 +875,9 @@ extension OrgEditor: NSTextLayoutManagerDelegate { |
| 829 | 875 | final class ShadedFragment: NSTextLayoutFragment, @unchecked Sendable { |
| 830 | 876 | private var band: CGRect { |
| 831 | 877 | let width = textLayoutManager?.textContainer?.size.width ?? layoutFragmentFrame.width |
| 832 | | return CGRect(x: -layoutFragmentFrame.minX, y: 0, width: width, height: layoutFragmentFrame.height) |
| 878 | // Not under the empty line TextKit adds after a final newline. |
| 879 | let height = textLineFragments.last { $0.characterRange.length > 0 }?.typographicBounds.maxY ?? layoutFragmentFrame.height |
| 880 | return CGRect(x: -layoutFragmentFrame.minX, y: 0, width: width, height: height) |
| 833 | 881 | } |
| 834 | 882 | |
| 835 | 883 | override var renderingSurfaceBounds: CGRect { super.renderingSurfaceBounds.union(band) } |
| @@ -843,10 +891,50 @@ final class ShadedFragment: NSTextLayoutFragment, @unchecked Sendable { |
| 843 | 891 | } |
| 844 | 892 | } |
| 845 | 893 | |
| 894 | extension NSAttributedString.Key { |
| 895 | /// The character an entity shows as, on the entity's last character. |
| 896 | static let orgEntityDisplay = NSAttributedString.Key("OrgEntityDisplay") |
| 897 | } |
| 898 | |
| 899 | /// A paragraph with pretty entities: each is drawn as its character where the entity's last |
| 900 | /// (now invisible) character sits, so it takes one column as in Emacs. |
| 901 | final class EntityFragment: NSTextLayoutFragment, @unchecked Sendable { |
| 902 | static func hasEntities(_ string: NSAttributedString) -> Bool { |
| 903 | var found = false |
| 904 | string.enumerateAttribute(.orgEntityDisplay, in: NSRange(location: 0, length: string.length)) { value, _, stop in |
| 905 | if value != nil { found = true; stop.pointee = true } |
| 906 | } |
| 907 | return found |
| 908 | } |
| 909 | |
| 910 | override func draw(at point: CGPoint, in context: CGContext) { |
| 911 | super.draw(at: point, in: context) |
| 912 | Self.drawEntities(of: self, at: point, in: context) |
| 913 | } |
| 914 | |
| 915 | static func drawEntities(of fragment: NSTextLayoutFragment, at point: CGPoint, in context: CGContext) { |
| 916 | guard let string = (fragment.textElement as? NSTextParagraph)?.attributedString else { return } |
| 917 | NSGraphicsContext.saveGraphicsState() |
| 918 | NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: true) |
| 919 | string.enumerateAttribute(.orgEntityDisplay, in: NSRange(location: 0, length: string.length)) { value, range, _ in |
| 920 | guard let display = value as? String, |
| 921 | let line = fragment.textLineFragments.first(where: { NSLocationInRange(range.location, $0.characterRange) }) else { return } |
| 922 | var attributes = string.attributes(at: range.location, effectiveRange: nil) |
| 923 | attributes[.foregroundColor] = NSColor.textColor |
| 924 | attributes[.orgEntityDisplay] = nil |
| 925 | let x = line.locationForCharacter(at: range.location).x |
| 926 | let bounds = line.typographicBounds |
| 927 | NSAttributedString(string: display, attributes: attributes).draw(at: CGPoint(x: point.x + bounds.minX + x, y: point.y + bounds.minY)) |
| 928 | } |
| 929 | NSGraphicsContext.restoreGraphicsState() |
| 930 | } |
| 931 | } |
| 932 | |
| 846 | 933 | /// A heading line drawn with an ellipsis after it, like org's folded headings. |
| 847 | 934 | final class FoldedHeadingFragment: NSTextLayoutFragment, @unchecked Sendable { |
| 848 | 935 | override func draw(at point: CGPoint, in context: CGContext) { |
| 849 | 936 | super.draw(at: point, in: context) |
| 937 | EntityFragment.drawEntities(of: self, at: point, in: context) |
| 850 | 938 | guard let line = textLineFragments.first else { return } |
| 851 | 939 | let bounds = line.typographicBounds |
| 852 | 940 | let ellipsis = NSAttributedString(string: " …", attributes: [ |