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

GH-264 Implement filter

This commit is contained in:
Tae Won Ha 2016-09-03 23:13:52 +02:00
parent c9e4626c32
commit 32e564cd3d
No known key found for this signature in database
GPG Key ID: E40743465B5B8B44
10 changed files with 218 additions and 64 deletions

View File

@ -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 {

View File

@ -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 = "<group>"; };
4B0C905A1D5DED69007753A3 /* NeoVimBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NeoVimBuffer.m; sourceTree = "<group>"; };
4B1BB3521D16C5E500CA4FEF /* InputTestView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTestView.swift; sourceTree = "<group>"; };
4B22F7EF1D7C029400929B0E /* ScorerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScorerTest.swift; sourceTree = "<group>"; };
4B22F7F11D7C6B9000929B0E /* ImageAndTextTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageAndTextTableCell.swift; sourceTree = "<group>"; };
4B238BE01D3BF24200CBDD98 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
4B238BEB1D3ED54D00CBDD98 /* AppearancePrefPane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppearancePrefPane.swift; sourceTree = "<group>"; };
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 */,

View File

@ -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 {

View File

@ -13,10 +13,10 @@
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Open Quickly" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" oneShot="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<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="1440" height="877"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1177"/>
<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

@ -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<FileItemIgnorePattern>) {
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 {

View File

@ -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")
}
}

View File

@ -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<Any>, 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..<min(201, self.scoredItems.count)])
// }
// .observeOn(MainScheduler.instance)
// .subscribe(onNext: { [unowned self] items in
// self.fileView.reloadData()
// })
.addDisposableTo(self.disposeBag)
}
@ -78,9 +111,10 @@ class OpenQuicklyWindowComponent: WindowComponent, NSWindowDelegate, NSTableView
let searchField = self.searchField
let fileView = self.fileView
fileView.intercellSpacing = CGSize(width: 4, height: 4)
fileView.setDataSource(self)
fileView.setDelegate(self)
let fileScrollView = NSScrollView.standardScrollView()
fileScrollView.autoresizesSubviews = true
fileScrollView.documentView = fileView
@ -133,14 +167,14 @@ class OpenQuicklyWindowComponent: WindowComponent, NSWindowDelegate, NSTableView
let flatFiles = self.fileItemService.flatFileItems(ofUrl: self.cwd)
.subscribeOn(self.userInitiatedScheduler)
.replayAll()
flatFiles.connect()
flatFiles.connect().addDisposableTo(self.flatFilesDisposeBag)
flatFiles
.observeOn(MainScheduler.instance)
.subscribeNext { [unowned self] items in
.subscribe(onNext: { [unowned self] items in
self.count += items.count
self.countField.stringValue = "\(self.count) items"
}
})
.addDisposableTo(self.flatFilesDisposeBag)
self.flatFiles = flatFiles
@ -156,30 +190,70 @@ extension OpenQuicklyWindowComponent {
func numberOfRowsInTableView(_: NSTableView) -> 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..<pathComps.count]
let name = truncatedPathComps.last!
let pathInfo = truncatedPathComps.dropLast().reverse().joinWithSeparator(" / ")
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
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
// }
}
// MARK: - NSTableViewDelegate
extension OpenQuicklyWindowComponent {
func tableViewSelectionDidChange(_: NSNotification) {
Swift.print("selection changed")
// NSLog("\(#function): selection changed")
}
// func tableView(tableView: NSTableView, heightOfRow row: Int) -> 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"
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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)))
}
}