Async perform text search

This commit is contained in:
1024jp 2022-06-22 23:43:43 +09:00
parent 6fa0142fb0
commit 6403bc71b0
4 changed files with 93 additions and 63 deletions

View File

@ -13,6 +13,7 @@ Change Log
### Improvements
- Optimize the performance for editor splitting.
- Avoid sluggishness by incremental search in large documents.
- Update Swift syntax style to add keywords.
- [trivial] Update French localization.

View File

@ -187,7 +187,8 @@ final class TextFind {
/// - isWrap: Whether the search wraps around.
/// - includingCurrentSelection: Whether includes the current selection to search.
/// - Returns:An FindResult object.
func find(forward: Bool, isWrap: Bool, includingSelection: Bool = false) -> FindResult {
/// - Throws: `CancellationError`
func find(forward: Bool, isWrap: Bool, includingSelection: Bool = false) throws -> FindResult {
assert(forward || !includingSelection)
@ -202,13 +203,23 @@ final class TextFind {
var forwardMatches: [NSRange] = [] // matches after the start location
let forwardRange = NSRange(startLocation..<self.string.length)
self.enumerateMatchs(in: forwardRange) { (matchedRange, _, _) in
self.enumerateMatchs(in: forwardRange) { (matchedRange, _, stop) in
if Task.isCancelled {
stop = true
return
}
forwardMatches.append(matchedRange)
}
try Task.checkCancellation()
var wrappedMatches: [NSRange] = [] // matches before the start location
var intersectionMatches: [NSRange] = [] // matches including the start location
self.enumerateMatchs(in: self.string.range) { (matchedRange, _, stop) in
if Task.isCancelled {
stop = true
return
}
if matchedRange.location >= startLocation {
stop = true
return
@ -220,6 +231,8 @@ final class TextFind {
}
}
try Task.checkCancellation()
var foundRange = forward ? forwardMatches.first : wrappedMatches.last
// wrap search

View File

@ -72,6 +72,7 @@ final class TextFinder: NSResponder, NSMenuItemValidation {
// MARK: Private Properties
private var searchTask: Task<Void, Error>?
private var applicationActivationObserver: AnyCancellable?
private var highlightObserver: AnyCancellable?
@ -154,7 +155,10 @@ final class TextFinder: NSResponder, NSMenuItemValidation {
/// - Returns: The number of found.
func incrementalSearch() {
self.find(forward: true, marksAllMatches: true, isIncremental: true)
self.searchTask?.cancel()
self.searchTask = Task(priority: .userInitiated) {
try await self.find(forward: true, marksAllMatches: true, isIncremental: true)
}
}
@ -188,14 +192,20 @@ final class TextFinder: NSResponder, NSMenuItemValidation {
// find backwards if Shift key pressed
let isShiftPressed = NSEvent.modifierFlags.contains(.shift)
self.find(forward: !isShiftPressed)
self.searchTask?.cancel()
self.searchTask = Task(priority: .userInitiated) {
try await self.find(forward: !isShiftPressed)
}
}
/// Find previous matched string.
@IBAction func findPrevious(_ sender: Any?) {
self.find(forward: false)
self.searchTask?.cancel()
self.searchTask = Task(priority: .userInitiated) {
try await self.find(forward: false)
}
}
@ -268,7 +278,11 @@ final class TextFinder: NSResponder, NSMenuItemValidation {
@IBAction func replaceAndFind(_ sender: Any?) {
self.replace()
self.find(forward: true)
self.searchTask?.cancel()
self.searchTask = Task(priority: .userInitiated) {
try await self.find(forward: true)
}
UserDefaults.standard.appendHistory(self.findString, forKey: .findHistory)
UserDefaults.standard.appendHistory(self.replacementString, forKey: .replaceHistory)
@ -388,7 +402,7 @@ final class TextFinder: NSResponder, NSMenuItemValidation {
///
/// - Parameter forEditing: When true, perform only when the textView is editable.
/// - Returns: The target textView and a TextFind object.
private func prepareTextFind(forEditing: Bool) -> (NSTextView, TextFind)? {
@MainActor private func prepareTextFind(forEditing: Bool) -> (NSTextView, TextFind)? {
guard
let textView = self.client,
@ -435,56 +449,58 @@ final class TextFinder: NSResponder, NSMenuItemValidation {
/// - forward: The flag whether finds forward or backward.
/// - marksAllMatches: Whether marks all matches in the editor.
/// - isIncremental: Whether is the incremental search.
private func find(forward: Bool, marksAllMatches: Bool = false, isIncremental: Bool = false) {
private nonisolated func find(forward: Bool, marksAllMatches: Bool = false, isIncremental: Bool = false) async throws {
assert(forward || !isIncremental)
guard let (textView, textFind) = self.prepareTextFind(forEditing: false) else { return }
guard let (textView, textFind) = await self.prepareTextFind(forEditing: false) else { return }
let result = textFind.find(forward: forward, isWrap: UserDefaults.standard[.findIsWrap], includingSelection: isIncremental)
let result = try textFind.find(forward: forward, isWrap: UserDefaults.standard[.findIsWrap], includingSelection: isIncremental)
// mark all matches
if marksAllMatches, let layoutManager = textView.layoutManager {
layoutManager.groupTemporaryAttributesUpdate(in: textView.string.nsRange) {
layoutManager.removeTemporaryAttribute(.backgroundColor, forCharacterRange: textView.string.nsRange)
for range in result.ranges {
layoutManager.addTemporaryAttribute(.backgroundColor, value: NSColor.unemphasizedSelectedTextBackgroundColor, forCharacterRange: range)
}
}
// unmark either when the client view resigned the key window or when the Find panel closed
self.highlightObserver = NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification)
.sink { [weak self, weak textView] _ in
textView?.unhighlight()
self?.highlightObserver = nil
}
}
// found feedback
if let range = result.range {
textView.select(range: range)
textView.showFindIndicator(for: range)
if result.wrapped {
if let view = textView.enclosingScrollView?.superview {
let hudController = HUDController.instantiate(storyboard: "HUDView")
hudController.symbol = .wrap(reversed: !forward)
hudController.show(in: view)
Task { @MainActor in
// mark all matches
if marksAllMatches, let layoutManager = textView.layoutManager {
layoutManager.groupTemporaryAttributesUpdate(in: textView.string.nsRange) {
layoutManager.removeTemporaryAttribute(.backgroundColor, forCharacterRange: textView.string.nsRange)
for range in result.ranges {
layoutManager.addTemporaryAttribute(.backgroundColor, value: NSColor.unemphasizedSelectedTextBackgroundColor, forCharacterRange: range)
}
}
if let window = NSApp.mainWindow {
NSAccessibility.post(element: window, notification: .announcementRequested,
userInfo: [.announcement: "Search wrapped.".localized])
}
// unmark either when the client view resigned the key window or when the Find panel closed
self.highlightObserver = NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification)
.sink { [weak self, weak textView] _ in
textView?.unhighlight()
self?.highlightObserver = nil
}
}
// found feedback
if let range = result.range {
textView.select(range: range)
textView.showFindIndicator(for: range)
if result.wrapped {
if let view = textView.enclosingScrollView?.superview {
let hudController = HUDController.instantiate(storyboard: "HUDView")
hudController.symbol = .wrap(reversed: !forward)
hudController.show(in: view)
}
if let window = NSApp.mainWindow {
NSAccessibility.post(element: window, notification: .announcementRequested,
userInfo: [.announcement: "Search wrapped.".localized])
}
}
} else if !isIncremental {
NSSound.beep()
}
self.delegate?.textFinder(self, didFind: result.ranges.count, textView: textView)
if !isIncremental {
UserDefaults.standard.appendHistory(self.findString, forKey: .findHistory)
}
} else if !isIncremental {
NSSound.beep()
}
self.delegate?.textFinder(self, didFind: result.ranges.count, textView: textView)
if !isIncremental {
UserDefaults.standard.appendHistory(self.findString, forKey: .findHistory)
}
}

View File

@ -58,12 +58,12 @@ final class TextFindTests: XCTestCase {
textFind = try TextFind(for: text, findString: findString, mode: .textual(options: [], fullWord: false))
result = textFind.find(forward: true, isWrap: false)
result = try textFind.find(forward: true, isWrap: false)
XCTAssertEqual(result.ranges.count, 2)
XCTAssertEqual(result.range, NSRange(location: 0, length: 3))
XCTAssertFalse(result.wrapped)
result = textFind.find(forward: false, isWrap: false)
result = try textFind.find(forward: false, isWrap: false)
XCTAssertEqual(result.ranges.count, 2)
XCTAssertNil(result.range)
XCTAssertFalse(result.wrapped)
@ -71,12 +71,12 @@ final class TextFindTests: XCTestCase {
textFind = try TextFind(for: text, findString: findString, mode: .textual(options: [], fullWord: false), selectedRanges: [NSRange(location: 1, length: 0)])
result = textFind.find(forward: true, isWrap: true)
result = try textFind.find(forward: true, isWrap: true)
XCTAssertEqual(result.ranges.count, 2)
XCTAssertEqual(result.range, NSRange(location: 8, length: 3))
XCTAssertFalse(result.wrapped)
result = textFind.find(forward: false, isWrap: true)
result = try textFind.find(forward: false, isWrap: true)
XCTAssertEqual(result.ranges.count, 2)
XCTAssertEqual(result.range, NSRange(location: 8, length: 3))
XCTAssertTrue(result.wrapped)
@ -84,7 +84,7 @@ final class TextFindTests: XCTestCase {
textFind = try TextFind(for: text, findString: findString, mode: .textual(options: .caseInsensitive, fullWord: false), selectedRanges: [NSRange(location: 1, length: 0)])
result = textFind.find(forward: false, isWrap: true)
result = try textFind.find(forward: false, isWrap: true)
XCTAssertEqual(result.ranges.count, 3)
XCTAssertEqual(result.range, NSRange(location: 16, length: 3))
XCTAssertTrue(result.wrapped)
@ -98,25 +98,25 @@ final class TextFindTests: XCTestCase {
textFind = try TextFind(for: "apples apple Apple", findString: "apple",
mode: .textual(options: .caseInsensitive, fullWord: true))
result = textFind.find(forward: true, isWrap: true)
result = try textFind.find(forward: true, isWrap: true)
XCTAssertEqual(result.ranges.count, 2)
XCTAssertEqual(result.range, NSRange(location: 7, length: 5))
textFind = try TextFind(for: "apples apple Apple", findString: "apple",
mode: .textual(options: [.caseInsensitive, .literal], fullWord: true))
result = textFind.find(forward: true, isWrap: true)
result = try textFind.find(forward: true, isWrap: true)
XCTAssertEqual(result.ranges.count, 2)
XCTAssertEqual(result.range, NSRange(location: 7, length: 5))
textFind = try TextFind(for: "Apfel Äpfel Äpfelchen", findString: "Äpfel",
mode: .textual(options: .diacriticInsensitive, fullWord: true))
result = textFind.find(forward: true, isWrap: true)
result = try textFind.find(forward: true, isWrap: true)
XCTAssertEqual(result.ranges.count, 2)
XCTAssertEqual(result.range, NSRange(location: 0, length: 5))
textFind = try TextFind(for: "イヌら イヌ イヌ", findString: "イヌ",
mode: .textual(options: .widthInsensitive, fullWord: true))
result = textFind.find(forward: true, isWrap: true)
result = try textFind.find(forward: true, isWrap: true)
XCTAssertEqual(result.ranges.count, 2)
XCTAssertEqual(result.range, NSRange(location: 4, length: 2))
}
@ -129,7 +129,7 @@ final class TextFindTests: XCTestCase {
let mode: TextFind.Mode = .regularExpression(options: .caseInsensitive, unescapesReplacement: true)
let textFind = try TextFind(for: text, findString: findString, mode: mode)
let result = textFind.find(forward: false, isWrap: true)
let result = try textFind.find(forward: false, isWrap: true)
XCTAssertEqual(result.ranges.count, 1)
// wrong pattern with raw NSRegularExpression
@ -160,12 +160,12 @@ final class TextFindTests: XCTestCase {
textFind = try TextFind(for: "abcdefg abcdefg ABCDEFG", findString: findString, mode: mode, selectedRanges: [NSRange(location: 1, length: 1)])
result = textFind.find(forward: true, isWrap: true)
result = try textFind.find(forward: true, isWrap: true)
XCTAssertEqual(result.ranges.count, 3)
XCTAssertEqual(result.range, NSRange(location: 9, length: 2))
XCTAssertFalse(result.wrapped)
result = textFind.find(forward: false, isWrap: true)
result = try textFind.find(forward: false, isWrap: true)
XCTAssertEqual(result.ranges.count, 3)
XCTAssertEqual(result.range, NSRange(location: 17, length: 2))
XCTAssertTrue(result.wrapped)
@ -173,12 +173,12 @@ final class TextFindTests: XCTestCase {
textFind = try TextFind(for: "ABCDEFG", findString: findString, mode: mode, selectedRanges: [NSRange(location: 1, length: 1)])
result = textFind.find(forward: true, isWrap: true)
result = try textFind.find(forward: true, isWrap: true)
XCTAssertEqual(result.ranges.count, 1)
XCTAssertEqual(result.range, NSRange(location: 1, length: 2))
XCTAssertTrue(result.wrapped)
result = textFind.find(forward: false, isWrap: true)
result = try textFind.find(forward: false, isWrap: true)
XCTAssertEqual(result.ranges.count, 1)
XCTAssertEqual(result.range, NSRange(location: 1, length: 2))
XCTAssertTrue(result.wrapped)