|
| 1 | +// |
| 2 | +// SuggestionTriggerCharacterModel.swift |
| 3 | +// CodeEditSourceEditor |
| 4 | +// |
| 5 | +// Created by Khan Winter on 8/25/25. |
| 6 | +// |
| 7 | + |
| 8 | +import AppKit |
| 9 | +import CodeEditTextView |
| 10 | +import TextStory |
| 11 | + |
| 12 | +/// Triggers the suggestion window when trigger characters are typed. |
| 13 | +/// Designed to be called in the ``TextViewDelegate``'s didReplaceCharacters method. |
| 14 | +/// |
| 15 | +/// Was originally a `TextFilter` model, however those are called before text is changed and cursors are updated. |
| 16 | +/// The suggestion model expects up-to-date cursor positions as well as complete text contents. This being |
| 17 | +/// essentially a textview delegate ensures both of those promises are upheld. |
| 18 | +final class SuggestionTriggerCharacterModel { |
| 19 | + weak var controller: TextViewController? |
| 20 | + private var lastPosition: NSRange? |
| 21 | + |
| 22 | + var triggerCharacters: Set<String>? { |
| 23 | + controller?.configuration.peripherals.codeSuggestionTriggerCharacters |
| 24 | + } |
| 25 | + |
| 26 | + func textView(_ textView: TextView, didReplaceContentsIn range: NSRange, with string: String) { |
| 27 | + guard let controller, let completionDelegate = controller.completionDelegate, let triggerCharacters else { |
| 28 | + return |
| 29 | + } |
| 30 | + |
| 31 | + let mutation = TextMutation( |
| 32 | + string: string, |
| 33 | + range: range, |
| 34 | + limit: textView.textStorage.length |
| 35 | + ) |
| 36 | + guard mutation.delta >= 0, |
| 37 | + let lastChar = mutation.string.last else { |
| 38 | + lastPosition = nil |
| 39 | + return |
| 40 | + } |
| 41 | + |
| 42 | + guard triggerCharacters.contains(String(lastChar)) || lastChar.isNumber || lastChar.isLetter else { |
| 43 | + lastPosition = nil |
| 44 | + return |
| 45 | + } |
| 46 | + |
| 47 | + let range = NSRange(location: mutation.postApplyRange.max, length: 0) |
| 48 | + lastPosition = range |
| 49 | + SuggestionController.shared.cursorsUpdated( |
| 50 | + textView: controller, |
| 51 | + delegate: completionDelegate, |
| 52 | + position: CursorPosition(range: range), |
| 53 | + presentIfNot: true |
| 54 | + ) |
| 55 | + } |
| 56 | + |
| 57 | + func selectionUpdated(_ position: CursorPosition) { |
| 58 | + guard let controller, let completionDelegate = controller.completionDelegate else { |
| 59 | + return |
| 60 | + } |
| 61 | + |
| 62 | + if lastPosition != position.range { |
| 63 | + SuggestionController.shared.cursorsUpdated( |
| 64 | + textView: controller, |
| 65 | + delegate: completionDelegate, |
| 66 | + position: position |
| 67 | + ) |
| 68 | + } |
| 69 | + } |
| 70 | +} |
0 commit comments