Skip to content

Commit c7a138d

Browse files
authored
Add a view and actions for managing edit sessions (microsoft#156199)
* Add basic view for managing edit sessions * Allow to resume/delete edit sessions from view * Allow opening individual working changes from view * Make edit session actions visible by default * Update tests
1 parent 0b7a991 commit c7a138d

File tree

6 files changed

+297
-5
lines changed

6 files changed

+297
-5
lines changed

src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
1010
import { Action2, IAction2Options, registerAction2 } from 'vs/platform/actions/common/actions';
1111
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
1212
import { localize } from 'vs/nls';
13-
import { IEditSessionsWorkbenchService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_CATEGORY, EditSessionSchemaVersion, IEditSessionsLogService } from 'vs/workbench/contrib/editSessions/common/editSessions';
13+
import { IEditSessionsWorkbenchService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_CATEGORY, EDIT_SESSIONS_CONTAINER_ID, EditSessionSchemaVersion, IEditSessionsLogService, EDIT_SESSIONS_VIEW_ICON, EDIT_SESSIONS_TITLE, EDIT_SESSIONS_SCHEME } from 'vs/workbench/contrib/editSessions/common/editSessions';
1414
import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm';
1515
import { IFileService } from 'vs/platform/files/common/files';
1616
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
@@ -39,6 +39,13 @@ import { Schemas } from 'vs/base/common/network';
3939
import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys';
4040
import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
4141
import { EditSessionsLogService } from 'vs/workbench/contrib/editSessions/common/editSessionsLogService';
42+
import { IViewContainersRegistry, Extensions as ViewExtensions, ViewContainerLocation } from 'vs/workbench/common/views';
43+
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
44+
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
45+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
46+
import { EditSessionsDataViews } from 'vs/workbench/contrib/editSessions/browser/editSessionsViews';
47+
import { ITextModelService } from 'vs/editor/common/services/resolverService';
48+
import { EditSessionsContentProvider } from 'vs/workbench/contrib/editSessions/browser/editSessionsContentProvider';
4249

4350
registerSingleton(IEditSessionsLogService, EditSessionsLogService);
4451
registerSingleton(IEditSessionsWorkbenchService, EditSessionsWorkbenchService);
@@ -74,9 +81,11 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
7481
@IDialogService private readonly dialogService: IDialogService,
7582
@IEditSessionsLogService private readonly logService: IEditSessionsLogService,
7683
@IEnvironmentService private readonly environmentService: IEnvironmentService,
84+
@IInstantiationService private readonly instantiationService: IInstantiationService,
7785
@IProductService private readonly productService: IProductService,
7886
@IConfigurationService private configurationService: IConfigurationService,
7987
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
88+
@ITextModelService textModelResolverService: ITextModelService,
8089
@IQuickInputService private readonly quickInputService: IQuickInputService,
8190
@ICommandService private commandService: ICommandService,
8291
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@@ -101,6 +110,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
101110
});
102111

103112
this.registerActions();
113+
this.registerViews();
104114

105115
continueEditSessionExtPoint.setHandler(extensions => {
106116
const continueEditSessionOptions: ContinueEditSessionItem[] = [];
@@ -128,6 +138,24 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
128138
}
129139
this.continueEditSessionOptions = continueEditSessionOptions;
130140
});
141+
142+
textModelResolverService.registerTextModelContentProvider(EDIT_SESSIONS_SCHEME, instantiationService.createInstance(EditSessionsContentProvider));
143+
}
144+
145+
private registerViews() {
146+
const container = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer(
147+
{
148+
id: EDIT_SESSIONS_CONTAINER_ID,
149+
title: EDIT_SESSIONS_TITLE,
150+
ctorDescriptor: new SyncDescriptor(
151+
ViewPaneContainer,
152+
[EDIT_SESSIONS_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]
153+
),
154+
icon: EDIT_SESSIONS_VIEW_ICON,
155+
hideIfEmpty: true
156+
}, ViewContainerLocation.Sidebar
157+
);
158+
this._register(this.instantiationService.createInstance(EditSessionsDataViews, container));
131159
}
132160

133161
private registerActions() {
@@ -195,7 +223,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
195223
});
196224
}
197225

198-
async run(accessor: ServicesAccessor): Promise<void> {
226+
async run(accessor: ServicesAccessor, editSessionId?: string): Promise<void> {
199227
await that.progressService.withProgress({
200228
location: ProgressLocation.Notification,
201229
title: localize('resuming edit session', 'Resuming edit session...')
@@ -206,7 +234,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
206234
};
207235
that.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume');
208236

209-
await that.resumeEditSession();
237+
await that.resumeEditSession(editSessionId);
210238
});
211239
}
212240
}));
@@ -536,7 +564,7 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat
536564
'workbench.experimental.editSessions.enabled': {
537565
'type': 'boolean',
538566
'tags': ['experimental', 'usesOnlineServices'],
539-
'default': false,
567+
'default': true,
540568
'markdownDescription': localize('editSessionsEnabled', "Controls whether to display cloud-enabled actions to store and resume uncommitted changes when switching between web, desktop, or devices."),
541569
},
542570
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { URI } from 'vs/base/common/uri';
7+
import { ITextModel } from 'vs/editor/common/model';
8+
import { IModelService } from 'vs/editor/common/services/model';
9+
import { ITextModelContentProvider } from 'vs/editor/common/services/resolverService';
10+
import { EDIT_SESSIONS_SCHEME, IEditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/common/editSessions';
11+
12+
export class EditSessionsContentProvider implements ITextModelContentProvider {
13+
14+
constructor(
15+
@IEditSessionsWorkbenchService private editSessionsWorkbenchService: IEditSessionsWorkbenchService,
16+
@IModelService private readonly modelService: IModelService,
17+
) { }
18+
19+
async provideTextContent(uri: URI): Promise<ITextModel | null> {
20+
let model: ITextModel | null = null;
21+
if (uri.scheme === EDIT_SESSIONS_SCHEME) {
22+
const match = /(?<ref>[^/]+)\/(?<folderName>[^/]+)\/(?<filePath>.*)/.exec(uri.path.substring(1));
23+
if (match?.groups) {
24+
const { ref, folderName, filePath } = match.groups;
25+
const data = await this.editSessionsWorkbenchService.read(ref);
26+
const content = data?.editSession.folders.find((f) => f.name === folderName)?.workingChanges.find((change) => change.relativeFilePath === filePath)?.contents;
27+
if (content) {
28+
model = this.modelService.createModel(content, null, uri);
29+
}
30+
}
31+
}
32+
return model;
33+
}
34+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Disposable } from 'vs/base/common/lifecycle';
7+
import { localize } from 'vs/nls';
8+
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
9+
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
10+
import { Registry } from 'vs/platform/registry/common/platform';
11+
import { TreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView';
12+
import { Extensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewsRegistry, TreeItemCollapsibleState, TreeViewItemHandleArg, ViewContainer } from 'vs/workbench/common/views';
13+
import { EDIT_SESSIONS_SCHEME, EDIT_SESSIONS_TITLE, IEditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/common/editSessions';
14+
import { URI } from 'vs/base/common/uri';
15+
import { fromNow } from 'vs/base/common/date';
16+
import { Codicon } from 'vs/base/common/codicons';
17+
import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
18+
import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions';
19+
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
20+
import { ICommandService } from 'vs/platform/commands/common/commands';
21+
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
22+
23+
export class EditSessionsDataViews extends Disposable {
24+
constructor(
25+
container: ViewContainer,
26+
@IInstantiationService private readonly instantiationService: IInstantiationService,
27+
) {
28+
super();
29+
this.registerViews(container);
30+
}
31+
32+
private registerViews(container: ViewContainer): void {
33+
const viewId = 'workbench.views.editSessions.data';
34+
const name = localize('edit sessions data', 'All Sessions');
35+
const treeView = this.instantiationService.createInstance(TreeView, viewId, name);
36+
treeView.showCollapseAllAction = true;
37+
treeView.showRefreshAction = true;
38+
const disposable = treeView.onDidChangeVisibility(visible => {
39+
if (visible && !treeView.dataProvider) {
40+
disposable.dispose();
41+
treeView.dataProvider = this.instantiationService.createInstance(EditSessionDataViewDataProvider);
42+
}
43+
});
44+
Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).registerViews([<ITreeViewDescriptor>{
45+
id: viewId,
46+
name,
47+
ctorDescriptor: new SyncDescriptor(TreeViewPane),
48+
canToggleVisibility: true,
49+
canMoveView: false,
50+
treeView,
51+
collapsed: false,
52+
order: 100,
53+
hideByDefault: true,
54+
}], container);
55+
56+
registerAction2(class extends Action2 {
57+
constructor() {
58+
super({
59+
id: 'workbench.editSessions.actions.resume',
60+
title: localize('workbench.editSessions.actions.resume', "Resume Edit Session"),
61+
icon: Codicon.repoPull,
62+
menu: {
63+
id: MenuId.ViewItemContext,
64+
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /edit-session/i)),
65+
group: 'inline'
66+
}
67+
});
68+
}
69+
70+
async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {
71+
const editSessionId = URI.parse(handle.$treeItemHandle).path.substring(1);
72+
const commandService = accessor.get(ICommandService);
73+
await commandService.executeCommand('workbench.experimental.editSessions.actions.resumeLatest', editSessionId);
74+
await treeView.refresh();
75+
}
76+
});
77+
78+
registerAction2(class extends Action2 {
79+
constructor() {
80+
super({
81+
id: 'workbench.editSessions.actions.delete',
82+
title: localize('workbench.editSessions.actions.delete', "Delete Edit Session"),
83+
icon: Codicon.trash,
84+
menu: {
85+
id: MenuId.ViewItemContext,
86+
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /edit-session/i)),
87+
group: 'inline'
88+
}
89+
});
90+
}
91+
92+
async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {
93+
const editSessionId = URI.parse(handle.$treeItemHandle).path.substring(1);
94+
const dialogService = accessor.get(IDialogService);
95+
const editSessionWorkbenchService = accessor.get(IEditSessionsWorkbenchService);
96+
const result = await dialogService.confirm({
97+
message: localize('confirm delete', 'Are you sure you want to permanently delete the edit session with ref {0}? You cannot undo this action.', editSessionId),
98+
type: 'warning',
99+
title: EDIT_SESSIONS_TITLE
100+
});
101+
if (result.confirmed) {
102+
await editSessionWorkbenchService.delete(editSessionId);
103+
await treeView.refresh();
104+
}
105+
}
106+
});
107+
}
108+
}
109+
110+
class EditSessionDataViewDataProvider implements ITreeViewDataProvider {
111+
constructor(
112+
@IEditSessionsWorkbenchService private readonly editSessionsWorkbenchService: IEditSessionsWorkbenchService
113+
) { }
114+
115+
async getChildren(element?: ITreeItem): Promise<ITreeItem[]> {
116+
if (!element) {
117+
return this.getAllEditSessions();
118+
}
119+
120+
const [ref, folderName, filePath] = URI.parse(element.handle).path.substring(1).split('/');
121+
122+
if (ref && !folderName) {
123+
return this.getEditSession(ref);
124+
} else if (ref && folderName && !filePath) {
125+
return this.getEditSessionFolderContents(ref, folderName);
126+
}
127+
128+
return [];
129+
}
130+
131+
private async getAllEditSessions(): Promise<ITreeItem[]> {
132+
const allEditSessions = await this.editSessionsWorkbenchService.list();
133+
return allEditSessions.map((session) => {
134+
const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${session.ref}` });
135+
return {
136+
handle: resource.toString(),
137+
collapsibleState: TreeItemCollapsibleState.Collapsed,
138+
label: { label: session.ref },
139+
description: fromNow(session.created, true),
140+
themeIcon: Codicon.repo,
141+
contextValue: `edit-session`
142+
};
143+
});
144+
}
145+
146+
private async getEditSession(ref: string): Promise<ITreeItem[]> {
147+
const data = await this.editSessionsWorkbenchService.read(ref);
148+
149+
if (!data) {
150+
return [];
151+
}
152+
153+
return data.editSession.folders.map((folder) => {
154+
const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${data.ref}/${folder.name}` });
155+
return {
156+
handle: resource.toString(),
157+
collapsibleState: TreeItemCollapsibleState.Collapsed,
158+
label: { label: folder.name },
159+
themeIcon: Codicon.folder
160+
};
161+
});
162+
}
163+
164+
private async getEditSessionFolderContents(ref: string, folderName: string): Promise<ITreeItem[]> {
165+
const data = await this.editSessionsWorkbenchService.read(ref);
166+
167+
if (!data) {
168+
return [];
169+
}
170+
171+
return (data.editSession.folders.find((folder) => folder.name === folderName)?.workingChanges ?? []).map((change) => {
172+
const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${data.ref}/${folderName}/${change.relativeFilePath}` });
173+
return {
174+
handle: resource.toString(),
175+
resourceUri: resource,
176+
collapsibleState: TreeItemCollapsibleState.None,
177+
label: { label: change.relativeFilePath },
178+
themeIcon: Codicon.file,
179+
command: {
180+
id: API_OPEN_EDITOR_COMMAND_ID,
181+
title: localize('open file', 'Open File'),
182+
arguments: [resource, undefined, undefined]
183+
}
184+
};
185+
});
186+
}
187+
}

src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { IProductService } from 'vs/platform/product/common/productService';
1414
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
1515
import { IRequestService } from 'vs/platform/request/common/request';
1616
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
17-
import { createSyncHeaders, IAuthenticationProvider } from 'vs/platform/userDataSync/common/userDataSync';
17+
import { createSyncHeaders, IAuthenticationProvider, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync';
1818
import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
1919
import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
2020
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
@@ -122,6 +122,21 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes
122122
}
123123
}
124124

125+
async list(): Promise<IResourceRefHandle[]> {
126+
await this.initialize();
127+
if (!this.initialized) {
128+
throw new Error(`Unable to list edit sessions.`);
129+
}
130+
131+
try {
132+
return this.storeClient?.getAllRefs('editSessions') ?? [];
133+
} catch (ex) {
134+
this.logService.error(ex);
135+
}
136+
137+
return [];
138+
}
139+
125140
private async initialize() {
126141
if (this.initialized) {
127142
return;

src/vs/workbench/contrib/editSessions/common/editSessions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { Codicon } from 'vs/base/common/codicons';
67
import { localize } from 'vs/nls';
78
import { ILocalizedString } from 'vs/platform/action/common/action';
89
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
910
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
1011
import { ILogService } from 'vs/platform/log/common/log';
12+
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
13+
import { IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync';
1114

1215
export const EDIT_SESSION_SYNC_CATEGORY: ILocalizedString = {
1316
original: 'Edit Sessions',
@@ -21,6 +24,7 @@ export interface IEditSessionsWorkbenchService {
2124
read(ref: string | undefined): Promise<{ ref: string; editSession: EditSession } | undefined>;
2225
write(editSession: EditSession): Promise<string>;
2326
delete(ref: string): Promise<void>;
27+
list(): Promise<IResourceRefHandle[]>;
2428
}
2529

2630
export const IEditSessionsLogService = createDecorator<IEditSessionsLogService>('IEditSessionsLogService');
@@ -65,3 +69,10 @@ export interface EditSession {
6569

6670
export const EDIT_SESSIONS_SIGNED_IN_KEY = 'editSessionsSignedIn';
6771
export const EDIT_SESSIONS_SIGNED_IN = new RawContextKey<boolean>(EDIT_SESSIONS_SIGNED_IN_KEY, false);
72+
73+
export const EDIT_SESSIONS_CONTAINER_ID = 'workbench.view.editSessions';
74+
export const EDIT_SESSIONS_TITLE = localize('edit sessions', 'Edit Sessions');
75+
76+
export const EDIT_SESSIONS_VIEW_ICON = registerIcon('edit-sessions-view-icon', Codicon.cloudDownload, localize('editSessionViewIcon', 'View icon of the edit sessions view.'));
77+
78+
export const EDIT_SESSIONS_SCHEME = 'vscode-edit-sessions';

0 commit comments

Comments
 (0)