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.