Skip to content

Commit

Permalink
Allow to store/resume open editors across Continue On transitions (fix
Browse files Browse the repository at this point in the history
…#193704) (#201631)

* Allow to store/resume open editors across Continue On transitions (fix #193704)

* address feedback
  • Loading branch information
bpasero authored Jan 4, 2024
1 parent f405cbf commit 901ba07
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 82 deletions.
100 changes: 72 additions & 28 deletions src/vs/workbench/browser/parts/editor/editorPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEdit
import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ISerializedEditorGroupModel, isSerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel';
import { EditorDropTarget } from 'vs/workbench/browser/parts/editor/editorDropTarget';
import { Color } from 'vs/base/common/color';
Expand Down Expand Up @@ -135,7 +135,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {

//#endregion

private readonly workspaceMemento = this.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE);
private readonly workspaceMemento = this.getMemento(StorageScope.WORKSPACE, StorageTarget.USER);
private readonly profileMemento = this.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);

private readonly groupViews = new Map<GroupIdentifier, IEditorGroupView>();
Expand Down Expand Up @@ -172,6 +172,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {
private registerListeners(): void {
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));
this._register(this.themeService.onDidFileIconThemeChange(() => this.handleChangedPartOptions()));
this._register(this.onDidChangeMementoValue(StorageScope.WORKSPACE, this._store)(e => this.onDidChangeMementoState(e)));
}

private onConfigurationUpdated(event: IConfigurationChangeEvent): void {
Expand Down Expand Up @@ -493,23 +494,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {
});

// Recreate gridwidget with descriptor
this.doCreateGridControlWithState(gridDescriptor, activeGroup.id, currentGroupViews);

// Layout
this.doLayout(this._contentDimension);

// Update container
this.updateContainer();

// Events for groups that got added
for (const groupView of this.getGroups(GroupsOrder.GRID_APPEARANCE)) {
if (!currentGroupViews.includes(groupView)) {
this._onDidAddGroup.fire(groupView);
}
}

// Notify group index change given layout has changed
this.notifyGroupIndexChange();
this.doApplyGridState(gridDescriptor, activeGroup.id, currentGroupViews);

// Restore focus as needed
if (restoreFocus) {
Expand Down Expand Up @@ -712,15 +697,21 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {
}

private doRestoreGroup(group: IEditorGroupView): void {
if (this.gridWidget) {
if (this.hasMaximizedGroup() && !this.isGroupMaximized(group)) {
this.unmaximizeGroup();
}
if (!this.gridWidget) {
return; // method is called as part of state restore very early
}

if (this.hasMaximizedGroup() && !this.isGroupMaximized(group)) {
this.unmaximizeGroup();
}

try {
const viewSize = this.gridWidget.getViewSize(group);
if (viewSize.width === group.minimumWidth || viewSize.height === group.minimumHeight) {
this.arrangeGroups(GroupsArrangement.EXPAND, group);
}
} catch (error) {
// ignore: method might be called too early before view is known to grid
}
}

Expand Down Expand Up @@ -1171,19 +1162,19 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {
}

private doCreateGridControlWithPreviousState(): boolean {
const uiState: IEditorPartUIState | undefined = this.loadState();
if (uiState?.serializedGrid) {
const state: IEditorPartUIState | undefined = this.loadState();
if (state?.serializedGrid) {
try {

// MRU
this.mostRecentActiveGroups = uiState.mostRecentActiveGroups;
this.mostRecentActiveGroups = state.mostRecentActiveGroups;

// Grid Widget
this.doCreateGridControlWithState(uiState.serializedGrid, uiState.activeGroup);
this.doCreateGridControlWithState(state.serializedGrid, state.activeGroup);
} catch (error) {

// Log error
onUnexpectedError(new Error(`Error restoring editor grid widget: ${error} (with state: ${JSON.stringify(uiState)})`));
onUnexpectedError(new Error(`Error restoring editor grid widget: ${error} (with state: ${JSON.stringify(state)})`));

// Clear any state we have from the failing restore
this.disposeGroups();
Expand Down Expand Up @@ -1345,6 +1336,59 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView {
};
}

async applyState(state: IEditorPartUIState): Promise<boolean> {

// Close all opened editors and dispose groups
for (const group of this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
const closed = await group.closeAllEditors();
if (!closed) {
return false;
}
}
this.disposeGroups();

// MRU
this.mostRecentActiveGroups = state.mostRecentActiveGroups;

// Grid Widget
this.doApplyGridState(state.serializedGrid, state.activeGroup);

return true;
}

private doApplyGridState(gridState: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void {

// Recreate grid widget from state
this.doCreateGridControlWithState(gridState, activeGroupId, editorGroupViewsToReuse);

// Layout
this.doLayout(this._contentDimension);

// Update container
this.updateContainer();

// Events for groups that got added
for (const groupView of this.getGroups(GroupsOrder.GRID_APPEARANCE)) {
if (!editorGroupViewsToReuse?.includes(groupView)) {
this._onDidAddGroup.fire(groupView);
}
}

// Notify group index change given layout has changed
this.notifyGroupIndexChange();
}

private onDidChangeMementoState(e: IStorageValueChangeEvent): void {
if (e.external && e.scope === StorageScope.WORKSPACE) {
this.reloadMemento(e.scope);

const state = this.loadState();
if (state) {
this.applyState(state);
}
}
}

toJSON(): object {
return {
type: Parts.EDITOR_PART
Expand Down
117 changes: 85 additions & 32 deletions src/vs/workbench/browser/parts/editor/editorParts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { distinct, firstOrDefault } from 'vs/base/common/arrays';
import { AuxiliaryEditorPart, IAuxiliaryEditorPartOpenOptions } from 'vs/workbench/browser/parts/editor/auxiliaryEditorPart';
import { MultiWindowParts } from 'vs/workbench/browser/part';
import { DeferredPromise } from 'vs/base/common/async';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IRectangle } from 'vs/platform/window/common/window';
import { getWindow } from 'vs/base/browser/dom';
Expand Down Expand Up @@ -51,6 +51,11 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
this._register(this.registerPart(this.mainPart));

this.restoreParts();
this.registerListeners();
}

private registerListeners(): void {
this._register(this.onDidChangeMementoValue(StorageScope.WORKSPACE, this._store)(e => this.onDidChangeMementoState(e)));
}

protected createMainEditorPart(): MainEditorPart {
Expand Down Expand Up @@ -180,7 +185,7 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor

private static readonly EDITOR_PARTS_UI_STATE_STORAGE_KEY = 'editorparts.state';

private readonly workspaceMemento = this.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE);
private readonly workspaceMemento = this.getMemento(StorageScope.WORKSPACE, StorageTarget.USER);

private _isReady = false;
get isReady(): boolean { return this._isReady; }
Expand All @@ -204,34 +209,12 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
// that restoring was not attempted because specific
// editors were opened.
if (this.mainPart.willRestoreState) {
const uiState: IEditorPartsUIState | undefined = this.workspaceMemento[EditorParts.EDITOR_PARTS_UI_STATE_STORAGE_KEY];
if (uiState?.auxiliary.length) {
const auxiliaryEditorPartPromises: Promise<IAuxiliaryEditorPart>[] = [];

// Create auxiliary editor parts
for (const auxiliaryEditorPartState of uiState.auxiliary) {
auxiliaryEditorPartPromises.push(this.createAuxiliaryEditorPart({
bounds: auxiliaryEditorPartState.bounds,
state: auxiliaryEditorPartState.state,
zoomLevel: auxiliaryEditorPartState.zoomLevel
}));
}

// Await creation
await Promise.allSettled(auxiliaryEditorPartPromises);

// Update MRU list
if (uiState.mru.length === this.parts.length) {
this.mostRecentActiveParts = uiState.mru.map(index => this.parts[index]);
} else {
this.mostRecentActiveParts = [...this.parts];
}
const state = this.loadState();
if (state) {
await this.restoreState(state);
}
}

// Await ready
await Promise.allSettled(this.parts.map(part => part.whenReady));

const mostRecentActivePart = firstOrDefault(this.mostRecentActiveParts);
mostRecentActivePart?.activeGroup.focus();

Expand All @@ -243,8 +226,21 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
this.whenRestoredPromise.complete();
}

private loadState(): IEditorPartsUIState | undefined {
return this.workspaceMemento[EditorParts.EDITOR_PARTS_UI_STATE_STORAGE_KEY];
}

protected override saveState(): void {
const uiState: IEditorPartsUIState = {
const state = this.createState();
if (state.auxiliary.length === 0) {
delete this.workspaceMemento[EditorParts.EDITOR_PARTS_UI_STATE_STORAGE_KEY];
} else {
this.workspaceMemento[EditorParts.EDITOR_PARTS_UI_STATE_STORAGE_KEY] = state;
}
}

private createState(): IEditorPartsUIState {
return {
auxiliary: this.parts.filter(part => part !== this.mainPart).map(part => {
return {
state: part.createState(),
Expand Down Expand Up @@ -273,18 +269,75 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
}),
mru: this.mostRecentActiveParts.map(part => this.parts.indexOf(part))
};
}

if (uiState.auxiliary.length === 0) {
delete this.workspaceMemento[EditorParts.EDITOR_PARTS_UI_STATE_STORAGE_KEY];
} else {
this.workspaceMemento[EditorParts.EDITOR_PARTS_UI_STATE_STORAGE_KEY] = uiState;
private async restoreState(state: IEditorPartsUIState): Promise<void> {
if (state.auxiliary.length) {
const auxiliaryEditorPartPromises: Promise<IAuxiliaryEditorPart>[] = [];

// Create auxiliary editor parts
for (const auxiliaryEditorPartState of state.auxiliary) {
auxiliaryEditorPartPromises.push(this.createAuxiliaryEditorPart({
bounds: auxiliaryEditorPartState.bounds,
state: auxiliaryEditorPartState.state,
zoomLevel: auxiliaryEditorPartState.zoomLevel
}));
}

// Await creation
await Promise.allSettled(auxiliaryEditorPartPromises);

// Update MRU list
if (state.mru.length === this.parts.length) {
this.mostRecentActiveParts = state.mru.map(index => this.parts[index]);
} else {
this.mostRecentActiveParts = [...this.parts];
}

// Await ready
await Promise.allSettled(this.parts.map(part => part.whenReady));
}
}

get hasRestorableState(): boolean {
return this.parts.some(part => part.hasRestorableState);
}

private onDidChangeMementoState(e: IStorageValueChangeEvent): void {
if (e.external && e.scope === StorageScope.WORKSPACE) {
this.reloadMemento(e.scope);

const state = this.loadState();
if (state) {
this.applyState(state);
}
}
}

private async applyState(state: IEditorPartsUIState): Promise<boolean> {

// Close all editors and auxiliary parts first
for (const part of this.parts) {
if (part === this.mainPart) {
continue; // main part takes care on its own
}

for (const group of part.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
const closed = await group.closeAllEditors();
if (!closed) {
return false;
}
}

(part as unknown as IAuxiliaryEditorPart).close();
}

// Restore auxiliary state
await this.restoreState(state);

return true;
}

//#endregion

//#region Events
Expand Down
12 changes: 11 additions & 1 deletion src/vs/workbench/common/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

import { Memento, MementoObject } from 'vs/workbench/common/memento';
import { IThemeService, Themable } from 'vs/platform/theme/common/themeService';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';

export class Component extends Themable {

Expand Down Expand Up @@ -39,6 +41,14 @@ export class Component extends Themable {
return this.memento.getMemento(scope, target);
}

protected reloadMemento(scope: StorageScope): void {
return this.memento.reloadMemento(scope);
}

protected onDidChangeMementoValue(scope: StorageScope, disposables: DisposableStore): Event<IStorageValueChangeEvent> {
return this.memento.onDidChangeValue(scope, disposables);
}

protected saveState(): void {
// Subclasses to implement for storing state
}
Expand Down
Loading

0 comments on commit 901ba07

Please sign in to comment.