Skip to content

Commit 9db0309

Browse files
Fix Empty Editor State Bug (#1556)
1 parent 2ba130d commit 9db0309

4 files changed

Lines changed: 165 additions & 74 deletions

File tree

CodeEdit/Features/Editor/Models/EditorLayout+StateRestoration.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,83 @@ import Foundation
99
import SwiftUI
1010
import OrderedCollections
1111

12+
extension EditorManager {
13+
/// Restores the tab manager from a captured state obtained using `saveRestorationState`
14+
/// - Parameter workspace: The workspace to retrieve state from.
15+
func restoreFromState(_ workspace: WorkspaceDocument) {
16+
guard let fileManager = workspace.workspaceFileManager,
17+
let data = workspace.getFromWorkspaceState(.openTabs) as? Data,
18+
let state = try? JSONDecoder().decode(EditorRestorationState.self, from: data) else {
19+
return
20+
}
21+
22+
guard !state.groups.isEmpty else {
23+
logger.warning("Empty Editor State found, restoring to clean editor state.")
24+
initCleanState()
25+
return
26+
}
27+
28+
fixRestoredEditorLayout(state.groups, fileManager: fileManager)
29+
self.editorLayout = state.groups
30+
self.activeEditor = activeEditor
31+
switchToActiveEditor()
32+
}
33+
34+
/// Fix any hanging files after restoring from saved state.
35+
///
36+
/// After decoding the state, we're left with `CEWorkspaceFile`s that don't exist in the file manager
37+
/// so this function maps all those to 'real' files. Works recursively on all the tab groups.
38+
/// - Parameters:
39+
/// - group: The tab group to fix.
40+
/// - fileManager: The file manager to use to map files.
41+
private func fixRestoredEditorLayout(_ group: EditorLayout, fileManager: CEWorkspaceFileManager) {
42+
switch group {
43+
case let .one(data):
44+
fixEditor(data, fileManager: fileManager)
45+
case let .vertical(splitData):
46+
splitData.editorLayouts.forEach { group in
47+
fixRestoredEditorLayout(group, fileManager: fileManager)
48+
}
49+
case let .horizontal(splitData):
50+
splitData.editorLayouts.forEach { group in
51+
fixRestoredEditorLayout(group, fileManager: fileManager)
52+
}
53+
}
54+
}
55+
56+
private func findEditorLayout(group: EditorLayout, searchFor id: UUID) -> Editor? {
57+
switch group {
58+
case let .one(data):
59+
return data.id == id ? data : nil
60+
case let .vertical(splitData):
61+
return splitData.editorLayouts.compactMap { findEditorLayout(group: $0, searchFor: id) }.first
62+
case let .horizontal(splitData):
63+
return splitData.editorLayouts.compactMap { findEditorLayout(group: $0, searchFor: id) }.first
64+
}
65+
}
66+
67+
/// Fixes any hanging files after restoring from saved state.
68+
/// - Parameters:
69+
/// - data: The tab group to fix.
70+
/// - fileManager: The file manager to use to map files.a
71+
private func fixEditor(_ editor: Editor, fileManager: CEWorkspaceFileManager) {
72+
editor.tabs = OrderedSet(editor.tabs.compactMap { fileManager.getFile($0.url.path, createIfNotFound: true) })
73+
if let selectedTab = editor.selectedTab {
74+
editor.selectedTab = fileManager.getFile(selectedTab.url.path, createIfNotFound: true)
75+
}
76+
}
77+
78+
func saveRestorationState(_ workspace: WorkspaceDocument) {
79+
if let data = try? JSONEncoder().encode(
80+
EditorRestorationState(focus: activeEditor, groups: editorLayout)
81+
) {
82+
workspace.addToWorkspaceState(key: .openTabs, value: data)
83+
} else {
84+
workspace.addToWorkspaceState(key: .openTabs, value: nil)
85+
}
86+
}
87+
}
88+
1289
struct EditorRestorationState: Codable {
1390
var focus: Editor
1491
var groups: EditorLayout

CodeEdit/Features/Editor/Models/EditorLayout.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import Foundation
99

10-
enum EditorLayout {
10+
enum EditorLayout: Equatable {
1111
case one(Editor)
1212
case vertical(SplitViewData)
1313
case horizontal(SplitViewData)
@@ -71,4 +71,28 @@ enum EditorLayout {
7171
}
7272
}
7373
}
74+
75+
var isEmpty: Bool {
76+
switch self {
77+
case .one:
78+
return false
79+
case .vertical(let splitViewData), .horizontal(let splitViewData):
80+
return splitViewData.editorLayouts.allSatisfy { editorLayout in
81+
editorLayout.isEmpty
82+
}
83+
}
84+
}
85+
86+
static func == (lhs: EditorLayout, rhs: EditorLayout) -> Bool {
87+
switch (lhs, rhs) {
88+
case let (.one(lhs), .one(rhs)):
89+
return lhs == rhs
90+
case let (.vertical(lhs), .vertical(rhs)):
91+
return lhs.editorLayouts == rhs.editorLayouts
92+
case let (.horizontal(lhs), .horizontal(rhs)):
93+
return lhs.editorLayouts == rhs.editorLayouts
94+
default:
95+
return false
96+
}
97+
}
7498
}

CodeEdit/Features/Editor/Models/EditorManager.swift

Lines changed: 51 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import Combine
99
import Foundation
1010
import DequeModule
1111
import OrderedCollections
12+
import os
1213

1314
class EditorManager: ObservableObject {
15+
let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "EditorManager")
16+
1417
/// The complete editor layout.
1518
@Published var editorLayout: EditorLayout
1619

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

39+
// MARK: - Init
40+
3641
init() {
3742
let tab = Editor()
3843
self.activeEditor = tab
@@ -42,10 +47,24 @@ class EditorManager: ObservableObject {
4247
switchToActiveEditor()
4348
}
4449

50+
/// Initializes the editor manager's state to the "initial" state.
51+
///
52+
/// Functionally identical to the initializer for this class.
53+
func initCleanState() {
54+
let tab = Editor()
55+
self.activeEditor = tab
56+
self.activeEditorHistory.prepend { [weak tab] in tab }
57+
self.editorLayout = .horizontal(.init(.horizontal, editorLayouts: [.one(tab)]))
58+
self.isFocusingActiveEditor = false
59+
switchToActiveEditor()
60+
}
61+
4562
/// Flattens the splitviews.
4663
func flatten() {
4764
if case .horizontal(let data) = editorLayout {
4865
data.flatten()
66+
} else if case .vertical(let data) = editorLayout {
67+
data.flatten()
4968
}
5069
}
5170

@@ -68,74 +87,48 @@ class EditorManager: ObservableObject {
6887
}
6988
}
7089

71-
/// Restores the tab manager from a captured state obtained using `saveRestorationState`
72-
/// - Parameter workspace: The workspace to retrieve state from.
73-
func restoreFromState(_ workspace: WorkspaceDocument) {
74-
guard let fileManager = workspace.workspaceFileManager,
75-
let data = workspace.getFromWorkspaceState(.openTabs) as? Data,
76-
let state = try? JSONDecoder().decode(EditorRestorationState.self, from: data) else {
77-
return
78-
}
79-
fixRestoredEditorLayout(state.groups, fileManager: fileManager)
80-
self.editorLayout = state.groups
81-
self.activeEditor = findEditorLayout(
82-
group: state.groups,
83-
searchFor: state.focus.id
84-
) ?? editorLayout.findSomeEditor()!
85-
switchToActiveEditor()
86-
}
90+
// MARK: - Close Editor
8791

88-
/// Fix any hanging files after restoring from saved state.
89-
///
90-
/// After decoding the state, we're left with `CEWorkspaceFile`s that don't exist in the file manager
91-
/// so this function maps all those to 'real' files. Works recursively on all the tab groups.
92-
/// - Parameters:
93-
/// - group: The tab group to fix.
94-
/// - fileManager: The file manager to use to map files.
95-
private func fixRestoredEditorLayout(_ group: EditorLayout, fileManager: CEWorkspaceFileManager) {
96-
switch group {
97-
case let .one(data):
98-
fixEditor(data, fileManager: fileManager)
99-
case let .vertical(splitData):
100-
splitData.editorLayouts.forEach { group in
101-
fixRestoredEditorLayout(group, fileManager: fileManager)
102-
}
103-
case let .horizontal(splitData):
104-
splitData.editorLayouts.forEach { group in
105-
fixRestoredEditorLayout(group, fileManager: fileManager)
106-
}
92+
/// Close an editor and fix editor manager state, updating active editor, etc.
93+
/// - Parameter editor: The editor to close
94+
func closeEditor(_ editor: Editor) {
95+
editor.close()
96+
if activeEditor == editor {
97+
setNewActiveEditor(excluding: editor)
10798
}
99+
100+
flatten()
101+
objectWillChange.send()
108102
}
109103

110-
private func findEditorLayout(group: EditorLayout, searchFor id: UUID) -> Editor? {
111-
switch group {
112-
case let .one(data):
113-
return data.id == id ? data : nil
114-
case let .vertical(splitData):
115-
return splitData.editorLayouts.compactMap { findEditorLayout(group: $0, searchFor: id) }.first
116-
case let .horizontal(splitData):
117-
return splitData.editorLayouts.compactMap { findEditorLayout(group: $0, searchFor: id) }.first
104+
/// Set a new active editor.
105+
/// - Parameter editor: The editor to exclude.
106+
func setNewActiveEditor(excluding editor: Editor) {
107+
activeEditorHistory.removeAll { $0() == nil || $0() == editor }
108+
if activeEditorHistory.isEmpty {
109+
activeEditor = findSomeEditor(excluding: editor)
110+
} else {
111+
activeEditor = activeEditorHistory.removeFirst()()!
118112
}
119113
}
120114

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

132-
func saveRestorationState(_ workspace: WorkspaceDocument) {
133-
if let data = try? JSONEncoder().encode(
134-
EditorRestorationState(focus: activeEditor, groups: editorLayout)
135-
) {
136-
workspace.addToWorkspaceState(key: .openTabs, value: data)
137-
} else {
138-
workspace.addToWorkspaceState(key: .openTabs, value: nil)
126+
// MARK: - Focus
127+
128+
func toggleFocusingEditor(from editor: Editor) {
129+
if !isFocusingActiveEditor {
130+
activeEditor = editor
139131
}
132+
isFocusingActiveEditor.toggle()
140133
}
141134
}

CodeEdit/Features/Editor/TabBar/Views/EditorTabBarLeadingAccessories.swift

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,19 @@ struct EditorTabBarLeadingAccessories: View {
1515

1616
@EnvironmentObject private var editor: Editor
1717

18+
@State private var otherEditor: Editor?
19+
1820
@AppSettings(\.general.tabBarStyle)
1921
var tabBarStyle
2022

2123
var body: some View {
2224
HStack(spacing: 0) {
23-
if let otherGroup = editorManager.editorLayout.findSomeEditor(except: editor) {
25+
if let otherEditor {
2426
EditorTabBarAccessoryIcon(
2527
icon: .init(systemName: "multiply"),
2628
action: { [weak editor] in
27-
editor?.close()
28-
if editorManager.activeEditor == editor {
29-
editorManager.activeEditorHistory.removeAll { $0() == nil || $0() == editor }
30-
if editorManager.activeEditorHistory.isEmpty {
31-
editorManager.activeEditor = otherGroup
32-
} else {
33-
editorManager.activeEditor = editorManager.activeEditorHistory.removeFirst()()!
34-
}
35-
}
36-
editorManager.flatten()
29+
guard let editor else { return }
30+
editorManager.closeEditor(editor)
3731
}
3832
)
3933
.help("Close this Editor")
@@ -48,10 +42,7 @@ struct EditorTabBarLeadingAccessories: View {
4842
),
4943
isActive: editorManager.isFocusingActiveEditor,
5044
action: {
51-
if !editorManager.isFocusingActiveEditor {
52-
editorManager.activeEditor = editor
53-
}
54-
editorManager.isFocusingActiveEditor.toggle()
45+
editorManager.toggleFocusingEditor(from: editor)
5546
}
5647
)
5748
.help(
@@ -137,6 +128,12 @@ struct EditorTabBarLeadingAccessories: View {
137128
EditorTabBarAccessoryNativeBackground(dividerAt: .trailing)
138129
}
139130
}
131+
.onAppear {
132+
otherEditor = editorManager.editorLayout.findSomeEditor(except: editor)
133+
}
134+
.onReceive(editorManager.objectWillChange) { _ in
135+
otherEditor = editorManager.editorLayout.findSomeEditor(except: editor)
136+
}
140137
}
141138
}
142139

0 commit comments

Comments
 (0)