1
1
mirror of https://github.com/qvacua/vimr.git synced 2024-12-26 07:13:24 +03:00

Merge remote-tracking branch 'origin/develop' into update-neovim

This commit is contained in:
Tae Won Ha 2019-02-26 07:27:05 +01:00
commit 70e508816b
14 changed files with 486 additions and 422 deletions

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.25.0</string> <string>SNAPSHOT-298</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>297</string> <string>298</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>

View File

@ -783,7 +783,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 297; CURRENT_PROJECT_VERSION = 298;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@ -845,7 +845,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 297; CURRENT_PROJECT_VERSION = 298;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -874,7 +874,7 @@
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 297; DYLIB_CURRENT_VERSION = 298;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac"; FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac";
FRAMEWORK_VERSION = A; FRAMEWORK_VERSION = A;
@ -896,7 +896,7 @@
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 297; DYLIB_CURRENT_VERSION = 298;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac"; FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac";
FRAMEWORK_VERSION = A; FRAMEWORK_VERSION = A;

View File

@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>FMWK</string> <string>FMWK</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.25.0</string> <string>SNAPSHOT-298</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>297</string> <string>298</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>Copyright © 2017 Tae Won Ha. All rights reserved.</string> <string>Copyright © 2017 Tae Won Ha. All rights reserved.</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>

View File

@ -15,8 +15,8 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>BNDL</string> <string>BNDL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.25.0</string> <string>SNAPSHOT-298</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>297</string> <string>298</string>
</dict> </dict>
</plist> </plist>

View File

@ -1272,7 +1272,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 297; CURRENT_PROJECT_VERSION = 298;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@ -1330,7 +1330,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 297; CURRENT_PROJECT_VERSION = 298;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;

View File

@ -219,7 +219,7 @@ extension NSOutlineView {
return self.item(atRow: self.clickedRow) return self.item(atRow: self.clickedRow)
} }
func toggle(item: Any) { func toggle(item: Any?) {
if self.isItemExpanded(item) { if self.isItemExpanded(item) {
self.collapseItem(item) self.collapseItem(item)
} else { } else {

View File

@ -21,9 +21,14 @@ class BuffersList: NSView,
case open(NvimView.Buffer) case open(NvimView.Buffer)
} }
private(set) var lastThemeMark = Token()
private(set) var theme = Theme.default private(set) var theme = Theme.default
required init(source: Observable<StateType>, emitter: ActionEmitter, state: StateType) { required init(
source: Observable<StateType>,
emitter: ActionEmitter,
state: StateType
) {
self.emit = emitter.typedEmit() self.emit = emitter.typedEmit()
self.uuid = state.uuid self.uuid = state.uuid
@ -52,14 +57,15 @@ class BuffersList: NSView,
self.usesTheme = state.appearance.usesTheme 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 return
} }
self.showsFileIcon = state.appearance.showsFileIcon self.showsFileIcon = state.appearance.showsFileIcon
self.buffers = state.buffers self.buffers = state.buffers
self.bufferList.reloadData() self.bufferList.reloadData()
self.adjustFileViewWidth()
}) })
.disposed(by: self.disposeBag) .disposed(by: self.disposeBag)
} }
@ -69,7 +75,6 @@ class BuffersList: NSView,
private let uuid: String private let uuid: String
private var usesTheme: Bool private var usesTheme: Bool
private var lastThemeMark = Token()
private var showsFileIcon: Bool private var showsFileIcon: Bool
private let bufferList = NSTableView.standardTableView() private let bufferList = NSTableView.standardTableView()
@ -95,16 +100,6 @@ class BuffersList: NSView,
self.addSubview(scrollView) self.addSubview(scrollView)
scrollView.autoPinEdgesToSuperviewEdges() 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 // MARK: - Actions
@ -132,14 +127,26 @@ extension BuffersList {
// MARK: - NSTableViewDelegate // MARK: - NSTableViewDelegate
extension BuffersList { extension BuffersList {
public func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { public func tableView(
return tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("buffer-row-view"), owner: self) _ tableView: NSTableView,
as? ThemedTableRow ?? ThemedTableRow(withIdentifier: "buffer-row-view", themedView: self) 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? { func tableView(
let cachedCell = (tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("buffer-cell-view"), owner: self) _ tableView: NSTableView,
as? ThemedTableCell)?.reset() 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") let cell = cachedCell ?? ThemedTableCell(withIdentifier: "buffer-cell-view")
@ -155,6 +162,22 @@ extension BuffersList {
return cell 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 { private func text(for buffer: NvimView.Buffer) -> NSAttributedString {
guard let name = buffer.name else { guard let name = buffer.name else {
return NSAttributedString(string: "No Name") return NSAttributedString(string: "No Name")
@ -164,16 +187,22 @@ extension BuffersList {
return NSAttributedString(string: name) 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)") let rowText = NSMutableAttributedString(string: "\(name)\(pathInfo)")
rowText.addAttribute(NSAttributedString.Key.foregroundColor, rowText.addAttribute(NSAttributedString.Key.foregroundColor,
value: self.theme.foreground, value: self.theme.foreground,
range: NSRange(location: 0, length: name.count)) range: NSRange(location: 0, length: name.count))
rowText.addAttribute(NSAttributedString.Key.foregroundColor, rowText.addAttribute(
value: self.theme.foreground.brightening(by: 1.15), NSAttributedString.Key.foregroundColor,
range: NSRange(location: name.count, length: pathInfo.count + 3)) value: self.theme.foreground.brightening(by: 1.15),
range: NSRange(location: name.count, length: pathInfo.count + 3)
)
return rowText return rowText
} }

View File

@ -181,7 +181,9 @@ extension FileBrowser {
return return
} }
#if NOPE
self.fileView.select(url) self.fileView.select(url)
#endif
} }
@objc func refreshAction(_ sender: Any?) { @objc func refreshAction(_ sender: Any?) {

View File

@ -7,67 +7,52 @@ import Cocoa
import NvimView import NvimView
import PureLayout import PureLayout
import RxSwift import RxSwift
import CocoaFontAwesome
class FileOutlineView: NSOutlineView, class FileOutlineView: NSOutlineView,
UiComponent, UiComponent,
NSOutlineViewDataSource,
NSOutlineViewDelegate, NSOutlineViewDelegate,
ThemedView { ThemedView {
typealias StateType = MainWindow.State typealias StateType = MainWindow.State
@objc dynamic var content = [Node]()
private(set) var lastThemeMark = Token()
private(set) var theme = Theme.default private(set) var theme = Theme.default
required init(source: Observable<StateType>, emitter: ActionEmitter, state: StateType) { required init(
source: Observable<StateType>,
emitter: ActionEmitter,
state: StateType
) {
self.emit = emitter.typedEmit() self.emit = emitter.typedEmit()
self.uuid = state.uuid self.uuid = state.uuid
self.root = Node(url: state.cwd)
self.root = FileBrowserItem(state.cwd)
self.isShowHidden = state.fileBrowserShowHidden
self.usesTheme = state.appearance.usesTheme self.usesTheme = state.appearance.usesTheme
self.showsFileIcon = state.appearance.showsFileIcon 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) super.init(frame: .zero)
NSOutlineView.configure(toStandard: self) NSOutlineView.configure(toStandard: self)
self.dataSource = self
self.delegate = 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 source
.observeOn(MainScheduler.instance) .observeOn(MainScheduler.instance)
.subscribe(onNext: { state in .subscribe(onNext: { state in
if state.viewToBeFocused != nil, case .fileBrowser = state.viewToBeFocused! { if state.viewToBeFocused != nil,
case .fileBrowser = state.viewToBeFocused! {
self.beFirstResponder() self.beFirstResponder()
} }
@ -80,76 +65,235 @@ class FileOutlineView: NSOutlineView,
self.usesTheme = state.appearance.usesTheme self.usesTheme = state.appearance.usesTheme
guard self.shouldReloadData(for: state, themeChanged: themeChanged) else { guard self.shouldReloadData(
for: state, themeChanged: themeChanged
) else {
return return
} }
self.showsFileIcon = state.appearance.showsFileIcon self.showsFileIcon = state.appearance.showsFileIcon
self.isShowHidden = state.fileBrowserShowHidden self.isShowHidden = state.fileBrowserShowHidden
self.lastFileSystemUpdateMark = state.lastFileSystemUpdate.mark self.lastFileSystemUpdateMark = state.lastFileSystemUpdate.mark
self.root = FileBrowserItem(state.cwd) self.root = Node(url: state.cwd)
self.reloadData() self.reloadRoot()
}) })
.disposed(by: self.disposeBag) .disposed(by: self.disposeBag)
}
override func reloadData() { source
self.cells.removeAll() .filter { !self.shouldReloadData(for: $0) }
self.widths.removeAll() .filter { $0.lastFileSystemUpdate.mark != self.lastFileSystemUpdateMark }
super.reloadData() .map { $0.lastFileSystemUpdate.payload }
} .throttle(2 * FileMonitor.fileSystemEventsLatency + 1,
latest: true,
func select(_ url: URL) { scheduler: SerialDispatchQueueScheduler(qos: .background))
var stack = [self.root] .map { ($0, Set(self.childUrls(for: $0))) }
.observeOn(MainScheduler.instance)
while let item = stack.popLast() { .subscribe(onNext: { (url, newChildUrls) in
self.expandItem(item) guard let changeTreeNode = self.changeRootTreeNode(for: url) else {
return
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)
} }
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<FileBrowser.Action>) -> Void private let emit: (UuidAction<FileBrowser.Action>) -> Void
private let disposeBag = DisposeBag() private let disposeBag = DisposeBag()
private let uuid: String 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 { private var cwd: URL {
return self.root.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 isShowHidden: Bool
private var root: FileBrowserItem private var triangleClosed: NSImage
private var triangleOpen: NSImage
private var widths = [String: CGFloat]() private func initContextMenu() {
private var cells = [String: ThemedTableCell]() // 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) { private func initBindings() {
fatalError("init(coder:) has not been implemented") 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<URL>
) {
let existingUrls = changeTreeNode.children?
.compactMap { $0.node?.url } ?? []
let newNodes = newChildUrls
.subtracting(existingUrls)
.map { Node(url: $0) }
let newIndexPaths = (0..<newNodes.count)
.map { i in changeTreeNode.indexPath.appending(i) }
self.treeController.insert(newNodes,
atArrangedObjectIndexPaths: newIndexPaths)
}
private func handleRemoval(
changeTreeNode: NSTreeNode, newChildUrls: Set<URL>
) {
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<Theme>) { private func updateTheme(_ theme: Marked<Theme>) {
self.theme = theme.payload self.theme = theme.payload
self.enclosingScrollView?.backgroundColor = self.theme.background self.enclosingScrollView?.backgroundColor = self.theme.background
self.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 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 { if self.isShowHidden != state.fileBrowserShowHidden {
return true return true
} }
@ -169,233 +313,8 @@ class FileOutlineView: NSOutlineView,
return false return false
} }
private func handleRemovals(for fileBrowserItem: FileBrowserItem, private func node(from item: Any?) -> Node? {
new newChildren: [FileBrowserItem]) { return (item as? NSTreeNode)?.node
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..<pathComps.count]
return childPart.reduce(self.root) { (resultItem, childName) -> 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
} }
} }
@ -403,84 +322,184 @@ extension FileOutlineView {
extension FileOutlineView { extension FileOutlineView {
@IBAction func doubleClickAction(_: Any?) { @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 return
} }
if item.url.isDir { if node.isDir {
self.toggle(item: item) self.toggle(item: clickedTreeNode)
} else { } else {
self.emit( 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?) { @IBAction func openInNewTab(_: Any?) {
guard let item = self.clickedItem as? FileBrowserItem else { guard let node = self.node(from: self.clickedItem) else {
return return
} }
self.emit( 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?) { @IBAction func openInCurrentTab(_: Any?) {
guard let item = self.clickedItem as? FileBrowserItem else { guard let node = self.node(from: self.clickedItem) else {
return return
} }
self.emit( 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?) { @IBAction func openInHorizontalSplit(_: Any?) {
guard let item = self.clickedItem as? FileBrowserItem else { guard let node = self.node(from: self.clickedItem) else {
return return
} }
self.emit( 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?) { @IBAction func openInVerticalSplit(_: Any?) {
guard let item = self.clickedItem as? FileBrowserItem else { guard let node = self.node(from: self.clickedItem) else {
return return
} }
self.emit( 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?) { @IBAction func setAsWorkingDirectory(_: Any?) {
guard let item = self.clickedItem as? FileBrowserItem else { guard let node = self.node(from: self.clickedItem) else {
return return
} }
guard item.url.isDir else { guard node.url.isDir else {
return return
} }
self.emit( 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 // MARK: - NSUserInterfaceValidations
extension FileOutlineView { extension FileOutlineView {
override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { override func validateUserInterfaceItem(
guard let clickedItem = self.clickedItem as? FileBrowserItem else { _ item: NSValidatedUserInterfaceItem
) -> Bool {
guard let clickedNode = self.node(from: self.clickedItem) else {
return true return true
} }
if item.action == #selector(setAsWorkingDirectory(_:)) { if item.action == #selector(setAsWorkingDirectory(_:)) {
return clickedItem.url.isDir return clickedNode.url.isDir
} }
return true return true
@ -496,18 +515,19 @@ extension FileOutlineView {
return return
} }
guard let item = self.selectedItem as? FileBrowserItem else { guard let node = self.node(from: self.selectedItem) else {
super.keyDown(with: event) super.keyDown(with: event)
return return
} }
switch char { switch char {
case " ", "\r": // Why "\r" and not "\n"? case " ", "\r": // Why "\r" and not "\n"?
if item.url.isDir || item.url.isPackage { if node.url.isDir || node.url.isPackage {
self.toggle(item: item) self.toggle(item: node)
} else { } else {
self.emit( 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 { static func <(lhs: Node, rhs: Node) -> Bool {
return left.url == right.url return lhs.displayName < rhs.displayName
} }
static func <(left: FileBrowserItem, right: FileBrowserItem) -> Bool { @objc dynamic var url: URL
return left.url.lastPathComponent < right.url.lastPathComponent @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 "<Node: \(self.url): \(self.childrenCount) children>"
}
override var hash: Int {
return self.url.hashValue return self.url.hashValue
} }
var description: String { init(url: URL) {
return self.url.path
}
let url: URL
let isDir: Bool
let isHidden: Bool
var children: [FileBrowserItem] = []
var isChildrenScanned = false
init(_ url: URL) {
self.url = url self.url = url
self.isLeaf = !url.isDir
// 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.isHidden = url.isHidden 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)

View File

@ -1224,7 +1224,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.25.0</string> <string>SNAPSHOT-298</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -1241,7 +1241,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>297</string> <string>298</string>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string> <string>public.app-category.productivity</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

@ -10,18 +10,30 @@ import PureLayout
protocol ThemedView: class { protocol ThemedView: class {
var theme: Theme { get } var theme: Theme { get }
var lastThemeMark: Token { get }
} }
class ThemedTableRow: NSTableRowView { class ThemedTableRow: NSTableRowView {
weak var triangleView: NSButton?
var themeToken: Token
init(withIdentifier identifier: String, themedView: ThemedView) { init(withIdentifier identifier: String, themedView: ThemedView) {
self.themedView = themedView self.themedView = themedView
self.themeToken = themedView.lastThemeMark
super.init(frame: .zero) super.init(frame: .zero)
self.identifier = NSUserInterfaceItemIdentifier(identifier) 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) { open override func drawBackground(in dirtyRect: NSRect) {
if let cell = self.view(atColumn: 0) as? ThemedTableCell { if let cell = self.view(atColumn: 0) as? ThemedTableCell {
if cell.isDir { if cell.isDir {
@ -57,16 +69,16 @@ class ThemedTableCell: NSTableCellView {
static let font = NSFont.systemFont(ofSize: 12) static let font = NSFont.systemFont(ofSize: 12)
static let widthWithoutText = CGFloat(2 + 16 + 4 + 2) static let widthWithoutText = CGFloat(2 + 16 + 4 + 2)
static func width(with text: String) -> CGFloat { // static func width(with text: String) -> CGFloat {
let attrStr = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: ThemedTableCell.font]) // let attrStr = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: ThemedTableCell.font])
//
return self.widthWithoutText + attrStr.size().width // return self.widthWithoutText + attrStr.size().width
} // }
//
override var intrinsicContentSize: CGSize { // override var intrinsicContentSize: CGSize {
return CGSize(width: ThemedTableCell.widthWithoutText + self._textField.intrinsicContentSize.width, // return CGSize(width: ThemedTableCell.widthWithoutText + self._textField.intrinsicContentSize.width,
height: max(self._textField.intrinsicContentSize.height, 16)) // height: max(self._textField.intrinsicContentSize.height, 16))
} // }
var isDir = false var isDir = false

View File

@ -15,10 +15,10 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>BNDL</string> <string>BNDL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.25.0</string> <string>SNAPSHOT-298</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>297</string> <string>298</string>
</dict> </dict>
</plist> </plist>

View File

@ -7,36 +7,22 @@
<description>Most recent changes with links to updates for VimR.</description> <description>Most recent changes with links to updates for VimR.</description>
<language>en</language> <language>en</language>
<item> <item>
<title>v0.25.0-297</title> <title>SNAPSHOT-298</title>
<description><![CDATA[ <description><![CDATA[
<ul> <ul>
<li>Neovim 0.3.4</li> <li>Improved handling of changes of <code>cwd</code> in the file browser.</li>
<li>GH-625: <code>vimr --cur-env</code> will pass the current environment variables to the new neovim process. This will result in <code>virtualenv</code> support.</li>
<li>GH-443: <code>vimr --line ${LINE_NUMBER} ${SOME_FILE}</code> 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.</li>
<li>GH-603: Bugfix: <code>Cmd-V</code> pastes at the wrong location in the insert mode.</li>
<li>GH-659: Bugfix (introduced in a snapshot): Turning off ligatures does not really turn off ligatures.</li>
<li>GH-664: Bugfix: VimR crashes for some shell configurations.</li>
<li>GH-666: Adapt to the new UI-API of Neovim</li>
<li>Dependencies updates:<ul>
<li>ReactiveX/RxSwift@4.4.1</li>
<li>httpswift/swifter@1.4.5</li>
<li>PureLayout/PureLayout@3.1.4</li>
<li>sindresorhus/github-markdown-css@3.0.1</li>
<li>sparkle-project/Sparkle@1.21.3</li>
</ul>
</li>
</ul> </ul>
]]></description> ]]></description>
<releaseNotesLink> <releaseNotesLink>
https://github.com/qvacua/vimr/releases/tag/v0.25.0-297 https://github.com/qvacua/vimr/releases/tag/snapshot/298
</releaseNotesLink> </releaseNotesLink>
<pubDate>2019-02-23T17:55:14.811734</pubDate> <pubDate>2019-02-26T07:24:18.032526</pubDate>
<minimumSystemVersion>10.10.0</minimumSystemVersion> <minimumSystemVersion>10.10.0</minimumSystemVersion>
<enclosure url="https://github.com/qvacua/vimr/releases/download/v0.25.0-297/VimR-v0.25.0-297.tar.bz2" <enclosure url="https://github.com/qvacua/vimr/releases/download/snapshot/298/VimR-SNAPSHOT-298.tar.bz2"
sparkle:version="297" sparkle:version="298"
sparkle:shortVersionString="0.25.0" sparkle:shortVersionString="SNAPSHOT-298"
sparkle:dsaSignature="MC4CFQC7d60NkbkgZndkDtFrmfJ2Um1yBAIVAKwxaxYy+9FpNNJXOFhAhVUTdxfK" sparkle:dsaSignature="MC0CFQCvFlIJAyiqFN5B1OqsorK6OV1IJwIUJPVNVeWDXCpCSj76EWCYPnNxD4A="
length="14935040" length="14969397"
type="application/octet-stream"/> type="application/octet-stream"/>
</item> </item>
</channel> </channel>

View File

@ -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 * 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-625: `vimr --cur-env` will pass the current environment variables to the new neovim process. This will result in `virtualenv` support.