Skip to content

Commit c276d03

Browse files
authored
Another search speed up 💨 (#1500)
* Added regX patters instead of normal file search * Update SearchIndexer+InternalMethods.swift * Performance update * fix: out of bounds * That ain't working :( * Fixed line previews for new search function Chnaged `lineNumber` to `rangeWIthinFile` which is a Range<String.Index> instead of a int. Removed `hasKeywordInfo` from `SearchResultMatchmodel` * Split the evaluateFile function into smaller, focused functions * Vertical Whitespace Violation
1 parent 8bd8edc commit c276d03

File tree

3 files changed

+185
-120
lines changed

3 files changed

+185
-120
lines changed

CodeEdit/Features/Documents/Indexer/SearchIndexer+InternalMethods.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ extension SearchIndexer {
7373
if isLeaf, inParentDocument != nil,
7474
kSKDocumentStateNotIndexed != SKIndexGetDocumentState(index, inParentDocument) {
7575
if let temp = SKDocumentCopyURL(inParentDocument) {
76-
let baseURL = temp.takeUnretainedValue()
76+
let baseURL = temp.takeUnretainedValue() as URL
7777
let documentID = SKIndexGetDocumentID(index, inParentDocument)
7878
docs.append(
7979
DocumentID(
80-
url: temp.takeRetainedValue() as URL,
80+
url: baseURL,
8181
docuemnt: inParentDocument!,
82-
documentID: SKIndexGetDocumentID(index, inParentDocument)
82+
documentID: documentID
8383
)
8484
)
8585
}

CodeEdit/Features/Documents/WorkspaceDocument+Search.swift

Lines changed: 174 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)