Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,83 @@ import Foundation
import SwiftUI
import OrderedCollections

extension EditorManager {
/// Restores the tab manager from a captured state obtained using `saveRestorationState`
/// - Parameter workspace: The workspace to retrieve state from.
func restoreFromState(_ workspace: WorkspaceDocument) {
guard let fileManager = workspace.workspaceFileManager,
let data = workspace.getFromWorkspaceState(.openTabs) as? Data,
let state = try? JSONDecoder().decode(EditorRestorationState.self, from: data) else {
return
}

guard !state.groups.isEmpty else {
logger.warning("Empty Editor State found, restoring to clean editor state.")
initCleanState()
return
}

fixRestoredEditorLayout(state.groups, fileManager: fileManager)
self.editorLayout = state.groups
self.activeEditor = activeEditor
switchToActiveEditor()
}

/// Fix any hanging files after restoring from saved state.
///
/// After decoding the state, we're left with `CEWorkspaceFile`s that don't exist in the file manager
/// so this function maps all those to 'real' files. Works recursively on all the tab groups.
/// - Parameters:
/// - group: The tab group to fix.
/// - fileManager: The file manager to use to map files.
private func fixRestoredEditorLayout(_ group: EditorLayout, fileManager: CEWorkspaceFileManager) {
switch group {
case let .one(data):
fixEditor(data, fileManager: fileManager)
case let .vertical(splitData):
splitData.editorLayouts.forEach { group in
fixRestoredEditorLayout(group, fileManager: fileManager)
}
case let .horizontal(splitData):
splitData.editorLayouts.forEach { group in
fixRestoredEditorLayout(group, fileManager: fileManager)
}
}
}

private func findEditorLayout(group: EditorLayout, searchFor id: UUID) -> Editor? {
switch group {
case let .one(data):
return data.id == id ? data : nil
case let .vertical(splitData):
return splitData.editorLayouts.compactMap { findEditorLayout(group: $0, searchFor: id) }.first
case let .horizontal(splitData):
return splitData.editorLayouts.compactMap { findEditorLayout(group: $0, searchFor: id) }.first
}
}

/// Fixes any hanging files after restoring from saved state.
/// - Parameters:
/// - data: The tab group to fix.
/// - fileManager: The file manager to use to map files.a
private func fixEditor(_ editor: Editor, fileManager: CEWorkspaceFileManager) {
editor.tabs = OrderedSet(editor.tabs.compactMap { fileManager.getFile($0.url.path, createIfNotFound: true) })
if let selectedTab = editor.selectedTab {
editor.selectedTab = fileManager.getFile(selectedTab.url.path, createIfNotFound: true)
}
}

func saveRestorationState(_ workspace: WorkspaceDocument) {
if let data = try? JSONEncoder().encode(
EditorRestorationState(focus: activeEditor, groups: editorLayout)
) {
workspace.addToWorkspaceState(key: .openTabs, value: data)
} else {
workspace.addToWorkspaceState(key: .openTabs, value: nil)
}
}
}

struct EditorRestorationState: Codable {
var focus: Editor
var groups: EditorLayout
Expand Down
26 changes: 25 additions & 1 deletion CodeEdit/Features/Editor/Models/EditorLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

enum EditorLayout {
enum EditorLayout: Equatable {
case one(Editor)
case vertical(SplitViewData)
case horizontal(SplitViewData)
Expand Down Expand Up @@ -71,4 +71,28 @@ enum EditorLayout {
}
}
}

var isEmpty: Bool {
switch self {
case .one:
return false
case .vertical(let splitViewData), .horizontal(let splitViewData):
return splitViewData.editorLayouts.allSatisfy { editorLayout in
editorLayout.isEmpty
}
}
}

static func == (lhs: EditorLayout, rhs: EditorLayout) -> Bool {
switch (lhs, rhs) {
case let (.one(lhs), .one(rhs)):
return lhs == rhs
case let (.vertical(lhs), .vertical(rhs)):
return lhs.editorLayouts == rhs.editorLayouts
case let (.horizontal(lhs), .horizontal(rhs)):
return lhs.editorLayouts == rhs.editorLayouts
default:
return false
}
}
}
109 changes: 51 additions & 58 deletions CodeEdit/Features/Editor/Models/EditorManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import Combine
import Foundation
import DequeModule
import OrderedCollections
import os

class EditorManager: ObservableObject {
let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "EditorManager")

/// The complete editor layout.
@Published var editorLayout: EditorLayout

Expand All @@ -33,6 +36,8 @@ class EditorManager: ObservableObject {
var tabBarTabIdSubject = PassthroughSubject<String?, Never>()
var cancellable: AnyCancellable?

// MARK: - Init

init() {
let tab = Editor()
self.activeEditor = tab
Expand All @@ -42,10 +47,24 @@ class EditorManager: ObservableObject {
switchToActiveEditor()
}

/// Initializes the editor manager's state to the "initial" state.
///
/// Functionally identical to the initializer for this class.
func initCleanState() {
let tab = Editor()
self.activeEditor = tab
self.activeEditorHistory.prepend { [weak tab] in tab }
self.editorLayout = .horizontal(.init(.horizontal, editorLayouts: [.one(tab)]))
self.isFocusingActiveEditor = false
switchToActiveEditor()
}

/// Flattens the splitviews.
func flatten() {
if case .horizontal(let data) = editorLayout {
data.flatten()
} else if case .vertical(let data) = editorLayout {
data.flatten()
}
}

Expand All @@ -68,74 +87,48 @@ class EditorManager: ObservableObject {
}
}

/// Restores the tab manager from a captured state obtained using `saveRestorationState`
/// - Parameter workspace: The workspace to retrieve state from.
func restoreFromState(_ workspace: WorkspaceDocument) {
guard let fileManager = workspace.workspaceFileManager,
let data = workspace.getFromWorkspaceState(.openTabs) as? Data,
let state = try? JSONDecoder().decode(EditorRestorationState.self, from: data) else {
return
}
fixRestoredEditorLayout(state.groups, fileManager: fileManager)
self.editorLayout = state.groups
self.activeEditor = findEditorLayout(
group: state.groups,
searchFor: state.focus.id
) ?? editorLayout.findSomeEditor()!
switchToActiveEditor()
}
// MARK: - Close Editor

/// Fix any hanging files after restoring from saved state.
///
/// After decoding the state, we're left with `CEWorkspaceFile`s that don't exist in the file manager
/// so this function maps all those to 'real' files. Works recursively on all the tab groups.
/// - Parameters:
/// - group: The tab group to fix.
/// - fileManager: The file manager to use to map files.
private func fixRestoredEditorLayout(_ group: EditorLayout, fileManager: CEWorkspaceFileManager) {
switch group {
case let .one(data):
fixEditor(data, fileManager: fileManager)
case let .vertical(splitData):
splitData.editorLayouts.forEach { group in
fixRestoredEditorLayout(group, fileManager: fileManager)
}
case let .horizontal(splitData):
splitData.editorLayouts.forEach { group in
fixRestoredEditorLayout(group, fileManager: fileManager)
}
/// Close an editor and fix editor manager state, updating active editor, etc.
/// - Parameter editor: The editor to close
func closeEditor(_ editor: Editor) {
editor.close()
if activeEditor == editor {
setNewActiveEditor(excluding: editor)
}

flatten()
objectWillChange.send()
}

private func findEditorLayout(group: EditorLayout, searchFor id: UUID) -> Editor? {
switch group {
case let .one(data):
return data.id == id ? data : nil
case let .vertical(splitData):
return splitData.editorLayouts.compactMap { findEditorLayout(group: $0, searchFor: id) }.first
case let .horizontal(splitData):
return splitData.editorLayouts.compactMap { findEditorLayout(group: $0, searchFor: id) }.first
/// Set a new active editor.
/// - Parameter editor: The editor to exclude.
func setNewActiveEditor(excluding editor: Editor) {
activeEditorHistory.removeAll { $0() == nil || $0() == editor }
if activeEditorHistory.isEmpty {
activeEditor = findSomeEditor(excluding: editor)
} else {
activeEditor = activeEditorHistory.removeFirst()()!
}
}

/// Fixes any hanging files after restoring from saved state.
/// - Parameters:
/// - data: The tab group to fix.
/// - fileManager: The file manager to use to map files.a
private func fixEditor(_ editor: Editor, fileManager: CEWorkspaceFileManager) {
editor.tabs = OrderedSet(editor.tabs.compactMap { fileManager.getFile($0.url.path, createIfNotFound: true) })
if let selectedTab = editor.selectedTab {
editor.selectedTab = fileManager.getFile(selectedTab.url.path, createIfNotFound: true)
/// Find some editor, or if one cannot be found set up the editor manager with a clean state.
/// - Parameter editor: The editor to exclude.
/// - Returns: Some editor, order is not guaranteed.
func findSomeEditor(excluding editor: Editor) -> Editor {
guard let someEditor = editorLayout.findSomeEditor(except: editor) else {
initCleanState()
return activeEditor
}
return someEditor
}

func saveRestorationState(_ workspace: WorkspaceDocument) {
if let data = try? JSONEncoder().encode(
EditorRestorationState(focus: activeEditor, groups: editorLayout)
) {
workspace.addToWorkspaceState(key: .openTabs, value: data)
} else {
workspace.addToWorkspaceState(key: .openTabs, value: nil)
// MARK: - Focus

func toggleFocusingEditor(from editor: Editor) {
if !isFocusingActiveEditor {
activeEditor = editor
}
isFocusingActiveEditor.toggle()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,19 @@ struct EditorTabBarLeadingAccessories: View {

@EnvironmentObject private var editor: Editor

@State private var otherEditor: Editor?

@AppSettings(\.general.tabBarStyle)
var tabBarStyle

var body: some View {
HStack(spacing: 0) {
if let otherGroup = editorManager.editorLayout.findSomeEditor(except: editor) {
if let otherEditor {
EditorTabBarAccessoryIcon(
icon: .init(systemName: "multiply"),
action: { [weak editor] in
editor?.close()
if editorManager.activeEditor == editor {
editorManager.activeEditorHistory.removeAll { $0() == nil || $0() == editor }
if editorManager.activeEditorHistory.isEmpty {
editorManager.activeEditor = otherGroup
} else {
editorManager.activeEditor = editorManager.activeEditorHistory.removeFirst()()!
}
}
editorManager.flatten()
guard let editor else { return }
editorManager.closeEditor(editor)
}
)
.help("Close this Editor")
Expand All @@ -48,10 +42,7 @@ struct EditorTabBarLeadingAccessories: View {
),
isActive: editorManager.isFocusingActiveEditor,
action: {
if !editorManager.isFocusingActiveEditor {
editorManager.activeEditor = editor
}
editorManager.isFocusingActiveEditor.toggle()
editorManager.toggleFocusingEditor(from: editor)
}
)
.help(
Expand Down Expand Up @@ -137,6 +128,12 @@ struct EditorTabBarLeadingAccessories: View {
EditorTabBarAccessoryNativeBackground(dividerAt: .trailing)
}
}
.onAppear {
otherEditor = editorManager.editorLayout.findSomeEditor(except: editor)
}
.onReceive(editorManager.objectWillChange) { _ in
otherEditor = editorManager.editorLayout.findSomeEditor(except: editor)
}
}
}

Expand Down