Path: blob/main/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts
5272 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable } from '../../../../base/common/lifecycle.js';6import { localize } from '../../../../nls.js';7import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';8import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';9import { Registry } from '../../../../platform/registry/common/platform.js';10import { TreeView, TreeViewPane } from '../../../browser/parts/views/treeView.js';11import { Extensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewsRegistry, TreeItemCollapsibleState, TreeViewItemHandleArg, ViewContainer } from '../../../common/views.js';12import { ChangeType, EDIT_SESSIONS_DATA_VIEW_ID, EDIT_SESSIONS_SCHEME, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_TITLE, EditSession, IEditSessionsStorageService } from '../common/editSessions.js';13import { URI } from '../../../../base/common/uri.js';14import { fromNow } from '../../../../base/common/date.js';15import { Codicon } from '../../../../base/common/codicons.js';16import { API_OPEN_EDITOR_COMMAND_ID } from '../../../browser/parts/editor/editorCommands.js';17import { registerAction2, Action2, MenuId } from '../../../../platform/actions/common/actions.js';18import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';19import { ICommandService } from '../../../../platform/commands/common/commands.js';20import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';21import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';22import { joinPath } from '../../../../base/common/resources.js';23import { IFileService } from '../../../../platform/files/common/files.js';24import { basename } from '../../../../base/common/path.js';25import { createCommandUri } from '../../../../base/common/htmlContent.js';2627const EDIT_SESSIONS_COUNT_KEY = 'editSessionsCount';28const EDIT_SESSIONS_COUNT_CONTEXT_KEY = new RawContextKey<number>(EDIT_SESSIONS_COUNT_KEY, 0);2930export class EditSessionsDataViews extends Disposable {31constructor(32container: ViewContainer,33@IInstantiationService private readonly instantiationService: IInstantiationService,34) {35super();36this.registerViews(container);37}3839private registerViews(container: ViewContainer): void {40const viewId = EDIT_SESSIONS_DATA_VIEW_ID;41const treeView = this.instantiationService.createInstance(TreeView, viewId, EDIT_SESSIONS_TITLE.value);42treeView.showCollapseAllAction = true;43treeView.showRefreshAction = true;44treeView.dataProvider = this.instantiationService.createInstance(EditSessionDataViewDataProvider);4546const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);47// eslint-disable-next-line local/code-no-dangerous-type-assertions48viewsRegistry.registerViews([<ITreeViewDescriptor>{49id: viewId,50name: EDIT_SESSIONS_TITLE,51ctorDescriptor: new SyncDescriptor(TreeViewPane),52canToggleVisibility: true,53canMoveView: false,54treeView,55collapsed: false,56when: ContextKeyExpr.and(EDIT_SESSIONS_SHOW_VIEW),57order: 100,58hideByDefault: true,59}], container);6061viewsRegistry.registerViewWelcomeContent(viewId, {62content: localize(63'noStoredChanges',64'You have no stored changes in the cloud to display.\n{0}',65`[${localize('storeWorkingChangesTitle', 'Store Working Changes')}](${createCommandUri('workbench.editSessions.actions.store')})`,66),67when: ContextKeyExpr.equals(EDIT_SESSIONS_COUNT_KEY, 0),68order: 169});7071this._register(registerAction2(class extends Action2 {72constructor() {73super({74id: 'workbench.editSessions.actions.resume',75title: localize('workbench.editSessions.actions.resume.v2', "Resume Working Changes"),76icon: Codicon.desktopDownload,77menu: {78id: MenuId.ViewItemContext,79when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /edit-session/i)),80group: 'inline'81}82});83}8485async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {86const editSessionId = URI.parse(handle.$treeItemHandle).path.substring(1);87const commandService = accessor.get(ICommandService);88await commandService.executeCommand('workbench.editSessions.actions.resumeLatest', editSessionId, true);89await treeView.refresh();90}91}));9293this._register(registerAction2(class extends Action2 {94constructor() {95super({96id: 'workbench.editSessions.actions.store',97title: localize('workbench.editSessions.actions.store.v2', "Store Working Changes"),98icon: Codicon.cloudUpload,99});100}101102async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {103const commandService = accessor.get(ICommandService);104await commandService.executeCommand('workbench.editSessions.actions.storeCurrent');105await treeView.refresh();106}107}));108109this._register(registerAction2(class extends Action2 {110constructor() {111super({112id: 'workbench.editSessions.actions.delete',113title: localize('workbench.editSessions.actions.delete.v2', "Delete Working Changes"),114icon: Codicon.trash,115menu: {116id: MenuId.ViewItemContext,117when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /edit-session/i)),118group: 'inline'119}120});121}122123async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {124const editSessionId = URI.parse(handle.$treeItemHandle).path.substring(1);125const dialogService = accessor.get(IDialogService);126const editSessionStorageService = accessor.get(IEditSessionsStorageService);127const result = await dialogService.confirm({128message: localize('confirm delete.v2', 'Are you sure you want to permanently delete your working changes with ref {0}?', editSessionId),129detail: localize('confirm delete detail.v2', ' You cannot undo this action.'),130type: 'warning',131title: EDIT_SESSIONS_TITLE.value132});133if (result.confirmed) {134await editSessionStorageService.delete('editSessions', editSessionId);135await treeView.refresh();136}137}138}));139140this._register(registerAction2(class extends Action2 {141constructor() {142super({143id: 'workbench.editSessions.actions.deleteAll',144title: localize('workbench.editSessions.actions.deleteAll', "Delete All Working Changes from Cloud"),145icon: Codicon.trash,146menu: {147id: MenuId.ViewTitle,148when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.greater(EDIT_SESSIONS_COUNT_KEY, 0)),149}150});151}152153async run(accessor: ServicesAccessor): Promise<void> {154const dialogService = accessor.get(IDialogService);155const editSessionStorageService = accessor.get(IEditSessionsStorageService);156const result = await dialogService.confirm({157message: localize('confirm delete all', 'Are you sure you want to permanently delete all stored changes from the cloud?'),158detail: localize('confirm delete all detail', ' You cannot undo this action.'),159type: 'warning',160title: EDIT_SESSIONS_TITLE.value161});162if (result.confirmed) {163await editSessionStorageService.delete('editSessions', null);164await treeView.refresh();165}166}167}));168}169}170171class EditSessionDataViewDataProvider implements ITreeViewDataProvider {172173private editSessionsCount;174175constructor(176@IEditSessionsStorageService private readonly editSessionsStorageService: IEditSessionsStorageService,177@IContextKeyService private readonly contextKeyService: IContextKeyService,178@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,179@IFileService private readonly fileService: IFileService,180) {181this.editSessionsCount = EDIT_SESSIONS_COUNT_CONTEXT_KEY.bindTo(this.contextKeyService);182}183184async getChildren(element?: ITreeItem): Promise<ITreeItem[]> {185if (!element) {186return this.getAllEditSessions();187}188189const [ref, folderName, filePath] = URI.parse(element.handle).path.substring(1).split('/');190191if (ref && !folderName) {192return this.getEditSession(ref);193} else if (ref && folderName && !filePath) {194return this.getEditSessionFolderContents(ref, folderName);195}196197return [];198}199200private async getAllEditSessions(): Promise<ITreeItem[]> {201const allEditSessions = await this.editSessionsStorageService.list('editSessions');202this.editSessionsCount.set(allEditSessions.length);203const editSessions = [];204205for (const session of allEditSessions) {206const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${session.ref}` });207const sessionData = await this.editSessionsStorageService.read('editSessions', session.ref);208if (!sessionData) {209continue;210}211const content: EditSession = JSON.parse(sessionData.content);212const label = content.folders.map((folder) => folder.name).join(', ') ?? session.ref;213const machineId = content.machine;214const machineName = machineId ? await this.editSessionsStorageService.getMachineById(machineId) : undefined;215const description = machineName === undefined ? fromNow(session.created, true) : `${fromNow(session.created, true)}\u00a0\u00a0\u2022\u00a0\u00a0${machineName}`;216217editSessions.push({218handle: resource.toString(),219collapsibleState: TreeItemCollapsibleState.Collapsed,220label: { label },221description: description,222themeIcon: Codicon.repo,223contextValue: `edit-session`224});225}226227return editSessions;228}229230private async getEditSession(ref: string): Promise<ITreeItem[]> {231const data = await this.editSessionsStorageService.read('editSessions', ref);232233if (!data) {234return [];235}236const content: EditSession = JSON.parse(data.content);237238if (content.folders.length === 1) {239const folder = content.folders[0];240return this.getEditSessionFolderContents(ref, folder.name);241}242243return content.folders.map((folder) => {244const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${data.ref}/${folder.name}` });245return {246handle: resource.toString(),247collapsibleState: TreeItemCollapsibleState.Collapsed,248label: { label: folder.name },249themeIcon: Codicon.folder250};251});252}253254private async getEditSessionFolderContents(ref: string, folderName: string): Promise<ITreeItem[]> {255const data = await this.editSessionsStorageService.read('editSessions', ref);256257if (!data) {258return [];259}260const content: EditSession = JSON.parse(data.content);261262const currentWorkspaceFolder = this.workspaceContextService.getWorkspace().folders.find((folder) => folder.name === folderName);263const editSessionFolder = content.folders.find((folder) => folder.name === folderName);264265if (!editSessionFolder) {266return [];267}268269return Promise.all(editSessionFolder.workingChanges.map(async (change) => {270const cloudChangeUri = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${data.ref}/${folderName}/${change.relativeFilePath}` });271272if (currentWorkspaceFolder?.uri) {273// find the corresponding file in the workspace274const localCopy = joinPath(currentWorkspaceFolder.uri, change.relativeFilePath);275if (change.type === ChangeType.Addition && await this.fileService.exists(localCopy)) {276return {277handle: cloudChangeUri.toString(),278resourceUri: cloudChangeUri,279collapsibleState: TreeItemCollapsibleState.None,280label: { label: change.relativeFilePath },281themeIcon: Codicon.file,282command: {283id: 'vscode.diff',284title: localize('compare changes', 'Compare Changes'),285arguments: [286localCopy,287cloudChangeUri,288`${basename(change.relativeFilePath)} (${localize('local copy', 'Local Copy')} \u2194 ${localize('cloud changes', 'Cloud Changes')})`,289undefined290]291}292};293}294}295296return {297handle: cloudChangeUri.toString(),298resourceUri: cloudChangeUri,299collapsibleState: TreeItemCollapsibleState.None,300label: { label: change.relativeFilePath },301themeIcon: Codicon.file,302command: {303id: API_OPEN_EDITOR_COMMAND_ID,304title: localize('open file', 'Open File'),305arguments: [cloudChangeUri, undefined, undefined]306}307};308}));309}310}311312313