Path: blob/main/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts
3296 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';2526const EDIT_SESSIONS_COUNT_KEY = 'editSessionsCount';27const EDIT_SESSIONS_COUNT_CONTEXT_KEY = new RawContextKey<number>(EDIT_SESSIONS_COUNT_KEY, 0);2829export class EditSessionsDataViews extends Disposable {30constructor(31container: ViewContainer,32@IInstantiationService private readonly instantiationService: IInstantiationService,33) {34super();35this.registerViews(container);36}3738private registerViews(container: ViewContainer): void {39const viewId = EDIT_SESSIONS_DATA_VIEW_ID;40const treeView = this.instantiationService.createInstance(TreeView, viewId, EDIT_SESSIONS_TITLE.value);41treeView.showCollapseAllAction = true;42treeView.showRefreshAction = true;43treeView.dataProvider = this.instantiationService.createInstance(EditSessionDataViewDataProvider);4445const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);46// eslint-disable-next-line local/code-no-dangerous-type-assertions47viewsRegistry.registerViews([<ITreeViewDescriptor>{48id: viewId,49name: EDIT_SESSIONS_TITLE,50ctorDescriptor: new SyncDescriptor(TreeViewPane),51canToggleVisibility: true,52canMoveView: false,53treeView,54collapsed: false,55when: ContextKeyExpr.and(EDIT_SESSIONS_SHOW_VIEW),56order: 100,57hideByDefault: true,58}], container);5960viewsRegistry.registerViewWelcomeContent(viewId, {61content: localize(62'noStoredChanges',63'You have no stored changes in the cloud to display.\n{0}',64`[${localize('storeWorkingChangesTitle', 'Store Working Changes')}](command:workbench.editSessions.actions.store)`,65),66when: ContextKeyExpr.equals(EDIT_SESSIONS_COUNT_KEY, 0),67order: 168});6970this._register(registerAction2(class extends Action2 {71constructor() {72super({73id: 'workbench.editSessions.actions.resume',74title: localize('workbench.editSessions.actions.resume.v2', "Resume Working Changes"),75icon: Codicon.desktopDownload,76menu: {77id: MenuId.ViewItemContext,78when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /edit-session/i)),79group: 'inline'80}81});82}8384async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {85const editSessionId = URI.parse(handle.$treeItemHandle).path.substring(1);86const commandService = accessor.get(ICommandService);87await commandService.executeCommand('workbench.editSessions.actions.resumeLatest', editSessionId, true);88await treeView.refresh();89}90}));9192this._register(registerAction2(class extends Action2 {93constructor() {94super({95id: 'workbench.editSessions.actions.store',96title: localize('workbench.editSessions.actions.store.v2', "Store Working Changes"),97icon: Codicon.cloudUpload,98});99}100101async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {102const commandService = accessor.get(ICommandService);103await commandService.executeCommand('workbench.editSessions.actions.storeCurrent');104await treeView.refresh();105}106}));107108this._register(registerAction2(class extends Action2 {109constructor() {110super({111id: 'workbench.editSessions.actions.delete',112title: localize('workbench.editSessions.actions.delete.v2', "Delete Working Changes"),113icon: Codicon.trash,114menu: {115id: MenuId.ViewItemContext,116when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.regex('viewItem', /edit-session/i)),117group: 'inline'118}119});120}121122async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {123const editSessionId = URI.parse(handle.$treeItemHandle).path.substring(1);124const dialogService = accessor.get(IDialogService);125const editSessionStorageService = accessor.get(IEditSessionsStorageService);126const result = await dialogService.confirm({127message: localize('confirm delete.v2', 'Are you sure you want to permanently delete your working changes with ref {0}?', editSessionId),128detail: localize('confirm delete detail.v2', ' You cannot undo this action.'),129type: 'warning',130title: EDIT_SESSIONS_TITLE.value131});132if (result.confirmed) {133await editSessionStorageService.delete('editSessions', editSessionId);134await treeView.refresh();135}136}137}));138139this._register(registerAction2(class extends Action2 {140constructor() {141super({142id: 'workbench.editSessions.actions.deleteAll',143title: localize('workbench.editSessions.actions.deleteAll', "Delete All Working Changes from Cloud"),144icon: Codicon.trash,145menu: {146id: MenuId.ViewTitle,147when: ContextKeyExpr.and(ContextKeyExpr.equals('view', viewId), ContextKeyExpr.greater(EDIT_SESSIONS_COUNT_KEY, 0)),148}149});150}151152async run(accessor: ServicesAccessor): Promise<void> {153const dialogService = accessor.get(IDialogService);154const editSessionStorageService = accessor.get(IEditSessionsStorageService);155const result = await dialogService.confirm({156message: localize('confirm delete all', 'Are you sure you want to permanently delete all stored changes from the cloud?'),157detail: localize('confirm delete all detail', ' You cannot undo this action.'),158type: 'warning',159title: EDIT_SESSIONS_TITLE.value160});161if (result.confirmed) {162await editSessionStorageService.delete('editSessions', null);163await treeView.refresh();164}165}166}));167}168}169170class EditSessionDataViewDataProvider implements ITreeViewDataProvider {171172private editSessionsCount;173174constructor(175@IEditSessionsStorageService private readonly editSessionsStorageService: IEditSessionsStorageService,176@IContextKeyService private readonly contextKeyService: IContextKeyService,177@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,178@IFileService private readonly fileService: IFileService,179) {180this.editSessionsCount = EDIT_SESSIONS_COUNT_CONTEXT_KEY.bindTo(this.contextKeyService);181}182183async getChildren(element?: ITreeItem): Promise<ITreeItem[]> {184if (!element) {185return this.getAllEditSessions();186}187188const [ref, folderName, filePath] = URI.parse(element.handle).path.substring(1).split('/');189190if (ref && !folderName) {191return this.getEditSession(ref);192} else if (ref && folderName && !filePath) {193return this.getEditSessionFolderContents(ref, folderName);194}195196return [];197}198199private async getAllEditSessions(): Promise<ITreeItem[]> {200const allEditSessions = await this.editSessionsStorageService.list('editSessions');201this.editSessionsCount.set(allEditSessions.length);202const editSessions = [];203204for (const session of allEditSessions) {205const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${session.ref}` });206const sessionData = await this.editSessionsStorageService.read('editSessions', session.ref);207if (!sessionData) {208continue;209}210const content: EditSession = JSON.parse(sessionData.content);211const label = content.folders.map((folder) => folder.name).join(', ') ?? session.ref;212const machineId = content.machine;213const machineName = machineId ? await this.editSessionsStorageService.getMachineById(machineId) : undefined;214const description = machineName === undefined ? fromNow(session.created, true) : `${fromNow(session.created, true)}\u00a0\u00a0\u2022\u00a0\u00a0${machineName}`;215216editSessions.push({217handle: resource.toString(),218collapsibleState: TreeItemCollapsibleState.Collapsed,219label: { label },220description: description,221themeIcon: Codicon.repo,222contextValue: `edit-session`223});224}225226return editSessions;227}228229private async getEditSession(ref: string): Promise<ITreeItem[]> {230const data = await this.editSessionsStorageService.read('editSessions', ref);231232if (!data) {233return [];234}235const content: EditSession = JSON.parse(data.content);236237if (content.folders.length === 1) {238const folder = content.folders[0];239return this.getEditSessionFolderContents(ref, folder.name);240}241242return content.folders.map((folder) => {243const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${data.ref}/${folder.name}` });244return {245handle: resource.toString(),246collapsibleState: TreeItemCollapsibleState.Collapsed,247label: { label: folder.name },248themeIcon: Codicon.folder249};250});251}252253private async getEditSessionFolderContents(ref: string, folderName: string): Promise<ITreeItem[]> {254const data = await this.editSessionsStorageService.read('editSessions', ref);255256if (!data) {257return [];258}259const content: EditSession = JSON.parse(data.content);260261const currentWorkspaceFolder = this.workspaceContextService.getWorkspace().folders.find((folder) => folder.name === folderName);262const editSessionFolder = content.folders.find((folder) => folder.name === folderName);263264if (!editSessionFolder) {265return [];266}267268return Promise.all(editSessionFolder.workingChanges.map(async (change) => {269const cloudChangeUri = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${data.ref}/${folderName}/${change.relativeFilePath}` });270271if (currentWorkspaceFolder?.uri) {272// find the corresponding file in the workspace273const localCopy = joinPath(currentWorkspaceFolder.uri, change.relativeFilePath);274if (change.type === ChangeType.Addition && await this.fileService.exists(localCopy)) {275return {276handle: cloudChangeUri.toString(),277resourceUri: cloudChangeUri,278collapsibleState: TreeItemCollapsibleState.None,279label: { label: change.relativeFilePath },280themeIcon: Codicon.file,281command: {282id: 'vscode.diff',283title: localize('compare changes', 'Compare Changes'),284arguments: [285localCopy,286cloudChangeUri,287`${basename(change.relativeFilePath)} (${localize('local copy', 'Local Copy')} \u2194 ${localize('cloud changes', 'Cloud Changes')})`,288undefined289]290}291};292}293}294295return {296handle: cloudChangeUri.toString(),297resourceUri: cloudChangeUri,298collapsibleState: TreeItemCollapsibleState.None,299label: { label: change.relativeFilePath },300themeIcon: Codicon.file,301command: {302id: API_OPEN_EDITOR_COMMAND_ID,303title: localize('open file', 'Open File'),304arguments: [cloudChangeUri, undefined, undefined]305}306};307}));308}309}310311312