1
1
mirror of https://github.com/qvacua/vimr.git synced 2024-11-28 11:35:35 +03:00

GH-264 Use NSOperation to filer

- Using NSOperation, we can cancel the filtering better
This commit is contained in:
Tae Won Ha 2016-09-05 19:15:54 +02:00
parent 32e564cd3d
commit 923f07cba6
No known key found for this signature in database
GPG Key ID: E40743465B5B8B44
4 changed files with 212 additions and 124 deletions

View File

@ -72,6 +72,7 @@
4B97E2CC1D33F53D00FC0660 /* MainWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B97E2CE1D33F53D00FC0660 /* MainWindow.xib */; };
4B9A15241D2993DA009F9F67 /* Nimble.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4B56F29B1D29926600C1F92E /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
4B9A15261D2993DF009F9F67 /* SwiftNeoVim.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4B2A2BF71D0351810074CE9A /* SwiftNeoVim.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
4BAD84E81D7CA8FC00A79CC3 /* OpenQuicklyFilterOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BAD84E71D7CA8FC00A79CC3 /* OpenQuicklyFilterOperation.swift */; };
4BB1BEA91D48773200463C29 /* RxSwiftUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB1BEA81D48773200463C29 /* RxSwiftUtils.swift */; };
4BCADE081D11ED12004DAD0F /* CocoaExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCADE071D11ED12004DAD0F /* CocoaExtensions.swift */; };
4BD3BF931D32A95800082605 /* MainWindowComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD3BF921D32A95800082605 /* MainWindowComponent.swift */; };
@ -259,6 +260,7 @@
4B854A1A1D31447C00E08DE1 /* NeoVimServer */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = NeoVimServer; sourceTree = BUILT_PRODUCTS_DIR; };
4B854A1C1D31447C00E08DE1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
4B97E2CD1D33F53D00FC0660 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainWindow.xib; sourceTree = "<group>"; };
4BAD84E71D7CA8FC00A79CC3 /* OpenQuicklyFilterOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenQuicklyFilterOperation.swift; sourceTree = "<group>"; };
4BB1BEA81D48773200463C29 /* RxSwiftUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxSwiftUtils.swift; sourceTree = "<group>"; };
4BCADE071D11ED12004DAD0F /* CocoaExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CocoaExtensions.swift; sourceTree = "<group>"; };
4BCF638F1D323CFD00F15CE4 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = neovim/src/nvim; sourceTree = SOURCE_ROOT; };
@ -487,6 +489,7 @@
4BDF501A1D77596500D8FBC3 /* OpenQuicklyWindowManager.swift */,
4BDF50131D7617EA00D8FBC3 /* OpenQuicklyWindowComponent.swift */,
4B22F7F11D7C6B9000929B0E /* ImageAndTextTableCell.swift */,
4BAD84E71D7CA8FC00A79CC3 /* OpenQuicklyFilterOperation.swift */,
4B238BED1D3ED55300CBDD98 /* Preferences */,
);
name = UI;
@ -874,6 +877,7 @@
1929B3F5743967125F357C9F /* Matcher.swift in Sources */,
1929B53876E6952D378C2B30 /* ScoredFileItem.swift in Sources */,
1929BD3F9E609BFADB27584B /* Scorer.swift in Sources */,
4BAD84E81D7CA8FC00A79CC3 /* OpenQuicklyFilterOperation.swift in Sources */,
1929B0E0C3BC59F52713D5A2 /* FoundationCommons.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -16,7 +16,7 @@
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" topStrut="YES"/>
<rect key="contentRect" x="196" y="420" width="365" height="255"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1177"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
<view key="contentView" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="365" height="255"/>
<autoresizingMask key="autoresizingMask"/>

View File

@ -0,0 +1,102 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Cocoa
class OpenQuicklyFilterOperation: NSOperation {
private let chunkSize = 100
private let maxResultCount = 500
private unowned let openQuicklyWindow: OpenQuicklyWindowComponent
private let pattern: String
private let flatFileItems: [FileItem]
private let cwd: NSURL
private var spinLock: OSSpinLock = OS_SPINLOCK_INIT
init(forOpenQuicklyWindow openQuicklyWindow: OpenQuicklyWindowComponent) {
self.openQuicklyWindow = openQuicklyWindow
self.pattern = openQuicklyWindow.pattern
self.cwd = openQuicklyWindow.cwd
self.flatFileItems = openQuicklyWindow.flatFileItems
super.init()
}
override func main() {
self.openQuicklyWindow.scanCondition.lock()
self.openQuicklyWindow.pauseScan = true
defer {
self.openQuicklyWindow.pauseScan = false
self.openQuicklyWindow.scanCondition.broadcast()
self.openQuicklyWindow.scanCondition.unlock()
}
if self.cancelled {
return
}
let sorted: [ScoredFileItem]
if pattern.characters.count == 0 {
let truncatedItems = self.flatFileItems[0...min(self.maxResultCount, self.flatFileItems.count - 1)]
sorted = truncatedItems.map { ScoredFileItem(score: 0, url: $0.url) }
} else {
DispatchUtils.gui { self.openQuicklyWindow.startProgress() }
defer { DispatchUtils.gui { self.openQuicklyWindow.endProgress() } }
let count = self.flatFileItems.count
let chunksCount = Int(ceil(Float(count) / Float(self.chunkSize)))
let useFullPath = pattern.containsString("/")
let cwdPath = self.cwd.path! + "/"
var result = [ScoredFileItem]()
dispatch_apply(chunksCount, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) { [unowned self] idx in
if self.cancelled {
return
}
let startIndex = min(idx * self.chunkSize, count)
let endIndex = min(startIndex + self.chunkSize, count)
let chunkedItems = self.flatFileItems[startIndex..<endIndex]
let chunkedResult: [ScoredFileItem] = chunkedItems.flatMap {
if self.cancelled {
return nil
}
let url = $0.url
let target = useFullPath ? url.path!.stringByReplacingOccurrencesOfString(cwdPath, withString: "")
: url.lastPathComponent!
return ScoredFileItem(score: Scorer.score(target, pattern: self.pattern), url: url)
}
if self.cancelled {
return
}
OSSpinLockLock(&self.spinLock)
result.appendContentsOf(chunkedResult)
OSSpinLockUnlock(&self.spinLock)
}
if self.cancelled {
return
}
sorted = result.sort(>)
}
if self.cancelled {
return
}
DispatchUtils.gui {
let result = Array(sorted[0...min(self.maxResultCount, sorted.count - 1)])
self.openQuicklyWindow.reloadFileView(withScoredItems: result)
}
}
}

View File

@ -10,7 +10,23 @@ import RxCocoa
class OpenQuicklyWindowComponent: WindowComponent, NSWindowDelegate, NSTableViewDelegate, NSTableViewDataSource {
let scanCondition = NSCondition()
var pauseScan = false
private(set) var pattern = ""
private(set) var cwd = NSURL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) {
didSet {
self.cwdPathCompsCount = self.cwd.pathComponents!.count
self.cwdControl.URL = self.cwd
}
}
private(set) var flatFileItems = [FileItem]()
private(set) var fileViewItems = [ScoredFileItem]()
private let userInitiatedScheduler = ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .UserInitiated)
private let searchField = NSTextField(forAutoLayout: ())
private let progressIndicator = NSProgressIndicator(forAutoLayout: ())
private let cwdControl = NSPathControl(forAutoLayout: ())
private let countField = NSTextField(forAutoLayout: ())
private let fileView = NSTableView.standardSourceListTableView()
@ -18,97 +34,32 @@ class OpenQuicklyWindowComponent: WindowComponent, NSWindowDelegate, NSTableView
private let fileItemService: FileItemService
private var count = 0
private var flatFiles = Observable<[FileItem]>.empty()
private var flatFilesDisposeBag = DisposeBag()
private let userInitiatedScheduler = ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .UserInitiated)
private var scoredItems = [ScoredFileItem]()
private var sortedScoredItems = [ScoredFileItem]()
private var perSessionDisposeBag = DisposeBag()
private var cwdPathCompsCount = 0
private var cwd = NSURL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) {
didSet {
self.cwdPathCompsCount = self.cwd.pathComponents!.count
self.cwdControl.URL = self.cwd
}
}
private let searchStream: Observable<String>
private let filterOpQueue = NSOperationQueue()
init(source: Observable<Any>, fileItemService: FileItemService) {
self.fileItemService = fileItemService
self.searchStream = self.searchField.rx_text
.throttle(0.2, scheduler: MainScheduler.instance)
.distinctUntilChanged()
super.init(source: source, nibName: "OpenQuicklyWindow")
self.window.delegate = self
self.filterOpQueue.qualityOfService = .UserInitiated
self.filterOpQueue.name = "open-quickly-filter-operation-queue"
self.searchField.rx_text
.throttle(0.2, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { [unowned self] pattern in
self.flatFiles.
self.filterOpQueue.addOperationWithBlock {
}
return self.flatFiles
}
.subscribe(onNext: { [unowned self] pattern in
NSLog("filtering \(pattern)")
})
// .subscribeOn(self.userInitiatedScheduler)
// .doOnNext { _ in
// self.scoredItems = []
// self.sortedScoredItems = []
// }
// .subscribeOn(MainScheduler.instance)
// .doOnNext { _ in
// self.fileView.reloadData()
// }
// .subscribeOn(self.userInitiatedScheduler)
// .flatMapLatest { [unowned self] pattern -> Observable<[ScoredFileItem]> in
// NSLog("Flat map start: \(pattern)")
// if pattern.characters.count == 0 {
// return self.flatFiles
// .map { fileItems in
// return fileItems.concurrentChunkMap(200) { ScoredFileItem(score: 0, url: $0.url) }
// }
// }
//
// let useFullPath = pattern.containsString("/")
// let cwdPath = self.cwd.path! + "/"
//
// let result: Observable<[ScoredFileItem]> = self.flatFiles
// .map { fileItems in
// return fileItems.concurrentChunkMap(200) { item in
// let url = item.url
// let target = useFullPath ? url.path!.stringByReplacingOccurrencesOfString(cwdPath, withString: "")
// : url.lastPathComponent!
//
// return ScoredFileItem(score: Scorer.score(target, pattern: pattern), url: url)
// }
// }
// NSLog("Flat map end: \(pattern)")
//
// return result
// }
// .doOnNext { [unowned self] items in
// self.scoredItems.appendContentsOf(items)
// self.sortedScoredItems = Array(self.scoredItems.sort(>)[0..<min(201, self.scoredItems.count)])
// }
// .observeOn(MainScheduler.instance)
// .subscribe(onNext: { [unowned self] items in
// self.fileView.reloadData()
// })
.addDisposableTo(self.disposeBag)
}
override func addViews() {
let searchField = self.searchField
let progressIndicator = self.progressIndicator
progressIndicator.indeterminate = true
progressIndicator.displayedWhenStopped = false
progressIndicator.style = .SpinningStyle
progressIndicator.controlSize = .SmallControlSize
let fileView = self.fileView
fileView.intercellSpacing = CGSize(width: 4, height: 4)
@ -136,6 +87,7 @@ class OpenQuicklyWindowComponent: WindowComponent, NSWindowDelegate, NSTableView
let contentView = self.window.contentView!
contentView.addSubview(searchField)
contentView.addSubview(progressIndicator)
contentView.addSubview(fileScrollView)
contentView.addSubview(cwdControl)
contentView.addSubview(countField)
@ -144,6 +96,9 @@ class OpenQuicklyWindowComponent: WindowComponent, NSWindowDelegate, NSTableView
searchField.autoPinEdgeToSuperviewEdge(.Right, withInset: 18)
searchField.autoPinEdgeToSuperviewEdge(.Left, withInset: 18)
progressIndicator.autoAlignAxis(.Horizontal, toSameAxisOfView: searchField)
progressIndicator.autoPinEdge(.Right, toEdge: .Right, ofView: searchField, withOffset: -4)
fileScrollView.autoPinEdge(.Top, toEdge: .Bottom, ofView: searchField, withOffset: 18)
fileScrollView.autoPinEdge(.Right, toEdge: .Right, ofView: searchField)
fileScrollView.autoPinEdge(.Left, toEdge: .Left, ofView: searchField)
@ -162,72 +117,98 @@ class OpenQuicklyWindowComponent: WindowComponent, NSWindowDelegate, NSTableView
return NopDisposable.instance
}
func reloadFileView(withScoredItems items: [ScoredFileItem]) {
self.fileViewItems = items
self.fileView.reloadData()
}
func startProgress() {
self.progressIndicator.startAnimation(self)
}
func endProgress() {
self.progressIndicator.stopAnimation(self)
}
func show(forMainWindow mainWindow: MainWindowComponent) {
self.cwd = mainWindow.cwd
let flatFiles = self.fileItemService.flatFileItems(ofUrl: self.cwd)
.subscribeOn(self.userInitiatedScheduler)
.replayAll()
flatFiles.connect().addDisposableTo(self.flatFilesDisposeBag)
self.searchStream
.subscribe(onNext: { [unowned self] pattern in
self.pattern = pattern
self.resetAndAddFilterOperation()
})
.addDisposableTo(self.perSessionDisposeBag)
flatFiles
.subscribeOn(self.userInitiatedScheduler)
.doOnNext{ [unowned self] items in
self.scanCondition.lock()
while self.pauseScan {
self.scanCondition.wait()
}
self.scanCondition.unlock()
self.flatFileItems.appendContentsOf(items)
self.resetAndAddFilterOperation()
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [unowned self] items in
self.count += items.count
self.countField.stringValue = "\(self.count) items"
})
.addDisposableTo(self.flatFilesDisposeBag)
self.flatFiles = flatFiles
.addDisposableTo(self.perSessionDisposeBag)
self.show()
self.searchField.becomeFirstResponder()
}
private func resetAndAddFilterOperation() {
self.filterOpQueue.cancelAllOperations()
let op = OpenQuicklyFilterOperation(forOpenQuicklyWindow: self)
self.filterOpQueue.addOperation(op)
}
}
// MARK: - NSTableViewDataSource
extension OpenQuicklyWindowComponent {
func numberOfRowsInTableView(_: NSTableView) -> Int {
return self.sortedScoredItems.count
return self.fileViewItems.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
let url = self.sortedScoredItems[row].url
let pathComps = url.pathComponents!
let truncatedPathComps = pathComps[self.cwdPathCompsCount..<pathComps.count]
let name = truncatedPathComps.last!
let pathInfo = truncatedPathComps.dropLast().reverse().joinWithSeparator(" / ")
func tableView(tableView: NSTableView, viewForTableColumn _: NSTableColumn?, row: Int) -> NSView? {
let cachedCell = tableView.makeViewWithIdentifier("file-view-row", owner: self)
let cell = cachedCell as? ImageAndTextTableCell ?? ImageAndTextTableCell(withIdentifier: "file-view-row")
let result = NSMutableAttributedString(string: "\(name)\(pathInfo)")
result.addAttribute(NSForegroundColorAttributeName, value: NSColor.lightGrayColor(),
range: NSRange(location:name.characters.count, length: pathInfo.characters.count + 3))
let cell = tableView.makeViewWithIdentifier("file-view-row", owner: self) as? ImageAndTextTableCell
?? ImageAndTextTableCell(withIdentifier: "file-view-row")
cell.textField.attributedStringValue = result
let url = self.fileViewItems[row].url
cell.textField.attributedStringValue = self.rowText(forUrl: url)
cell.imageView.image = self.fileItemService.icon(forUrl: url)
return cell
}
// func tableView(_: NSTableView, objectValueForTableColumn _: NSTableColumn?, row: Int) -> AnyObject? {
// let url = self.sortedScoredItems[row].url
// let pathComps = self.sortedScoredItems[row].url.pathComponents!
// let truncatedPathComps = pathComps[self.cwdPathCompsCount..<pathComps.count]
// let name = truncatedPathComps.last!
// let pathInfo = truncatedPathComps.dropLast().reverse().joinWithSeparator("/")
//
// let textAttachment = NSTextAttachment()
// textAttachment.image = self.fileItemService.icon(forUrl: url)
//
// let result: NSMutableAttributedString = NSAttributedString(attachment: textAttachment).mutableCopy() as! NSMutableAttributedString
// result.mutableString.appendString("\(name) \(pathInfo)")
// result.addAttribute(NSForegroundColorAttributeName, value: NSColor.lightGrayColor(),
// range: NSRange(location:name.characters.count, length: pathInfo.characters.count + 3))
//
// return result
// }
private func rowText(forUrl url: NSURL) -> NSAttributedString {
let pathComps = url.pathComponents!
let truncatedPathComps = pathComps[self.cwdPathCompsCount..<pathComps.count]
let name = truncatedPathComps.last!
if truncatedPathComps.dropLast().isEmpty {
return NSMutableAttributedString(string: name)
}
let rowText: NSMutableAttributedString
let pathInfo = truncatedPathComps.dropLast().reverse().joinWithSeparator(" / ")
rowText = NSMutableAttributedString(string: "\(name)\(pathInfo)")
rowText.addAttribute(NSForegroundColorAttributeName,
value: NSColor.lightGrayColor(),
range: NSRange(location:name.characters.count,
length: pathInfo.characters.count + 3))
return rowText
}
}
// MARK: - NSTableViewDelegate
@ -236,22 +217,23 @@ extension OpenQuicklyWindowComponent {
func tableViewSelectionDidChange(_: NSNotification) {
// NSLog("\(#function): selection changed")
}
// func tableView(tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
// return 34
// }
}
// MARK: - NSWindowDelegate
extension OpenQuicklyWindowComponent {
func windowWillClose(notification: NSNotification) {
self.flatFilesDisposeBag = DisposeBag()
self.flatFiles = Observable.empty()
self.filterOpQueue.cancelAllOperations()
self.endProgress()
self.perSessionDisposeBag = DisposeBag()
self.pauseScan = false
self.count = 0
self.scoredItems = []
self.sortedScoredItems = []
self.pattern = ""
self.flatFileItems = []
self.fileViewItems = []
self.searchField.stringValue = ""
self.countField.stringValue = "0 items"