Skip to content

Commit 7abca91

Browse files
committed
Centralize Suggestion Updates
1 parent 64b15cd commit 7abca91

File tree

8 files changed

+88
-55
lines changed

8 files changed

+88
-55
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
}

Sources/CodeEditSourceEditor/CodeSuggestion/Model/SuggestionViewModel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ final class SuggestionViewModel: ObservableObject {
1616

1717
weak var delegate: CodeSuggestionDelegate?
1818

19+
private var cursorPosition: CursorPosition?
1920
private var syntaxHighlightedCache: [Int: NSAttributedString] = [:]
2021

2122
func showCompletions(

Sources/CodeEditSourceEditor/Controller/TextViewController+Cursor.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ extension TextViewController {
8282
}
8383
isPostingCursorNotification = false
8484

85-
if let completionDelegate = completionDelegate, let position = cursorPositions.first {
86-
SuggestionController.shared.cursorsUpdated(textView: self, delegate: completionDelegate, position: position)
85+
if let position = cursorPositions.first {
86+
suggestionTriggerModel.selectionUpdated(position)
8787
}
8888
}
8989

Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ extension TextViewController {
2424
setUpNewlineTabFilters(indentOption: configuration.behavior.indentOption)
2525
setUpDeletePairFilters(pairs: BracketPairs.allValues)
2626
setUpDeleteWhitespaceFilter(indentOption: configuration.behavior.indentOption)
27-
setUpSuggestionsFilter()
2827
}
2928

3029
/// Returns a `TextualIndenter` based on available language configuration.
@@ -121,24 +120,4 @@ extension TextViewController {
121120

122121
return true
123122
}
124-
125-
func setUpSuggestionsFilter() {
126-
textFilters.append(
127-
CodeSuggestionTriggerFilter(
128-
triggerCharacters: configuration.peripherals.codeSuggestionTriggerCharacters,
129-
didTrigger: { [weak self] in
130-
guard let self else { return }
131-
if let completionDelegate = self.completionDelegate,
132-
let position = self.cursorPositions.first {
133-
SuggestionController.shared.cursorsUpdated(
134-
textView: self,
135-
delegate: completionDelegate,
136-
position: position,
137-
presentIfNot: true
138-
)
139-
}
140-
}
141-
)
142-
)
143-
}
144123
}

Sources/CodeEditSourceEditor/Controller/TextViewController+TextViewDelegate.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ extension TextViewController: TextViewDelegate {
2727
coordinator.textViewDidChangeText(controller: self)
2828
}
2929
}
30+
31+
suggestionTriggerModel.textView(textView, didReplaceContentsIn: range, with: string)
3032
}
3133

3234
public func textView(_ textView: TextView, shouldReplaceContentsIn range: NSRange, with string: String) -> Bool {

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public class TextViewController: NSViewController {
5151
/// A default `NSParagraphStyle` with a set `lineHeight`
5252
lazy var paragraphStyle: NSMutableParagraphStyle = generateParagraphStyle()
5353

54+
var suggestionTriggerModel = SuggestionTriggerCharacterModel()
55+
5456
// MARK: - Public Variables
5557

5658
/// Passthrough value for the `textView`s string
@@ -224,6 +226,8 @@ public class TextViewController: NSViewController {
224226

225227
super.init(nibName: nil, bundle: nil)
226228

229+
suggestionTriggerModel.controller = self
230+
227231
if let idx = highlightProviders.firstIndex(where: { $0 is TreeSitterClient }),
228232
let client = highlightProviders[idx] as? TreeSitterClient {
229233
self.treeSitterClient = client

Sources/CodeEditSourceEditor/Filters/CodeSuggestionTriggerFilter.swift

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)