77
88import Foundation
99
10+ extension WorkspaceDocument . SearchState : @unchecked Sendable { }
11+
1012extension WorkspaceDocument . SearchState {
1113 /// Creates a search term based on the given query and search mode.
1214 ///
@@ -94,9 +96,6 @@ extension WorkspaceDocument.SearchState {
9496 }
9597
9698 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
10099 let regexPattern = getRegexPattern ( query)
101100
102101 guard let indexer = indexer else {
@@ -105,24 +104,32 @@ extension WorkspaceDocument.SearchState {
105104 }
106105
107106 let asyncController = SearchIndexer . AsyncManager ( index: indexer)
108-
109107 let evaluateResultGroup = DispatchGroup ( )
110108 let evaluateSearchQueue = DispatchQueue ( label: " app.codeedit.CodeEdit.EvaluateSearch " )
111109
112110 let searchStream = await asyncController. search ( query: searchQuery, 20 )
113111 for try await result in searchStream {
114112 for file in result. results {
113+ let fileURL = file. url
114+ let fileScore = file. score
115+ let capturedRegexPattern = regexPattern
116+
115117 evaluateSearchQueue. async ( group: evaluateResultGroup) {
116118 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)
126133 }
127134 evaluateResultGroup. leave ( )
128135 }
@@ -131,33 +138,33 @@ extension WorkspaceDocument.SearchState {
131138 }
132139
133140 evaluateResultGroup. notify ( queue: evaluateSearchQueue) {
134- self . setSearchResults ( )
141+ Task { @MainActor [ weak self] in
142+ self ? . setSearchResults ( )
143+ }
135144 }
136145 }
137146
138147 /// Appends a new search result to the temporary search results array on the main thread.
139148 ///
140149 /// - Parameters:
141150 /// - newResult: The `SearchResultModel` to be appended to the temporary search results.
151+ @MainActor
142152 func appendNewResultsToTempResults( newResult: SearchResultModel ) {
143- DispatchQueue . main. async {
144- self . tempSearchResults. append ( newResult)
145- }
153+ self . tempSearchResults. append ( newResult)
146154 }
147155
148156 /// Sets the search results by updating various properties on the main thread.
149157 /// This function updates `findNavigatorStatus`, `searchResult`, `searchResultCount`, and `searchResultsFileCount`
150158 /// and sets the `tempSearchResults` to an empty array.
151159 /// - Important: Call this function when you are ready to
152160 /// display or use the final search results.
161+ @MainActor
153162 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 = [ ]
161168 }
162169
163170 /// Evaluates a search query within the content of a file and updates
@@ -183,7 +190,10 @@ extension WorkspaceDocument.SearchState {
183190 guard let data = try ? Data ( contentsOf: searchResult. file. url) else {
184191 return
185192 }
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+ }
187197
188198 // Attempt to create a regular expression from the provided query
189199 guard let regex = try ? NSRegularExpression (
@@ -343,4 +353,29 @@ extension WorkspaceDocument.SearchState {
343353 self . findNavigatorStatus = . none
344354 }
345355 }
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+ }
346381}
0 commit comments