From 7fd718143769440905478de86a01447f41a8800c Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Tue, 17 Jan 2017 19:47:59 +0100 Subject: [PATCH] First commit for redesign, experimenting around --- VimR.xcodeproj/project.pbxproj | 52 +++ VimR/AppDelegate.swift | 24 +- VimR/AppDelegateTransformer.swift | 32 ++ VimR/Context.swift | 98 +++++ VimR/MainWindow.swift | 215 ++++++++++ VimR/MainWindowTransformer.swift | 38 ++ VimR/States.swift | 628 ++++++++++++++++++++++++++++++ VimR/SwiftCommons.swift | 10 + VimR/UiRoot.swift | 52 +++ VimR/UiRootTransformer.swift | 34 ++ 10 files changed, 1178 insertions(+), 5 deletions(-) create mode 100644 VimR/AppDelegateTransformer.swift create mode 100644 VimR/Context.swift create mode 100644 VimR/MainWindow.swift create mode 100644 VimR/MainWindowTransformer.swift create mode 100644 VimR/States.swift create mode 100644 VimR/UiRoot.swift create mode 100644 VimR/UiRootTransformer.swift diff --git a/VimR.xcodeproj/project.pbxproj b/VimR.xcodeproj/project.pbxproj index 4d5e1978..95c030e1 100644 --- a/VimR.xcodeproj/project.pbxproj +++ b/VimR.xcodeproj/project.pbxproj @@ -8,13 +8,16 @@ /* Begin PBXBuildFile section */ 1929B0E0C3BC59F52713D5A2 /* FoundationCommons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B9AF20D7BD6E5C975128 /* FoundationCommons.swift */; }; + 1929B0FF696312F754BC96E2 /* MainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD8CBADC191CF8C85309 /* MainWindow.swift */; }; 1929B165820D7177743B537A /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B39DA7AC4A9B62D7CD39 /* Component.swift */; }; 1929B18A0D7C7407C51DB642 /* DataWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 1929BB6CFF4CC0B5E8B00C62 /* DataWrapper.m */; }; 1929B1E05C116514C1D3A384 /* CocoaCategories.m in Sources */ = {isa = PBXBuildFile; fileRef = 1929B5C3F2F1CA4113DABFFD /* CocoaCategories.m */; }; + 1929B29B95AD176D57942E08 /* UiRootTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B457B9D0FA4D21F3751E /* UiRootTransformer.swift */; }; 1929B3CEE0C1A1850E9CCE2F /* BasicTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B2FBE11048569391E092 /* BasicTypes.swift */; }; 1929B3F5743967125F357C9F /* Matcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BEEB33113B0E33C3830F /* Matcher.swift */; }; 1929B4145AA81F006BAF3B5C /* PreviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BB8BCA48637156F92945 /* PreviewService.swift */; }; 1929B462CD4935AFF6D69457 /* FileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B7CB4863F80230C32D3C /* FileItem.swift */; }; + 1929B4FEE6EB56EF3F56B805 /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B34FC23D805A8B29E8F7 /* Context.swift */; }; 1929B53876E6952D378C2B30 /* ScoredFileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BDF9EBAF1D9D44399045 /* ScoredFileItem.swift */; }; 1929B5CF6ECCBCC3FB5292CE /* HttpServerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B0D767ED19EE1281ECD9 /* HttpServerService.swift */; }; 1929B6388EAF16C190B82955 /* FileItemIgnorePattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B69499B2569793350CEC /* FileItemIgnorePattern.swift */; }; @@ -25,11 +28,15 @@ 1929B93DBAD09835E428F610 /* PrefPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BB251F74BEFC82CEEF84 /* PrefPane.swift */; }; 1929BA120290D6A2A61A4468 /* ArrayCommonsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B477E1E62433BC48E10B /* ArrayCommonsTest.swift */; }; 1929BA3BB94B77E9AE051FE5 /* PreviewComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B8DA5AA33536F0082200 /* PreviewComponent.swift */; }; + 1929BAFF1E011321D3186EE6 /* UiRoot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD4149D5A25C82064DD8 /* UiRoot.swift */; }; + 1929BB4A9B2FA42A64CCCC76 /* MainWindowTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD83A13BF133741766CC /* MainWindowTransformer.swift */; }; 1929BCF444CE7F1D14D421DE /* FileItemTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B4778E20696E3AAFB69B /* FileItemTest.swift */; }; 1929BD2F41D93ADFF43C1C98 /* NetUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 1929B02440BC99C42F9EBD45 /* NetUtils.m */; }; + 1929BD3878A3A47B8D685CD2 /* AppDelegateTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B7A68B7109CEFAF105E8 /* AppDelegateTransformer.swift */; }; 1929BD3F9E609BFADB27584B /* Scorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B9D510177918080BE39B /* Scorer.swift */; }; 1929BD4CA2204E061A86A140 /* MatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BC19C1BC19246AFF1621 /* MatcherTests.swift */; }; 1929BD52275A6570C666A7BA /* PreviewRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B1EC32D8A26958FB39B1 /* PreviewRenderer.swift */; }; + 1929BE0DAEE9664C5BCFA211 /* States.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BB6608B4F0E037CA0F4C /* States.swift */; }; 1929BEB90DCDAF7A2B68C886 /* ColorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BA6128BFDD54CA92F46E /* ColorUtils.swift */; }; 1929BEFEABA0448306CDB6D4 /* FileItemIgnorePatternTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BBC84557C8351EC6183E /* FileItemIgnorePatternTest.swift */; }; 1929BF81A40B4154D3EA33CE /* server_ui.m in Sources */ = {isa = PBXBuildFile; fileRef = 1929B93013228985F509C8F6 /* server_ui.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; @@ -288,13 +295,16 @@ 1929B1A51F076E088EF4CCA4 /* server_globals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = server_globals.h; sourceTree = ""; }; 1929B1EC32D8A26958FB39B1 /* PreviewRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewRenderer.swift; sourceTree = ""; }; 1929B2FBE11048569391E092 /* BasicTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicTypes.swift; sourceTree = ""; }; + 1929B34FC23D805A8B29E8F7 /* Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = ""; }; 1929B39DA7AC4A9B62D7CD39 /* Component.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Component.swift; sourceTree = ""; }; 1929B3A98687DF171307AAC8 /* FileItemService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItemService.swift; sourceTree = ""; }; + 1929B457B9D0FA4D21F3751E /* UiRootTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UiRootTransformer.swift; sourceTree = ""; }; 1929B4778E20696E3AAFB69B /* FileItemTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItemTest.swift; sourceTree = ""; }; 1929B477E1E62433BC48E10B /* ArrayCommonsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayCommonsTest.swift; sourceTree = ""; }; 1929B5C3F2F1CA4113DABFFD /* CocoaCategories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CocoaCategories.m; sourceTree = ""; }; 1929B5D977261F1EBFA9E8F1 /* FileUtilsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtilsTest.swift; sourceTree = ""; }; 1929B69499B2569793350CEC /* FileItemIgnorePattern.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItemIgnorePattern.swift; sourceTree = ""; }; + 1929B7A68B7109CEFAF105E8 /* AppDelegateTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegateTransformer.swift; sourceTree = ""; }; 1929B7CB4863F80230C32D3C /* FileItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItem.swift; sourceTree = ""; }; 1929B8DA5AA33536F0082200 /* PreviewComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewComponent.swift; sourceTree = ""; }; 1929B93013228985F509C8F6 /* server_ui.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = server_ui.m; sourceTree = ""; }; @@ -304,10 +314,14 @@ 1929BA8AC40B901B20F20B71 /* FileUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; 1929BADEB143008EFA6F3318 /* NetUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NetUtils.h; sourceTree = ""; }; 1929BB251F74BEFC82CEEF84 /* PrefPane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefPane.swift; sourceTree = ""; }; + 1929BB6608B4F0E037CA0F4C /* States.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = States.swift; sourceTree = ""; }; 1929BB6CFF4CC0B5E8B00C62 /* DataWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DataWrapper.m; sourceTree = ""; }; 1929BB8BCA48637156F92945 /* PreviewService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewService.swift; sourceTree = ""; }; 1929BBC84557C8351EC6183E /* FileItemIgnorePatternTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItemIgnorePatternTest.swift; sourceTree = ""; }; 1929BC19C1BC19246AFF1621 /* MatcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MatcherTests.swift; sourceTree = ""; }; + 1929BD4149D5A25C82064DD8 /* UiRoot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UiRoot.swift; sourceTree = ""; }; + 1929BD83A13BF133741766CC /* MainWindowTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindowTransformer.swift; sourceTree = ""; }; + 1929BD8CBADC191CF8C85309 /* MainWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindow.swift; sourceTree = ""; }; 1929BDF9EBAF1D9D44399045 /* ScoredFileItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScoredFileItem.swift; sourceTree = ""; }; 1929BE69CF9AB1A10D0DD4F2 /* CocoaCategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CocoaCategories.h; sourceTree = ""; }; 1929BEEB33113B0E33C3830F /* Matcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Matcher.swift; sourceTree = ""; }; @@ -503,6 +517,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1929B32401E8914DE9BF76CA /* UI */ = { + isa = PBXGroup; + children = ( + 1929BD8CBADC191CF8C85309 /* MainWindow.swift */, + 1929BD4149D5A25C82064DD8 /* UiRoot.swift */, + ); + name = UI; + sourceTree = ""; + }; 1929B41F745CDCDFE09ACDCF /* resources */ = { isa = PBXGroup; children = ( @@ -511,6 +534,16 @@ path = resources; sourceTree = ""; }; + 1929B5E773BDB3B4EE9D00C1 /* Transformers */ = { + isa = PBXGroup; + children = ( + 1929B7A68B7109CEFAF105E8 /* AppDelegateTransformer.swift */, + 1929B457B9D0FA4D21F3751E /* UiRootTransformer.swift */, + 1929BD83A13BF133741766CC /* MainWindowTransformer.swift */, + ); + name = Transformers; + sourceTree = ""; + }; 1929BA610ADEA2BA4424FBE5 /* Preview */ = { isa = PBXGroup; children = ( @@ -521,6 +554,17 @@ name = Preview; sourceTree = ""; }; + 1929BA652D3B88FC071531EC /* Redesign */ = { + isa = PBXGroup; + children = ( + 1929BB6608B4F0E037CA0F4C /* States.swift */, + 1929B34FC23D805A8B29E8F7 /* Context.swift */, + 1929B32401E8914DE9BF76CA /* UI */, + 1929B5E773BDB3B4EE9D00C1 /* Transformers */, + ); + name = Redesign; + sourceTree = ""; + }; 1929BFC86BF38D341F2DDCBD /* NeoVim Objects */ = { isa = PBXGroup; children = ( @@ -815,6 +859,7 @@ 4B6423941D8EFD6100FC78C8 /* Workspace */, 4B97E2CF1D33F92200FC0660 /* resources */, 1929B0D767ED19EE1281ECD9 /* HttpServerService.swift */, + 1929BA652D3B88FC071531EC /* Redesign */, ); path = VimR; sourceTree = ""; @@ -1310,6 +1355,13 @@ 1929BD52275A6570C666A7BA /* PreviewRenderer.swift in Sources */, 1929BD2F41D93ADFF43C1C98 /* NetUtils.m in Sources */, 1929B5CF6ECCBCC3FB5292CE /* HttpServerService.swift in Sources */, + 1929BE0DAEE9664C5BCFA211 /* States.swift in Sources */, + 1929B4FEE6EB56EF3F56B805 /* Context.swift in Sources */, + 1929B0FF696312F754BC96E2 /* MainWindow.swift in Sources */, + 1929BD3878A3A47B8D685CD2 /* AppDelegateTransformer.swift in Sources */, + 1929BAFF1E011321D3186EE6 /* UiRoot.swift in Sources */, + 1929B29B95AD176D57942E08 /* UiRootTransformer.swift in Sources */, + 1929BB4A9B2FA42A64CCCC76 /* MainWindowTransformer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/VimR/AppDelegate.swift b/VimR/AppDelegate.swift index bd4897d5..77f21a07 100644 --- a/VimR/AppDelegate.swift +++ b/VimR/AppDelegate.swift @@ -19,6 +19,11 @@ private enum VimRUrlAction: String { @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { + enum Action { + + case newMainWindow(urls: [URL], cwd: URL) + } + @IBOutlet var debugMenu: NSMenuItem? @IBOutlet var updater: SUUpdater? @@ -45,6 +50,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { fileprivate var launching = true override init() { + let source = self.stateContext.stateSource.mapOmittingNil { $0 as? MainWindowStates } + self.uiRoot = UiRoot(source: source, + emitter: self.stateContext.actionEmitter, + state: AppState.default.mainWindows) + + self.actionSink = self.actionSubject.asObservable() self.changeSink = self.changeSubject.asObservable() let actionAndChangeSink = [self.changeSink, self.actionSink].toMergedObservables() @@ -116,6 +127,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } } + + fileprivate let stateContext = StateContext() + fileprivate let uiRoot: UiRoot } // MARK: - NSApplicationDelegate @@ -243,6 +257,10 @@ extension AppDelegate { // MARK: - IBActions extension AppDelegate { + @IBAction func newDocument(_ sender: Any?) { + self.stateContext.actionEmitter.emit(Action.newMainWindow(urls: [], cwd: FileUtils.userHomeUrl)) + } + @IBAction func openInNewWindow(_ sender: Any?) { self.openDocument(sender) } @@ -251,10 +269,6 @@ extension AppDelegate { self.prefWindowComponent.show() } - @IBAction func newDocument(_ sender: Any?) { - _ = self.mainWindowManager.newMainWindow() - } - // Invoked when no main window is open. @IBAction func openDocument(_ sender: Any?) { let panel = NSOpenPanel() @@ -268,7 +282,7 @@ extension AppDelegate { let urls = panel.urls let commonParentUrl = FileUtils.commonParent(of: urls) - _ = self.mainWindowManager.newMainWindow(urls: urls, cwd: commonParentUrl) + self.stateContext.actionEmitter.emit(Action.newMainWindow(urls: urls, cwd: commonParentUrl)) } } } diff --git a/VimR/AppDelegateTransformer.swift b/VimR/AppDelegateTransformer.swift new file mode 100644 index 00000000..8936e74c --- /dev/null +++ b/VimR/AppDelegateTransformer.swift @@ -0,0 +1,32 @@ +// +// Created by Tae Won Ha on 1/16/17. +// Copyright (c) 2017 Tae Won Ha. All rights reserved. +// + +import Foundation +import RxSwift + +class AppDelegateTransformer: Transformer { + + typealias Pair = StateActionPair + + func transform(_ source: Observable) -> Observable { + return source.map { pair in + switch pair.action { + + case let .newMainWindow(urls, cwd): + var state = pair.state + + var mainWindow = state.last + mainWindow.uuid = UUID().uuidString + mainWindow.urlsToOpen = urls.toDict { url in MainWindow.OpenMode.default } + mainWindow.cwd = cwd + + state.current[mainWindow.uuid] = mainWindow + + return StateActionPair(state: state, action: pair.action) + + } + } + } +} diff --git a/VimR/Context.swift b/VimR/Context.swift new file mode 100644 index 00000000..69d4ff56 --- /dev/null +++ b/VimR/Context.swift @@ -0,0 +1,98 @@ +// +// Created by Tae Won Ha on 1/16/17. +// Copyright (c) 2017 Tae Won Ha. All rights reserved. +// + +import Foundation +import RxSwift + +class DummyService: Transformer { + + typealias State = MainWindowStates + typealias Action = AppDelegate.Action + + func transform(_ source: Observable>) -> Observable> { + NSLog("\(#function) dummy transform") + return source + } +} + +class StateContext { + + let stateSource: Observable + let actionEmitter = Emitter() + + init() { + self.stateSource = self.stateSubject.asObservable() + let actionSource = self.actionEmitter.observable + + actionSource + .mapOmittingNil { $0 as? AppDelegate.Action } + .map { StateActionPair(state: self.appState.mainWindows, action: $0) } + .transform(by: [appDelegateTransformer]) + .map { $0.state } + .subscribe(onNext: { state in + self.appState.mainWindows = state + self.stateSubject.onNext(state) + }) + .addDisposableTo(self.disposeBag) + + actionSource + .mapOmittingNil { $0 as? UuidAction } + .map { StateActionPair(state: self.appState.mainWindows, action: $0) } + .transform(by: self.uiRootTransformer) + .map { $0.state } + .subscribe(onNext: { state in + self.appState.mainWindows = state + self.stateSubject.onNext(state) + }) + .addDisposableTo(self.disposeBag) + + actionSource + .mapOmittingNil { $0 as? UuidAction } + .mapOmittingNil { action in + guard let mainWindowState = self.appState.mainWindows.current[action.uuid] else { + return nil + } + + return StateActionPair(state: UuidState(uuid: action.uuid, state: mainWindowState), action: action.payload) + } + .transform(by: self.mainWindowTransformer) + .subscribe(onNext: { pair in + self.appState.mainWindows.current[pair.state.uuid] = pair.state.payload + self.stateSubject.onNext(pair.state) + }) + .addDisposableTo(self.disposeBag) + + actionSource + .subscribe(onNext: { action in + NSLog("ACTION: \(action)") + }) + .addDisposableTo(self.disposeBag) + stateSource + .subscribe(onNext: { state in + NSLog("STATE : \(self.appState.mainWindows.current)") + }) + .addDisposableTo(self.disposeBag) + } + + fileprivate let stateSubject = PublishSubject() + fileprivate let disposeBag = DisposeBag() + + fileprivate var appState = AppState.default + + fileprivate let appDelegateTransformer = AppDelegateTransformer() + fileprivate let uiRootTransformer = UiRootTransformer() + fileprivate let mainWindowTransformer = MainWindowTransformer() +} + +extension Observable { + + fileprivate func transform(by transformers: [T]) -> Observable where T.Pair == Element { + return transformers.reduce(self) { (prevSource, transformer) in transformer.transform(prevSource) } + } + + fileprivate func transform(by transformer: T) -> Observable where T.Pair == Element { + return transformer.transform(self) + } +} diff --git a/VimR/MainWindow.swift b/VimR/MainWindow.swift new file mode 100644 index 00000000..3785e66d --- /dev/null +++ b/VimR/MainWindow.swift @@ -0,0 +1,215 @@ +// +// Created by Tae Won Ha on 1/16/17. +// Copyright (c) 2017 Tae Won Ha. All rights reserved. +// + +import Cocoa +import RxSwift +import SwiftNeoVim +import PureLayout + +protocol UiComponent { + + associatedtype StateType + + init(source: Observable, emitter: ActionEmitter, state: StateType) +} + +class MainWindow: NSObject, + UiComponent, + NeoVimViewDelegate, + NSWindowDelegate { + + typealias StateType = State + + enum Action { + + case cd(to: URL) + case setBufferList([NeoVimBuffer]) + + case becomeKey + + case close + } + + enum OpenMode { + + case `default` + case currentTab + case newTab + case horizontalSplit + case verticalSplit + } + + required init(source: Observable, emitter: ActionEmitter, state: StateType) { + self.uuid = state.uuid + self.emitter = emitter + + self.neoVimView = NeoVimView(frame: CGRect.zero, + config: NeoVimView.Config(useInteractiveZsh: state.isUseInteractiveZsh)) + self.neoVimView.configureForAutoLayout() + + self.workspace = Workspace(mainView: self.neoVimView) + + self.windowController = NSWindowController(windowNibName: "MainWindow") + + super.init() + self.addViews() + + self.windowController.window?.delegate = self + + source + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [unowned self] state in + }) + .addDisposableTo(self.disposeBag) + + let neoVimView = self.neoVimView + neoVimView.delegate = self + neoVimView.font = state.font + neoVimView.linespacing = state.linespacing + neoVimView.usesLigatures = state.isUseLigatures + if neoVimView.cwd != state.cwd { + self.neoVimView.cwd = state.cwd + } + + // 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 { + state.urlsToOpen.forEach { (url: URL, openMode: OpenMode) in + switch openMode { + + case .default: + self.neoVimView.open(urls: [url]) + + case .currentTab: + self.neoVimView.openInCurrentTab(url: url) + + case .newTab: + self.neoVimView.openInNewTab(urls: [url]) + + case .horizontalSplit: + self.neoVimView.openInHorizontalSplit(urls: [url]) + + case .verticalSplit: + self.neoVimView.openInVerticalSplit(urls: [url]) + + } + } + } + + self.window.makeFirstResponder(neoVimView) + } + + func show() { + self.windowController.showWindow(self) + } + + fileprivate func addViews() { + let contentView = self.window.contentView! + + contentView.addSubview(self.workspace) + + self.workspace.autoPinEdgesToSuperviewEdges() + } + + fileprivate let emitter: ActionEmitter + fileprivate let disposeBag = DisposeBag() + + fileprivate let uuid: String + + fileprivate let windowController: NSWindowController + fileprivate var window: NSWindow { return self.windowController.window! } + + fileprivate let workspace: Workspace + fileprivate let neoVimView: NeoVimView +} + +// MARK: - NeoVimViewDelegate +extension MainWindow { + + func neoVimStopped() { + self.windowController.close() + } + + func set(title: String) { + self.window.title = title + } + + func set(dirtyStatus: Bool) { + self.windowController.setDocumentEdited(dirtyStatus) + } + + func cwdChanged() { + self.emitter.emit(UuidAction(uuid: self.uuid, action: Action.cd(to: self.neoVimView.cwd))) + } + + func bufferListChanged() { + let buffers = self.neoVimView.allBuffers() + self.emitter.emit(UuidAction(uuid: self.uuid, action: Action.setBufferList(buffers))) + } + + func currentBufferChanged(_ currentBuffer: NeoVimBuffer) { +// self.publish(event: MainWindowAction.currentBufferChanged(mainWindow: self, buffer: currentBuffer)) + } + + func tabChanged() { +// guard let currentBuffer = self.neoVimView.currentBuffer() else { +// return +// } +// +// self.publish(event: MainWindowAction.currentBufferChanged(mainWindow: self, buffer: 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.scrollFlow.publish(event: ScrollAction.scroll(to: self.neoVimView.currentPosition)) + } + + func cursor(to position: Position) { +// self.scrollFlow.publish(event: ScrollAction.cursor(to: self.neoVimView.currentPosition)) + } +} + +// MARK: - NSWindowDelegate +extension MainWindow { + + func windowDidBecomeKey(_: Notification) { + self.emitter.emit(UuidAction(uuid: self.uuid, action: Action.becomeKey)) + } + + func windowWillClose(_: Notification) { + self.emitter.emit(UuidAction(uuid: self.uuid, action: Action.close)) + } + + 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 + } +} diff --git a/VimR/MainWindowTransformer.swift b/VimR/MainWindowTransformer.swift new file mode 100644 index 00000000..a5377536 --- /dev/null +++ b/VimR/MainWindowTransformer.swift @@ -0,0 +1,38 @@ +// +// Created by Tae Won Ha on 1/17/17. +// Copyright (c) 2017 Tae Won Ha. All rights reserved. +// + +import Foundation +import RxSwift + +class MainWindowTransformer: Transformer { + + typealias Pair = StateActionPair, MainWindow.Action> + + func transform(_ source: Observable) -> Observable { + return source.map { pair in + var state = pair.state.payload + + switch pair.action { + + case let .cd(to: cwd): + if state.cwd != cwd { + state.cwd = cwd + } + + case let .setBufferList(buffers): + buffers + .flatMap { $0.url } + .forEach { state.urlsToOpen.removeValue(forKey: $0) } + state.buffers = buffers + + default: + break + + } + + return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action) + } + } +} diff --git a/VimR/States.swift b/VimR/States.swift new file mode 100644 index 00000000..126e759b --- /dev/null +++ b/VimR/States.swift @@ -0,0 +1,628 @@ +// +// Created by Tae Won Ha on 1/13/17. +// Copyright (c) 2017 Tae Won Ha. All rights reserved. +// + +import Foundation +import RxSwift + +typealias ActionEmitter = Emitter + +class Emitter { + + let observable: Observable + + init() { + self.observable = self.subject.asObservable().observeOn(scheduler) + } + + func emit(_ action: T) { + self.subject.onNext(action) + } + + deinit { + self.subject.onCompleted() + } + + fileprivate let scheduler = SerialDispatchQueueScheduler(qos: .userInitiated) + fileprivate let subject = PublishSubject() +} + +class StateActionPair { + + let state: S + let action: A + + init(state: S, action: A) { + self.state = state + self.action = action + } +} + +class UuidAction: CustomStringConvertible { + + let uuid: String + let payload: A + + var description: String { + return "UuidAction(uuid: \(uuid), payload: \(String(reflecting: payload)))" + } + + init(uuid: String, action: A) { + self.uuid = uuid + self.payload = action + } +} + +class UuidState: CustomStringConvertible { + + let uuid: String + let payload: S + + var description: String { + return "UuidAction(uuid: \(uuid), payload: \(String(reflecting: payload)))" + } + + init(uuid: String, state: S) { + self.uuid = uuid + self.payload = state + } +} + +protocol Transformer { + + associatedtype Pair + + func transform(_ source: Observable) -> Observable +} + +protocol PersistableState { + + init?(dict: [String: Any]) + + func dict() -> [String: Any] +} + +struct MainWindowStates: PersistableState { + + var last: MainWindow.State + + var current: [String: MainWindow.State] + + init(last: MainWindow.State) { + self.last = last + self.current = [:] + } + + init?(dict: [String: Any]) { + guard let lastDict: [String: Any] = PrefUtils.value(from: dict, for: MainWindowStates.last) else { + return nil + } + + guard let last = MainWindow.State(dict: lastDict) else { + return nil + } + + self.init(last: last) + } + + func dict() -> [String: Any] { + return [ + MainWindowStates.last: self.last.dict(), + ] + } + + fileprivate static let last = "last" +} + +struct AppState: PersistableState { + + static let `default` = AppState(general: GeneralPrefState.default, + appearance: AppearancePrefState.default, + advanced: AdvancedPrefState.default, + mainWindow: MainWindow.State.default) + + var general: GeneralPrefState + var appearance: AppearancePrefState + var advanced: AdvancedPrefState + + var mainWindows: MainWindowStates + + init(general: GeneralPrefState, + appearance: AppearancePrefState, + advanced: AdvancedPrefState, + mainWindow: MainWindow.State) { + self.general = general + self.appearance = appearance + self.advanced = advanced + self.mainWindows = MainWindowStates(last: mainWindow) + } + + init?(dict: [String: Any]) { + guard let generalDict: [String: Any] = PrefUtils.value(from: dict, for: AppState.general), + let appearanceDict: [String: Any] = PrefUtils.value(from: dict, for: AppState.appearance), + let advancedDict: [String: Any] = PrefUtils.value(from: dict, for: AppState.advanced), + let mainWindowDict: [String: Any] = PrefUtils.value(from: dict, for: AppState.mainWindow) + else { + return nil + } + + guard let general = GeneralPrefState(dict: generalDict), + let appearance = AppearancePrefState(dict: appearanceDict), + let advanced = AdvancedPrefState(dict: advancedDict), + let mainWindow = MainWindow.State(dict: mainWindowDict) + else { + return nil + } + + self.init(general: general, appearance: appearance, advanced: advanced, mainWindow: mainWindow) + } + + func dict() -> [String: Any] { + return [ + AppState.general: self.general.dict(), + AppState.appearance: self.appearance.dict(), + AppState.advanced: self.advanced.dict(), + AppState.mainWindow: self.mainWindows.dict(), + ] + } + + fileprivate static let general = "general" + fileprivate static let appearance = "appearance" + fileprivate static let advanced = "advanced" + fileprivate static let mainWindow = "mainWindow" +} + +struct GeneralPrefState: Equatable, PersistableState { + + static func ==(left: GeneralPrefState, right: GeneralPrefState) -> Bool { + return left.openNewWindowWhenLaunching == right.openNewWindowWhenLaunching + && left.openNewWindowOnReactivation == right.openNewWindowOnReactivation + && left.ignorePatterns == right.ignorePatterns + } + + static let `default` = GeneralPrefState(openNewWindowWhenLaunching: true, + openNewWindowOnReactivation: true, + ignorePatterns: GeneralPrefState.defaultIgnorePatterns) + + var openNewWindowWhenLaunching: Bool + var openNewWindowOnReactivation: Bool + var ignorePatterns: Set + + init(openNewWindowWhenLaunching: Bool, + openNewWindowOnReactivation: Bool, + ignorePatterns: Set) { + self.openNewWindowWhenLaunching = openNewWindowWhenLaunching + self.openNewWindowOnReactivation = openNewWindowOnReactivation + self.ignorePatterns = ignorePatterns + } + + init?(dict: [String: Any]) { + guard let openNewWinWhenLaunching = PrefUtils.bool(from: dict, for: GeneralPrefState.openNewWindowWhenLaunching), + let openNewWinOnReactivation = PrefUtils.bool(from: dict, for: GeneralPrefState.openNewWindowOnReactivation), + let ignorePatternsStr = dict[GeneralPrefState.ignorePatterns] as? String + else { + return nil + } + + self.init(openNewWindowWhenLaunching: openNewWinWhenLaunching, + openNewWindowOnReactivation: openNewWinOnReactivation, + ignorePatterns: PrefUtils.ignorePatterns(fromString: ignorePatternsStr)) + } + + func dict() -> [String: Any] { + return [ + GeneralPrefState.openNewWindowWhenLaunching: self.openNewWindowWhenLaunching, + GeneralPrefState.openNewWindowOnReactivation: self.openNewWindowOnReactivation, + GeneralPrefState.ignorePatterns: PrefUtils.ignorePatternString(fromSet: self.ignorePatterns), + ] + } + + + fileprivate static let defaultIgnorePatterns = Set( + ["*/.git", "*.o", "*.d", "*.dia"].map(FileItemIgnorePattern.init) + ) + fileprivate static let openNewWindowWhenLaunching = "open-new-window-when-launching" + fileprivate static let openNewWindowOnReactivation = "open-new-window-on-reactivation" + fileprivate static let ignorePatterns = "ignore-patterns" +} + +struct AppearancePrefState: Equatable, PersistableState { + + static func ==(left: AppearancePrefState, right: AppearancePrefState) -> Bool { + return left.editorUsesLigatures == right.editorUsesLigatures + && left.editorFont.isEqual(to: right.editorFont) + && left.editorLinespacing == right.editorLinespacing + } + + static let `default` = AppearancePrefState(editorFont: NeoVimView.defaultFont, + editorLinespacing: NeoVimView.defaultLinespacing, + editorUsesLigatures: false) + + var editorFont: NSFont + var editorLinespacing: CGFloat + var editorUsesLigatures: Bool + + init(editorFont: NSFont, editorLinespacing: CGFloat, editorUsesLigatures: Bool) { + self.editorFont = editorFont + self.editorLinespacing = editorLinespacing + self.editorUsesLigatures = editorUsesLigatures + } + + init?(dict: [String: Any]) { + guard let editorFontName = dict[AppearancePrefState.editorFontName] as? String, + let fEditorFontSize = PrefUtils.float(from: dict, for: AppearancePrefState.editorFontSize), + let fEditorLinespacing = PrefUtils.float(from: dict, for: AppearancePrefState.editorLinespacing), + let editorUsesLigatures = PrefUtils.bool(from: dict, for: AppearancePrefState.editorUsesLigatures) + else { + return nil + } + + self.init(editorFont: PrefUtils.saneFont(editorFontName, fontSize: CGFloat(fEditorFontSize)), + editorLinespacing: CGFloat(fEditorLinespacing), + editorUsesLigatures: editorUsesLigatures) + } + + func dict() -> [String: Any] { + return [ + AppearancePrefState.editorFontName: self.editorFont.fontName, + AppearancePrefState.editorFontSize: Float(self.editorFont.pointSize), + AppearancePrefState.editorLinespacing: Float(self.editorLinespacing), + AppearancePrefState.editorUsesLigatures: self.editorUsesLigatures, + ] + } + + fileprivate static let editorFontName = "editor-font-name" + fileprivate static let editorFontSize = "editor-font-size" + fileprivate static let editorLinespacing = "editor-linespacing" + fileprivate static let editorUsesLigatures = "editor-uses-ligatures" +} + +struct AdvancedPrefState: Equatable, PersistableState { + + static func ==(left: AdvancedPrefState, right: AdvancedPrefState) -> Bool { + return left.useSnapshotUpdateChannel == right.useSnapshotUpdateChannel + && left.useInteractiveZsh == right.useInteractiveZsh + } + + static let `default` = AdvancedPrefState(useSnapshotUpdateChannel: false, useInteractiveZsh: false) + + let useSnapshotUpdateChannel: Bool + let useInteractiveZsh: Bool + + init(useSnapshotUpdateChannel: Bool, useInteractiveZsh: Bool) { + self.useSnapshotUpdateChannel = useSnapshotUpdateChannel + self.useInteractiveZsh = useInteractiveZsh + } + + init?(dict: [String: Any]) { + guard let useSnapshot = PrefUtils.bool(from: dict, for: AdvancedPrefState.useSnapshotUpdateChannel), + let useInteractiveZsh = PrefUtils.bool(from: dict, for: AdvancedPrefState.useInteractiveZsh) + else { + return nil + } + + self.init(useSnapshotUpdateChannel: useSnapshot, useInteractiveZsh: useInteractiveZsh) + } + + func dict() -> [String: Any] { + return [ + AdvancedPrefState.useSnapshotUpdateChannel: self.useSnapshotUpdateChannel, + AdvancedPrefState.useInteractiveZsh: self.useInteractiveZsh, + ] + } + + fileprivate static let useSnapshotUpdateChannel = "use-snapshot-update-channel" + fileprivate static let useInteractiveZsh = "use-interactive-zsh" +} + +extension MainWindow { + + struct State: PersistableState { + + static let `default` = State(isAllToolsVisible: true, + isToolButtonsVisible: true) + + var isAllToolsVisible = true + var isToolButtonsVisible = true + + // transient + var uuid = UUID().uuidString + var buffers = [NeoVimBuffer]() + var cwd = FileUtils.userHomeUrl + + var isDirty = false + + var font = NSFont.userFixedPitchFont(ofSize: 13)! + var linespacing: CGFloat = 1 + var isUseLigatures = false + var isUseInteractiveZsh = false + + // transient^2 + var urlsToOpen = [URL: OpenMode]() + + init(isAllToolsVisible: Bool, isToolButtonsVisible: Bool) { + self.isAllToolsVisible = isAllToolsVisible + self.isToolButtonsVisible = isToolButtonsVisible + } + + init?(dict: [String: Any]) { + guard let isAllToolsVisible = PrefUtils.bool(from: dict, for: State.isAllToolsVisible), + let isToolButtonsVisible = PrefUtils.bool(from: dict, for: State.isToolButtonsVisible) + else { + return nil + } + + self.init(isAllToolsVisible: isAllToolsVisible, isToolButtonsVisible: isToolButtonsVisible) + } + + func dict() -> [String: Any] { + return [ + State.isAllToolsVisible: self.isAllToolsVisible, + State.isToolButtonsVisible: self.isToolButtonsVisible, + ] + } + + fileprivate static let isAllToolsVisible = "is-all-tools-visible" + fileprivate static let isToolButtonsVisible = "is-tool-buttons-visible" + } +} + +//struct ToolsState: PersistableState { +// +// static let `default` = ToolsState(fileBrowser: FileBrowserComponent.State.default, +// bufferList: BufferListComponent.State.default, +// preview: PreviewComponent.State.default) +// +// var fileBrowser: FileBrowserComponent.State +// var bufferList: BufferListComponent.State +// var preview: PreviewComponent.State +// +// init(fileBrowser: FileBrowserComponent.State, +// bufferList: BufferListComponent.State, +// preview: PreviewComponent.State) { +// self.fileBrowser = fileBrowser +// self.bufferList = bufferList +// self.preview = preview +// } +// +// init?(dict: [String: Any]) { +// guard let fileBrowserDict = dict[FileBrowserComponent.identifier] as? [String: Any], +// let bufferListDict = dict[BufferListComponent.identifier] as? [String: Any], +// let previewDict = dict[PreviewComponent.identifier] as? [String: Any] +// else { +// return nil +// } +// +// guard let fileBrowser = FileBrowserComponent.State(dict: fileBrowserDict), +// let bufferList = BufferListComponent.State(dict: bufferListDict), +// let preview = PreviewComponent.State(dict: previewDict) +// else { +// return nil +// } +// +// self.init(fileBrowser: fileBrowser, bufferList: bufferList, preview: preview) +// } +// +// func dict() -> [String: Any] { +// return [ +// FileBrowserComponent.identifier: self.fileBrowser.dict(), +// BufferListComponent.identifier: self.bufferList.dict(), +// PreviewComponent.identifier: self.preview.dict(), +// ] +// } +//} +// +//struct ToolState: PersistableState { +// +// static let identifier = "tool-state" +// static let `default` = ToolState(location: .left, isVisible: false, dimension: 200) +// +// var location: WorkspaceBarLocation +// var isVisible: Bool +// var dimension: CGFloat +// +// init(location: WorkspaceBarLocation, isVisible: Bool, dimension: CGFloat) { +// self.location = location +// self.isVisible = isVisible +// self.dimension = dimension +// } +// +// init?(dict: [String: Any]) { +// guard let locationRawValue = dict[ToolState.location] as? String, +// let isVisible = PrefUtils.bool(from: dict, for: ToolState.isVisible), +// let fDimension = PrefUtils.float(from: dict, for: ToolState.dimension) +// else { +// return nil +// } +// +// guard let location = PrefUtils.location(from: locationRawValue) else { +// return nil +// } +// +// self.init(location: location, isVisible: isVisible, dimension: CGFloat(fDimension)) +// } +// +// func dict() -> [String: Any] { +// return [ +// ToolState.location: PrefUtils.locationAsString(for: self.location), +// ToolState.isVisible: self.isVisible, +// ToolState.dimension: Float(self.dimension), +// ] +// } +// +// fileprivate static let location = "location" +// fileprivate static let isVisible = "is-visible" +// fileprivate static let dimension = "dimension" +//} +// +//extension FileBrowserComponent { +// +// struct State: PersistableState { +// +// static let `default` = State(isShowHidden: false, toolState: ToolState.default) +// +// var isShowHidden: Bool +// var toolState: ToolState +// +// init(isShowHidden: Bool, toolState: ToolState) { +// self.isShowHidden = isShowHidden +// self.toolState = toolState +// } +// +// init?(dict: [String: Any]) { +// guard let isShowHidden = PrefUtils.bool(from: dict, for: State.isShowHidden), +// let toolStateDict = dict[ToolState.identifier] as? [String: Any] +// else { +// return nil +// } +// +// guard let toolState = ToolState(dict: toolStateDict) else { +// return nil +// } +// +// self.init(isShowHidden: isShowHidden, toolState: toolState) +// } +// +// func dict() -> [String: Any] { +// return [ +// ToolState.identifier: self.toolState, +// State.isShowHidden: self.isShowHidden, +// ] +// } +// +// fileprivate static let isShowHidden = "is-show-hidden" +// } +//} +// +//extension BufferListComponent { +// +// struct State: PersistableState { +// +// static let `default` = State(toolState: ToolState.default) +// +// var toolState: ToolState +// +// init(toolState: ToolState) { +// self.toolState = toolState +// } +// +// init?(dict: [String: Any]) { +// guard let toolStateDict = dict[ToolState.identifier] as? [String: Any] else { +// return nil +// } +// +// guard let toolState = ToolState(dict: toolStateDict) else { +// return nil +// } +// +// self.init(toolState: toolState) +// } +// +// func dict() -> [String: Any] { +// return [ +// ToolState.identifier: self.toolState, +// ] +// } +// } +//} +// +//extension PreviewComponent { +// +// struct State: PersistableState { +// +// static let `default` = State(markdown: MarkdownRenderer.State.default, toolState: ToolState.default) +// +// var markdown: MarkdownRenderer.State +// var toolState: ToolState +// +// init(markdown: MarkdownRenderer.State, toolState: ToolState) { +// self.markdown = markdown +// self.toolState = toolState +// } +// +// init?(dict: [String: Any]) { +// guard let markdownDict = dict[MarkdownRenderer.identifier] as? [String: Any], +// let toolStateDict = dict[ToolState.identifier] as? [String: Any] +// else { +// return nil +// } +// +// guard let markdown = MarkdownRenderer.State(dict: markdownDict) else { +// return nil +// } +// +// guard let toolState = ToolState(dict: toolStateDict) else { +// return nil +// } +// +// self.init(markdown: markdown, toolState: toolState) +// } +// +// func dict() -> [String: Any] { +// return [ +// ToolState.identifier: self.toolState, +// MarkdownRenderer.identifier: self.markdown.dict(), +// ] +// } +// } +//} +// +//extension MarkdownRenderer { +// +// struct State: PersistableState { +// +// static let `default` = State(isForwardSearchAutomatically: false, +// isReverseSearchAutomatically: false, +// isRefreshOnWrite: true, +// renderTime: Date.distantPast) +// +// var isForwardSearchAutomatically: Bool +// var isReverseSearchAutomatically: Bool +// var isRefreshOnWrite: Bool +// +// // transient +// var renderTime: Date +// +// init(isForwardSearchAutomatically: Bool, +// isReverseSearchAutomatically: Bool, +// isRefreshOnWrite: Bool, +// renderTime: Date) { +// self.isForwardSearchAutomatically = isForwardSearchAutomatically +// self.isReverseSearchAutomatically = isReverseSearchAutomatically +// self.isRefreshOnWrite = isRefreshOnWrite +// self.renderTime = renderTime +// } +// +// init?(dict: [String: Any]) { +// guard let isForward = PrefUtils.bool(from: dict, for: State.isForwardSearchAutomatically) else { +// return nil +// } +// +// guard let isReverse = PrefUtils.bool(from: dict, for: State.isReverseSearchAutomatically) else { +// return nil +// } +// +// guard let isRefreshOnWrite = PrefUtils.bool(from: dict, for: State.isRefreshOnWrite) else { +// return nil +// } +// +// self.init(isForwardSearchAutomatically: isForward, +// isReverseSearchAutomatically: isReverse, +// isRefreshOnWrite: isRefreshOnWrite, +// renderTime: Date.distantPast) +// } +// +// func dict() -> [String: Any] { +// return [ +// State.isForwardSearchAutomatically: self.isForwardSearchAutomatically, +// State.isReverseSearchAutomatically: self.isReverseSearchAutomatically, +// State.isRefreshOnWrite: self.isRefreshOnWrite, +// ] +// } +// +// fileprivate static let isForwardSearchAutomatically = "is-forward-search-automatically" +// fileprivate static let isReverseSearchAutomatically = "is-reverse-search-automatically" +// fileprivate static let isRefreshOnWrite = "is-refresh-on-write" +// } +//} diff --git a/VimR/SwiftCommons.swift b/VimR/SwiftCommons.swift index 991f826b..badb7477 100644 --- a/VimR/SwiftCommons.swift +++ b/VimR/SwiftCommons.swift @@ -56,6 +56,16 @@ extension Array { } } +extension Array where Element: Hashable { + + func toDict(by mapper: @escaping (Element) -> V) -> Dictionary { + var result = Dictionary(minimumCapacity: self.count) + self.forEach { result[$0] = mapper($0) } + + return result + } +} + func toDict(_ sequence: S) -> Dictionary where S.Iterator.Element == (K, V) { var result = Dictionary(minimumCapacity: sequence.underestimatedCount) diff --git a/VimR/UiRoot.swift b/VimR/UiRoot.swift new file mode 100644 index 00000000..a89a06a0 --- /dev/null +++ b/VimR/UiRoot.swift @@ -0,0 +1,52 @@ +// +// Created by Tae Won Ha on 1/16/17. +// Copyright (c) 2017 Tae Won Ha. All rights reserved. +// + +import Cocoa +import RxSwift + +class UiRoot: UiComponent { + + typealias StateType = MainWindowStates + + required init(source: Observable, emitter: ActionEmitter, state: StateType) { + self.source = source + self.emitter = emitter + + source + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [unowned self] state in + let keys = Set(self.mainWindows.keys) + let keysInState = Set(state.current.keys) + + keysInState + .subtracting(self.mainWindows.keys) + .flatMap { state.current[$0] } + .forEach(self.createNewMainWindow) + + keys + .subtracting(keysInState) + .forEach { + self.mainWindows.removeValue(forKey: $0) + } + + }) + .addDisposableTo(self.disposeBag) + } + + fileprivate func createNewMainWindow(with state: MainWindow.State) { + let mainWindow = MainWindow(source: self.source.mapOmittingNil { $0.current[state.uuid] }, + emitter: self.emitter, + state: state) + self.mainWindows[state.uuid] = mainWindow + + mainWindow.show() + } + + fileprivate let source: Observable + fileprivate let emitter: ActionEmitter + fileprivate let disposeBag = DisposeBag() + + fileprivate var mainWindows = [String: MainWindow]() +} diff --git a/VimR/UiRootTransformer.swift b/VimR/UiRootTransformer.swift new file mode 100644 index 00000000..adc9fb3e --- /dev/null +++ b/VimR/UiRootTransformer.swift @@ -0,0 +1,34 @@ +// +// Created by Tae Won Ha on 1/17/17. +// Copyright (c) 2017 Tae Won Ha. All rights reserved. +// + +import Foundation +import RxSwift + +class UiRootTransformer: Transformer { + + typealias Pair = StateActionPair> + + func transform(_ source: Observable) -> Observable { + return source.map { pair in + var state = pair.state + let uuid = pair.action.uuid + + switch pair.action.payload { + + case .becomeKey: + state.last = state.current[uuid] ?? state.last + + case .close: + state.current.removeValue(forKey: uuid) + + default: + break + + } + + return StateActionPair(state: state, action: pair.action) + } + } +}