| @@ -534,6 +534,7 @@ public final class OrgEditor: NSObject { |
| 534 | } |
534 | } |
| 535 | } |
535 | } |
| 536 | let revealed = caretLine |
536 | let revealed = caretLine |
| |
537 | var scriptExclusions: [Range<Int>] = [] |
| 537 | for run in Presentation.styles(document.tree, in: range, highlighter: highlighter) { |
538 | for run in Presentation.styles(document.tree, in: range, highlighter: highlighter) { |
| 538 | guard let clipped = clip(run.range, to: range) else { continue } |
539 | guard let clipped = clip(run.range, to: range) else { continue } |
| 539 | if !showMarkup, hides(run.role), NSIntersectionRange(clipped, revealed).length == 0 { |
540 | if !showMarkup, hides(run.role), NSIntersectionRange(clipped, revealed).length == 0 { |
| @@ -541,6 +542,48 @@ public final class OrgEditor: NSObject { |
| 541 | } else { |
542 | } else { |
| 542 | theme.apply(run.role, to: storage, range: clipped) |
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 | let offset = manager.offset(from: manager.documentRange.location, to: range.location) |
862 | let offset = manager.offset(from: manager.documentRange.location, to: range.location) |
| 820 | if folded.contains(offset) { return FoldedHeadingFragment(textElement: textElement, range: range) } |
863 | if folded.contains(offset) { return FoldedHeadingFragment(textElement: textElement, range: range) } |
| 821 | if shaded.containing(offset) != nil { return ShadedFragment(textElement: textElement, range: range) } |
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 | return NSTextLayoutFragment(textElement: textElement, range: range) |
869 | return NSTextLayoutFragment(textElement: textElement, range: range) |
| 824 | } |
870 | } |
| @@ -829,7 +875,9 @@ extension OrgEditor: NSTextLayoutManagerDelegate { |
| 829 | final class ShadedFragment: NSTextLayoutFragment, @unchecked Sendable { |
875 | final class ShadedFragment: NSTextLayoutFragment, @unchecked Sendable { |
| 830 | private var band: CGRect { |
876 | private var band: CGRect { |
| 831 | let width = textLayoutManager?.textContainer?.size.width ?? layoutFragmentFrame.width |
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 | override var renderingSurfaceBounds: CGRect { super.renderingSurfaceBounds.union(band) } |
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 | /// A heading line drawn with an ellipsis after it, like org's folded headings. |
933 | /// A heading line drawn with an ellipsis after it, like org's folded headings. |
| 847 | final class FoldedHeadingFragment: NSTextLayoutFragment, @unchecked Sendable { |
934 | final class FoldedHeadingFragment: NSTextLayoutFragment, @unchecked Sendable { |
| 848 | override func draw(at point: CGPoint, in context: CGContext) { |
935 | override func draw(at point: CGPoint, in context: CGContext) { |
| 849 | super.draw(at: point, in: context) |
936 | super.draw(at: point, in: context) |
| |
937 | EntityFragment.drawEntities(of: self, at: point, in: context) |
| 850 | guard let line = textLineFragments.first else { return } |
938 | guard let line = textLineFragments.first else { return } |
| 851 | let bounds = line.typographicBounds |
939 | let bounds = line.typographicBounds |
| 852 | let ellipsis = NSAttributedString(string: " …", attributes: [ |
940 | let ellipsis = NSAttributedString(string: " …", attributes: [ |