66//
77
88import AppKit
9+ import SwiftTreeSitter
910import CodeEditTextView
1011import CodeEditSourceEditor
1112import LanguageServerProtocol
@@ -17,17 +18,19 @@ class AutoCompleteCoordinator: TextViewCoordinator {
1718 private unowned var file : CEWorkspaceFile
1819 /// The event monitor that looks for the keyboard shortcut to bring up the autocomplete menu
1920 private var localEventMonitor : Any ?
20- /// The `ItemBoxWindowController` lets us display the autocomplete items
21- private var itemBoxController : ItemBoxWindowController ?
21+ /// The `SuggestionController` lets us display the autocomplete items
22+ private var suggestionController : SuggestionController ?
23+ /// The current TreeSitter node that the main cursor is at
24+ private var currentNode : SwiftTreeSitter . Node ?
2225
2326 init ( _ file: CEWorkspaceFile ) {
2427 self . file = file
2528 }
2629
2730 func prepareCoordinator( controller: TextViewController ) {
28- itemBoxController = ItemBoxWindowController ( )
29- itemBoxController ? . delegate = self
30- itemBoxController ? . close ( )
31+ suggestionController = SuggestionController ( )
32+ suggestionController ? . delegate = self
33+ suggestionController ? . close ( )
3134 self . textViewController = controller
3235
3336 localEventMonitor = NSEvent . addLocalMonitorForEvents ( matching: . keyDown) { event in
@@ -48,19 +51,48 @@ class AutoCompleteCoordinator: TextViewCoordinator {
4851 guard let cursorPos = textViewController? . cursorPositions. first,
4952 let textView = textViewController? . textView,
5053 let window = NSApplication . shared. keyWindow,
51- let itemBoxController = itemBoxController
54+ let suggestionController = suggestionController
5255 else {
5356 return
5457 }
5558
59+ do {
60+ if let token = try textViewController? . treeSitterClient? . nodesAt ( range: cursorPos. range) . first {
61+ if tokenIsActionable ( token. node) {
62+ currentNode = token. node
63+ }
64+
65+ // Get the string from the start of the token to the location of the cursor
66+ if cursorPos. range. location > token. node. range. location {
67+ let selectedRange = NSRange (
68+ location: token. node. range. location,
69+ length: cursorPos. range. location - token. node. range. location
70+ )
71+ let tokenSubstring = textView. textStorage? . substring ( from: selectedRange)
72+ // print("Token word: \(String(describing: tokenSubstring))")
73+ }
74+ }
75+ } catch {
76+ print ( " Error getting TreeSitter node: \( error) " )
77+ }
78+
5679 Task {
5780 let textPosition = Position ( line: cursorPos. line - 1 , character: cursorPos. column - 1 )
81+ // If we are asking for completions in the middle of a token, then
82+ // query the language server for completion items at the start of the token
83+ // if let currentNode = currentNode, tokenIsActionable(currentNode) {
84+ // if let newPos = textView.lspRangeFrom(nsRange: currentNode.range) {
85+ // _currentNode
86+ // }
87+ // }
88+ print ( " Getting completion items at token position: \( textPosition) " )
89+
5890 let completionItems = await fetchCompletions ( position: textPosition)
59- itemBoxController . items = completionItems
91+ suggestionController . items = completionItems
6092
6193 let cursorRect = textView. firstRect ( forCharacterRange: cursorPos. range, actualRange: nil )
62- itemBoxController . constrainWindowToScreenEdges ( cursorRect: cursorRect)
63- itemBoxController . showWindow ( attachedTo: window)
94+ suggestionController . constrainWindowToScreenEdges ( cursorRect: cursorRect)
95+ suggestionController . showWindow ( attachedTo: window)
6496 }
6597 }
6698
@@ -97,60 +129,114 @@ class AutoCompleteCoordinator: TextViewCoordinator {
97129 }
98130 }
99131
132+ /// Determines if a TreeSitter node is a type where we can build featues off of. This helps filter out
133+ /// nodes that represent blank spaces or other information that is not useful.
134+ private func tokenIsActionable( _ node: SwiftTreeSitter . Node ) -> Bool {
135+ // List of node types that should have their text be replaced
136+ let replaceableTypes : Set < String > = [
137+ " identifier " ,
138+ " property_identifier " ,
139+ " field_identifier " ,
140+ " variable_name " ,
141+ " method_name " ,
142+ " function_name " ,
143+ " type_identifier "
144+ ]
145+ return replaceableTypes. contains ( node. nodeType ?? " " )
146+ }
147+
100148 deinit {
101- itemBoxController ? . close ( )
149+ suggestionController ? . close ( )
102150 if let localEventMonitor = localEventMonitor {
103151 NSEvent . removeMonitor ( localEventMonitor)
104152 self . localEventMonitor = nil
105153 }
106154 }
107155}
108156
109- extension AutoCompleteCoordinator : ItemBoxDelegate {
157+ extension AutoCompleteCoordinator : SuggestionControllerDelegate {
110158 /// Takes a `CompletionItem` and modifies the text view with the new string
111- func applyCompletionItem( _ item: CompletionItem ) {
159+ func applyCompletionItem( item: CompletionItem ) {
112160 guard let cursorPos = textViewController? . cursorPositions. first,
113161 let textView = textViewController? . textView else {
114162 return
115163 }
116164
117- let textPosition = Position (
118- line: cursorPos. line - 1 ,
119- character: cursorPos. column - 1
120- )
121- var textEdits = LSPCompletionItemsUtil . getCompletionItemEdits (
122- startPosition: textPosition,
123- item: item
124- )
125- // Appropriately order the text edits
126- textEdits = TextEdit . makeApplicable ( textEdits)
165+ // Get the token the cursor is currently on. Here we will check if we want to
166+ // replace the current token we are on or just add text onto it.
167+ var replacementRange = cursorPos. range
168+ do {
169+ if let token = try textViewController? . treeSitterClient? . nodesAt ( range: cursorPos. range) . first {
170+ if tokenIsActionable ( token. node) {
171+ replacementRange = token. node. range
172+ }
173+ }
174+ } catch {
175+ print ( " Error getting TreeSitter node: \( error) " )
176+ }
127177
128178 // Make the updates
179+ let insertText = LSPCompletionItemsUtil . getInsertText ( from: item)
129180 textView. undoManager? . beginUndoGrouping ( )
130- for textEdit in textEdits {
131- textView. replaceString (
132- in: cursorPos. range,
133- with: textEdit. newText
134- )
135- }
181+ textView. replaceString ( in: replacementRange, with: insertText)
136182 textView. undoManager? . endUndoGrouping ( )
137183
138- // Set the cursor to the end of the completion
139- let insertText = LSPCompletionItemsUtil . getInsertText ( from: item)
140- guard let newCursorPos = cursorPos. range. shifted ( by: insertText. count) else {
184+ // Set cursor position to end of inserted text
185+ let newCursorRange = NSRange ( location: replacementRange. location + insertText. count, length: 0 )
186+ textViewController? . setCursorPositions ( [ CursorPosition ( range: newCursorRange) ] )
187+
188+ self . onCompletion ( )
189+ }
190+
191+ func onCompletion( ) {
192+
193+ }
194+
195+ func onCursorMove( ) {
196+ guard let cursorPos = textViewController? . cursorPositions. first,
197+ let suggestionController = suggestionController,
198+ let textView = self . textViewController? . textView,
199+ suggestionController. isVisible
200+ else {
201+ return
202+ }
203+ guard let currentNode = currentNode,
204+ !suggestionController. items. isEmpty else {
205+ self . suggestionController? . close ( )
141206 return
142207 }
143- textViewController? . setCursorPositions ( [ CursorPosition ( range: newCursorPos) ] )
144208
145- // do {
146- // let token = try textViewController?.treeSitterClient?.nodesAt(range: cursorPos.range)
147- // guard let token = token?.first else {
148- // return
149- // }
150- // print("Token \(token)")
151- // } catch {
152- // print("\(error)")
153- // return
154- // }
209+ do {
210+ if let token = try textViewController? . treeSitterClient? . nodesAt ( range: cursorPos. range) . first {
211+ // Moving to a new token requires a new call to the language server
212+ // We extend the range so that the `contains` can include the end value of
213+ // the token, since its check is exclusive.
214+ let adjustedRange = currentNode. range. shifted ( endBy: 1 )
215+ if let adjustedRange = adjustedRange,
216+ !adjustedRange. contains ( cursorPos. range. location) {
217+ suggestionController. close ( )
218+ return
219+ }
220+
221+ // 1. Print cursor position and token range
222+ print ( " Current node: \( String ( describing: currentNode) ) " )
223+ print ( " Cursor pos: \( cursorPos. range. location) : Line: \( cursorPos. line) Col: \( cursorPos. column) " )
224+
225+ // Get the token string from the start of the token to the location of the cursor
226+ // print("Token contains cursor position: \(String(describing: currentNode.range.contains(cursorPos.range.location)))")
227+ // print("Token info: \(String(describing: tokenSubstring)) Range: \(String(describing: adjustedRange))")
228+ // print("Current cursor position: \(cursorPos.range)")
229+ }
230+ } catch {
231+ print ( " Error getting TreeSitter node: \( error) " )
232+ }
233+ }
234+
235+ func onItemSelect( item: LanguageServerProtocol . CompletionItem ) {
236+
237+ }
238+
239+ func onClose( ) {
240+ currentNode = nil
155241 }
156242}
0 commit comments