krz/orgstar

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

Sources/OrgCore/Commands/VisibleText.swift

16086b4cf2caff5328774b2cd5ae3ffe1ab65ca4
orgstar/Sources/OrgCore/Commands/VisibleText.swift history · blame · raw

104 lines · 4707 bytes

  1import Foundation
  2
  3/// How wide each character of a line is as Emacs displays Org text: bracket links show only
  4/// their description (or path), and with the options, emphasis markers are hidden and entities
  5/// and sub/superscripts show as their characters. Emacs's `current-column` counts only what is
  6/// visible, so tag alignment, filling and table widths all depend on this.
  7struct VisibleLine {
  8    /// Display width of the character starting at each UTF-16 offset; -1 for a tab.
  9    private var widths: [Int]
 10    /// Whether a character starts at each offset (false for the second half of a pair).
 11    private var starts: [Bool]
 12    private var hidden: [Bool]
 13
 14    /// The line at `range` (UTF-16) inside `context`, which holds the lines around it so that
 15    /// links spanning lines are seen whole.
 16    init(_ context: String, line range: Range<Int>, options: EditingOptions, settings: OrgSettings = .default) {
 17        let whole = VisibleLine(context, options: options, settings: settings)
 18        widths = Array(whole.widths[range])
 19        starts = Array(whole.starts[range])
 20        hidden = Array(whole.hidden[range])
 21    }
 22
 23    init(_ line: String, options: EditingOptions, settings: OrgSettings = .default) {
 24        let count = line.utf16.count
 25        widths = [Int](repeating: 0, count: count)
 26        starts = [Bool](repeating: false, count: count)
 27        hidden = [Bool](repeating: false, count: count)
 28        var index = 0
 29        for character in line {
 30            widths[index] = character == "\t" ? -1 : displayWidth(of: character)
 31            starts[index] = true
 32            index += String(character).utf16.count
 33        }
 34        let needsLinks = line.contains("[[")
 35        let needsEmphasis = options.hideEmphasisMarkers && line.contains(where: { "*/_+=~".contains($0) })
 36        if needsLinks || needsEmphasis {
 37            let tree = OrgParser.parse(line + "\n", defaults: settings)
 38            for node in tree.root.descendants() {
 39                switch node.kind {
 40                case .link where node.text.hasPrefix("[["):
 41                    if let description = node.firstChild(.linkDescription) {
 42                        hide(node.range.lowerBound..<description.range.lowerBound)
 43                        hide(description.range.upperBound..<node.range.upperBound)
 44                    } else {
 45                        hide(node.range.lowerBound..<(node.range.lowerBound + 2))
 46                        hide((node.range.upperBound - 2)..<node.range.upperBound)
 47                    }
 48                case .bold, .italic, .underline, .strikeThrough, .verbatim, .code:
 49                    guard options.hideEmphasisMarkers else { break }
 50                    hide(node.range.lowerBound..<(node.range.lowerBound + 1))
 51                    hide((node.range.upperBound - 1)..<node.range.upperBound)
 52                default:
 53                    break
 54                }
 55            }
 56        }
 57        if options.prettyEntities {
 58            // `org-fontify-entities`: a single-character entity is composed into one glyph.
 59            // Positions inside it stay at its starting column; its width counts at its end.
 60            for run in PrettyEntities.runs(in: line, subSuperscriptsNeedBraces: options.subSuperscriptsNeedBraces) {
 61                switch run {
 62                case .entity(let range, let display):
 63                    for i in range where i < widths.count { widths[i] = 0 }
 64                    widths[range.upperBound - 1] = displayWidth(display)
 65                case .script(let markers, _, _):
 66                    for marker in markers { hide(marker) }
 67                }
 68            }
 69        }
 70    }
 71
 72    private mutating func hide(_ range: Range<Int>) {
 73        for i in range where i >= 0 && i < hidden.count { hidden[i] = true }
 74    }
 75
 76    private func advance(_ column: Int, _ i: Int) -> Int {
 77        widths[i] < 0 ? (column / 8 + 1) * 8 : column + widths[i]
 78    }
 79
 80    /// Display column at UTF-16 `offset`.
 81    func column(at offset: Int) -> Int {
 82        var column = 0
 83        for i in 0..<min(offset, widths.count) where starts[i] && !hidden[i] {
 84            column = advance(column, i)
 85        }
 86        return column
 87    }
 88
 89    var width: Int { column(at: widths.count) }
 90
 91    /// `move-to-column`: hidden text is skipped before the goal test, so point ends past
 92    /// markup that sits at the goal column.
 93    func offset(ofColumn target: Int) -> Int {
 94        var column = 0
 95        var i = 0
 96        while true {
 97            while i < widths.count, hidden[i] || !starts[i] { i += 1 }
 98            if column >= target || i >= widths.count { break }
 99            column = advance(column, i)
100            i += 1
101        }
102        return i
103    }
104}