7
7
8
8
import Foundation
9
9
10
+ extension WorkspaceDocument . SearchState : @unchecked Sendable { }
11
+
10
12
extension WorkspaceDocument . SearchState {
11
13
/// Creates a search term based on the given query and search mode.
12
14
///
@@ -94,9 +96,6 @@ extension WorkspaceDocument.SearchState {
94
96
}
95
97
96
98
let searchQuery = getSearchTerm ( query)
97
-
98
- // The regexPattern is only used for the evaluateFile function
99
- // to ensure that the search terms are highlighted correctly
100
99
let regexPattern = getRegexPattern ( query)
101
100
102
101
guard let indexer = indexer else {
@@ -105,24 +104,32 @@ extension WorkspaceDocument.SearchState {
105
104
}
106
105
107
106
let asyncController = SearchIndexer . AsyncManager ( index: indexer)
108
-
109
107
let evaluateResultGroup = DispatchGroup ( )
110
108
let evaluateSearchQueue = DispatchQueue ( label: " app.codeedit.CodeEdit.EvaluateSearch " )
111
109
112
110
let searchStream = await asyncController. search ( query: searchQuery, 20 )
113
111
for try await result in searchStream {
114
112
for file in result. results {
113
+ let fileURL = file. url
114
+ let fileScore = file. score
115
+ let capturedRegexPattern = regexPattern
116
+
115
117
evaluateSearchQueue. async ( group: evaluateResultGroup) {
116
118
evaluateResultGroup. enter ( )
117
- Task {
118
- var newResult = SearchResultModel ( file: CEWorkspaceFile ( url: file. url) , score: file. score)
119
- await self . evaluateFile ( query: regexPattern, searchResult: & newResult)
120
-
121
- // Check if the new result has any line matches.
122
- if !newResult. lineMatches. isEmpty {
123
- // The function needs to be called because,
124
- // we are trying to modify the array from within a concurrent context.
125
- self . appendNewResultsToTempResults ( newResult: newResult)
119
+ Task { [ weak self] in
120
+ guard let self else {
121
+ evaluateResultGroup. leave ( )
122
+ return
123
+ }
124
+
125
+ let result = await self . evaluateSearchResult (
126
+ fileURL: fileURL,
127
+ fileScore: fileScore,
128
+ regexPattern: capturedRegexPattern
129
+ )
130
+
131
+ if let result = result {
132
+ await self . appendNewResultsToTempResults ( newResult: result)
126
133
}
127
134
evaluateResultGroup. leave ( )
128
135
}
@@ -131,33 +138,33 @@ extension WorkspaceDocument.SearchState {
131
138
}
132
139
133
140
evaluateResultGroup. notify ( queue: evaluateSearchQueue) {
134
- self . setSearchResults ( )
141
+ Task { @MainActor [ weak self] in
142
+ self ? . setSearchResults ( )
143
+ }
135
144
}
136
145
}
137
146
138
147
/// Appends a new search result to the temporary search results array on the main thread.
139
148
///
140
149
/// - Parameters:
141
150
/// - newResult: The `SearchResultModel` to be appended to the temporary search results.
151
+ @MainActor
142
152
func appendNewResultsToTempResults( newResult: SearchResultModel ) {
143
- DispatchQueue . main. async {
144
- self . tempSearchResults. append ( newResult)
145
- }
153
+ self . tempSearchResults. append ( newResult)
146
154
}
147
155
148
156
/// Sets the search results by updating various properties on the main thread.
149
157
/// This function updates `findNavigatorStatus`, `searchResult`, `searchResultCount`, and `searchResultsFileCount`
150
158
/// and sets the `tempSearchResults` to an empty array.
151
159
/// - Important: Call this function when you are ready to
152
160
/// display or use the final search results.
161
+ @MainActor
153
162
func setSearchResults( ) {
154
- DispatchQueue . main. async {
155
- self . searchResult = self . tempSearchResults. sorted { $0. score > $1. score }
156
- self . searchResultsCount = self . tempSearchResults. map { $0. lineMatches. count } . reduce ( 0 , + )
157
- self . searchResultsFileCount = self . tempSearchResults. count
158
- self . findNavigatorStatus = . found
159
- self . tempSearchResults = [ ]
160
- }
163
+ self . searchResult = self . tempSearchResults. sorted { $0. score > $1. score }
164
+ self . searchResultsCount = self . tempSearchResults. map { $0. lineMatches. count } . reduce ( 0 , + )
165
+ self . searchResultsFileCount = self . tempSearchResults. count
166
+ self . findNavigatorStatus = . found
167
+ self . tempSearchResults = [ ]
161
168
}
162
169
163
170
/// Evaluates a search query within the content of a file and updates
@@ -183,7 +190,10 @@ extension WorkspaceDocument.SearchState {
183
190
guard let data = try ? Data ( contentsOf: searchResult. file. url) else {
184
191
return
185
192
}
186
- let fileContent = String ( decoding: data, as: UTF8 . self)
193
+ guard let fileContent = String ( bytes: data, encoding: . utf8) else {
194
+ await setStatus ( . failed( errorMessage: " Failed to decode file content. " ) )
195
+ return
196
+ }
187
197
188
198
// Attempt to create a regular expression from the provided query
189
199
guard let regex = try ? NSRegularExpression (
@@ -343,4 +353,29 @@ extension WorkspaceDocument.SearchState {
343
353
self . findNavigatorStatus = . none
344
354
}
345
355
}
356
+
357
+ /// Evaluates a matched file to determine if it contains any search matches.
358
+ /// Requires a file score from the search model.
359
+ ///
360
+ /// Evaluates the file's contents asynchronously.
361
+ ///
362
+ /// - Parameters:
363
+ /// - fileURL: The `URL` of the file to evaluate.
364
+ /// - fileScore: The file's score from a ``SearchIndexer``
365
+ /// - regexPattern: The pattern to evaluate against the file's contents.
366
+ /// - Returns: `nil` if there are no relevant search matches, or a search result if matches are found.
367
+ private func evaluateSearchResult(
368
+ fileURL: URL ,
369
+ fileScore: Float ,
370
+ regexPattern: String
371
+ ) async -> SearchResultModel ? {
372
+ var newResult = SearchResultModel (
373
+ file: CEWorkspaceFile ( url: fileURL) ,
374
+ score: fileScore
375
+ )
376
+
377
+ await evaluateFile ( query: regexPattern, searchResult: & newResult)
378
+
379
+ return newResult. lineMatches. isEmpty ? nil : newResult
380
+ }
346
381
}
0 commit comments