Skip to content

Commit e81ac20

Browse files
UX updates
1 parent a432688 commit e81ac20

File tree

4 files changed

+143
-50
lines changed

4 files changed

+143
-50
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
3000516C2BBD3A9500A98562 /* ServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */; };
6666
3026F50F2AC006C80061227E /* InspectorAreaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3026F50E2AC006C80061227E /* InspectorAreaViewModel.swift */; };
6767
3046374E2CB15FA900180667 /* AutoCompleteCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3046374D2CB15F9200180667 /* AutoCompleteCoordinator.swift */; };
68+
3048523D2D182DA6000CD5CF /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */; };
6869
30AB4EBB2BF718A100ED4431 /* DeveloperSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBA2BF718A100ED4431 /* DeveloperSettings.swift */; };
6970
30AB4EBD2BF71CA800ED4431 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */; };
7071
30AB4EC22BF7253200ED4431 /* KeyValueTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */; };
@@ -458,8 +459,7 @@
458459
6CBA0D512A1BF524002C6FAA /* SegmentedControlImproved.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBA0D502A1BF524002C6FAA /* SegmentedControlImproved.swift */; };
459460
6CBD1BC62978DE53006639D5 /* Font+Caption3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBD1BC52978DE53006639D5 /* Font+Caption3.swift */; };
460461
6CBE1CFB2B71DAA6003AC32E /* Loopable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBE1CFA2B71DAA6003AC32E /* Loopable.swift */; };
461-
6CC00A8B2CBEF150004E8134 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */; };
462-
6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */; };
462+
6CC17B4F2C432AE000834E2C /* (null) in Frameworks */ = {isa = PBXBuildFile; };
463463
6CC17B512C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B502C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift */; };
464464
6CC17B532C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */; };
465465
6CC17B5B2C44258700834E2C /* WindowControllerPropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B5A2C44258700834E2C /* WindowControllerPropertyWrapper.swift */; };
@@ -1339,22 +1339,22 @@
13391339
isa = PBXFrameworksBuildPhase;
13401340
buildActionMask = 2147483647;
13411341
files = (
1342+
3048523D2D182DA6000CD5CF /* CodeEditSourceEditor in Frameworks */,
13421343
6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */,
13431344
6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */,
13441345
58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */,
13451346
6C147C4529A329350089B630 /* OrderedCollections in Frameworks */,
13461347
6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */,
13471348
6CD3CA552C8B508200D83DCD /* (null) in Frameworks */,
13481349
6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */,
1349-
6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */,
1350+
6CC17B4F2C432AE000834E2C /* (null) in Frameworks */,
13501351
30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */,
13511352
6C4E37FC2C73E00700AEE7B5 /* SwiftTerm in Frameworks */,
13521353
6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */,
13531354
6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */,
13541355
6CB446402B6DFF3A00539ED0 /* CodeEditSourceEditor in Frameworks */,
13551356
6C05CF9E2CDE8699006AAECD /* CodeEditSourceEditor in Frameworks */,
13561357
2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */,
1357-
6CD26C852C8F907800ADBA38 /* (null) in Frameworks */,
13581358
30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */,
13591359
6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */,
13601360
6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */,
@@ -3771,7 +3771,6 @@
37713771
6C0617D52BDB4432008C9C42 /* LogStream */,
37723772
6C85BB3F2C2105ED00EB5DEF /* CodeEditKit */,
37733773
6C85BB432C210EFD00EB5DEF /* SwiftUIIntrospect */,
3774-
6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */,
37753774
6C0824A02C5C0C9700A0751E /* SwiftTerm */,
37763775
6CE21E862C650D2C0031B056 /* SwiftTerm */,
37773776
6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */,

CodeEdit.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.

CodeEdit/Features/Editor/AutoCompleteCoordinator.swift

Lines changed: 128 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import AppKit
9+
import SwiftTreeSitter
910
import CodeEditTextView
1011
import CodeEditSourceEditor
1112
import 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
}

CodeEdit/Features/LSP/Views/CompletionItem.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
//
77

88
import SwiftUI
9-
import CodeEditTextView
10-
import LanguageServerProtocol
119
import CodeEditSourceEditor
10+
import LanguageServerProtocol
1211

1312
// TODO: REMOVE Y OFFSET ON 16 PX?
1413

@@ -40,7 +39,7 @@ let fontSizeToRightPadding: [CGFloat: CGFloat] = [
4039
18: 12.5,
4140
]
4241

43-
extension CompletionItem: @retroactive ItemBoxEntry {
42+
extension CompletionItem: @retroactive CodeSuggestionEntry {
4443
public var view: NSView {
4544
NSHostingView(
4645
rootView: HStack(spacing: 0) {

0 commit comments

Comments
 (0)