krz/orgstar

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

Sources/OrgEditorAppKit/ViewToggles.swift

b98a6509c5ae75e5172dd333d1b6105fd6ceee0e
orgstar/Sources/OrgEditorAppKit/ViewToggles.swift history · blame · raw

108 lines · 4366 bytes

7 symbols in this file
  1#if os(macOS)
  2import AppKit
  3import OrgCore
  4import OrgPresentation
  5
  6/// Spell checking, line truncation and mouse clicks on heading stars and checkboxes.
  7extension OrgEditor {
  8    // MARK: - Spell checking
  9
 10    public var spellChecking: Bool {
 11        get { textView.isContinuousSpellCheckingEnabled }
 12        set {
 13            textView.isContinuousSpellCheckingEnabled = newValue
 14            if !newValue, let storage = textView.textStorage {
 15                storage.removeAttribute(.spellingState, range: NSRange(location: 0, length: storage.length))
 16            }
 17        }
 18    }
 19
 20    func toggleSpellCheck() {
 21        spellChecking.toggle()
 22        onMessage?(spellChecking ? "Spell checking enabled" : "Spell checking disabled")
 23    }
 24
 25    /// Ranges spell-fu skips in Org, for this revision of the text.
 26    func spellSkipped() -> [Range<Int>] {
 27        if let cached = spellSkipCache, cached.text == document.text { return cached.ranges }
 28        let ranges = isOrg ? SpellCheck.skipped(document.tree) : []
 29        spellSkipCache = (document.text, ranges)
 30        return ranges
 31    }
 32
 33    // MARK: - Truncation
 34
 35    /// `toggle-truncate-lines`: long lines run off the right edge instead of wrapping.
 36    public var truncatesLines: Bool {
 37        get { !(textView.textContainer?.widthTracksTextView ?? true) }
 38        set {
 39            guard let container = textView.textContainer, newValue != truncatesLines else { return }
 40            let scroll = textView.enclosingScrollView
 41            scroll?.hasHorizontalScroller = newValue
 42            textView.isHorizontallyResizable = newValue
 43            if newValue {
 44                container.widthTracksTextView = false
 45                container.size = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
 46                textView.autoresizingMask = [.width, .height]
 47            } else {
 48                let width = scroll?.contentSize.width ?? textView.frame.width
 49                textView.setFrameSize(NSSize(width: width, height: textView.frame.height))
 50                container.size = NSSize(width: width, height: CGFloat.greatestFiniteMagnitude)
 51                container.widthTracksTextView = true
 52                textView.autoresizingMask = [.width]
 53            }
 54            textLayoutManager.invalidateLayout(for: textLayoutManager.documentRange)
 55            textView.needsDisplay = true
 56        }
 57    }
 58
 59    func toggleTruncateLines() {
 60        truncatesLines.toggle()
 61        onMessage?(truncatesLines ? "Truncate long lines enabled" : "Truncate long lines disabled")
 62    }
 63
 64    // MARK: - Clicks
 65
 66    /// A click on a heading's stars cycles it; a click on a checkbox toggles it.
 67    func click(at offset: Int) -> Bool {
 68        guard isOrg else { return false }
 69        var node = document.tree.root
 70        while let child = node.child(containing: offset) ?? node.child(containing: offset - 1) {
 71            node = child
 72            if child.kind == .heading || child.kind == .item { break }
 73        }
 74        let tokens = node.tokens.filter { $0.range.lowerBound <= offset && offset <= $0.range.upperBound }
 75        if node.kind == .heading, tokens.contains(where: { $0.kind == .stars }) {
 76            setCaret(node.range.lowerBound)
 77            _ = cycleAtCaret()
 78            return true
 79        }
 80        if node.kind == .item, let box = tokens.first(where: { $0.kind == .checkbox }) {
 81            setCaret(box.range.lowerBound)
 82            run("org.checkbox.toggle")
 83            return true
 84        }
 85        return false
 86    }
 87}
 88
 89extension OrgEditor: NSTextViewDelegate {
 90    public func textViewDidChangeSelection(_ notification: Notification) {
 91        selectionChanged()
 92        if completionPopup != nil, let popup = completionPopup, textView.selectedRange().location != popup.completion.range.upperBound {
 93            refreshCompletion()
 94        }
 95    }
 96
 97    public func textDidChange(_ notification: Notification) {
 98        textTyped()
 99    }
100
101    /// No spelling marks in code, links, dates, tags or keywords.
102    public func textView(_ textView: NSTextView, shouldSetSpellingState value: Int, range affectedCharRange: NSRange) -> Int {
103        guard value != 0 else { return value }
104        let range = affectedCharRange.location..<NSMaxRange(affectedCharRange)
105        return spellSkipped().contains { $0.overlaps(range) } ? 0 : value
106    }
107}
108#endif