1
1
mirror of https://github.com/qvacua/vimr.git synced 2024-12-01 01:32:04 +03:00
vimr/VimR/MainWindow.swift

560 lines
16 KiB
Swift
Raw Normal View History

2017-01-22 16:22:05 +03:00
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Cocoa
import RxSwift
import SwiftNeoVim
import PureLayout
class MainWindow: NSObject,
UiComponent,
NeoVimViewDelegate,
NSWindowDelegate {
typealias StateType = State
enum Action {
2017-02-23 00:51:24 +03:00
case open(Set<Token>)
case cd(to: URL)
case setBufferList([NeoVimBuffer])
2017-01-22 16:22:05 +03:00
case setCurrentBuffer(NeoVimBuffer)
2017-02-26 12:26:37 +03:00
case setDirtyStatus(Bool)
2017-01-22 16:22:05 +03:00
case becomeKey
case scroll(to: Marked<Position>)
case setCursor(to: Marked<Position>)
2017-01-22 16:22:05 +03:00
2017-02-25 00:47:32 +03:00
case focus(FocusableView)
2017-02-19 20:00:41 +03:00
case openQuickly
case close
}
2017-02-25 00:47:32 +03:00
enum FocusableView {
case neoVimView
case fileBrowser
case preview
}
enum OpenMode {
case `default`
case currentTab
case newTab
case horizontalSplit
case verticalSplit
}
2017-01-25 00:39:19 +03:00
required init(source: Observable<StateType>, emitter: ActionEmitter, state: StateType) {
self.uuid = state.uuid
self.emitter = emitter
2017-02-28 11:53:27 +03:00
self.defaultFont = state.appearance.font
self.linespacing = state.appearance.linespacing
self.usesLigatures = state.appearance.usesLigatures
2017-02-27 19:35:38 +03:00
self.editorPosition = state.preview.editorPosition
self.previewPosition = state.preview.previewPosition
self.neoVimView = NeoVimView(frame: CGRect.zero,
config: NeoVimView.Config(useInteractiveZsh: state.isUseInteractiveZsh))
self.neoVimView.configureForAutoLayout()
self.workspace = Workspace(mainView: self.neoVimView)
2017-02-06 00:39:55 +03:00
self.preview = PreviewTool(source: source, emitter: emitter, state: state)
self.fileBrowser = FileBrowser(source: source, emitter: emitter, state: state)
2017-02-26 14:00:19 +03:00
self.openedFileList = OpenedFileList(source: source, emitter: emitter, state: state)
self.windowController = NSWindowController(windowNibName: "MainWindow")
2017-02-25 00:47:32 +03:00
let previewConfig = WorkspaceTool.Config(title: "Preview",
view: self.preview,
customMenuItems: self.preview.menuItems)
self.previewContainer = WorkspaceTool(previewConfig)
previewContainer.dimension = 300
let fileBrowserConfig = WorkspaceTool.Config(title: "Files",
view: self.fileBrowser,
customToolbar: self.fileBrowser.innerCustomToolbar,
customMenuItems: self.fileBrowser.menuItems)
self.fileBrowserContainer = WorkspaceTool(fileBrowserConfig)
fileBrowserContainer.dimension = 200
2017-02-26 14:00:19 +03:00
let openedFileListConfig = WorkspaceTool.Config(title: "Opened", view: self.openedFileList)
self.openedFileListContainer = WorkspaceTool(openedFileListConfig)
self.openedFileListContainer.dimension = 200
2017-02-25 00:47:32 +03:00
self.workspace.append(tool: previewContainer, location: .right)
self.workspace.append(tool: fileBrowserContainer, location: .left)
2017-02-26 14:00:19 +03:00
self.workspace.append(tool: openedFileListContainer, location: .left)
2017-02-25 00:47:32 +03:00
fileBrowserContainer.toggle()
super.init()
2017-01-22 16:22:05 +03:00
2017-02-12 19:28:49 +03:00
Observable
.of(self.scrollDebouncer.observable, self.cursorDebouncer.observable)
.merge()
2017-01-22 16:22:05 +03:00
.subscribe(onNext: { [unowned self] action in
self.emitter.emit(self.uuidAction(for: action))
})
.addDisposableTo(self.disposeBag)
self.addViews()
self.windowController.window?.delegate = self
source
.observeOn(MainScheduler.instance)
.subscribe(
onNext: { [unowned self] state in
if state.isClosed {
return
}
2017-02-25 00:47:32 +03:00
if case .neoVimView = state.focusedView {
self.window.makeFirstResponder(self.neoVimView)
}
2017-02-26 12:26:37 +03:00
self.windowController.setDocumentEdited(state.isDirty)
if self.neoVimView.cwd != state.cwd {
self.neoVimView.cwd = state.cwd
}
if state.previewTool.isReverseSearchAutomatically
2017-02-12 19:28:49 +03:00
&& state.preview.previewPosition.hasDifferentMark(as: self.previewPosition) {
self.neoVimView.cursorGo(to: state.preview.previewPosition.payload)
2017-02-12 19:07:56 +03:00
} else if state.preview.forceNextReverse {
2017-02-12 18:40:49 +03:00
self.neoVimView.cursorGo(to: state.preview.previewPosition.payload)
}
self.previewPosition = state.preview.previewPosition
2017-02-23 00:51:24 +03:00
self.marksForOpenedUrls.subtracting(state.urlsToOpen.map { $0.mark }).forEach {
self.marksForOpenedUrls.remove($0)
}
self.open(markedUrls: state.urlsToOpen)
if self.currentBuffer != state.currentBuffer {
self.currentBuffer = state.currentBuffer
if let currentBuffer = self.currentBuffer {
self.neoVimView.select(buffer: currentBuffer)
}
}
2017-02-28 11:53:27 +03:00
if self.defaultFont != state.appearance.font
|| self.linespacing != state.appearance.linespacing
|| self.usesLigatures != state.appearance.usesLigatures {
self.defaultFont = state.appearance.font
self.linespacing = state.appearance.linespacing
self.usesLigatures = state.appearance.usesLigatures
self.updateNeoVimAppearance()
}
},
onCompleted: {
self.windowController.close()
})
.addDisposableTo(self.disposeBag)
2017-02-28 11:53:27 +03:00
self.updateNeoVimAppearance()
self.neoVimView.delegate = self
if self.neoVimView.cwd != state.cwd {
self.neoVimView.cwd = state.cwd
}
2017-02-23 00:51:24 +03:00
self.open(markedUrls: state.urlsToOpen)
2017-02-28 11:53:27 +03:00
self.window.makeFirstResponder(self.neoVimView)
2017-02-23 00:51:24 +03:00
}
func show() {
self.windowController.showWindow(self)
}
func closeAllNeoVimWindowsWithoutSaving() {
self.neoVimView.closeAllWindowsWithoutSaving()
}
fileprivate let emitter: ActionEmitter
fileprivate let disposeBag = DisposeBag()
fileprivate let uuid: String
fileprivate var currentBuffer: NeoVimBuffer?
fileprivate let windowController: NSWindowController
fileprivate var window: NSWindow { return self.windowController.window! }
2017-02-28 11:53:27 +03:00
fileprivate var defaultFont: NSFont
fileprivate var linespacing: CGFloat
fileprivate var usesLigatures: Bool
2017-02-27 19:35:38 +03:00
fileprivate let fontManager = NSFontManager.shared()
fileprivate let workspace: Workspace
fileprivate let neoVimView: NeoVimView
2017-02-25 00:47:32 +03:00
fileprivate let previewContainer: WorkspaceTool
fileprivate let fileBrowserContainer: WorkspaceTool
2017-02-26 14:00:19 +03:00
fileprivate let openedFileListContainer: WorkspaceTool
2017-02-25 00:47:32 +03:00
fileprivate let preview: PreviewTool
fileprivate var editorPosition: Marked<Position>
fileprivate var previewPosition: Marked<Position>
fileprivate let fileBrowser: FileBrowser
2017-02-26 14:00:19 +03:00
fileprivate let openedFileList: OpenedFileList
fileprivate let scrollDebouncer = Debouncer<Action>(interval: 0.75)
fileprivate let cursorDebouncer = Debouncer<Action>(interval: 0.75)
fileprivate var marksForOpenedUrls = Set<Token>()
2017-02-28 11:53:27 +03:00
fileprivate func updateNeoVimAppearance() {
self.neoVimView.font = self.defaultFont
self.neoVimView.linespacing = self.linespacing
self.neoVimView.usesLigatures = self.usesLigatures
}
fileprivate func uuidAction(for action: Action) -> UuidAction<Action> {
return UuidAction(uuid: self.uuid, action: action)
}
2017-02-23 00:51:24 +03:00
fileprivate func open(markedUrls: [Marked<[URL: OpenMode]>]) {
let markedUrlsToOpen = markedUrls.filter { !self.marksForOpenedUrls.contains($0.mark) }
markedUrls.map { $0.mark }.forEach {
self.marksForOpenedUrls.insert($0)
}
guard markedUrlsToOpen.count > 0 else {
return
}
// If we don't call the following in the next tick, only half of the existing swap file warning is displayed.
// Dunno why...
DispatchUtils.gui {
2017-02-23 00:51:24 +03:00
markedUrlsToOpen.forEach { marked in
marked.payload.forEach { (url: URL, openMode: OpenMode) in
switch openMode {
2017-02-23 00:51:24 +03:00
case .default:
self.neoVimView.open(urls: [url])
2017-02-23 00:51:24 +03:00
case .currentTab:
self.neoVimView.openInCurrentTab(url: url)
2017-02-23 00:51:24 +03:00
case .newTab:
self.neoVimView.openInNewTab(urls: [url])
2017-02-23 00:51:24 +03:00
case .horizontalSplit:
self.neoVimView.openInHorizontalSplit(urls: [url])
2017-02-23 00:51:24 +03:00
case .verticalSplit:
self.neoVimView.openInVerticalSplit(urls: [url])
2017-02-23 00:51:24 +03:00
}
}
}
2017-02-23 00:51:24 +03:00
// not good, but we need it because we don't want to re-build the whole tab/window/buffer state of neovim in
// MainWindow.State
self.emitter.emit(self.uuidAction(for: Action.open(Set(markedUrls.map { $0.mark }))))
}
}
fileprivate func addViews() {
let contentView = self.window.contentView!
contentView.addSubview(self.workspace)
self.workspace.autoPinEdgesToSuperviewEdges()
}
}
// MARK: - NeoVimViewDelegate
extension MainWindow {
func neoVimStopped() {
self.emitter.emit(self.uuidAction(for: .close))
}
func set(title: String) {
self.window.title = title
}
func set(dirtyStatus: Bool) {
2017-02-26 12:26:37 +03:00
self.emitter.emit(self.uuidAction(for: .setDirtyStatus(dirtyStatus)))
}
func cwdChanged() {
2017-01-22 16:22:05 +03:00
self.emitter.emit(self.uuidAction(for: .cd(to: self.neoVimView.cwd)))
}
func bufferListChanged() {
let buffers = self.neoVimView.allBuffers()
2017-01-22 16:22:05 +03:00
self.emitter.emit(self.uuidAction(for: .setBufferList(buffers)))
}
func currentBufferChanged(_ currentBuffer: NeoVimBuffer) {
if self.currentBuffer == currentBuffer {
return
}
2017-01-22 16:22:05 +03:00
self.emitter.emit(self.uuidAction(for: .setCurrentBuffer(currentBuffer)))
self.currentBuffer = currentBuffer
}
func tabChanged() {
2017-01-22 16:22:05 +03:00
guard let currentBuffer = self.neoVimView.currentBuffer() else {
return
}
self.currentBufferChanged(currentBuffer)
}
func ipcBecameInvalid(reason: String) {
let alert = NSAlert()
alert.addButton(withTitle: "Close")
alert.messageText = "Sorry, an error occurred."
alert.informativeText = "VimR encountered an error from which it cannot recover. This window will now close.\n"
+ reason
alert.alertStyle = .critical
alert.beginSheetModal(for: self.window) { response in
self.windowController.close()
}
}
func scroll() {
self.scrollDebouncer.call(.scroll(to: Marked(self.neoVimView.currentPosition)))
}
func cursor(to position: Position) {
if position == self.editorPosition.payload {
return
}
self.editorPosition = Marked(position)
self.cursorDebouncer.call(.setCursor(to: self.editorPosition))
}
}
// MARK: - NSWindowDelegate
extension MainWindow {
func windowDidBecomeKey(_: Notification) {
2017-01-22 16:22:05 +03:00
self.emitter.emit(self.uuidAction(for: .becomeKey))
}
func windowShouldClose(_: Any) -> Bool {
guard self.neoVimView.isCurrentBufferDirty() else {
self.neoVimView.closeCurrentTab()
return false
}
let alert = NSAlert()
alert.addButton(withTitle: "Cancel")
alert.addButton(withTitle: "Discard and Close")
alert.messageText = "The current buffer has unsaved changes!"
alert.alertStyle = .warning
alert.beginSheetModal(for: self.window, completionHandler: { response in
if response == NSAlertSecondButtonReturn {
self.neoVimView.closeCurrentTabWithoutSaving()
}
})
return false
}
}
2017-02-25 00:47:32 +03:00
// MARK: - File Menu Item Actions
extension MainWindow {
2017-02-25 00:47:32 +03:00
@IBAction func newTab(_ sender: Any?) {
self.neoVimView.newTab()
}
@IBAction func openDocument(_ sender: Any?) {
let panel = NSOpenPanel()
panel.canChooseDirectories = true
panel.allowsMultipleSelection = true
panel.beginSheetModal(for: self.window) { result in
guard result == NSFileHandlingPanelOKButton else {
return
}
let urls = panel.urls
if self.neoVimView.allBuffers().count == 1 {
let isTransient = self.neoVimView.allBuffers().first?.isTransient ?? false
if isTransient {
self.neoVimView.cwd = FileUtils.commonParent(of: urls)
}
}
self.neoVimView.open(urls: urls)
}
}
@IBAction func openQuickly(_ sender: Any?) {
self.emitter.emit(self.uuidAction(for: .openQuickly))
}
2017-02-25 00:47:32 +03:00
@IBAction func saveDocument(_ sender: Any?) {
guard let curBuf = self.neoVimView.currentBuffer() else {
return
}
if curBuf.url == nil {
self.savePanelSheet { self.neoVimView.saveCurrentTab(url: $0) }
return
}
self.neoVimView.saveCurrentTab()
}
@IBAction func saveDocumentAs(_ sender: Any?) {
if self.neoVimView.currentBuffer() == nil {
return
}
self.savePanelSheet { url in
self.neoVimView.saveCurrentTab(url: url)
if self.neoVimView.isCurrentBufferDirty() {
self.neoVimView.openInNewTab(urls: [url])
} else {
self.neoVimView.openInCurrentTab(url: url)
}
}
}
fileprivate func savePanelSheet(action: @escaping (URL) -> Void) {
let panel = NSSavePanel()
panel.beginSheetModal(for: self.window) { result in
guard result == NSFileHandlingPanelOKButton else {
return
}
let showAlert: () -> Void = {
let alert = NSAlert()
alert.addButton(withTitle: "OK")
alert.messageText = "Invalid File Name"
alert.informativeText = "The file name you have entered cannot be used. Please use a different name."
alert.alertStyle = .warning
alert.runModal()
}
guard let url = panel.url else {
showAlert()
return
}
action(url)
}
}
}
2017-02-27 19:35:38 +03:00
// MARK: - Font Menu Item Actions
extension MainWindow {
@IBAction func resetFontSize(_ sender: Any?) {
2017-02-28 11:53:27 +03:00
self.neoVimView.font = self.defaultFont
2017-02-27 19:35:38 +03:00
}
@IBAction func makeFontBigger(_ sender: Any?) {
let curFont = self.neoVimView.font
let font = self.fontManager.convert(curFont, toSize: min(curFont.pointSize + 1, NeoVimView.maxFontSize))
self.neoVimView.font = font
}
@IBAction func makeFontSmaller(_ sender: Any?) {
let curFont = self.neoVimView.font
let font = self.fontManager.convert(curFont, toSize: max(curFont.pointSize - 1, NeoVimView.minFontSize))
self.neoVimView.font = font
}
}
2017-02-25 00:47:32 +03:00
// MARK: - Tools Menu Item Actions
extension MainWindow {
@IBAction func toggleAllTools(_ sender: Any?) {
self.workspace.toggleAllTools()
self.focusNeoVimView(self)
}
@IBAction func toggleToolButtons(_ sender: Any?) {
self.workspace.toggleToolButtons()
}
@IBAction func toggleFileBrowser(_ sender: Any?) {
let fileBrowser = self.fileBrowserContainer
if fileBrowser.isSelected {
if fileBrowser.view.isFirstResponder {
fileBrowser.toggle()
self.focusNeoVimView(self)
} else {
self.emitter.emit(self.uuidAction(for: .focus(.fileBrowser)))
}
return
}
fileBrowser.toggle()
self.emitter.emit(self.uuidAction(for: .focus(.fileBrowser)))
}
@IBAction func focusNeoVimView(_: Any?) {
// self.window.makeFirstResponder(self.neoVimView)
self.emitter.emit(self.uuidAction(for: .focus(.neoVimView)))
}
}
// MARK: - NSUserInterfaceValidationsProtocol
extension MainWindow {
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
let canSave = self.neoVimView.currentBuffer() != nil
let canSaveAs = canSave
let canOpen = canSave
let canOpenQuickly = canSave
let canFocusNeoVimView = self.window.firstResponder != self.neoVimView
guard let action = item.action else {
return true
}
switch action {
case #selector(focusNeoVimView(_:)):
return canFocusNeoVimView
case #selector(openDocument(_:)):
return canOpen
case #selector(openQuickly(_:)):
return canOpenQuickly
case #selector(saveDocument(_:)):
return canSave
case #selector(saveDocumentAs(_:)):
return canSaveAs
default:
return true
}
}
}