Skip to content

Commit 7d0c33b

Browse files
committed
Respond to Incomplete Queries Correctly
1 parent f181739 commit 7d0c33b

File tree

3 files changed

+69
-27
lines changed

3 files changed

+69
-27
lines changed

CodeEdit/Features/LSP/Features/AutoComplete/AutoCompleteCoordinator.swift

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class AutoCompleteCoordinator {
2121
private var currentFilterText: String = ""
2222
/// Stores the unfiltered completion items
2323
private var completionItems: [AutoCompleteItem] = []
24+
/// Set to true when the server sends an incomplete list, indicating that we should not filter client-side.
25+
private var receivedIncompleteCompletionItems: Bool = false
2426

2527
init(_ file: CEWorkspaceFile) {
2628
self.file = file
@@ -51,9 +53,10 @@ class AutoCompleteCoordinator {
5153
// Extract the completion items list
5254
switch completions {
5355
case .optionA(let completionItems):
54-
return completionItems.map { AutoCompleteItem($0) }
56+
return completionItems.map { AutoCompleteItem($0) }.sorted()
5557
case .optionB(let completionList):
56-
return completionList.items.map { AutoCompleteItem($0) }
58+
receivedIncompleteCompletionItems = receivedIncompleteCompletionItems || completionList.isIncomplete
59+
return completionList.items.map { AutoCompleteItem($0) }.sorted()
5760
case .none:
5861
return []
5962
}
@@ -64,24 +67,15 @@ class AutoCompleteCoordinator {
6467

6568
/// Filters completion items based on the current partial token input
6669
private func filterCompletionItems(_ items: [AutoCompleteItem]) -> [AutoCompleteItem] {
67-
guard !currentFilterText.isEmpty else {
68-
return items
70+
guard !currentFilterText.isEmpty, !receivedIncompleteCompletionItems else {
71+
return items.sorted()
6972
}
7073

71-
let items = items.filter { item in
72-
let insertText = LSPCompletionItemsUtil.getInsertText(from: item)
73-
let label = item.label.lowercased()
74-
let filterText = currentFilterText.lowercased()
75-
if insertText.lowercased().hasPrefix(filterText) {
76-
return true
77-
}
78-
if label.hasPrefix(filterText) {
79-
return true
80-
}
81-
return false
82-
}
74+
let items = items
75+
.map { ($0.fuzzyMatch(query: currentFilterText), $0) }
76+
.compactMap { $0.0.weight > 0 ? $0.1 : nil }
8377

84-
return items
78+
return items.sorted()
8579
}
8680
}
8781

@@ -91,22 +85,44 @@ extension AutoCompleteCoordinator: CodeSuggestionDelegate {
9185
textView: TextViewController,
9286
cursorPosition: CursorPosition
9387
) async -> (windowPosition: CursorPosition, items: [CodeSuggestionEntry])? {
94-
let tokenSubstringCount = findTreeSitterNodeAtPosition(textView: textView, cursorPosition: cursorPosition)
9588
currentFilterText = ""
89+
let tokenSubstringCount = findTreeSitterNodeAtPosition(textView: textView, cursorPosition: cursorPosition)
90+
91+
let textPosition = Position(line: cursorPosition.start.line - 1, character: cursorPosition.start.column - 1)
9692

97-
var textPosition = Position(line: cursorPosition.start.line - 1, character: cursorPosition.start.column - 1)
98-
var cursorPosition = cursorPosition
9993
// If we are asking for completions in the middle of a token, then
10094
// query the language server for completion items at the start of the token
101-
if currentNode != nil {
102-
textPosition = Position(
95+
// but *only* if we haven't received an incomplete response.
96+
let queryPosition = if currentNode != nil && !receivedIncompleteCompletionItems {
97+
Position(
10398
line: cursorPosition.start.line - 1,
10499
character: cursorPosition.start.column - tokenSubstringCount - 1
105100
)
106-
cursorPosition = CursorPosition(line: textPosition.line + 1, column: textPosition.character + 1)
101+
} else {
102+
textPosition
107103
}
108-
completionItems = await fetchCompletions(position: textPosition)
109-
return (cursorPosition, completionItems)
104+
completionItems = await fetchCompletions(position: queryPosition)
105+
106+
if receivedIncompleteCompletionItems && queryPosition != textPosition {
107+
// We need to re-request this. We've requested the wrong location and since know that the server
108+
// returns incomplete items (meaning we can't filter them ourselves).
109+
return await completionSuggestionsRequested(textView: textView, cursorPosition: cursorPosition)
110+
}
111+
112+
// If we can detect that we're in a node, we still want to adjust the panel to be in the correct position
113+
let cursorPosition: CursorPosition = if currentNode != nil {
114+
CursorPosition(
115+
line: cursorPosition.start.line,
116+
column: cursorPosition.start.column - tokenSubstringCount
117+
)
118+
} else {
119+
CursorPosition(
120+
line: queryPosition.line + 1,
121+
column: queryPosition.character + 1
122+
)
123+
}
124+
125+
return (cursorPosition, filterCompletionItems(completionItems))
110126
}
111127

112128
func findTreeSitterNodeAtPosition(textView: TextViewController, cursorPosition: CursorPosition) -> Int {
@@ -140,7 +156,7 @@ extension AutoCompleteCoordinator: CodeSuggestionDelegate {
140156
textView: TextViewController,
141157
cursorPosition: CursorPosition
142158
) -> [CodeSuggestionEntry]? {
143-
guard var currentNode = currentNode, !completionItems.isEmpty else {
159+
guard var currentNode = currentNode, !completionItems.isEmpty, !receivedIncompleteCompletionItems else {
144160
return nil
145161
}
146162
_ = findTreeSitterNodeAtPosition(textView: textView, cursorPosition: cursorPosition)

CodeEdit/Features/LSP/Features/AutoComplete/AutoCompleteItem.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import LanguageServerProtocol
1111

1212
/// A Near 1:1 of `LanguageServerProtocol`'s `CompletionItem`. Wrapped for compatibility with the CESE's
1313
/// `CodeSuggestionEntry` protocol to deal with some optional bools.
14-
struct AutoCompleteItem: Hashable, Sendable, CodeSuggestionEntry {
14+
struct AutoCompleteItem: Hashable, Sendable, CodeSuggestionEntry, Comparable {
1515
let label: String
1616
let kind: CompletionItemKind?
1717
let detail: String?
@@ -62,4 +62,12 @@ struct AutoCompleteItem: Hashable, Sendable, CodeSuggestionEntry {
6262
self.command = item.command
6363
self.data = item.data
6464
}
65+
66+
static func < (lhs: AutoCompleteItem, rhs: AutoCompleteItem) -> Bool {
67+
lhs.sortText ?? lhs.label < rhs.sortText ?? rhs.label
68+
}
69+
}
70+
71+
extension AutoCompleteItem: FuzzySearchable {
72+
var searchableString: String { filterText ?? label }
6573
}

CodeEdit/Features/Search/FuzzySearch/Collection+FuzzySearch.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,22 @@ extension Collection where Iterator.Element: FuzzySearchable {
2626
$0.result.weight > $1.result.weight
2727
}
2828
}
29+
30+
/// Synchronously performs a fuzzy search on a collection of elements conforming to FuzzySearchable.
31+
///
32+
/// - Parameter query: The query string to match against the elements.
33+
///
34+
/// - Returns: An array of tuples containing FuzzySearchMatchResult and the corresponding element.
35+
///
36+
/// - Note: Because this is an extension on Collection and not only array,
37+
/// you can also use this on sets.
38+
func fuzzySearchSync(query: String) -> [(result: FuzzySearchMatchResult, item: Iterator.Element)] {
39+
return map {
40+
(result: $0.fuzzyMatch(query: query), item: $0)
41+
}.filter {
42+
$0.result.weight > 0
43+
}.sorted {
44+
$0.result.weight > $1.result.weight
45+
}
46+
}
2947
}

0 commit comments

Comments
 (0)