diff --git a/NvimView/DrawerDev/Info.plist b/NvimView/DrawerDev/Info.plist
index 4ace86a6..433aa38c 100644
--- a/NvimView/DrawerDev/Info.plist
+++ b/NvimView/DrawerDev/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.25.0
+ SNAPSHOT-298
CFBundleVersion
- 297
+ 298
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
diff --git a/NvimView/NvimView.xcodeproj/project.pbxproj b/NvimView/NvimView.xcodeproj/project.pbxproj
index 651b8bc6..8071cdfb 100644
--- a/NvimView/NvimView.xcodeproj/project.pbxproj
+++ b/NvimView/NvimView.xcodeproj/project.pbxproj
@@ -783,7 +783,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 297;
+ CURRENT_PROJECT_VERSION = 298;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -845,7 +845,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 297;
+ CURRENT_PROJECT_VERSION = 298;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -874,7 +874,7 @@
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 297;
+ DYLIB_CURRENT_VERSION = 298;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac";
FRAMEWORK_VERSION = A;
@@ -896,7 +896,7 @@
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 297;
+ DYLIB_CURRENT_VERSION = 298;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac";
FRAMEWORK_VERSION = A;
diff --git a/NvimView/NvimView/Info.plist b/NvimView/NvimView/Info.plist
index 4b7e3813..44a1aff8 100644
--- a/NvimView/NvimView/Info.plist
+++ b/NvimView/NvimView/Info.plist
@@ -15,9 +15,9 @@
CFBundlePackageType
FMWK
CFBundleShortVersionString
- 0.25.0
+ SNAPSHOT-298
CFBundleVersion
- 297
+ 298
NSHumanReadableCopyright
Copyright © 2017 Tae Won Ha. All rights reserved.
NSPrincipalClass
diff --git a/NvimView/NvimViewTests/Info.plist b/NvimView/NvimViewTests/Info.plist
index ed640e44..6beff906 100644
--- a/NvimView/NvimViewTests/Info.plist
+++ b/NvimView/NvimViewTests/Info.plist
@@ -15,8 +15,8 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 0.25.0
+ SNAPSHOT-298
CFBundleVersion
- 297
+ 298
diff --git a/VimR/VimR.xcodeproj/project.pbxproj b/VimR/VimR.xcodeproj/project.pbxproj
index 0e40e829..3a5dd8e0 100644
--- a/VimR/VimR.xcodeproj/project.pbxproj
+++ b/VimR/VimR.xcodeproj/project.pbxproj
@@ -1272,7 +1272,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 297;
+ CURRENT_PROJECT_VERSION = 298;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -1330,7 +1330,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 297;
+ CURRENT_PROJECT_VERSION = 298;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
diff --git a/VimR/VimR/AppKitCommons.swift b/VimR/VimR/AppKitCommons.swift
index 7653b921..63d211c1 100644
--- a/VimR/VimR/AppKitCommons.swift
+++ b/VimR/VimR/AppKitCommons.swift
@@ -219,7 +219,7 @@ extension NSOutlineView {
return self.item(atRow: self.clickedRow)
}
- func toggle(item: Any) {
+ func toggle(item: Any?) {
if self.isItemExpanded(item) {
self.collapseItem(item)
} else {
diff --git a/VimR/VimR/BufferList.swift b/VimR/VimR/BufferList.swift
index efe4bf85..174ad412 100644
--- a/VimR/VimR/BufferList.swift
+++ b/VimR/VimR/BufferList.swift
@@ -21,9 +21,14 @@ class BuffersList: NSView,
case open(NvimView.Buffer)
}
+ private(set) var lastThemeMark = Token()
private(set) var theme = Theme.default
- required init(source: Observable, emitter: ActionEmitter, state: StateType) {
+ required init(
+ source: Observable,
+ emitter: ActionEmitter,
+ state: StateType
+ ) {
self.emit = emitter.typedEmit()
self.uuid = state.uuid
@@ -52,14 +57,15 @@ class BuffersList: NSView,
self.usesTheme = state.appearance.usesTheme
- if self.buffers == state.buffers && !themeChanged && self.showsFileIcon == state.appearance.showsFileIcon {
+ if self.buffers == state.buffers
+ && !themeChanged
+ && self.showsFileIcon == state.appearance.showsFileIcon {
return
}
self.showsFileIcon = state.appearance.showsFileIcon
self.buffers = state.buffers
self.bufferList.reloadData()
- self.adjustFileViewWidth()
})
.disposed(by: self.disposeBag)
}
@@ -69,7 +75,6 @@ class BuffersList: NSView,
private let uuid: String
private var usesTheme: Bool
- private var lastThemeMark = Token()
private var showsFileIcon: Bool
private let bufferList = NSTableView.standardTableView()
@@ -95,16 +100,6 @@ class BuffersList: NSView,
self.addSubview(scrollView)
scrollView.autoPinEdgesToSuperviewEdges()
}
-
- private func adjustFileViewWidth() {
- let maxWidth = self.buffers.reduce(CGFloat(100)) { (curMaxWidth, buffer) in
- return max(self.text(for: buffer).size().width, curMaxWidth)
- }
-
- let column = self.bufferList.tableColumns[0]
- // If we set the minWidth and maxWidth here, the column does not get resized... Dunno why.
- column.width = maxWidth + ThemedTableCell.widthWithoutText
- }
}
// MARK: - Actions
@@ -132,14 +127,26 @@ extension BuffersList {
// MARK: - NSTableViewDelegate
extension BuffersList {
- public func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
- return tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("buffer-row-view"), owner: self)
- as? ThemedTableRow ?? ThemedTableRow(withIdentifier: "buffer-row-view", themedView: self)
+ public func tableView(
+ _ tableView: NSTableView,
+ rowViewForRow row: Int
+ ) -> NSTableRowView? {
+ return tableView.makeView(
+ withIdentifier: NSUserInterfaceItemIdentifier("buffer-row-view"),
+ owner: self
+ ) as? ThemedTableRow ?? ThemedTableRow(withIdentifier: "buffer-row-view",
+ themedView: self)
}
- func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
- let cachedCell = (tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("buffer-cell-view"), owner: self)
- as? ThemedTableCell)?.reset()
+ func tableView(
+ _ tableView: NSTableView,
+ viewFor tableColumn: NSTableColumn?,
+ row: Int
+ ) -> NSView? {
+ let cachedCell = (tableView.makeView(
+ withIdentifier: NSUserInterfaceItemIdentifier("buffer-cell-view"),
+ owner: self
+ ) as? ThemedTableCell)?.reset()
let cell = cachedCell ?? ThemedTableCell(withIdentifier: "buffer-cell-view")
@@ -155,6 +162,22 @@ extension BuffersList {
return cell
}
+ func tableView(
+ _ tableView: NSTableView,
+ didAdd rowView: NSTableRowView,
+ forRow row: Int
+ ) {
+ guard let cellWidth = (rowView.view(atColumn: 0) as? NSTableCellView)?
+ .fittingSize.width
+ else {
+ return
+ }
+
+ self.bufferList.tableColumns[0].width = max(
+ self.bufferList.tableColumns[0].width, cellWidth + CGFloat(10)
+ )
+ }
+
private func text(for buffer: NvimView.Buffer) -> NSAttributedString {
guard let name = buffer.name else {
return NSAttributedString(string: "No Name")
@@ -164,16 +187,22 @@ extension BuffersList {
return NSAttributedString(string: name)
}
- let pathInfo = url.pathComponents.dropFirst().dropLast().reversed().joined(separator: " / ") + " /"
+ let pathInfo = url.pathComponents
+ .dropFirst()
+ .dropLast()
+ .reversed()
+ .joined(separator: " / ") + " /"
let rowText = NSMutableAttributedString(string: "\(name) — \(pathInfo)")
rowText.addAttribute(NSAttributedString.Key.foregroundColor,
value: self.theme.foreground,
range: NSRange(location: 0, length: name.count))
- rowText.addAttribute(NSAttributedString.Key.foregroundColor,
- value: self.theme.foreground.brightening(by: 1.15),
- range: NSRange(location: name.count, length: pathInfo.count + 3))
+ rowText.addAttribute(
+ NSAttributedString.Key.foregroundColor,
+ value: self.theme.foreground.brightening(by: 1.15),
+ range: NSRange(location: name.count, length: pathInfo.count + 3)
+ )
return rowText
}
diff --git a/VimR/VimR/FileBrowser.swift b/VimR/VimR/FileBrowser.swift
index ca5b551d..06a474f3 100644
--- a/VimR/VimR/FileBrowser.swift
+++ b/VimR/VimR/FileBrowser.swift
@@ -181,7 +181,9 @@ extension FileBrowser {
return
}
+ #if NOPE
self.fileView.select(url)
+ #endif
}
@objc func refreshAction(_ sender: Any?) {
diff --git a/VimR/VimR/FileOutlineView.swift b/VimR/VimR/FileOutlineView.swift
index 35d01dda..7ce4a34b 100644
--- a/VimR/VimR/FileOutlineView.swift
+++ b/VimR/VimR/FileOutlineView.swift
@@ -7,67 +7,52 @@ import Cocoa
import NvimView
import PureLayout
import RxSwift
+import CocoaFontAwesome
class FileOutlineView: NSOutlineView,
UiComponent,
- NSOutlineViewDataSource,
NSOutlineViewDelegate,
ThemedView {
typealias StateType = MainWindow.State
+ @objc dynamic var content = [Node]()
+
+ private(set) var lastThemeMark = Token()
private(set) var theme = Theme.default
- required init(source: Observable, emitter: ActionEmitter, state: StateType) {
+ required init(
+ source: Observable,
+ emitter: ActionEmitter,
+ state: StateType
+ ) {
self.emit = emitter.typedEmit()
self.uuid = state.uuid
-
- self.root = FileBrowserItem(state.cwd)
- self.isShowHidden = state.fileBrowserShowHidden
-
+ self.root = Node(url: state.cwd)
self.usesTheme = state.appearance.usesTheme
self.showsFileIcon = state.appearance.showsFileIcon
+ self.isShowHidden = state.fileBrowserShowHidden
+ self.triangleClosed = NSImage.fontAwesomeIcon(
+ name: .caretRight,
+ textColor: self.theme.directoryForeground,
+ dimension: triangleImageSize
+ )
+ self.triangleOpen = NSImage.fontAwesomeIcon(
+ name: .caretDown,
+ textColor: self.theme.directoryForeground,
+ dimension: triangleImageSize
+ )
super.init(frame: .zero)
+
NSOutlineView.configure(toStandard: self)
-
- self.dataSource = self
self.delegate = self
- self.allowsEmptySelection = true
-
- guard Bundle.main.loadNibNamed(NSNib.Name("FileBrowserMenu"), owner: self, topLevelObjects: nil) else {
- NSLog("WARN: FileBrowserMenu.xib could not be loaded")
- return
- }
-
- // If the target of the menu items is set to the first responder, the actions are not invoked
- // at all when the file monitor fires in the background...
- // Dunno why it worked before the redesign... -_-
- self.menu?.items.forEach { $0.target = self }
-
- self.doubleAction = #selector(FileOutlineView.doubleClickAction)
-
- source
- .filter { !self.shouldReloadData(for: $0) }
- .filter { $0.lastFileSystemUpdate.mark != self.lastFileSystemUpdateMark }
- .throttle(2 * FileMonitor.fileSystemEventsLatency + 1,
- latest: true,
- scheduler: SerialDispatchQueueScheduler(qos: .background))
- .observeOn(MainScheduler.instance)
- .subscribe(onNext: { state in
- self.lastFileSystemUpdateMark = state.lastFileSystemUpdate.mark
- guard let fileBrowserItem = self.fileBrowserItem(with: state.lastFileSystemUpdate.payload) else {
- return
- }
-
- self.update(fileBrowserItem)
- })
- .disposed(by: self.disposeBag)
source
.observeOn(MainScheduler.instance)
.subscribe(onNext: { state in
- if state.viewToBeFocused != nil, case .fileBrowser = state.viewToBeFocused! {
+ if state.viewToBeFocused != nil,
+ case .fileBrowser = state.viewToBeFocused! {
self.beFirstResponder()
}
@@ -80,76 +65,235 @@ class FileOutlineView: NSOutlineView,
self.usesTheme = state.appearance.usesTheme
- guard self.shouldReloadData(for: state, themeChanged: themeChanged) else {
+ guard self.shouldReloadData(
+ for: state, themeChanged: themeChanged
+ ) else {
return
}
self.showsFileIcon = state.appearance.showsFileIcon
self.isShowHidden = state.fileBrowserShowHidden
self.lastFileSystemUpdateMark = state.lastFileSystemUpdate.mark
- self.root = FileBrowserItem(state.cwd)
- self.reloadData()
+ self.root = Node(url: state.cwd)
+ self.reloadRoot()
})
.disposed(by: self.disposeBag)
- }
- override func reloadData() {
- self.cells.removeAll()
- self.widths.removeAll()
- super.reloadData()
- }
-
- func select(_ url: URL) {
- var stack = [self.root]
-
- while let item = stack.popLast() {
- self.expandItem(item)
-
- if item.url.isDirectParent(of: url) {
- if let targetItem = item.children.first(where: { $0.url == url }) {
- let targetRow = self.row(forItem: targetItem)
- self.selectRowIndexes(IndexSet(integer: targetRow), byExtendingSelection: false)
- self.scrollRowToVisible(targetRow)
+ source
+ .filter { !self.shouldReloadData(for: $0) }
+ .filter { $0.lastFileSystemUpdate.mark != self.lastFileSystemUpdateMark }
+ .map { $0.lastFileSystemUpdate.payload }
+ .throttle(2 * FileMonitor.fileSystemEventsLatency + 1,
+ latest: true,
+ scheduler: SerialDispatchQueueScheduler(qos: .background))
+ .map { ($0, Set(self.childUrls(for: $0))) }
+ .observeOn(MainScheduler.instance)
+ .subscribe(onNext: { (url, newChildUrls) in
+ guard let changeTreeNode = self.changeRootTreeNode(for: url) else {
+ return
}
- break
- }
+ self.handleRemoval(changeTreeNode: changeTreeNode,
+ newChildUrls: newChildUrls)
+ self.handleAddition(changeTreeNode: changeTreeNode,
+ newChildUrls: newChildUrls)
+ })
+ .disposed(by: self.disposeBag)
- stack.append(contentsOf: item.children.filter { $0.url.isParent(of: url) })
+ self.initContextMenu()
+ self.initBindings()
+ self.reloadRoot()
+ }
+
+ // We cannot use outlineView(_:willDisplayOutlineCell:for:item:) delegate
+ // method to customize the disclosure triangle in a view-based
+ // NSOutlineView.
+ // See https://stackoverflow.com/a/20454413/9850227
+ override func makeView(
+ withIdentifier identifier: NSUserInterfaceItemIdentifier, owner: Any?
+ ) -> NSView? {
+ let result = super.makeView(withIdentifier: identifier, owner: owner)
+
+ if identifier == NSOutlineView.disclosureButtonIdentifier {
+ let triangleButton = result as? NSButton
+ triangleButton?.image = self.triangleClosed
+ triangleButton?.alternateImage = self.triangleOpen
}
+
+ return result
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
}
private let emit: (UuidAction) -> Void
private let disposeBag = DisposeBag()
private let uuid: String
- private var lastFileSystemUpdateMark = Token()
- private var usesTheme: Bool
- private var lastThemeMark = Token()
- private var showsFileIcon: Bool
+ private var root: Node
private var cwd: URL {
return self.root.url
}
+ private let treeController = NSTreeController()
+
+ private var cachedColumnWidth = CGFloat(20)
+ private var usesTheme: Bool
+ private var lastFileSystemUpdateMark = Token()
+ private var showsFileIcon: Bool
private var isShowHidden: Bool
- private var root: FileBrowserItem
+ private var triangleClosed: NSImage
+ private var triangleOpen: NSImage
- private var widths = [String: CGFloat]()
- private var cells = [String: ThemedTableCell]()
+ private func initContextMenu() {
+ // Loading the nib file will set self.menu.
+ guard Bundle.main.loadNibNamed(
+ NSNib.Name("FileBrowserMenu"),
+ owner: self,
+ topLevelObjects: nil
+ ) else {
+ fileLog.error("FileBrowserMenu.xib could not be loaded")
+ return
+ }
+ self.menu?.items.forEach { $0.target = self }
+ self.doubleAction = #selector(FileOutlineView.doubleClickAction)
+ }
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
+ private func initBindings() {
+ self.treeController.childrenKeyPath = "children"
+ self.treeController.leafKeyPath = "isLeaf"
+ self.treeController.countKeyPath = "childrenCount"
+ self.treeController.objectClass = Node.self
+ self.treeController.avoidsEmptySelection = false
+ self.treeController.preservesSelection = true
+ self.treeController.sortDescriptors = [
+ NSSortDescriptor(key: "isLeaf", ascending: true), // Folders first,
+ NSSortDescriptor(key: "displayName", ascending: true) // then, name
+ ]
+ self.treeController.bind(.contentArray, to: self, withKeyPath: "content")
+ self.bind(.content, to: self.treeController, withKeyPath: "arrangedObjects")
+ self.bind(.selectionIndexPaths,
+ to: self.treeController,
+ withKeyPath: "selectionIndexPaths")
+ }
+
+ private func changeRootTreeNode(`for` url: URL) -> NSTreeNode? {
+ if url == self.cwd {
+ return self.treeController.arrangedObjects
+ }
+
+ let cwdCompsCount = self.cwd.pathComponents.count
+ guard cwdCompsCount <= url.pathComponents.count else { return nil }
+ let comps = url.pathComponents.suffix(cwdCompsCount)
+
+ let rootTreeNode = self.treeController.arrangedObjects
+ let changeTreeNode = comps.reduce(rootTreeNode) { (prev, comp) in
+ return prev.children?.first { child in
+ return child.node?.displayName == comp
+ } ?? prev
+ }
+
+ guard let changeNode = changeTreeNode.node else {
+ return nil
+ }
+
+ guard changeNode.url == url && changeNode.children != nil else {
+ return nil
+ }
+
+ return changeTreeNode
+ }
+
+ private func handleAddition(
+ changeTreeNode: NSTreeNode, newChildUrls: Set
+ ) {
+ let existingUrls = changeTreeNode.children?
+ .compactMap { $0.node?.url } ?? []
+ let newNodes = newChildUrls
+ .subtracting(existingUrls)
+ .map { Node(url: $0) }
+ let newIndexPaths = (0..
+ ) {
+ let indexPathsToRemove =
+ changeTreeNode.children?
+ .filter { child in
+ guard let url = child.node?.url else { return true }
+ return newChildUrls.contains(url) == false
+ }
+ .map { $0.indexPath } ?? []
+
+ self.treeController.removeObjects(
+ atArrangedObjectIndexPaths: indexPathsToRemove
+ )
+ }
+
+ private func childUrls(for url: URL) -> [URL] {
+ let urls = FileUtils.directDescendants(of: url).sorted { lhs, rhs in
+ return lhs.lastPathComponent < rhs.lastPathComponent
+ }
+
+ if self.isShowHidden {
+ return urls
+ }
+
+ return urls.filter { !$0.isHidden }
+ }
+
+ private func childNodes(for node: Node) -> [Node] {
+ if node.isChildrenScanned {
+ return node.children ?? []
+ }
+
+ let nodes = FileUtils
+ .directDescendants(of: node.url)
+ .map { Node(url: $0) }
+
+ if self.isShowHidden {
+ return nodes
+ }
+
+ return nodes.filter { !$0.isHidden }
+ }
+
+ private func reloadRoot() {
+ let children = self.childNodes(for: self.root)
+
+ self.root.children = children
+ self.content.removeAll()
+ self.content.append(contentsOf: children)
}
private func updateTheme(_ theme: Marked) {
self.theme = theme.payload
self.enclosingScrollView?.backgroundColor = self.theme.background
self.backgroundColor = self.theme.background
+ self.triangleClosed = NSImage.fontAwesomeIcon(
+ name: .caretRight,
+ textColor: self.theme.directoryForeground,
+ dimension: triangleImageSize
+ )
+ self.triangleOpen = NSImage.fontAwesomeIcon(
+ name: .caretDown,
+ textColor: self.theme.directoryForeground,
+ dimension: triangleImageSize
+ )
+
self.lastThemeMark = theme.mark
}
- private func shouldReloadData(for state: StateType, themeChanged: Bool = false) -> Bool {
+ private func shouldReloadData(
+ for state: StateType, themeChanged: Bool = false
+ ) -> Bool {
if self.isShowHidden != state.fileBrowserShowHidden {
return true
}
@@ -169,233 +313,8 @@ class FileOutlineView: NSOutlineView,
return false
}
- private func handleRemovals(for fileBrowserItem: FileBrowserItem,
- new newChildren: [FileBrowserItem]) {
- let curChildren = fileBrowserItem.children
-
- let curPreparedChildren = self.prepare(curChildren)
- let newPreparedChildren = self.prepare(newChildren)
-
- let indicesToRemove = curPreparedChildren
- .enumerated()
- .filter { (_, fileBrowserItem) in newPreparedChildren.contains(fileBrowserItem) == false }
- .map { (idx, _) in idx }
-
- indicesToRemove.forEach { index in
- let path = curPreparedChildren[index].url.path
-
- self.cells.removeValue(forKey: path)
- self.widths.removeValue(forKey: path)
- }
-
- fileBrowserItem.children = curChildren.filter { newChildren.contains($0) }
-
- let parent = fileBrowserItem == self.root ? nil : fileBrowserItem
- self.removeItems(at: IndexSet(indicesToRemove), inParent: parent)
- }
-
- private func handleAdditions(for fileBrowserItem: FileBrowserItem,
- new newChildren: [FileBrowserItem]) {
- let curChildren = fileBrowserItem.children
-
- let curPreparedChildren = self.prepare(curChildren)
- let newPreparedChildren = self.prepare(newChildren)
-
- let indicesToInsert = newPreparedChildren
- .enumerated()
- .filter { (_, fileBrowserItem) in curPreparedChildren.contains(fileBrowserItem) == false }
- .map { (idx, _) in idx }
-
- // We don't just take newChildren because NSOutlineView look at the pointer equality for
- // preserving the expanded states...
- fileBrowserItem.children = newChildren.substituting(elements: curChildren)
-
- let parent = fileBrowserItem == self.root ? nil : fileBrowserItem
- self.insertItems(at: IndexSet(indicesToInsert), inParent: parent)
- }
-
- private func sortedChildren(of url: URL) -> [FileBrowserItem] {
- return FileUtils.directDescendants(of: url).map(FileBrowserItem.init).sorted{
- if ($0.isDir == $1.isDir) {
- return $0.url.absoluteString < $1.url.absoluteString
- }
-
- return $0.isDir
- }
- }
-
- private func update(_ fileBrowserItem: FileBrowserItem) {
- let url = fileBrowserItem.url
-
- // Sort the array to keep the order.
- let newChildren = self.sortedChildren(of: url)
-
- self.beginUpdates()
- self.handleRemovals(for: fileBrowserItem, new: newChildren)
- self.endUpdates()
-
- self.beginUpdates()
- self.handleAdditions(for: fileBrowserItem, new: newChildren)
- self.endUpdates()
-
- fileBrowserItem.isChildrenScanned = true
-
- fileBrowserItem.children.filter { self.isItemExpanded($0) }.forEach(self.update)
- }
-
- private func fileBrowserItem(with url: URL) -> FileBrowserItem? {
- if self.cwd == url {
- return self.root
- }
-
- guard self.cwd.isParent(of: url) else {
- return nil
- }
-
- let rootPathComps = self.cwd.pathComponents
- let pathComps = url.pathComponents
- let childPart = pathComps[rootPathComps.count.. FileBrowserItem? in
- guard let parent = resultItem else {
- return nil
- }
-
- return parent.child(with: parent.url.appendingPathComponent(childName))
- }
- }
-}
-
-// MARK: - NSOutlineViewDataSource
-extension FileOutlineView {
-
- private func scanChildrenIfNecessary(_ fileBrowserItem: FileBrowserItem) {
- guard fileBrowserItem.isChildrenScanned == false else {
- return
- }
-
- fileBrowserItem.children = self.sortedChildren(of: fileBrowserItem.url)
- fileBrowserItem.isChildrenScanned = true
- }
-
- private func prepare(_ children: [FileBrowserItem]) -> [FileBrowserItem] {
- return self.isShowHidden ? children : children.filter { !$0.isHidden }
- }
-
- func outlineView(_: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
- if item == nil {
- self.scanChildrenIfNecessary(self.root)
-
- return self.prepare(self.root.children).count
- }
-
- guard let fileBrowserItem = item as? FileBrowserItem else {
- return 0
- }
-
- if fileBrowserItem.url.isDir {
- self.scanChildrenIfNecessary(fileBrowserItem)
- return self.prepare(fileBrowserItem.children).count
- }
-
- return 0
- }
-
- func outlineView(_: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
- let level = self.level(forItem: item) + 2
- defer { self.adjustColumnWidths() }
-
- if item == nil {
- let child = self.prepare(self.root.children)[index]
-
- let cell = self.cell(forItem: child)
- self.cells[child.url.path] = cell
- self.widths[child.url.path] = self.cellWidth(for: cell, level: level)
-
- return child
- }
-
- guard let fileBrowserItem = item as? FileBrowserItem else {
- preconditionFailure("Should not happen")
- }
-
- let child = self.prepare(fileBrowserItem.children)[index]
-
- let cell = self.cell(forItem: child)
- self.cells[child.url.path] = cell
- self.widths[child.url.path] = self.cellWidth(for: cell, level: level)
-
- return child
- }
-
- private func cell(forItem item: FileBrowserItem) -> ThemedTableCell {
- if let existingCell = self.cells[item.url.path] {
- return existingCell
- }
-
- let cell = ThemedTableCell(withIdentifier: "file-cell-view")
-
- cell.isDir = item.isDir
- cell.text = item.url.lastPathComponent
-
- if self.showsFileIcon {
- let icon = FileUtils.icon(forUrl: item.url)
- cell.image = cell.isHidden ? icon?.tinting(with: NSColor.white.withAlphaComponent(0.4)) : icon
- }
-
- return cell
- }
-
- func outlineView(_: NSOutlineView, isItemExpandable item: Any) -> Bool {
- guard let fileBrowserItem = item as? FileBrowserItem else {
- return false
- }
- return fileBrowserItem.url.isDir
- }
-
- @objc(outlineView: objectValueForTableColumn:byItem:)
- func outlineView(_: NSOutlineView, objectValueFor: NSTableColumn?, byItem item: Any?) -> Any? {
- guard let fileBrowserItem = item as? FileBrowserItem else {
- return nil
- }
-
- return fileBrowserItem
- }
-
- private func cellWidth(for cell: NSView?, level: Int) -> CGFloat {
- let cellWidth = cell?.intrinsicContentSize.width ?? 0
- let indentation = CGFloat(level + 1) * self.indentationPerLevel + 4
- return cellWidth + indentation
- }
-
- private func adjustColumnWidths() {
- guard let column = self.outlineTableColumn else {
- return
- }
-
- column.minWidth = self.widths.values.max() ?? 100
- column.maxWidth = self.widths.values.max() ?? 100
- }
-}
-
-// MARK: - NSOutlineViewDelegate
-extension FileOutlineView {
-
- func outlineView(_ outlineView: NSOutlineView, rowViewForItem item: Any) -> NSTableRowView? {
- return self.makeView(withIdentifier: NSUserInterfaceItemIdentifier("file-row-view"), owner: self) as? ThemedTableRow
- ?? ThemedTableRow(withIdentifier: "file-row-view", themedView: self)
- }
-
- func outlineView(_: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
- guard let fileBrowserItem = item as? FileBrowserItem else {
- return nil
- }
-
- return self.cells[fileBrowserItem.url.path]
- }
-
- func outlineView(_: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
- return 20
+ private func node(from item: Any?) -> Node? {
+ return (item as? NSTreeNode)?.node
}
}
@@ -403,84 +322,184 @@ extension FileOutlineView {
extension FileOutlineView {
@IBAction func doubleClickAction(_: Any?) {
- guard let item = self.clickedItem as? FileBrowserItem else {
+ let clickedTreeNode = self.clickedItem
+ guard let node = self.node(from: clickedTreeNode) else {
return
}
- if item.url.isDir {
- self.toggle(item: item)
+ if node.isDir {
+ self.toggle(item: clickedTreeNode)
} else {
self.emit(
- UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .default))
+ UuidAction(uuid: self.uuid,
+ action: .open(url: node.url, mode: .default))
)
}
}
@IBAction func openInNewTab(_: Any?) {
- guard let item = self.clickedItem as? FileBrowserItem else {
+ guard let node = self.node(from: self.clickedItem) else {
return
}
self.emit(
- UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .newTab))
+ UuidAction(uuid: self.uuid, action: .open(url: node.url, mode: .newTab))
)
}
@IBAction func openInCurrentTab(_: Any?) {
- guard let item = self.clickedItem as? FileBrowserItem else {
+ guard let node = self.node(from: self.clickedItem) else {
return
}
self.emit(
- UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .currentTab))
+ UuidAction(uuid: self.uuid,
+ action: .open(url: node.url, mode: .currentTab))
)
}
@IBAction func openInHorizontalSplit(_: Any?) {
- guard let item = self.clickedItem as? FileBrowserItem else {
+ guard let node = self.node(from: self.clickedItem) else {
return
}
self.emit(
- UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .horizontalSplit))
+ UuidAction(uuid: self.uuid,
+ action: .open(url: node.url, mode: .horizontalSplit))
)
}
@IBAction func openInVerticalSplit(_: Any?) {
- guard let item = self.clickedItem as? FileBrowserItem else {
+ guard let node = self.node(from: self.clickedItem) else {
return
}
self.emit(
- UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .verticalSplit))
+ UuidAction(uuid: self.uuid,
+ action: .open(url: node.url, mode: .verticalSplit))
)
}
@IBAction func setAsWorkingDirectory(_: Any?) {
- guard let item = self.clickedItem as? FileBrowserItem else {
+ guard let node = self.node(from: self.clickedItem) else {
return
}
- guard item.url.isDir else {
+ guard node.url.isDir else {
return
}
self.emit(
- UuidAction(uuid: self.uuid, action: .setAsWorkingDirectory(item.url))
+ UuidAction(uuid: self.uuid, action: .setAsWorkingDirectory(node.url))
)
}
}
+// MARK: - NSOutlineViewDelegate
+extension FileOutlineView {
+
+ func outlineView(
+ _ outlineView: NSOutlineView,
+ rowViewForItem item: Any
+ ) -> NSTableRowView? {
+ let view = self.makeView(
+ withIdentifier: NSUserInterfaceItemIdentifier("file-row-view"),
+ owner: self
+ ) as? ThemedTableRow ?? ThemedTableRow(withIdentifier: "file-row-view",
+ themedView: self)
+
+ return view
+ }
+
+ func outlineView(
+ _: NSOutlineView,
+ viewFor tableColumn: NSTableColumn?,
+ item: Any
+ ) -> NSView? {
+ guard let node = self.node(from: item) else {
+ return nil
+ }
+
+ let cellView = self.makeView(
+ withIdentifier: NSUserInterfaceItemIdentifier("file-cell-view"),
+ owner: self
+ ) as? ThemedTableCell ?? ThemedTableCell(withIdentifier: "file-cell-view")
+
+ cellView.isDir = node.isDir
+ cellView.text = node.displayName
+
+ let icon = FileUtils.icon(forUrl: node.url)
+ cellView.image = node.isHidden
+ ? icon?.tinting(with: NSColor.white.withAlphaComponent(0.4))
+ : icon
+
+ return cellView
+ }
+
+ func outlineView(_: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
+ return 20
+ }
+
+ func outlineView(
+ _ outlineView: NSOutlineView,
+ shouldExpandItem item: Any
+ ) -> Bool {
+ guard let node = self.node(from: item) else {
+ return false
+ }
+
+ if node.isChildrenScanned {
+ return true
+ }
+
+ node.children = FileUtils.directDescendants(of: node.url).map { url in
+ return Node(url: url)
+ }
+
+ return true
+ }
+
+ func outlineView(
+ _ outlineView: NSOutlineView,
+ didAdd rowView: NSTableRowView,
+ forRow row: Int
+ ) {
+ guard let cellWidth = (rowView.view(atColumn: 0) as? NSTableCellView)?
+ .fittingSize.width
+ else {
+ return
+ }
+
+ let level = CGFloat(self.level(forRow: row))
+ let width = level * self.indentationPerLevel + cellWidth
+ + columnWidthRightPadding
+ self.cachedColumnWidth = max(self.cachedColumnWidth, width)
+ self.tableColumns[0].width = cachedColumnWidth
+
+ let rv = rowView as? ThemedTableRow
+ guard rv?.themeToken != self.lastThemeMark else {
+ return
+ }
+
+ let triangleView = rv?.triangleView
+ triangleView?.image = self.triangleClosed
+ triangleView?.alternateImage = self.triangleOpen
+ rv?.themeToken = self.lastThemeMark
+ }
+}
+
// MARK: - NSUserInterfaceValidations
extension FileOutlineView {
- override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
- guard let clickedItem = self.clickedItem as? FileBrowserItem else {
+ override func validateUserInterfaceItem(
+ _ item: NSValidatedUserInterfaceItem
+ ) -> Bool {
+ guard let clickedNode = self.node(from: self.clickedItem) else {
return true
}
if item.action == #selector(setAsWorkingDirectory(_:)) {
- return clickedItem.url.isDir
+ return clickedNode.url.isDir
}
return true
@@ -496,18 +515,19 @@ extension FileOutlineView {
return
}
- guard let item = self.selectedItem as? FileBrowserItem else {
+ guard let node = self.node(from: self.selectedItem) else {
super.keyDown(with: event)
return
}
switch char {
case " ", "\r": // Why "\r" and not "\n"?
- if item.url.isDir || item.url.isPackage {
- self.toggle(item: item)
+ if node.url.isDir || node.url.isPackage {
+ self.toggle(item: node)
} else {
self.emit(
- UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .newTab))
+ UuidAction(uuid: self.uuid,
+ action: .open(url: node.url, mode: .newTab))
)
}
@@ -517,40 +537,50 @@ extension FileOutlineView {
}
}
-private class FileBrowserItem: Hashable, Comparable, CustomStringConvertible {
+class Node: NSObject, Comparable {
- static func ==(left: FileBrowserItem, right: FileBrowserItem) -> Bool {
- return left.url == right.url
+ static func <(lhs: Node, rhs: Node) -> Bool {
+ return lhs.displayName < rhs.displayName
}
- static func <(left: FileBrowserItem, right: FileBrowserItem) -> Bool {
- return left.url.lastPathComponent < right.url.lastPathComponent
+ @objc dynamic var url: URL
+ @objc dynamic var isLeaf: Bool
+ @objc dynamic var isHidden: Bool
+ @objc dynamic var children: [Node]?
+
+ @objc dynamic var childrenCount: Int {
+ return self.children?.count ?? -1
+ }
+ @objc dynamic var displayName: String {
+ return self.url.lastPathComponent
}
- var hashValue: Int {
+ var isDir: Bool {
+ return !self.isLeaf
+ }
+ var isChildrenScanned = false
+
+ override var description: String {
+ return ""
+ }
+
+ override var hash: Int {
return self.url.hashValue
}
- var description: String {
- return self.url.path
- }
-
- let url: URL
- let isDir: Bool
- let isHidden: Bool
- var children: [FileBrowserItem] = []
- var isChildrenScanned = false
-
- init(_ url: URL) {
+ init(url: URL) {
self.url = url
-
- // We cache the value here since we often get the value when the file is not there, eg when
- // updating because the file gets deleted (in self.prepare() function)
+ self.isLeaf = !url.isDir
self.isHidden = url.isHidden
- self.isDir = url.isDir
- }
-
- func child(with url: URL) -> FileBrowserItem? {
- return self.children.first { $0.url == url }
}
}
+
+private extension NSTreeNode {
+
+ var node: Node? {
+ return self.representedObject as? Node
+ }
+}
+
+private let columnWidthRightPadding = CGFloat(40)
+private let triangleImageSize = CGFloat(18)
diff --git a/VimR/VimR/Info.plist b/VimR/VimR/Info.plist
index b07cace0..99458b2f 100644
--- a/VimR/VimR/Info.plist
+++ b/VimR/VimR/Info.plist
@@ -1224,7 +1224,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.25.0
+ SNAPSHOT-298
CFBundleSignature
????
CFBundleURLTypes
@@ -1241,7 +1241,7 @@
CFBundleVersion
- 297
+ 298
LSApplicationCategoryType
public.app-category.productivity
LSMinimumSystemVersion
diff --git a/VimR/VimR/ThemedTableSubviews.swift b/VimR/VimR/ThemedTableSubviews.swift
index d61c31b4..1b24b5a7 100644
--- a/VimR/VimR/ThemedTableSubviews.swift
+++ b/VimR/VimR/ThemedTableSubviews.swift
@@ -10,18 +10,30 @@ import PureLayout
protocol ThemedView: class {
var theme: Theme { get }
+ var lastThemeMark: Token { get }
}
class ThemedTableRow: NSTableRowView {
+ weak var triangleView: NSButton?
+ var themeToken: Token
+
init(withIdentifier identifier: String, themedView: ThemedView) {
self.themedView = themedView
+ self.themeToken = themedView.lastThemeMark
super.init(frame: .zero)
self.identifier = NSUserInterfaceItemIdentifier(identifier)
}
+ override func didAddSubview(_ subview: NSView) {
+ super.didAddSubview(subview)
+ if subview.identifier == NSOutlineView.disclosureButtonIdentifier {
+ self.triangleView = subview as? NSButton
+ }
+ }
+
open override func drawBackground(in dirtyRect: NSRect) {
if let cell = self.view(atColumn: 0) as? ThemedTableCell {
if cell.isDir {
@@ -57,16 +69,16 @@ class ThemedTableCell: NSTableCellView {
static let font = NSFont.systemFont(ofSize: 12)
static let widthWithoutText = CGFloat(2 + 16 + 4 + 2)
- static func width(with text: String) -> CGFloat {
- let attrStr = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: ThemedTableCell.font])
-
- return self.widthWithoutText + attrStr.size().width
- }
-
- override var intrinsicContentSize: CGSize {
- return CGSize(width: ThemedTableCell.widthWithoutText + self._textField.intrinsicContentSize.width,
- height: max(self._textField.intrinsicContentSize.height, 16))
- }
+// static func width(with text: String) -> CGFloat {
+// let attrStr = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: ThemedTableCell.font])
+//
+// return self.widthWithoutText + attrStr.size().width
+// }
+//
+// override var intrinsicContentSize: CGSize {
+// return CGSize(width: ThemedTableCell.widthWithoutText + self._textField.intrinsicContentSize.width,
+// height: max(self._textField.intrinsicContentSize.height, 16))
+// }
var isDir = false
diff --git a/VimR/VimRTests/Info.plist b/VimR/VimRTests/Info.plist
index 722ef430..6dc1caae 100644
--- a/VimR/VimRTests/Info.plist
+++ b/VimR/VimRTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 0.25.0
+ SNAPSHOT-298
CFBundleSignature
????
CFBundleVersion
- 297
+ 298
diff --git a/appcast_snapshot.xml b/appcast_snapshot.xml
index ed04f8fc..0297ffb4 100644
--- a/appcast_snapshot.xml
+++ b/appcast_snapshot.xml
@@ -7,36 +7,22 @@
Most recent changes with links to updates for VimR.
en
-
- v0.25.0-297
+ SNAPSHOT-298
-Neovim 0.3.4
-GH-625:
vimr --cur-env
will pass the current environment variables to the new neovim process. This will result in virtualenv
support.
-GH-443: vimr --line ${LINE_NUMBER} ${SOME_FILE}
will open the file and go to the given line. If the file is already open in a UI window, then that window will be selected and the cursor will be moved to the given line. This can be used for example to reverse-search LaTeX.
-GH-603: Bugfix: Cmd-V
pastes at the wrong location in the insert mode.
-GH-659: Bugfix (introduced in a snapshot): Turning off ligatures does not really turn off ligatures.
-GH-664: Bugfix: VimR crashes for some shell configurations.
-GH-666: Adapt to the new UI-API of Neovim
-Dependencies updates:
-- ReactiveX/RxSwift@4.4.1
-- httpswift/swifter@1.4.5
-- PureLayout/PureLayout@3.1.4
-- sindresorhus/github-markdown-css@3.0.1
-- sparkle-project/Sparkle@1.21.3
-
-
+Improved handling of changes of cwd
in the file browser.
]]>
- https://github.com/qvacua/vimr/releases/tag/v0.25.0-297
+ https://github.com/qvacua/vimr/releases/tag/snapshot/298
- 2019-02-23T17:55:14.811734
+ 2019-02-26T07:24:18.032526
10.10.0
-
diff --git a/resources/release-notes.md b/resources/release-notes.md
index 8765aa52..6802bce9 100644
--- a/resources/release-notes.md
+++ b/resources/release-notes.md
@@ -1,4 +1,9 @@
-# 0.25.0-???
+# 0.26.0-???
+
+* Draw the disclosure triangle in appropriate color of the current color scheme (and improve handling of changes of `cwd` in the file browser).
+* ...
+
+# 0.25.0-297
* Neovim 0.3.4
* GH-625: `vimr --cur-env` will pass the current environment variables to the new neovim process. This will result in `virtualenv` support.