oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2
3/// The IME owns its marked text until commit. Compare its committed native
4/// result to the exact source string it began with, using scalar-safe UTF-16.
5@MainActor enum DocumentComposition {
6 static func replacement(before: String, after: String) -> (NSRange, String)? {
7 let old = Array(before.unicodeScalars), new = Array(after.unicodeScalars)
8 var prefix = 0
9 while prefix < min(old.count, new.count), old[prefix] == new[prefix] { prefix += 1 }
10 var suffix = 0
11 while suffix < min(old.count, new.count) - prefix, old[old.count - suffix - 1] == new[new.count - suffix - 1] { suffix += 1 }
12 guard prefix != old.count || prefix != new.count else { return nil }
13 func units(_ values: ArraySlice<Unicode.Scalar>) -> Int { values.reduce(0) { $0 + ($1.value > 0xffff ? 2 : 1) } }
14 let range = NSRange(location: units(old[..<prefix]), length: units(old[prefix..<(old.count - suffix)]))
15 return (range, String(String.UnicodeScalarView(new[prefix..<(new.count - suffix)])))
16 }
17}
18#if os(macOS)
19import AppKit
20final class DocsTextView: DocsPagedTextView {
21 var action: ((String) -> Bool)?
22 var canAction: ((String) -> Bool)?
23 var ruleCommand: (([String: JSONValue]) -> Bool)?
24 var lastInputRule: DocumentInputRules.Plan?
25 var clipboardEdit: (([JSONValue]?, NSRange, String) -> Bool)?
26 var compositionChanged: ((Bool) -> Void)?
27 var compositionCommitted: ((NSAttributedString, String) -> Bool)?
28 private var original: NSAttributedString?
29 var isComposing: Bool { original != nil }
30 @objc func undo(_ sender: Any?) { _ = action?("undo") }
31 @objc func redo(_ sender: Any?) { _ = action?("redo") }
32 override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
33 if menuItem.action == #selector(undo(_:)) { return canAction?("undo") == true }
34 if menuItem.action == #selector(redo(_:)) { return canAction?("redo") == true }
35 return super.validateMenuItem(menuItem)
36 }
37 override func performKeyEquivalent(with event: NSEvent) -> Bool {
38 let flags = event.modifierFlags
39 if let key = event.charactersIgnoringModifiers,
40 let name = DocumentKeyboardBindings.action(key: key, command: flags.contains(.command), shift: flags.contains(.shift), option: flags.contains(.option), control: flags.contains(.control)) {
41 _ = action?(name); return true
42 }
43 return super.performKeyEquivalent(with: event)
44 }
45 override func insertTab(_ sender: Any?) { _ = action?("indent") }
46 override func insertBacktab(_ sender: Any?) { _ = action?("outdent") }
47 override func insertLineBreak(_ sender: Any?) { _ = action?("hard-break") }
48 override func deleteBackward(_ sender: Any?) {
49 if !revertInputRule() { super.deleteBackward(sender) }
50 }
51 override func copy(_ sender: Any?) {
52 lastInputRule = nil
53 let range = selectedRange()
54 guard range.length > 0 else { return }
55 _ = DocsRichClipboard.write(attributedString().attributedSubstring(from: range))
56 }
57 override func cut(_ sender: Any?) {
58 lastInputRule = nil
59 let range = selectedRange()
60 guard isEditable, !isComposing, range.length > 0,
61 DocsRichClipboard.write(attributedString().attributedSubstring(from: range)),
62 clipboardEdit?(nil, range, "") == true else { return }
63 textStorage?.replaceCharacters(in: range, with: "")
64 setSelectedRange(NSRange(location: range.location, length: 0)); didChangeText()
65 }
66 override func paste(_ sender: Any?) {
67 lastInputRule = nil
68 guard isEditable, !isComposing else { return }
69 guard let blocks = DocsRichClipboard.read() else { super.pasteAsPlainText(sender); return }
70 let range = selectedRange()
71 guard let replacement = DocumentClipboardInsertion.attributed(blocks: blocks, in: attributedString(), range: range),
72 clipboardEdit?(blocks, range, replacement.string) == true else { return }
73 textStorage?.replaceCharacters(in: range, with: replacement)
74 setSelectedRange(NSRange(location: range.location + replacement.length, length: 0)); didChangeText()
75 }
76 override func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
77 if original == nil { original = NSAttributedString(attributedString: attributedString()); compositionChanged?(true) }
78 super.setMarkedText(string, selectedRange: selectedRange, replacementRange: replacementRange)
79 }
80 override func unmarkText() { super.unmarkText(); finishComposition() }
81 override func insertText(_ insertString: Any, replacementRange: NSRange) {
82 let composing = isComposing
83 let range = replacementRange.location == NSNotFound ? selectedRange() : replacementRange
84 if !composing, let text = insertString as? String, interceptInput(text, range: range) { return }
85 super.insertText(insertString, replacementRange: replacementRange)
86 if composing && !hasMarkedText() { finishComposition() }
87 }
88 private func finishComposition() {
89 guard let original else { return }
90 if compositionCommitted?(original, string) == false { textStorage?.setAttributedString(original) }
91 self.original = nil; compositionChanged?(false)
92 }
93}
94#else
95import UIKit
96final class DocsTextView: DocsPagedTextView {
97 var action: ((String) -> Bool)?
98 var canAction: ((String) -> Bool)?
99 var ruleCommand: (([String: JSONValue]) -> Bool)?
100 var lastInputRule: DocumentInputRules.Plan?
101 var clipboardEdit: (([JSONValue]?, NSRange, String) -> Bool)?
102 var compositionChanged: ((Bool) -> Void)?
103 var compositionCommitted: ((NSAttributedString, String) -> Bool)?
104 private var original: NSAttributedString?
105 var isComposing: Bool { original != nil }
106 override var undoManager: UndoManager? { nil }
107 override var keyCommands: [UIKeyCommand]? {
108 DocumentKeyboardBindings.all.map { binding in
109 let command = UIKeyCommand(input: binding.key, modifierFlags: (binding.command ? UIKeyModifierFlags.command : []).union(binding.shift ? .shift : []), action: #selector(handleKey(_:)))
110 command.wantsPriorityOverSystemBehavior = true
111 return command
112 }
113 }
114 @objc private func handleKey(_ key: UIKeyCommand) {
115 guard let input = key.input,
116 let name = DocumentKeyboardBindings.action(key: input, command: key.modifierFlags.contains(.command), shift: key.modifierFlags.contains(.shift), option: key.modifierFlags.contains(.alternate), control: key.modifierFlags.contains(.control)) else { return }
117 _ = action?(name)
118 }
119 override func deleteBackward() {
120 if !revertInputRule() { super.deleteBackward() }
121 }
122 override func copy(_ sender: Any?) {
123 lastInputRule = nil
124 guard selectedRange.length > 0 else { return }
125 _ = DocsRichClipboard.write(attributedText.attributedSubstring(from: selectedRange))
126 }
127 override func cut(_ sender: Any?) {
128 lastInputRule = nil
129 let range = selectedRange
130 guard isEditable, !isComposing, range.length > 0,
131 DocsRichClipboard.write(attributedText.attributedSubstring(from: range)),
132 clipboardEdit?(nil, range, "") == true else { return }
133 textStorage.replaceCharacters(in: range, with: "")
134 selectedRange = NSRange(location: range.location, length: 0)
135 delegate?.textViewDidChange?(self)
136 }
137 override func paste(_ sender: Any?) {
138 lastInputRule = nil
139 guard isEditable, !isComposing else { return }
140 guard let blocks = DocsRichClipboard.read() else { super.paste(sender); return }
141 let range = selectedRange
142 guard let replacement = DocumentClipboardInsertion.attributed(blocks: blocks, in: attributedText, range: range),
143 clipboardEdit?(blocks, range, replacement.string) == true else { return }
144 textStorage.replaceCharacters(in: range, with: replacement)
145 selectedRange = NSRange(location: range.location + replacement.length, length: 0)
146 delegate?.textViewDidChange?(self)
147 }
148 override func setMarkedText(_ markedText: String?, selectedRange: NSRange) {
149 if original == nil { original = NSAttributedString(attributedString: attributedText); compositionChanged?(true) }
150 super.setMarkedText(markedText, selectedRange: selectedRange)
151 }
152 override func unmarkText() { super.unmarkText(); finishComposition() }
153 override func insertText(_ text: String) {
154 let composing = isComposing
155 if !composing, interceptInput(text, range: selectedRange) { return }
156 super.insertText(text)
157 if composing && markedTextRange == nil { finishComposition() }
158 }
159 private func finishComposition() {
160 guard let original else { return }
161 if compositionCommitted?(original, text) == false { attributedText = original }
162 self.original = nil; compositionChanged?(false)
163 }
164}
165#endif
166