mirror of
https://github.com/qvacua/vimr.git
synced 2024-12-30 09:12:09 +03:00
570 lines
18 KiB
Swift
570 lines
18 KiB
Swift
/**
|
|
* Tae Won Ha - http://taewon.de - @hataewon
|
|
* See LICENSE
|
|
*/
|
|
|
|
import Cocoa
|
|
import Commons
|
|
import NvimView
|
|
import RxSwift
|
|
import Workspace
|
|
|
|
struct AppState: Codable {
|
|
enum OpenFilesFromApplicationsAction: String, Codable, CaseIterable {
|
|
case inNewWindow
|
|
case inCurrentWindow
|
|
}
|
|
|
|
enum AfterLastWindowAction: String, Codable {
|
|
case doNothing = "do-nothing"
|
|
case hide
|
|
case quit
|
|
}
|
|
|
|
static let `default` = AppState()
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case openNewMainWindowOnLaunch = "open-new-window-when-launching"
|
|
case openNewMainWindowOnReactivation = "open-new-window-on-reactivation"
|
|
case openFilesFromApplicationsAction = "open-files-from-applications-action"
|
|
case afterLastWindowAction = "after-last-window-action"
|
|
case activateAsciiImInNormalMode = "activate-ascii-im-in-normal-mode"
|
|
case useSnapshotUpdate = "use-snapshot-update-channel"
|
|
|
|
case openQuickly = "open-quickly"
|
|
case mainWindowTemplate = "main-window"
|
|
}
|
|
|
|
var openNewMainWindowOnLaunch = true
|
|
var openNewMainWindowOnReactivation = true
|
|
|
|
var openFilesFromApplicationsAction = OpenFilesFromApplicationsAction.inNewWindow
|
|
var afterLastWindowAction = AfterLastWindowAction.doNothing
|
|
|
|
var activateAsciiImInNormalMode = true
|
|
|
|
var useSnapshotUpdate = false
|
|
|
|
var preferencesOpen = Marked(false)
|
|
|
|
var mainWindowTemplate = MainWindow.State.default
|
|
var currentMainWindowUuid: UUID?
|
|
|
|
var mainWindows: [UUID: MainWindow.State] = [:]
|
|
var currentMainWindow: MainWindow.State? {
|
|
guard let uuid = self.currentMainWindowUuid else { return nil }
|
|
return self.mainWindows[uuid]
|
|
}
|
|
|
|
var openQuickly = OpenQuicklyWindow.State.default
|
|
|
|
var quit = false
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
self.openNewMainWindowOnLaunch = try container.decode(
|
|
forKey: .openNewMainWindowOnLaunch,
|
|
default: AppState.default.openNewMainWindowOnLaunch
|
|
)
|
|
self.openNewMainWindowOnReactivation = try container.decode(
|
|
forKey: .openNewMainWindowOnReactivation,
|
|
default: AppState.default.openNewMainWindowOnReactivation
|
|
)
|
|
self.openFilesFromApplicationsAction = try container.decode(
|
|
forKey: .openFilesFromApplicationsAction,
|
|
default: .inNewWindow
|
|
)
|
|
self.afterLastWindowAction = try container.decode(
|
|
forKey: .afterLastWindowAction,
|
|
default: .doNothing
|
|
)
|
|
self.activateAsciiImInNormalMode = try container.decode(
|
|
forKey: .activateAsciiImInNormalMode,
|
|
default: true
|
|
)
|
|
self.useSnapshotUpdate = try container.decode(
|
|
forKey: .useSnapshotUpdate,
|
|
default: AppState.default.useSnapshotUpdate
|
|
)
|
|
|
|
self.openQuickly = try container.decode(
|
|
forKey: .openQuickly,
|
|
default: OpenQuicklyWindow.State.default
|
|
)
|
|
self.mainWindowTemplate = try container.decode(
|
|
forKey: .mainWindowTemplate,
|
|
default: MainWindow.State.default
|
|
)
|
|
}
|
|
|
|
// Use generated encode(to:)
|
|
|
|
private init() {}
|
|
}
|
|
|
|
extension OpenQuicklyWindow {
|
|
struct State: Codable {
|
|
static let `default` = State()
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case defaultUsesVcsIgnore = "default-uses-vcs-ignores"
|
|
}
|
|
|
|
var defaultUsesVcsIgnores = true
|
|
var open = false
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
self.defaultUsesVcsIgnores = try container.decode(
|
|
forKey: .defaultUsesVcsIgnore,
|
|
default: OpenQuicklyWindow.State.default.defaultUsesVcsIgnores
|
|
)
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(self.defaultUsesVcsIgnores, forKey: .defaultUsesVcsIgnore)
|
|
}
|
|
|
|
private init() {}
|
|
}
|
|
}
|
|
|
|
struct PreviewState {
|
|
static let `default` = PreviewState()
|
|
|
|
enum Status {
|
|
case none
|
|
case notSaved
|
|
case error
|
|
case markdown
|
|
}
|
|
|
|
enum SearchAction {
|
|
case none
|
|
case forward
|
|
case reverse
|
|
case reload
|
|
}
|
|
|
|
var status = Status.none
|
|
|
|
var buffer: URL?
|
|
var html: URL?
|
|
var server: URL?
|
|
|
|
var updateDate: Date
|
|
|
|
var editorPosition = Marked(Position.beginning)
|
|
var previewPosition = Marked(Position.beginning)
|
|
var lastSearch = SearchAction.none
|
|
|
|
init(
|
|
status: Status = .none,
|
|
buffer: URL? = nil,
|
|
html: URL? = nil,
|
|
server: URL? = nil,
|
|
updateDate: Date = Date(),
|
|
editorPosition: Marked<Position> = Marked(.beginning),
|
|
previewPosition: Marked<Position> = Marked(.beginning)
|
|
) {
|
|
self.status = status
|
|
self.buffer = buffer
|
|
self.html = html
|
|
self.server = server
|
|
self.updateDate = updateDate
|
|
self.editorPosition = editorPosition
|
|
self.previewPosition = previewPosition
|
|
}
|
|
}
|
|
|
|
struct HtmlPreviewState {
|
|
static let `default` = HtmlPreviewState()
|
|
|
|
var htmlFile: URL?
|
|
var server: Marked<URL>?
|
|
}
|
|
|
|
struct AppearanceState: Codable {
|
|
static let `default` = AppearanceState()
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case usesCustomTab = "uses-custom-tab"
|
|
case usesTheme = "uses-theme"
|
|
case showsFileIcon = "shows-file-icon"
|
|
case editorFontName = "editor-font-name"
|
|
case editorFontSize = "editor-font-size"
|
|
case editorLinespacing = "editor-linespacing"
|
|
case editorCharacterspacing = "editor-characterspacing"
|
|
case editorUsesLigatures = "editor-uses-ligatures"
|
|
case editorFontSmoothing = "editor-font-smoothing"
|
|
}
|
|
|
|
var font = NSFont.userFixedPitchFont(ofSize: 13)!
|
|
var linespacing: CGFloat = 1
|
|
var characterspacing: CGFloat = 1
|
|
var usesLigatures = true
|
|
var fontSmoothing = FontSmoothing.systemSetting
|
|
|
|
var usesCustomTab = true
|
|
var usesTheme = true
|
|
var showsFileIcon = true
|
|
var theme = Marked(Theme.default)
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
if let fontName = try container.decodeIfPresent(String.self, forKey: .editorFontName),
|
|
let fontSize = try container.decodeIfPresent(Double.self, forKey: .editorFontSize),
|
|
let font = NSFont(name: fontName, size: fontSize)
|
|
{
|
|
self.font = font
|
|
} else {
|
|
self.font = NvimView.defaultFont
|
|
}
|
|
|
|
self
|
|
.linespacing = (try container.decodeIfPresent(Double.self, forKey: .editorLinespacing) ?? 1.0)
|
|
self
|
|
.characterspacing = (
|
|
try container
|
|
.decodeIfPresent(Double.self, forKey: .editorCharacterspacing) ?? 1.0
|
|
)
|
|
self.usesLigatures = try container
|
|
.decodeIfPresent(Bool.self, forKey: .editorUsesLigatures) ?? true
|
|
self.fontSmoothing = try container.decodeIfPresent(
|
|
FontSmoothing.self,
|
|
forKey: .editorFontSmoothing
|
|
) ?? .systemSetting
|
|
|
|
self.usesTheme = try container.decodeIfPresent(Bool.self, forKey: .usesTheme) ?? true
|
|
self.usesCustomTab = try container.decodeIfPresent(Bool.self, forKey: .usesCustomTab) ?? true
|
|
self.showsFileIcon = try container.decodeIfPresent(Bool.self, forKey: .showsFileIcon) ?? true
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(self.usesCustomTab, forKey: .usesCustomTab)
|
|
try container.encode(self.usesTheme, forKey: .usesTheme)
|
|
try container.encode(self.showsFileIcon, forKey: .showsFileIcon)
|
|
try container.encode(self.font.fontName, forKey: .editorFontName)
|
|
try container.encode(self.font.pointSize, forKey: .editorFontSize)
|
|
try container.encode(self.linespacing, forKey: .editorLinespacing)
|
|
try container.encode(self.characterspacing, forKey: .editorCharacterspacing)
|
|
try container.encode(self.fontSmoothing, forKey: .editorFontSmoothing)
|
|
try container.encode(self.usesLigatures, forKey: .editorUsesLigatures)
|
|
}
|
|
|
|
private init() {}
|
|
}
|
|
|
|
extension MainWindow {
|
|
struct State: Codable {
|
|
static let `default` = State(isAllToolsVisible: true, isToolButtonsVisible: true)
|
|
|
|
static let defaultTools: [MainWindow.Tools: WorkspaceToolState] = [
|
|
.fileBrowser: WorkspaceToolState(location: .left, dimension: 200, open: true),
|
|
.buffersList: WorkspaceToolState(location: .left, dimension: 200, open: false),
|
|
.preview: WorkspaceToolState(location: .right, dimension: 250, open: false),
|
|
.htmlPreview: WorkspaceToolState(location: .right, dimension: 500, open: false),
|
|
]
|
|
|
|
static let orderedDefault: [MainWindow.Tools] = [.fileBrowser, .buffersList, .preview,
|
|
.htmlPreview]
|
|
|
|
var isAllToolsVisible = true
|
|
var isToolButtonsVisible = true
|
|
var activeTools = [
|
|
Tools.fileBrowser: true,
|
|
Tools.buffersList: true,
|
|
Tools.preview: true,
|
|
Tools.htmlPreview: true,
|
|
]
|
|
|
|
var frame = CGRect(x: 100, y: 100, width: 600, height: 400)
|
|
|
|
////// transient
|
|
var goToLineFromCli: Marked<Int>?
|
|
var lastFileSystemUpdate = Marked(FileUtils.userHomeUrl)
|
|
|
|
var tools = MainWindow.State.defaultTools
|
|
var orderedTools = MainWindow.State.orderedDefault
|
|
|
|
var preview = PreviewState.default
|
|
var htmlPreview = HtmlPreviewState.default
|
|
|
|
var previewTool = MarkdownTool.State.default
|
|
|
|
var fileBrowserShowHidden = false
|
|
|
|
var useLiveResize = false
|
|
var drawsParallel = false
|
|
|
|
var isTemporarySession = false
|
|
|
|
// neovim
|
|
var uuid = UUID()
|
|
var currentBuffer: NvimView.Buffer?
|
|
var buffers = [NvimView.Buffer]()
|
|
var cwd = FileUtils.userHomeUrl
|
|
|
|
var isDirty = false
|
|
|
|
var appearance = AppearanceState.default
|
|
var useInteractiveZsh = false
|
|
var nvimArgs: [String]?
|
|
var cliPipePath: String?
|
|
var envDict: [String: String]?
|
|
|
|
var usesVcsIgnores = true
|
|
|
|
var isLeftOptionMeta = false
|
|
var isRightOptionMeta = false
|
|
|
|
// to be cleaned
|
|
var urlsToOpen = [URL: OpenMode]()
|
|
var currentBufferToSet: NvimView.Buffer?
|
|
var cwdToSet: URL?
|
|
var viewToBeFocused: FocusableView? = FocusableView.neoVimView
|
|
|
|
init(isAllToolsVisible: Bool, isToolButtonsVisible: Bool) {
|
|
self.isAllToolsVisible = isAllToolsVisible
|
|
self.isToolButtonsVisible = isToolButtonsVisible
|
|
}
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case allToolsVisible = "is-all-tools-visible"
|
|
case toolButtonsVisible = "is-tool-buttons-visible"
|
|
case orderedTools = "ordered-tools"
|
|
case activeTools = "active-tools"
|
|
case frame
|
|
|
|
case isLeftOptionMeta = "is-left-option-meta"
|
|
case isRightOptionMeta = "is-right-option-meta"
|
|
|
|
case useInteractiveZsh = "use-interactive-zsh"
|
|
case useLiveResize = "use-live-resize"
|
|
case drawsParallel = "draws-parallel"
|
|
case isShowHidden = "is-show-hidden"
|
|
|
|
case appearance
|
|
case workspaceTools = "workspace-tool"
|
|
case previewTool = "preview-tool"
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
self.isLeftOptionMeta = try container.decode(
|
|
forKey: .isLeftOptionMeta,
|
|
default: State.default.isLeftOptionMeta
|
|
)
|
|
self.isRightOptionMeta = try container.decode(
|
|
forKey: .isRightOptionMeta,
|
|
default: State.default.isRightOptionMeta
|
|
)
|
|
self.useInteractiveZsh = try container.decode(
|
|
forKey: .useInteractiveZsh,
|
|
default: State.default.useInteractiveZsh
|
|
)
|
|
self.useLiveResize = try container.decode(
|
|
forKey: .useLiveResize,
|
|
default: State.default.useLiveResize
|
|
)
|
|
self.drawsParallel = try container.decode(
|
|
forKey: .drawsParallel,
|
|
default: State.default.drawsParallel
|
|
)
|
|
if let frameRawValue = try container.decodeIfPresent(String.self, forKey: .frame) {
|
|
self.frame = NSRectFromString(frameRawValue)
|
|
} else {
|
|
self.frame = CGRect(x: 100, y: 100, width: 600, height: 400)
|
|
}
|
|
|
|
self.isAllToolsVisible = try container.decode(
|
|
forKey: .allToolsVisible,
|
|
default: State.default.isAllToolsVisible
|
|
)
|
|
self.isToolButtonsVisible = try container.decode(
|
|
forKey: .toolButtonsVisible,
|
|
default: State.default.isToolButtonsVisible
|
|
)
|
|
|
|
self.appearance = try container.decode(forKey: .appearance, default: State.default.appearance)
|
|
|
|
self.orderedTools = try container.decode(
|
|
forKey: .orderedTools,
|
|
default: State.default.orderedTools
|
|
)
|
|
let missingOrderedTools = MainWindow.Tools.all.subtracting(self.orderedTools)
|
|
self.orderedTools.append(contentsOf: missingOrderedTools)
|
|
|
|
// See [1]
|
|
let rawActiveTools: [String: Bool] = try container.decode(forKey: .activeTools, default: [:])
|
|
self.activeTools = rawActiveTools.flatMapToDict { key, value in
|
|
guard let toolId = MainWindow.Tools(rawValue: key) else {
|
|
return nil
|
|
}
|
|
|
|
return (toolId, value)
|
|
}
|
|
let missingActiveTools = MainWindow.Tools.all.subtracting(self.activeTools.keys)
|
|
missingActiveTools.forEach { self.activeTools[$0] = true }
|
|
|
|
let rawTools: [String: WorkspaceToolState] = try container
|
|
.decode(forKey: .workspaceTools, default: [:])
|
|
self.tools = rawTools.flatMapToDict { key, value in
|
|
guard let tool = MainWindow.Tools(rawValue: key) else {
|
|
return nil
|
|
}
|
|
|
|
return (tool, value)
|
|
}
|
|
let missingTools = MainWindow.Tools.all.subtracting(self.tools.keys)
|
|
missingTools.forEach { missingTool in
|
|
self.tools[missingTool] = MainWindow.State.defaultTools[missingTool]!
|
|
}
|
|
|
|
self.previewTool = try container.decode(
|
|
forKey: .previewTool,
|
|
default: State.default.previewTool
|
|
)
|
|
self.fileBrowserShowHidden = try container.decode(
|
|
forKey: .isShowHidden,
|
|
default: State.default
|
|
.fileBrowserShowHidden
|
|
)
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(self.isAllToolsVisible, forKey: .allToolsVisible)
|
|
try container.encode(self.isToolButtonsVisible, forKey: .toolButtonsVisible)
|
|
try container.encode(NSStringFromRect(self.frame), forKey: .frame)
|
|
try container.encode(self.useLiveResize, forKey: .useLiveResize)
|
|
try container.encode(self.drawsParallel, forKey: .drawsParallel)
|
|
try container.encode(self.isLeftOptionMeta, forKey: .isLeftOptionMeta)
|
|
try container.encode(self.isRightOptionMeta, forKey: .isRightOptionMeta)
|
|
try container.encode(self.useInteractiveZsh, forKey: .useInteractiveZsh)
|
|
try container.encode(self.fileBrowserShowHidden, forKey: .isShowHidden)
|
|
|
|
// See [1]
|
|
try container.encode(
|
|
Dictionary(uniqueKeysWithValues: self.tools.map { k, v in (k.rawValue, v) }),
|
|
forKey: .workspaceTools
|
|
)
|
|
try container.encode(
|
|
Dictionary(uniqueKeysWithValues: self.activeTools.map { k, v in (k.rawValue, v) }),
|
|
forKey: .activeTools
|
|
)
|
|
|
|
try container.encode(self.appearance, forKey: .appearance)
|
|
try container.encode(self.orderedTools, forKey: .orderedTools)
|
|
try container.encode(self.previewTool, forKey: .previewTool)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct WorkspaceToolState: Codable {
|
|
static let `default` = WorkspaceToolState()
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case location
|
|
case open = "is-visible"
|
|
case dimension
|
|
}
|
|
|
|
var location = WorkspaceBarLocation.left
|
|
var dimension = 200.0
|
|
var open = false
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
self.location = try container.decode(
|
|
forKey: .location,
|
|
default: WorkspaceToolState.default.location
|
|
)
|
|
self.dimension = try container.decode(
|
|
forKey: .dimension,
|
|
default: WorkspaceToolState.default.dimension
|
|
)
|
|
self.open = try container.decode(
|
|
forKey: .open,
|
|
default: WorkspaceToolState.default.open
|
|
)
|
|
}
|
|
|
|
// Use generated encode(to:)
|
|
|
|
private init() {}
|
|
|
|
init(location: WorkspaceBarLocation, dimension: CGFloat, open: Bool) {
|
|
self.location = location
|
|
self.dimension = dimension
|
|
self.open = open
|
|
}
|
|
}
|
|
|
|
extension MarkdownTool {
|
|
struct State: Codable {
|
|
static let `default` = State()
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case forwardSearchAutomatically = "is-forward-search-automatically"
|
|
case reverseSearchAutomatically = "is-reverse-search-automatically"
|
|
case refreshOnWrite = "is-refresh-on-write"
|
|
}
|
|
|
|
var isForwardSearchAutomatically = false
|
|
var isReverseSearchAutomatically = false
|
|
var isRefreshOnWrite = true
|
|
|
|
private init() {}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
self.isForwardSearchAutomatically = try container.decode(
|
|
forKey: .forwardSearchAutomatically,
|
|
default: State.default
|
|
.isForwardSearchAutomatically
|
|
)
|
|
self.isReverseSearchAutomatically = try container.decode(
|
|
forKey: .reverseSearchAutomatically,
|
|
default: State.default
|
|
.isReverseSearchAutomatically
|
|
)
|
|
self.isRefreshOnWrite = try container.decode(
|
|
forKey: .refreshOnWrite,
|
|
default: State.default.isRefreshOnWrite
|
|
)
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(self.isForwardSearchAutomatically, forKey: .forwardSearchAutomatically)
|
|
try container.encode(self.isReverseSearchAutomatically, forKey: .reverseSearchAutomatically)
|
|
try container.encode(self.isRefreshOnWrite, forKey: .refreshOnWrite)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension KeyedDecodingContainer where K: CodingKey {
|
|
func decode<T: Decodable>(forKey key: K, default: T) throws -> T {
|
|
try self.decodeIfPresent(T.self, forKey: key) ?? `default`
|
|
}
|
|
}
|
|
|
|
/**
|
|
[1] Swift 4.2 has a bug: Only when a `Dictionary` has `String` or `Int` keys, it is encoded to dictionary.
|
|
This means that `Dictionary`s with `enum SomeEnum: String, Codable` keys are encoded as `Array`s.
|
|
The same problem persists also for decoding.
|
|
<https://forums.swift.org/t/json-encoding-decoding-weird-encoding-of-dictionary-with-enum-values/12995>
|
|
*/
|