Skip to content

Commit 302d8bb

Browse files
Implement better persistent tab system
1 parent 1f8e989 commit 302d8bb

7 files changed

Lines changed: 275 additions & 26 deletions

File tree

src/App.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { setupImporters } from '@/libs/import/Importers'
2929
import { TabManager } from '@/components/TabSystem/TabManager'
3030
import { FileExplorer } from '@/components/FileExplorer/FileExplorer'
3131
import { CreateProjectWindow } from '@/components/Windows/CreateProject/CreateProjectWindow'
32+
import { TabTypes } from '@/components/TabSystem/TabTypes'
3233

3334
export function setupBeforeComponents() {
3435
NotificationSystem.setup()
@@ -40,6 +41,7 @@ export function setupBeforeComponents() {
4041
TextTab.setup()
4142
TreeEditorTab.setup()
4243
Sidebar.setup()
44+
TabTypes.setup()
4345
TabManager.setup()
4446
FileExplorer.setup()
4547
CreateProjectWindow.setup()

src/components/TabSystem/Tab.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
1+
import { Event } from '@/libs/event/Event'
12
import { v4 as uuid } from 'uuid'
23
import { Component, Ref, ref } from 'vue'
34

5+
export type RecoveryState = { id: string; state: any; type: string; [key: string]: any }
6+
47
export class Tab {
58
public id = uuid()
69
public component: Component | null = null
710
public name = ref('New Tab')
811
public icon: Ref<string | null> = ref(null)
912

13+
public savedState = new Event<void>()
14+
1015
public async create() {}
1116
public async destroy() {}
1217
public async activate() {}
1318
public async deactivate() {}
19+
20+
public async getState(): Promise<any> {}
21+
public async recover(state: any) {}
22+
23+
public async saveState() {
24+
this.savedState.dispatch()
25+
}
1426
}
Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,83 @@
11
import { ShallowRef, shallowRef } from 'vue'
22
import { Tab } from './Tab'
3-
import { TabSystem } from './TabSystem'
4-
import { TextTab } from '@/components/Tabs/Text/TextTab'
5-
import { TreeEditorTab } from '@/components/Tabs/TreeEditor/TreeEditorTab'
6-
import { ImageTab } from '@/components/Tabs/Image/ImageTab'
3+
import { TabSystem, TabSystemRecoveryState } from './TabSystem'
74
import { FileTab } from './FileTab'
85
import { Settings } from '@/libs/settings/Settings'
6+
import { TabTypes } from './TabTypes'
7+
import { ProjectManager } from '@/libs/project/ProjectManager'
8+
import { Disposable } from '@/libs/disposeable/Disposeable'
9+
10+
type TabManagerRecoveryState = { tabSystems: TabSystemRecoveryState[]; focusedTabSystem: string }
911

1012
export class TabManager {
11-
public static tabSystems: TabSystem[] = [new TabSystem()]
13+
public static tabSystems: ShallowRef<TabSystem[]> = shallowRef([])
1214
public static focusedTabSystem: ShallowRef<TabSystem | null> = shallowRef(null)
1315

14-
private static tabTypes: (typeof FileTab)[] = [ImageTab, TextTab, TreeEditorTab]
16+
private static tabSystemSaveListenters: Record<string, Disposable> = {}
1517

1618
public static setup() {
17-
Settings.addSetting('jsonEditor', {
18-
default: 'text',
19-
})
20-
2119
Settings.addSetting('compactTabDesign', {
2220
default: true,
2321
})
2422

25-
Settings.updated.on((event) => {
26-
const { id, value } = event as { id: string; value: any }
27-
28-
if (id !== 'jsonEditor') return
29-
30-
if (value === 'text') {
31-
this.tabTypes = [ImageTab, TextTab, TreeEditorTab]
23+
ProjectManager.updatedCurrentProject.on(() => {
24+
if (ProjectManager.currentProject === null) {
25+
TabManager.clear()
3226
} else {
33-
this.tabTypes = [ImageTab, TreeEditorTab, TextTab]
27+
TabManager.loadedProject()
3428
}
3529
})
3630
}
3731

32+
public static async addTabSystem(recoveryState?: TabSystemRecoveryState): Promise<TabSystem> {
33+
const tabSystem = new TabSystem()
34+
35+
if (recoveryState) await tabSystem.applyRecoverState(recoveryState)
36+
37+
TabManager.tabSystemSaveListenters[tabSystem.id] = tabSystem.savedState.on(() => {
38+
TabManager.save()
39+
})
40+
41+
TabManager.tabSystems.value.push(tabSystem)
42+
43+
return tabSystem
44+
}
45+
46+
public static async removeTabSystem(tabSystem: TabSystem) {
47+
if (!TabManager.tabSystems.value.includes(tabSystem)) return
48+
49+
if (TabManager.focusedTabSystem.value?.id === tabSystem.id) TabManager.focusedTabSystem.value = null
50+
51+
TabManager.tabSystems.value.splice(TabManager.tabSystems.value.indexOf(tabSystem), 1)
52+
53+
TabManager.tabSystemSaveListenters[tabSystem.id].dispose()
54+
delete TabManager.tabSystemSaveListenters[tabSystem.id]
55+
56+
await tabSystem.clear()
57+
}
58+
59+
public static async clear() {
60+
const tabSystems = TabManager.tabSystems.value
61+
62+
for (const tabSystem of tabSystems) {
63+
await TabManager.removeTabSystem(tabSystem)
64+
}
65+
}
66+
67+
public static async loadedProject() {
68+
if (!ProjectManager.currentProject) return
69+
70+
TabManager.tabSystems.value = []
71+
TabManager.focusedTabSystem.value = null
72+
TabManager.tabSystemSaveListenters = {}
73+
74+
await TabManager.addTabSystem()
75+
76+
await TabManager.recover()
77+
}
78+
3879
public static async openTab(tab: Tab) {
39-
for (const tabSystem of TabManager.tabSystems) {
80+
for (const tabSystem of TabManager.tabSystems.value) {
4081
for (const otherTab of tabSystem.tabs.value) {
4182
if (otherTab === tab) {
4283
await tabSystem.selectTab(tab)
@@ -54,7 +95,7 @@ export class TabManager {
5495
}
5596

5697
public static async openFile(path: string) {
57-
for (const tabSystem of TabManager.tabSystems) {
98+
for (const tabSystem of TabManager.tabSystems.value) {
5899
for (const tab of tabSystem.tabs.value) {
59100
if (tab instanceof FileTab && tab.is(path)) {
60101
await tabSystem.selectTab(tab)
@@ -66,7 +107,7 @@ export class TabManager {
66107
}
67108
}
68109

69-
for (const TabType of TabManager.tabTypes) {
110+
for (const TabType of TabTypes.fileTabTypes) {
70111
if (TabType.canEdit(path)) {
71112
await TabManager.openTab(new TabType(path))
72113

@@ -76,7 +117,7 @@ export class TabManager {
76117
}
77118

78119
public static getTabByType<T extends Tab>(tabType: { new (...args: any[]): T }): T | null {
79-
for (const tabSystem of TabManager.tabSystems) {
120+
for (const tabSystem of TabManager.tabSystems.value) {
80121
for (const tab of tabSystem.tabs.value) {
81122
if (tab instanceof tabType) {
82123
return tab
@@ -88,12 +129,41 @@ export class TabManager {
88129
}
89130

90131
public static getDefaultTabSystem(): TabSystem {
91-
return TabManager.tabSystems[0]
132+
return TabManager.tabSystems.value[0]
92133
}
93134

94135
public static getFocusedTab(): Tab | null {
95136
if (TabManager.focusedTabSystem.value === null) return null
96137

97138
return TabManager.focusedTabSystem.value.selectedTab.value
98139
}
140+
141+
public static async save() {
142+
if (!ProjectManager.currentProject) return
143+
144+
const state = {
145+
tabSystems: await Promise.all(TabManager.tabSystems.value.map((tabSystem) => tabSystem.getRecoveryState())),
146+
focusedTabSystem: TabManager.focusedTabSystem.value ? TabManager.focusedTabSystem.value.id : null,
147+
}
148+
149+
await ProjectManager.currentProject.saveTabManagerState(state)
150+
}
151+
152+
public static async recover() {
153+
if (!ProjectManager.currentProject) return
154+
155+
const state = (await ProjectManager.currentProject.getTabManagerState()) as TabManagerRecoveryState | null
156+
157+
if (state === null) return
158+
159+
await TabManager.clear()
160+
161+
for (const tabSystemState of state.tabSystems) {
162+
await TabManager.addTabSystem(tabSystemState)
163+
}
164+
165+
TabManager.tabSystems.value = [...TabManager.tabSystems.value]
166+
167+
TabManager.focusedTabSystem.value = TabManager.tabSystems.value.find((tabSystem) => tabSystem.id === state.focusedTabSystem) ?? null
168+
}
99169
}

src/components/TabSystem/TabSystem.ts

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,44 @@
11
import { v4 as uuid } from 'uuid'
2-
import { Tab } from './Tab'
2+
import { RecoveryState as TabRecoveryState, Tab, RecoveryState } from './Tab'
33
import { Ref, shallowRef } from 'vue'
44
import { Editor } from '@/components/Editor/Editor'
5+
import { Event } from '@/libs/event/Event'
6+
import { Disposable } from '@/libs/disposeable/Disposeable'
7+
import { TabTypes } from './TabTypes'
8+
import { FileTab } from './FileTab'
9+
10+
export type TabSystemRecoveryState = { id: string; selectedTab: string | null; tabs: TabRecoveryState[] }
511

612
export class TabSystem {
713
public id = uuid()
814
// Monaco editor freezes browser when made deep reactive, so instead we make it shallow reactive
915
public tabs: Ref<Tab[]> = shallowRef([])
1016
public selectedTab: Ref<Tab | null> = shallowRef(null)
1117

18+
public savedState = new Event<void>()
19+
20+
private tabSaveListenters: Record<string, Disposable> = {}
21+
1222
public async addTab(tab: Tab, select = true) {
1323
if (this.tabs.value.includes(tab)) {
1424
if (select) await this.selectTab(tab)
1525

26+
await this.saveState()
27+
1628
return
1729
}
1830

31+
this.tabSaveListenters[tab.id] = tab.savedState.on(() => {
32+
this.saveState()
33+
})
34+
1935
await tab.create()
2036

2137
this.tabs.value.push(tab)
2238

2339
if (select) await this.selectTab(tab)
40+
41+
await this.saveState()
2442
}
2543

2644
public async selectTab(tab: Tab) {
@@ -33,12 +51,14 @@ export class TabSystem {
3351
Editor.showTabs()
3452

3553
await tab.activate()
54+
55+
await this.saveState()
3656
}
3757

3858
public async removeTab(tab: Tab) {
3959
if (!this.tabs.value.includes(tab)) return
4060

41-
if (this.selectedTab.value === tab) await tab.deactivate()
61+
if (this.selectedTab.value?.id === tab.id) await tab.deactivate()
4262

4363
this.selectedTab.value = null
4464

@@ -50,6 +70,101 @@ export class TabSystem {
5070

5171
await tab.destroy()
5272

73+
this.tabSaveListenters[tab.id].dispose()
74+
delete this.tabSaveListenters[tab.id]
75+
5376
if (this.tabs.value.length === 0) Editor.hideTabs()
77+
78+
await this.saveState()
79+
}
80+
81+
public async clear() {
82+
for (const tab of this.tabs.value) {
83+
if (this.selectedTab.value?.id === tab.id) await tab.deactivate()
84+
85+
await tab.destroy()
86+
87+
this.tabSaveListenters[tab.id].dispose()
88+
delete this.tabSaveListenters[tab.id]
89+
}
90+
91+
this.tabs.value = []
92+
this.selectedTab.value = null
93+
}
94+
95+
public async saveState() {
96+
this.savedState.dispatch()
97+
}
98+
99+
public async getTabRecoveryState(tab: Tab): Promise<RecoveryState> {
100+
if (tab instanceof FileTab)
101+
return {
102+
id: tab.id,
103+
path: tab.path,
104+
state: await tab.getState(),
105+
type: tab.constructor.name,
106+
}
107+
108+
return {
109+
id: tab.id,
110+
state: await tab.getState(),
111+
type: tab.constructor.name,
112+
}
113+
}
114+
115+
public async getRecoveryState(): Promise<TabSystemRecoveryState> {
116+
return {
117+
id: this.id,
118+
selectedTab: this.selectedTab.value ? this.selectedTab.value.id : null,
119+
tabs: await Promise.all(this.tabs.value.map((tab) => this.getTabRecoveryState(tab))),
120+
}
121+
}
122+
123+
public async applyRecoverState(recoveryState: TabSystemRecoveryState) {
124+
this.id = recoveryState.id
125+
126+
this.tabs.value = []
127+
128+
for (const tabRecoveryState of recoveryState.tabs) {
129+
const tabType = TabTypes.getType(tabRecoveryState.type)
130+
131+
if (tabType === null) continue
132+
133+
if (tabType.prototype instanceof FileTab) {
134+
const tab = new (tabType as typeof FileTab)(tabRecoveryState.path)
135+
136+
tab.id = tabRecoveryState.id
137+
138+
this.tabSaveListenters[tab.id] = tab.savedState.on(() => {
139+
this.saveState()
140+
})
141+
142+
await tab.create()
143+
await tab.recover(tabRecoveryState.state)
144+
145+
this.tabs.value.push(tab)
146+
} else {
147+
const tab = new (tabType as typeof Tab)()
148+
149+
tab.id = tabRecoveryState.id
150+
151+
this.tabSaveListenters[tab.id] = tab.savedState.on(() => {
152+
this.saveState()
153+
})
154+
155+
await tab.create()
156+
await tab.recover(tabRecoveryState.state)
157+
158+
this.tabs.value.push(tab)
159+
}
160+
}
161+
162+
this.tabs.value = [...this.tabs.value]
163+
164+
this.selectedTab.value = this.tabs.value.find((tab) => tab.id === recoveryState.selectedTab) ?? null
165+
166+
if (this.selectedTab.value === null) return
167+
168+
await this.selectedTab.value.activate()
54169
}
55170
}

0 commit comments

Comments
 (0)