mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-19 05:57:12 +01:00
* Allow to store/resume open editors across Continue On transitions (fix #193704) * address feedback
This commit is contained in:
@@ -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';
|
||||
@@ -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>();
|
||||
@@ -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 {
|
||||
@@ -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) {
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
@@ -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 {
|
||||
@@ -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; }
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
@@ -273,11 +269,33 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +303,41 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
|
||||
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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { isEmptyObject } from 'vs/base/common/types';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export type MementoObject = { [key: string]: any };
|
||||
|
||||
@@ -25,8 +27,6 @@ export class Memento {
|
||||
|
||||
getMemento(scope: StorageScope, target: StorageTarget): MementoObject {
|
||||
switch (scope) {
|
||||
|
||||
// Scope by Workspace
|
||||
case StorageScope.WORKSPACE: {
|
||||
let workspaceMemento = Memento.workspaceMementos.get(this.id);
|
||||
if (!workspaceMemento) {
|
||||
@@ -37,7 +37,6 @@ export class Memento {
|
||||
return workspaceMemento.getMemento();
|
||||
}
|
||||
|
||||
// Scope Profile
|
||||
case StorageScope.PROFILE: {
|
||||
let profileMemento = Memento.profileMementos.get(this.id);
|
||||
if (!profileMemento) {
|
||||
@@ -48,7 +47,6 @@ export class Memento {
|
||||
return profileMemento.getMemento();
|
||||
}
|
||||
|
||||
// Scope Application
|
||||
case StorageScope.APPLICATION: {
|
||||
let applicationMemento = Memento.applicationMementos.get(this.id);
|
||||
if (!applicationMemento) {
|
||||
@@ -61,12 +59,33 @@ export class Memento {
|
||||
}
|
||||
}
|
||||
|
||||
onDidChangeValue(scope: StorageScope, disposables: DisposableStore): Event<IStorageValueChangeEvent> {
|
||||
return this.storageService.onDidChangeValue(scope, this.id, disposables);
|
||||
}
|
||||
|
||||
saveMemento(): void {
|
||||
Memento.workspaceMementos.get(this.id)?.save();
|
||||
Memento.profileMementos.get(this.id)?.save();
|
||||
Memento.applicationMementos.get(this.id)?.save();
|
||||
}
|
||||
|
||||
reloadMemento(scope: StorageScope): void {
|
||||
let memento: ScopedMemento | undefined;
|
||||
switch (scope) {
|
||||
case StorageScope.APPLICATION:
|
||||
memento = Memento.applicationMementos.get(this.id);
|
||||
break;
|
||||
case StorageScope.PROFILE:
|
||||
memento = Memento.profileMementos.get(this.id);
|
||||
break;
|
||||
case StorageScope.WORKSPACE:
|
||||
memento = Memento.workspaceMementos.get(this.id);
|
||||
break;
|
||||
}
|
||||
|
||||
memento?.reload();
|
||||
}
|
||||
|
||||
static clear(scope: StorageScope): void {
|
||||
switch (scope) {
|
||||
case StorageScope.WORKSPACE:
|
||||
@@ -84,36 +103,44 @@ export class Memento {
|
||||
|
||||
class ScopedMemento {
|
||||
|
||||
private readonly mementoObj: MementoObject;
|
||||
private mementoObj: MementoObject;
|
||||
|
||||
constructor(private id: string, private scope: StorageScope, private target: StorageTarget, private storageService: IStorageService) {
|
||||
this.mementoObj = this.load();
|
||||
this.mementoObj = this.doLoad();
|
||||
}
|
||||
|
||||
private doLoad(): MementoObject {
|
||||
try {
|
||||
return this.storageService.getObject<MementoObject>(this.id, this.scope, {});
|
||||
} catch (error) {
|
||||
// Seeing reports from users unable to open editors
|
||||
// from memento parsing exceptions. Log the contents
|
||||
// to diagnose further
|
||||
// https://github.com/microsoft/vscode/issues/102251
|
||||
onUnexpectedError(`[memento]: failed to parse contents: ${error} (id: ${this.id}, scope: ${this.scope}, contents: ${this.storageService.get(this.id, this.scope)})`);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
getMemento(): MementoObject {
|
||||
return this.mementoObj;
|
||||
}
|
||||
|
||||
private load(): MementoObject {
|
||||
const memento = this.storageService.get(this.id, this.scope);
|
||||
if (memento) {
|
||||
try {
|
||||
return JSON.parse(memento);
|
||||
} catch (error) {
|
||||
// Seeing reports from users unable to open editors
|
||||
// from memento parsing exceptions. Log the contents
|
||||
// to diagnose further
|
||||
// https://github.com/microsoft/vscode/issues/102251
|
||||
onUnexpectedError(`[memento]: failed to parse contents: ${error} (id: ${this.id}, scope: ${this.scope}, contents: ${memento})`);
|
||||
}
|
||||
reload(): void {
|
||||
|
||||
// Clear old
|
||||
for (const name of Object.getOwnPropertyNames(this.mementoObj)) {
|
||||
delete this.mementoObj[name];
|
||||
}
|
||||
|
||||
return {};
|
||||
// Assign new
|
||||
Object.assign(this.mementoObj, this.doLoad());
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!isEmptyObject(this.mementoObj)) {
|
||||
this.storageService.store(this.id, JSON.stringify(this.mementoObj), this.scope, this.target);
|
||||
this.storageService.store(this.id, this.mementoObj, this.scope, this.target);
|
||||
} else {
|
||||
this.storageService.remove(this.id, this.scope);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ suite('NotebookProviderInfoStore', function () {
|
||||
new class extends mock<IStorageService>() {
|
||||
override get() { return ''; }
|
||||
override store() { }
|
||||
override getObject() { return {}; }
|
||||
},
|
||||
new class extends mock<IExtensionService>() {
|
||||
override onDidRegisterExtensions = Event.None;
|
||||
|
||||
Reference in New Issue
Block a user