krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgPresentation/ReadingText.swift

322a87d47526d505d903e05d14e65a5464e1468f
orgstar/Sources/OrgPresentation/ReadingText.swift history · blame · raw

100 lines · 5688 bytes

  1import Foundation
  2import OrgCore
  3
  4/// A range of a file to read rather than edit: its style runs as segments, with the brackets
  5/// and targets of described links hidden (`org-link-descriptive`) and folded drawers shown as
  6/// their first line and `…`, as the editor shows them.
  7public enum ReadingText {
  8    public struct Segment: Sendable, Equatable {
  9        public var text: String
 10        public var roles: Set<StyleRole>
 11        /// The link this text belongs to.
 12        public var link: OrgLink?
 13        /// Where the text starts in the file.
 14        public var offset: Int
 15        /// A pretty sub/superscript's body: true raised, false lowered.
 16        public var superscript: Bool?
 17    }
 18
 19    /// How the text shows, as the editor's display settings make it.
 20    public struct Display: Sendable {
 21        /// Link brackets and targets, and emphasis markers, shown; no pretty entities.
 22        public var showMarkup = false
 23        /// `org-hide-emphasis-markers`, `org-pretty-entities` and its braces.
 24        public var options = EditingOptions.org
 25        /// Colors code in src blocks.
 26        public var highlighter: (any CodeHighlighter)?
 27
 28        public init(showMarkup: Bool = false, options: EditingOptions = .org, highlighter: (any CodeHighlighter)? = nil) {
 29            self.showMarkup = showMarkup
 30            self.options = options
 31            self.highlighter = highlighter
 32        }
 33    }
 34
 35    /// `folded` are drawers' and blocks' hidden ranges (`Wrapper.hidden`).
 36    public static func segments(_ tree: OrgTree, in range: Range<Int>, folded: [Range<Int>] = [], display: Display = Display()) -> [Segment] {
 37        let text = tree.text as NSString
 38        let runs = Presentation.styles(tree, in: range, highlighter: display.highlighter)
 39        // A fold hides from the end of the opening line through the closing line, so the
 40        // marker sits on the opening line.
 41        let folds = folded.compactMap { r -> Range<Int>? in
 42            guard r.lowerBound > range.lowerBound, r.lowerBound <= range.upperBound else { return nil }
 43            let end = r.upperBound > r.lowerBound && r.upperBound <= text.length && text.character(at: r.upperBound - 1) == 10 ? r.upperBound - 1 : r.upperBound
 44            return (r.lowerBound - 1)..<min(end, range.upperBound)
 45        }
 46        var hidden = runs.filter { !display.showMarkup && Presentation.hidesWithMarkup($0.role, options: display.options) }.map(\.range) + folds
 47        // `org-pretty-entities`: an entity's last character shows its character, the rest hidden.
 48        var replaced: [Int: String] = [:]
 49        var scripts: [(range: Range<Int>, superscript: Bool)] = []
 50        if !display.showMarkup, display.options.prettyEntities {
 51            let blocks = Presentation.shadedBlocks(tree)
 52            let pretty = Presentation.prettyRuns(
 53                text, in: range, revealedLine: nil, inBlock: { offset in blocks.contains { $0.contains(offset) } },
 54                scriptExclusions: runs.filter { Presentation.excludesScripts($0.role) }.map(\.range),
 55                subSuperscriptsNeedBraces: display.options.subSuperscriptsNeedBraces
 56            )
 57            for (lineStart, run) in pretty {
 58                switch run {
 59                case .entity(let r, let shown):
 60                    let last = lineStart + r.upperBound - 1
 61                    hidden.append((lineStart + r.lowerBound)..<last)
 62                    replaced[last] = shown
 63                case .script(let markers, let body, let superscript):
 64                    hidden += markers.map { (lineStart + $0.lowerBound)..<(lineStart + $0.upperBound) }
 65                    scripts.append(((lineStart + body.lowerBound)..<(lineStart + body.upperBound), superscript))
 66                }
 67            }
 68        }
 69        var bounds: Set<Int> = [range.lowerBound, range.upperBound]
 70        let replacedRanges = replaced.keys.map { $0..<($0 + 1) }
 71        for r in runs.map(\.range) + hidden + replacedRanges + scripts.map(\.range) {
 72            bounds.insert(max(range.lowerBound, min(range.upperBound, r.lowerBound)))
 73            bounds.insert(max(range.lowerBound, min(range.upperBound, r.upperBound)))
 74        }
 75        let points = bounds.sorted()
 76        var segments: [Segment] = []
 77        func append(_ segment: Segment) {
 78            if var last = segments.last, last.roles == segment.roles, last.link == segment.link, last.superscript == segment.superscript,
 79               last.offset + (last.text as NSString).length == segment.offset {
 80                last.text += segment.text
 81                segments[segments.count - 1] = last
 82            } else {
 83                segments.append(segment)
 84            }
 85        }
 86        for (a, b) in zip(points, points.dropFirst()) where a < b {
 87            if folds.contains(where: { $0.lowerBound == a }) {
 88                segments.append(Segment(text: "…", roles: [.metadata], link: nil, offset: a))
 89            }
 90            if hidden.contains(where: { $0.lowerBound <= a && b <= $0.upperBound }) { continue }
 91            let roles = Set(runs.filter { $0.range.lowerBound <= a && b <= $0.range.upperBound }.map(\.role))
 92            let link = roles.contains(.link) ? Links.link(at: a, text: tree.text, tree: tree)?.link : nil
 93            let superscript = scripts.first { $0.range.lowerBound <= a && b <= $0.range.upperBound }?.superscript
 94            let shown = replaced[a] ?? text.substring(with: NSRange(location: a, length: b - a))
 95            let segment = Segment(text: shown, roles: roles, link: link, offset: a, superscript: superscript)
 96            if replaced[a] != nil { segments.append(segment) } else { append(segment) }
 97        }
 98        return segments
 99    }
100}