From 32e564cd3dec8a1bda335a60c62efbe4b1a6d29f Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Sat, 3 Sep 2016 23:13:52 +0200 Subject: [PATCH] GH-264 Implement filter --- SwiftNeoVim/NeoVimAgent.m | 3 +- VimR.xcodeproj/project.pbxproj | 8 ++ VimR/AppKitCommons.swift | 16 +-- VimR/Base.lproj/OpenQuicklyWindow.xib | 4 +- VimR/FileItemService.swift | 24 +++- VimR/ImageAndTextTableCell.swift | 45 ++++++++ VimR/OpenQuicklyWindowComponent.swift | 156 +++++++++++++++++++------- VimR/ScoredFileItem.swift | 2 +- VimR/Scorer.swift | 4 +- VimRTests/ScorerTest.swift | 20 ++++ 10 files changed, 218 insertions(+), 64 deletions(-) create mode 100644 VimR/ImageAndTextTableCell.swift create mode 100644 VimRTests/ScorerTest.swift diff --git a/SwiftNeoVim/NeoVimAgent.m b/SwiftNeoVim/NeoVimAgent.m index c19619cf..4fc22739 100644 --- a/SwiftNeoVim/NeoVimAgent.m +++ b/SwiftNeoVim/NeoVimAgent.m @@ -288,7 +288,8 @@ static CFDataRef local_server_callback(CFMessagePortRef local, SInt32 msgid, CFD return nil; } - return [NSData dataWithData:(__bridge NSData *) responseData]; + NSData *result = (__bridge_transfer NSData *) responseData; + return result; } - (NSString *)neoVimServerExecutablePath { diff --git a/VimR.xcodeproj/project.pbxproj b/VimR.xcodeproj/project.pbxproj index a4c262bf..98be6a95 100644 --- a/VimR.xcodeproj/project.pbxproj +++ b/VimR.xcodeproj/project.pbxproj @@ -39,6 +39,8 @@ 4B0C905B1D5DED69007753A3 /* NeoVimBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B0C905A1D5DED69007753A3 /* NeoVimBuffer.m */; }; 4B0E22581D5DEDC700C072E6 /* NeoVimBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B0C90591D5DED69007753A3 /* NeoVimBuffer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4B0E22591D5DF62E00C072E6 /* NeoVimBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B0C905A1D5DED69007753A3 /* NeoVimBuffer.m */; }; + 4B22F7F01D7C029400929B0E /* ScorerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B22F7EF1D7C029400929B0E /* ScorerTest.swift */; }; + 4B22F7F21D7C6B9000929B0E /* ImageAndTextTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B22F7F11D7C6B9000929B0E /* ImageAndTextTableCell.swift */; }; 4B238BE11D3BF24200CBDD98 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B238BE01D3BF24200CBDD98 /* Application.swift */; }; 4B238BEC1D3ED54D00CBDD98 /* AppearancePrefPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B238BEB1D3ED54D00CBDD98 /* AppearancePrefPane.swift */; }; 4B2A2BEC1D02261F0074CE9A /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B2A2BE21D0225800074CE9A /* RxCocoa.framework */; }; @@ -227,6 +229,8 @@ 4B0C90591D5DED69007753A3 /* NeoVimBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NeoVimBuffer.h; sourceTree = ""; }; 4B0C905A1D5DED69007753A3 /* NeoVimBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NeoVimBuffer.m; sourceTree = ""; }; 4B1BB3521D16C5E500CA4FEF /* InputTestView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTestView.swift; sourceTree = ""; }; + 4B22F7EF1D7C029400929B0E /* ScorerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScorerTest.swift; sourceTree = ""; }; + 4B22F7F11D7C6B9000929B0E /* ImageAndTextTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageAndTextTableCell.swift; sourceTree = ""; }; 4B238BE01D3BF24200CBDD98 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 4B238BEB1D3ED54D00CBDD98 /* AppearancePrefPane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppearancePrefPane.swift; sourceTree = ""; }; 4B2A2BE21D0225800074CE9A /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxCocoa.framework; path = Carthage/Build/Mac/RxCocoa.framework; sourceTree = SOURCE_ROOT; }; @@ -482,6 +486,7 @@ 4BD3BF921D32A95800082605 /* MainWindowComponent.swift */, 4BDF501A1D77596500D8FBC3 /* OpenQuicklyWindowManager.swift */, 4BDF50131D7617EA00D8FBC3 /* OpenQuicklyWindowComponent.swift */, + 4B22F7F11D7C6B9000929B0E /* ImageAndTextTableCell.swift */, 4B238BED1D3ED55300CBDD98 /* Preferences */, ); name = UI; @@ -559,6 +564,7 @@ 1929BBC84557C8351EC6183E /* FileItemIgnorePatternTest.swift */, 1929B5D977261F1EBFA9E8F1 /* FileUtilsTest.swift */, 1929BC19C1BC19246AFF1621 /* MatcherTests.swift */, + 4B22F7EF1D7C029400929B0E /* ScorerTest.swift */, 1929B41F745CDCDFE09ACDCF /* resources */, ); path = VimRTests; @@ -845,6 +851,7 @@ buildActionMask = 2147483647; files = ( 4B238BE11D3BF24200CBDD98 /* Application.swift in Sources */, + 4B22F7F21D7C6B9000929B0E /* ImageAndTextTableCell.swift in Sources */, 4BDF50141D7617EA00D8FBC3 /* OpenQuicklyWindowComponent.swift in Sources */, 4B6A70941D60E04200E12030 /* AppKitCommons.swift in Sources */, 4BD3BF971D32B0DB00082605 /* MainWindowManager.swift in Sources */, @@ -883,6 +890,7 @@ 1929B3BF1DB87B57559DC27D /* Matcher.swift in Sources */, 1929B63CD9CBB9C122BD99A5 /* ScoredFileItem.swift in Sources */, 1929B10DD8CD7EE0B8BE529F /* Scorer.swift in Sources */, + 4B22F7F01D7C029400929B0E /* ScorerTest.swift in Sources */, 1929B4D3A4429651C2AF55E5 /* FoundationCommons.swift in Sources */, 1929BEFEABA0448306CDB6D4 /* FileItemIgnorePatternTest.swift in Sources */, 1929B7A2F2B423AA9740FD45 /* FileUtilsTest.swift in Sources */, diff --git a/VimR/AppKitCommons.swift b/VimR/AppKitCommons.swift index 52761626..c39f2514 100644 --- a/VimR/AppKitCommons.swift +++ b/VimR/AppKitCommons.swift @@ -23,7 +23,7 @@ extension NSTableView { static func standardSourceListTableView() -> NSTableView { let tableView = NSTableView(frame: CGRect.zero) - tableView.addTableColumn(NSTableColumn.standardCellBasedColumn(withName: "name")) + tableView.addTableColumn(NSTableColumn(identifier: "name")) tableView.rowSizeStyle = .Default tableView.sizeLastColumnToFit() tableView.allowsEmptySelection = false @@ -36,20 +36,6 @@ extension NSTableView { } } -extension NSTableColumn { - - static func standardCellBasedColumn(withName name: String) -> NSTableColumn { - let tableColumn = NSTableColumn(identifier: name) - - let textFieldCell = NSTextFieldCell() - textFieldCell.allowsEditingTextAttributes = false - textFieldCell.lineBreakMode = .ByTruncatingTail - tableColumn.dataCell = textFieldCell - - return tableColumn - } -} - extension NSScrollView { static func standardScrollView() -> NSScrollView { diff --git a/VimR/Base.lproj/OpenQuicklyWindow.xib b/VimR/Base.lproj/OpenQuicklyWindow.xib index d1cb89d9..576d181d 100644 --- a/VimR/Base.lproj/OpenQuicklyWindow.xib +++ b/VimR/Base.lproj/OpenQuicklyWindow.xib @@ -13,10 +13,10 @@ - + - + diff --git a/VimR/FileItemService.swift b/VimR/FileItemService.swift index 93ac6e3a..7ecad232 100644 --- a/VimR/FileItemService.swift +++ b/VimR/FileItemService.swift @@ -3,7 +3,7 @@ * See LICENSE */ -import Foundation +import Cocoa import RxSwift import EonilFileSystemEvents @@ -25,7 +25,7 @@ class FileItemService { private var ignoreToken = Token() /// When at least this much of non-directory and visible files are scanned, they are emitted. - private let emitChunkSize = 200 + private let emitChunkSize = 1000 private let scanDispatchQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0) private let monitorDispatchQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0) @@ -34,10 +34,30 @@ class FileItemService { private let fileSystemEventsLatency = Double(2) private var monitors = [NSURL: FileSystemEventMonitor]() + + private let workspace = NSWorkspace.sharedWorkspace() + private let iconsCache = NSCache() + + init() { + self.iconsCache.countLimit = 2000 + self.iconsCache.name = "icon-cache" + } func set(ignorePatterns patterns: Set) { self.ignorePatterns = patterns } + + func icon(forUrl url: NSURL) -> NSImage? { + guard let path = url.path else { + return nil + } + + let icon = workspace.iconForFile(path) + icon.size = CGSize(width: 16, height: 16) + self.iconsCache.setObject(icon, forKey: url) + + return icon + } func monitor(url url: NSURL) { guard let path = url.path else { diff --git a/VimR/ImageAndTextTableCell.swift b/VimR/ImageAndTextTableCell.swift new file mode 100644 index 00000000..af121b4d --- /dev/null +++ b/VimR/ImageAndTextTableCell.swift @@ -0,0 +1,45 @@ +/** + * Tae Won Ha - http://taewon.de - @hataewon + * See LICENSE + */ + +import Cocoa +import PureLayout + +class ImageAndTextTableCell: NSView { + + let textField: NSTextField = NSTextField(forAutoLayout: ()) + let imageView: NSImageView = NSImageView(forAutoLayout: ()) + + init(withIdentifier identifier: String) { + super.init(frame: CGRect.zero) + + self.identifier = identifier + + let textField = self.textField + textField.bordered = false + textField.editable = false + textField.lineBreakMode = .ByTruncatingTail + textField.drawsBackground = false + + let imageView = self.imageView + + self.addSubview(textField) + self.addSubview(imageView) + + imageView.autoPinEdgeToSuperviewEdge(.Top, withInset: 2) + imageView.autoPinEdgeToSuperviewEdge(.Left, withInset: 2) + imageView.autoSetDimension(.Width, toSize: 16) + imageView.autoSetDimension(.Height, toSize: 16) + +// textField.autoSetDimension(.Height, toSize: 23) + textField.autoPinEdgeToSuperviewEdge(.Top, withInset: 2) + textField.autoPinEdgeToSuperviewEdge(.Right, withInset: 2) + textField.autoPinEdgeToSuperviewEdge(.Bottom, withInset: 2) + textField.autoPinEdge(.Left, toEdge: .Right, ofView: imageView, withOffset: 4) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} \ No newline at end of file diff --git a/VimR/OpenQuicklyWindowComponent.swift b/VimR/OpenQuicklyWindowComponent.swift index de61cd80..4ec4e135 100644 --- a/VimR/OpenQuicklyWindowComponent.swift +++ b/VimR/OpenQuicklyWindowComponent.swift @@ -25,11 +25,15 @@ class OpenQuicklyWindowComponent: WindowComponent, NSWindowDelegate, NSTableView private var scoredItems = [ScoredFileItem]() private var sortedScoredItems = [ScoredFileItem]() + 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 filterOpQueue = NSOperationQueue() init(source: Observable, fileItemService: FileItemService) { self.fileItemService = fileItemService @@ -37,40 +41,69 @@ class OpenQuicklyWindowComponent: WindowComponent, NSWindowDelegate, NSTableView 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() - .subscribeOn(self.userInitiatedScheduler) - .doOnNext { _ in - self.scoredItems = [] - self.sortedScoredItems = [] - } - .flatMapLatest { [unowned self] pattern -> Observable<[ScoredFileItem]> in - if pattern.characters.count == 0 { - return self.flatFiles - .map { fileItems in - return fileItems.concurrentChunkMap(50) { item in - return ScoredFileItem(score: 0, url: item.url) - } - } + .flatMapLatest { [unowned self] pattern in + self.flatFiles. + self.filterOpQueue.addOperationWithBlock { + } - - let useFullPath = pattern.containsString("/") + return self.flatFiles - .map { fileItems in - return fileItems.concurrentChunkMap(50) { item in - let url = item.url - let target = useFullPath ? url.path! : url.lastPathComponent! - return ScoredFileItem(score: Scorer.score(target, pattern: pattern), url: url) - } - } - } - .observeOn(MainScheduler.instance) - .subscribeNext { [unowned self] items in - self.scoredItems.appendContentsOf(items) - self.sortedScoredItems = Array(self.scoredItems.sort(>)[0...min(500, self.scoredItems.count - 1)]) - self.fileView.reloadData() } + .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.. Int { return self.sortedScoredItems.count } - - func tableView(_: NSTableView, objectValueForTableColumn _: NSTableColumn?, row: Int) -> AnyObject? { - return self.sortedScoredItems[row].url.lastPathComponent! + + 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.. AnyObject? { +// let url = self.sortedScoredItems[row].url +// let pathComps = self.sortedScoredItems[row].url.pathComponents! +// let truncatedPathComps = pathComps[self.cwdPathCompsCount.. CGFloat { +// return 34 +// } } // MARK: - NSWindowDelegate extension OpenQuicklyWindowComponent { - func windowDidClose(notification: NSNotification) { - self.searchField.stringValue = "" - self.countField.stringValue = "0 items" + func windowWillClose(notification: NSNotification) { + self.flatFilesDisposeBag = DisposeBag() + self.flatFiles = Observable.empty() self.count = 0 + self.scoredItems = [] self.sortedScoredItems = [] - self.flatFiles = Observable.empty() - self.flatFilesDisposeBag = DisposeBag() + + self.searchField.stringValue = "" + self.countField.stringValue = "0 items" } } diff --git a/VimR/ScoredFileItem.swift b/VimR/ScoredFileItem.swift index bca55ae0..9933b5ca 100644 --- a/VimR/ScoredFileItem.swift +++ b/VimR/ScoredFileItem.swift @@ -8,7 +8,7 @@ import RxSwift class ScoredFileItem: Comparable { let score: Float - let url: NSURL + unowned let url: NSURL init(score: Float, url: NSURL) { self.score = score diff --git a/VimR/Scorer.swift b/VimR/Scorer.swift index ef77ca26..e3f0bafc 100644 --- a/VimR/Scorer.swift +++ b/VimR/Scorer.swift @@ -9,7 +9,7 @@ class Scorer { static func score(target: String, pattern: String) -> Float { let wf = Matcher.wagnerFisherDistance(target, pattern: pattern) - let fuzzy = Matcher.fuzzyIgnoringCase(target, pattern: pattern) +// let fuzzy = Matcher.fuzzyIgnoringCase(target, pattern: pattern) let upper = Matcher.numberOfUppercaseMatches(target, pattern: pattern) let exactMatch = Matcher.exactMatchIgnoringCase(target, pattern: pattern) @@ -24,7 +24,7 @@ class Scorer { } return (wf == 0 ? 0 : 5.0 / Float(wf)) - + Float(fuzzy.matches) +// + Float(fuzzy.matches) + Float(upper) + exactScore } diff --git a/VimRTests/ScorerTest.swift b/VimRTests/ScorerTest.swift new file mode 100644 index 00000000..8e15063d --- /dev/null +++ b/VimRTests/ScorerTest.swift @@ -0,0 +1,20 @@ +/** + * Tae Won Ha - http://taewon.de - @hataewon + * See LICENSE + */ + +import XCTest +import Nimble + +class ScorerTest: XCTestCase { + + func testScore() { + let pattern = "s/nvv" + let targets = [ + "SwiftNeoVim/NeoVimView.swift", + "build/Release/NeoVimServer.dSYM/Contents/Resources/DWARF/NeoVimServer" + ] + + expect(Scorer.score(targets[0], pattern: pattern)).to(beGreaterThan(Scorer.score(targets[1], pattern: pattern))) + } +}