@@ -52,6 +52,7 @@ extension WorkspaceDocument {
5252
5353 Task . detached {
5454 let filePaths = self . getFileURLs ( at: url)
55+
5556 let asyncController = SearchIndexer . AsyncManager ( index: indexer)
5657 var lastProgress : Double = 0
5758
@@ -89,6 +90,23 @@ extension WorkspaceDocument {
8990 return enumerator? . allObjects as? [ URL ] ?? [ ]
9091 }
9192
93+ /// Retrieves the contents of a files from the specified file paths.
94+ ///
95+ /// - Parameter filePaths: An array of file URLs representing the paths of the files.
96+ ///
97+ /// - Returns: An array of `TextFile` objects containing the standardized file URLs and text content.
98+ func getfileContent( from filePaths: [ URL ] ) async -> [ SearchIndexer . AsyncManager . TextFile ] {
99+ var textFiles = [ SearchIndexer . AsyncManager. TextFile] ( )
100+ for file in filePaths {
101+ if let content = try ? String ( contentsOf: file) {
102+ textFiles. append (
103+ SearchIndexer . AsyncManager. TextFile ( url: file. standardizedFileURL, text: content)
104+ )
105+ }
106+ }
107+ return textFiles
108+ }
109+
92110 /// Creates a search term based on the given query and search mode.
93111 ///
94112 /// - Parameter query: The original user query string.
@@ -117,7 +135,7 @@ extension WorkspaceDocument {
117135 /// within each file for the given string.
118136 ///
119137 /// This method will update
120- /// ``WorkspaceDocument/SearchState-swift.class/searchResult``,
138+ /// ``WorkspaceDocument/SearchState-swift.class/searchResult``,
121139 /// ``WorkspaceDocument/SearchState-swift.class/searchResultsFileCount``
122140 /// and ``WorkspaceDocument/SearchState-swift.class/searchResultCount`` with any matched
123141 /// search results. See ``SearchResultModel`` and ``SearchResultMatchModel``
@@ -138,16 +156,16 @@ extension WorkspaceDocument {
138156
139157 let searchStream = await asyncController. search ( query: searchQuery, 20 )
140158 for try await result in searchStream {
141- let urls2 : [ ( URL , Float ) ] = result. results. map {
159+ let urls : [ ( URL , Float ) ] = result. results. map {
142160 ( $0. url, $0. score)
143161 }
144162
145- for (url, score) in urls2 {
163+ for (url, score) in urls {
146164 evaluateSearchQueue. async ( group: evaluateResultGroup) {
147165 evaluateResultGroup. enter ( )
148166 Task {
149167 var newResult = SearchResultModel ( file: CEWorkspaceFile ( url: url) , score: score)
150- await self . evaluateResult ( query: query. lowercased ( ) , searchResult: & newResult)
168+ await self . evaluateFile ( query: query. lowercased ( ) , searchResult: & newResult)
151169
152170 // Check if the new result has any line matches.
153171 if !newResult. lineMatches. isEmpty {
@@ -190,115 +208,172 @@ extension WorkspaceDocument {
190208 }
191209 }
192210
193- /// Adds line matchings to a `SearchResultsViewModel` array.
194- /// That means if a search result is a file, and the search term appears in the file,
195- /// the function will add the line number, line content, and keyword range to the `SearchResultsViewModel`.
211+ /// Evaluates a search query within the content of a file and updates
212+ /// the provided `SearchResultModel` with matching occurrences.
196213 ///
197214 /// - Parameters:
198- /// - query: The search query string.
199- /// - searchResults: An inout parameter containing the array of `SearchResultsViewModel` to be evaluated.
200- /// It will be modified to include line matches.
201- private func evaluateResult( query: String , searchResult: inout SearchResultModel ) async {
202- let searchResultCopy = searchResult
203- var newMatches = [ SearchResultMatchModel] ( )
204-
215+ /// - query: The search query to be evaluated, potentially containing a regular expression.
216+ /// - searchResult: The `SearchResultModel` object to be updated with the matching occurrences.
217+ ///
218+ /// This function retrieves the content of a file specified in the `searchResult` parameter
219+ /// and applies a search query using a regular expression.
220+ /// It then iterates over the matches found in the file content,
221+ /// creating `SearchResultMatchModel` instances for each match.
222+ /// The resulting matches are appended to the `lineMatches` property of the `searchResult`.
223+ /// Line matches are the preview lines that are shown in the search results.
224+ ///
225+ /// # Example Usage
226+ /// ```swift
227+ /// var resultModel = SearchResultModel()
228+ /// await evaluateFile(query: "example", searchResult: &resultModel)
229+ /// ```
230+ private func evaluateFile( query: String , searchResult: inout SearchResultModel ) async {
205231 guard let data = try ? Data ( contentsOf: searchResult. file. url) ,
206- let string = String ( data: data, encoding: . utf8) else {
232+ let fileContent = String ( data: data, encoding: . utf8) else {
207233 return
208234 }
209235
210- await withTaskGroup ( of: SearchResultMatchModel ? . self) { group in
211- for (lineNumber, line) in string. components ( separatedBy: . newlines) . lazy. enumerated ( ) {
212- group. addTask {
213- let rawNoSpaceLine = line. trimmingCharacters ( in: . whitespacesAndNewlines)
214- let noSpaceLine = rawNoSpaceLine. lowercased ( )
215- if self . lineContainsSearchTerm ( line: noSpaceLine, term: query) {
216- let matches = noSpaceLine. ranges ( of: query) . map { range in
217- return [ lineNumber, rawNoSpaceLine, range]
218- }
236+ // Attempt to create a regular expression from the provided query
237+ guard let regex = try ? NSRegularExpression ( pattern: query, options: [ . caseInsensitive] ) else {
238+ return
239+ }
219240
220- for match in matches {
221- if let lineNumber = match [ 0 ] as? Int ,
222- let lineContent = match [ 1 ] as? String ,
223- let keywordRange = match [ 2 ] as? Range < String . Index > {
224- let matchModel = SearchResultMatchModel (
225- lineNumber: lineNumber,
226- file: searchResultCopy. file,
227- lineContent: lineContent,
228- keywordRange: keywordRange
229- )
230-
231- return matchModel
232- }
233- }
234- }
235- return nil
236- }
237- for await groupRes in group {
238- if let groupRes {
239- newMatches. append ( groupRes)
240- }
241- }
241+ // Find all matches of the query within the file content using the regular expression
242+ let matches = regex. matches ( in: fileContent, range: NSRange ( location: 0 , length: fileContent. utf16. count) )
243+
244+ var newMatches = [ SearchResultMatchModel] ( )
245+
246+ // Process each match and add it to the array of `newMatches`
247+ for match in matches {
248+ if let matchRange = Range ( match. range, in: fileContent) {
249+ let matchWordLength = match. range. length
250+ let matchModel = createMatchModel (
251+ from: matchRange,
252+ fileContent: fileContent,
253+ file: searchResult. file,
254+ matchWordLength: matchWordLength
255+ )
256+ newMatches. append ( matchModel)
242257 }
243258 }
259+
244260 searchResult. lineMatches = newMatches
245261 }
246262
247- // see if the line contains search term, obeying selectedMode
248- // swiftlint:disable:next cyclomatic_complexity
249- func lineContainsSearchTerm( line rawLine: String , term searchterm: String ) -> Bool {
250- var line = rawLine
251- if line. hasSuffix ( " " ) { line. removeLast ( ) }
252- if line. hasPrefix ( " " ) { line. removeFirst ( ) }
253-
254- // Text
255- let findMode = selectedMode [ 1 ]
256- if findMode == . Text {
257- let textMatching = selectedMode [ 2 ]
258- let textContainsSearchTerm = line. contains ( searchterm)
259- guard textContainsSearchTerm == true else { return false }
260- guard textMatching != . Containing else { return textContainsSearchTerm }
261-
262- // get the index of the search term's appearance in the line
263- // and get the characters to the left and right
264- let appearances = line. appearancesOfSubstring ( substring: searchterm, toLeft: 1 , toRight: 1 )
265- var foundMatch = false
266- for appearance in appearances {
267- let appearanceString = String ( line [ appearance] )
268- guard appearanceString. count >= 2 else { continue }
269-
270- var startsWith = false
271- var endsWith = false
272- if appearanceString. hasPrefix ( searchterm) ||
273- !appearanceString. first!. isLetter ||
274- !( appearanceString. character ( at: 2 ) ? . isLetter ?? false ) {
275- startsWith = true
276- }
277- if appearanceString. hasSuffix ( searchterm) ||
278- !appearanceString. last!. isLetter ||
279- !( appearanceString. character ( at: appearanceString. count- 2 ) ? . isLetter ?? false ) {
280- endsWith = true
281- }
263+ /// Creates a `SearchResultMatchModel` instance based on the provided parameters,
264+ /// representing a matching occurrence within a file.
265+ ///
266+ /// - Parameters:
267+ /// - matchRange: The range of the matched substring within the entire file content.
268+ /// - fileContent: The content of the file where the match was found.
269+ /// - file: The `CEWorkspaceFile` object representing the file containing the match.
270+ /// - matchWordLength: The length of the matched substring.
271+ ///
272+ /// - Returns: A `SearchResultMatchModel` instance representing the matching occurrence.
273+ ///
274+ /// This function is responsible for constructing a `SearchResultMatchModel`
275+ /// based on the provided parameters. It extracts the relevant portions of the file content,
276+ /// including the lines before and after the match, and combines them into a final line.
277+ /// The resulting model includes information about the match's range within the file,
278+ /// the file itself, the content of the line containing the match,
279+ /// and the range of the matched keyword within that line.
280+ private func createMatchModel(
281+ from matchRange: Range < String . Index > ,
282+ fileContent: String ,
283+ file: CEWorkspaceFile ,
284+ matchWordLength: Int
285+ ) -> SearchResultMatchModel {
286+ let preLine = extractPreLine ( from: matchRange, fileContent: fileContent)
287+ let keywordRange = extractKeywordRange ( from: preLine, matchWordLength: matchWordLength)
288+ let postLine = extractPostLine ( from: matchRange, fileContent: fileContent)
289+
290+ let finalLine = preLine + postLine
291+
292+ return SearchResultMatchModel (
293+ rangeWithinFile: matchRange,
294+ file: file,
295+ lineContent: finalLine,
296+ keywordRange: keywordRange
297+ )
298+ }
282299
283- switch textMatching {
284- case . MatchingWord:
285- foundMatch = startsWith && endsWith ? true : foundMatch
286- case . StartingWith:
287- foundMatch = startsWith ? true : foundMatch
288- case . EndingWith:
289- foundMatch = endsWith ? true : foundMatch
290- default : continue
291- }
292- }
293- return foundMatch
294- } else if findMode == . RegularExpression {
295- guard let regex = try ? NSRegularExpression ( pattern: searchterm) else { return false }
296- // swiftlint:disable:next legacy_constructor
297- return regex. firstMatch ( in: String ( line) , range: NSMakeRange ( 0 , line. utf16. count) ) != nil
298- }
300+ /// Extracts the line preceding a matching occurrence within a file.
301+ ///
302+ /// - Parameters:
303+ /// - matchRange: The range of the matched substring within the entire file content.
304+ /// - fileContent: The content of the file where the match was found.
305+ ///
306+ /// - Returns: A string representing the line preceding the match.
307+ ///
308+ /// This function retrieves the line preceding a matching occurrence within the provided file content.
309+ /// It considers a context of up to 60 characters before the match and clips the result to the last
310+ /// occurrence of a newline character, ensuring that only the line containing the search term is displayed.
311+ /// The extracted line is then trimmed of leading and trailing whitespaces and
312+ /// newline characters before being returned.
313+ private func extractPreLine( from matchRange: Range < String . Index > , fileContent: String ) -> String {
314+ let preRangeStart = fileContent. index (
315+ matchRange. lowerBound,
316+ offsetBy: - 60 ,
317+ limitedBy: fileContent. startIndex
318+ ) ?? fileContent. startIndex
319+
320+ let preRangeEnd = matchRange. upperBound
321+ let preRange = preRangeStart..< preRangeEnd
322+
323+ let preLineWithNewLines = fileContent [ preRange]
324+ // Clip the range of the preview to the last occurrence of a new line
325+ let lastNewLineIndexInPreLine = preLineWithNewLines. lastIndex ( of: " \n " ) ?? preLineWithNewLines. startIndex
326+ let preLineWithNewLinesPrefix = preLineWithNewLines [ lastNewLineIndexInPreLine... ]
327+ return preLineWithNewLinesPrefix. trimmingCharacters ( in: . whitespacesAndNewlines)
328+ }
329+
330+ /// Extracts the range of the search term within the line preceding a matching occurrence.
331+ ///
332+ /// - Parameters:
333+ /// - preLine: The line preceding the matching occurrence within the file content.
334+ /// - matchWordLength: The length of the search term.
335+ ///
336+ /// - Returns: A range representing the position of the search term within the `preLine`.
337+ ///
338+ /// This function calculates the range of the search term within
339+ /// the provided line preceding a matching occurrence.
340+ /// It considers the length of the search term to determine
341+ /// the lower and upper bounds of the keyword range within the line.
342+ private func extractKeywordRange( from preLine: String , matchWordLength: Int ) -> Range < String . Index > {
343+ let keywordLowerbound = preLine. index (
344+ preLine. endIndex,
345+ offsetBy: - matchWordLength,
346+ limitedBy: preLine. startIndex
347+ ) ?? preLine. endIndex
348+ let keywordUpperbound = preLine. endIndex
349+ return keywordLowerbound..< keywordUpperbound
350+ }
299351
300- return false
301- // TODO: references and definitions
352+ /// Extracts the line following a matching occurrence within a file.
353+ ///
354+ /// - Parameters:
355+ /// - matchRange: The range of the matched substring within the entire file content.
356+ /// - fileContent: The content of the file where the match was found.
357+ ///
358+ /// - Returns: A string representing the line following the match.
359+ ///
360+ /// This function retrieves the line following a matching occurrence within the provided file content.
361+ /// It considers a context of up to 60 characters after the match and clips the result to the first
362+ /// occurrence of a newline character, ensuring that only the relevant portion of the line is displayed.
363+ /// The extracted line is then converted to a string before being returned.
364+ private func extractPostLine( from matchRange: Range < String . Index > , fileContent: String ) -> String {
365+ let postRangeStart = matchRange. upperBound
366+ let postRangeEnd = fileContent. index (
367+ matchRange. upperBound,
368+ offsetBy: 60 ,
369+ limitedBy: fileContent. endIndex
370+ ) ?? fileContent. endIndex
371+
372+ let postRange = postRangeStart..< postRangeEnd
373+ let postLineWithNewLines = fileContent [ postRange]
374+
375+ let firstNewLineIndexInPostLine = postLineWithNewLines. firstIndex ( of: " \n " ) ?? postLineWithNewLines. endIndex
376+ return String ( postLineWithNewLines [ ..< firstNewLineIndexInPostLine] )
302377 }
303378
304379 /// Resets the search results along with counts for overall results and file-specific results.
0 commit comments