Sources/OrgEditorAppKit/ViewToggles.swift
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