diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts new file mode 100644 index 00000000000..e085b18c560 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; + +export const IWorkingCopyHistoryService = createDecorator('workingCopyHistoryService'); + +export interface IWorkingCopyHistoryService { + + readonly _serviceBrand: undefined; + + /** + * Adds a new entry to the history for the given working copy. + */ + addEntry(workingCopy: IWorkingCopy, token: CancellationToken): Promise; +} diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts new file mode 100644 index 00000000000..1cb707cc10b --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { WorkingCopyHistoryTracker } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryTracker'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { URI } from 'vs/base/common/uri'; +import { DeferredPromise } from 'vs/base/common/async'; +import { extname, joinPath } from 'vs/base/common/resources'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { hash } from 'vs/base/common/hash'; +import { randomPath } from 'vs/base/common/extpath'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +export class WorkingCopyHistoryService extends Disposable implements IWorkingCopyHistoryService { + + declare readonly _serviceBrand: undefined; + + private readonly localHistoryHome = new DeferredPromise(); + + constructor( + @IFileService private readonly fileService: IFileService, + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IEnvironmentService private readonly environmentService: IEnvironmentService + ) { + super(); + + this.resolveLocalHistoryHome(); + } + + private async resolveLocalHistoryHome(): Promise { + let historyHome: URI; + + // Prefer history to be stored in the remote if we are connected to a remote + const remoteEnv = await this.remoteAgentService.getEnvironment(); + if (remoteEnv) { + historyHome = remoteEnv.localHistoryHome; + } else { + historyHome = this.environmentService.localHistoryHome; + } + + this.localHistoryHome.complete(historyHome); + } + + private async resolveWorkingCopyLocalHistoryHome(workingCopy: IWorkingCopy): Promise { + const historyHome = await this.localHistoryHome.p; + + return joinPath(historyHome, hash(workingCopy.resource.toString()).toString(16)); + } + + async addEntry(workingCopy: IWorkingCopy, token: CancellationToken): Promise { + if (!this.fileService.hasProvider(workingCopy.resource)) { + return; // we require the working copy resource to be file service accessible + } + + const workingCopyHistoryHome = await this.resolveWorkingCopyLocalHistoryHome(workingCopy); + + if (token.isCancellationRequested) { + return; + } + + const target = joinPath(workingCopyHistoryHome, `${randomPath(undefined, undefined, 4)}${extname(workingCopy.resource)}`); + await this.fileService.cloneFile(workingCopy.resource, target); + } +} + +// Register Service +registerSingleton(IWorkingCopyHistoryService, WorkingCopyHistoryService, true); + +// Register History Tracker +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkingCopyHistoryTracker, LifecyclePhase.Restored); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryTracker.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryTracker.ts new file mode 100644 index 00000000000..a9ca1282e28 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryTracker.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Limiter } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { IWorkingCopySaveEvent, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; + +export class WorkingCopyHistoryTracker extends Disposable { + + // Adding history entries from the tracker should not be + // an operation that should be unbounded and as such we + // limit the write operations up to a maximum degree. + private static readonly MAX_PARALLEL_HISTORY_WRITES = 10; + + private readonly limiter = this._register(new Limiter(WorkingCopyHistoryTracker.MAX_PARALLEL_HISTORY_WRITES)); + + private readonly pendingAddHistoryEntryOperations = new ResourceMap(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); + + private readonly workingCopyContentVersion = new ResourceMap(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); + private readonly historyEntryContentVersion = new ResourceMap(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); + + constructor( + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @IWorkingCopyHistoryService private readonly workingCopyHistoryService: IWorkingCopyHistoryService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + ) { + super(); + + this.registerListeners(); + } + + private registerListeners() { + + // Working Copy events + this._register(this.workingCopyService.onDidChangeContent(workingCopy => this.onDidChangeContent(workingCopy))); + this._register(this.workingCopyService.onDidSave(e => this.onDidSave(e))); + } + + private onDidChangeContent(workingCopy: IWorkingCopy): void { + + // Increment content version ID for resource + const contentVersionId = this.getContentVersion(workingCopy); + this.workingCopyContentVersion.set(workingCopy.resource, contentVersionId + 1); + } + + private getContentVersion(workingCopy: IWorkingCopy): number { + return this.workingCopyContentVersion.get(workingCopy.resource) || 0; + } + + private onDidSave(e: IWorkingCopySaveEvent): void { + const contentVersion = this.getContentVersion(e.workingCopy); + if (this.historyEntryContentVersion.get(e.workingCopy.resource) === contentVersion) { + return; // return early when content version already has associated history entry + } + + // Cancel any previous operation for this resource + this.pendingAddHistoryEntryOperations.get(e.workingCopy.resource)?.dispose(true); + + // Create new cancellation token support and remember + const cts = new CancellationTokenSource(); + this.pendingAddHistoryEntryOperations.set(e.workingCopy.resource, cts); + + // Queue new operation to add to history + this.limiter.queue(async () => { + if (cts.token.isCancellationRequested) { + return; + } + + const contentVersion = this.getContentVersion(e.workingCopy); + + // Add entry + await this.workingCopyHistoryService.addEntry(e.workingCopy, cts.token); + + if (cts.token.isCancellationRequested) { + return; + } + + // Remember content version as being added to history + this.historyEntryContentVersion.set(e.workingCopy.resource, contentVersion); + + // Finally remove from pending operations + this.pendingAddHistoryEntryOperations.delete(e.workingCopy.resource); + }); + } +} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 08b0a088b92..7ffece9fc03 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -96,8 +96,9 @@ import 'vs/workbench/services/hover/browser/hoverService'; import 'vs/workbench/services/assignment/common/assignmentService'; import 'vs/workbench/services/outline/browser/outlineService'; import 'vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl'; - import 'vs/editor/common/services/languageFeaturesService'; +import 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; + import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService';