Merge from vscode 8a997f7321ae6612fc0e6eb3eac4f358a6233bfb

This commit is contained in:
ADS Merger
2020-02-11 07:08:19 +00:00
parent 0f934081e1
commit 085752f111
217 changed files with 2561 additions and 2063 deletions

View File

@@ -4,15 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, IFileContent } from 'vs/platform/files/common/files';
import { IFileService, IFileContent, FileChangesEvent, FileSystemProviderError, FileSystemProviderErrorCode, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
import { SyncSource, SyncStatus, IUserData, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync';
import { SyncSource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath, dirname } from 'vs/base/common/resources';
import { toLocalISOString } from 'vs/base/common/date';
import { ThrottledDelayer } from 'vs/base/common/async';
import { ThrottledDelayer, CancelablePromise } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ParseError, parse } from 'vs/base/common/json';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
type SyncConflictsClassification = {
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
export abstract class AbstractSynchroniser extends Disposable {
@@ -34,6 +41,9 @@ export abstract class AbstractSynchroniser extends Disposable {
@IFileService protected readonly fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService,
) {
super();
this.syncFolder = joinPath(environmentService.userDataSyncHome, source);
@@ -43,20 +53,53 @@ export abstract class AbstractSynchroniser extends Disposable {
protected setStatus(status: SyncStatus): void {
if (this._status !== status) {
const oldStatus = this._status;
this._status = status;
this._onDidChangStatus.fire(status);
if (status === SyncStatus.HasConflicts) {
// Log to telemetry when there is a sync conflict
this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsDetected', { source: this.source });
}
if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) {
// Log to telemetry when conflicts are resolved
this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsResolved', { source: this.source });
}
}
}
protected get enabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resourceKey); }
async sync(ref?: string): Promise<void> {
if (!this.enabled) {
this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as it is disabled.`);
return;
}
if (this.status === SyncStatus.HasConflicts) {
this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as there are conflicts.`);
return;
}
if (this.status === SyncStatus.Syncing) {
this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as it is running already.`);
return;
}
this.logService.trace(`${this.source}: Started synchronizing ${this.source.toLowerCase()}...`);
this.setStatus(SyncStatus.Syncing);
const lastSyncUserData = await this.getLastSyncUserData();
const remoteUserData = ref && lastSyncUserData && lastSyncUserData.ref === ref ? lastSyncUserData : await this.getRemoteUserData(lastSyncUserData);
return this.doSync(remoteUserData, lastSyncUserData);
}
async hasPreviouslySynced(): Promise<boolean> {
const lastSyncData = await this.getLastSyncUserData();
return !!lastSyncData;
}
async hasRemoteData(): Promise<boolean> {
async getRemoteContent(): Promise<string | null> {
const lastSyncData = await this.getLastSyncUserData();
const remoteUserData = await this.getRemoteUserData(lastSyncData);
return remoteUserData.content !== null;
return remoteUserData.content;
}
async resetLocal(): Promise<void> {
@@ -79,11 +122,11 @@ export abstract class AbstractSynchroniser extends Disposable {
}
protected async getRemoteUserData(lastSyncData: IUserData | null): Promise<IUserData> {
return this.userDataSyncStoreService.read(this.getRemoteDataResourceKey(), lastSyncData, this.source);
return this.userDataSyncStoreService.read(this.resourceKey, lastSyncData, this.source);
}
protected async updateRemoteUserData(content: string, ref: string | null): Promise<string> {
return this.userDataSyncStoreService.write(this.getRemoteDataResourceKey(), content, ref, this.source);
return this.userDataSyncStoreService.write(this.resourceKey, content, ref, this.source);
}
protected async backupLocal(content: VSBuffer): Promise<void> {
@@ -101,21 +144,56 @@ export abstract class AbstractSynchroniser extends Disposable {
}
}
protected abstract getRemoteDataResourceKey(): string;
abstract readonly resourceKey: ResourceKey;
protected abstract doSync(remoteUserData: IUserData, lastSyncUserData: IUserData | null): Promise<void>;
}
export interface IFileSyncPreviewResult {
readonly fileContent: IFileContent | null;
readonly remoteUserData: IUserData;
readonly lastSyncUserData: IUserData | null;
readonly content: string | null;
readonly hasLocalChanged: boolean;
readonly hasRemoteChanged: boolean;
readonly hasConflicts: boolean;
}
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
protected syncPreviewResultPromise: CancelablePromise<IFileSyncPreviewResult> | null = null;
constructor(
protected readonly file: URI,
readonly source: SyncSource,
source: SyncSource,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
) {
super(source, fileService, environmentService, userDataSyncStoreService);
super(source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService);
this._register(this.fileService.watch(dirname(file)));
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(file))(() => this._onDidChangeLocal.fire()));
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
}
async stop(): Promise<void> {
this.cancel();
this.logService.trace(`${this.source}: Stopped synchronizing ${this.source.toLowerCase()}.`);
try {
await this.fileService.del(this.conflictsPreviewResource);
} catch (e) { /* ignore */ }
this.setStatus(SyncStatus.Idle);
}
async getRemoteContent(preview?: boolean): Promise<string | null> {
if (preview) {
if (this.syncPreviewResultPromise) {
const result = await this.syncPreviewResultPromise;
return result.remoteUserData ? result.remoteUserData.content : null;
}
}
return super.getRemoteContent();
}
protected async getLocalFileContent(): Promise<IFileContent | null> {
@@ -127,14 +205,87 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
}
protected async updateLocalFileContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
if (oldContent) {
// file exists already
await this.backupLocal(oldContent.value);
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), oldContent);
} else {
// file does not exist
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false });
try {
if (oldContent) {
// file exists already
await this.backupLocal(oldContent.value);
await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), oldContent);
} else {
// file does not exist
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false });
}
} catch (e) {
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
throw new UserDataSyncError(e.message, UserDataSyncErrorCode.LocalPreconditionFailed);
} else {
throw e;
}
}
}
private onFileChanges(e: FileChangesEvent): void {
if (!e.contains(this.file)) {
return;
}
if (!this.enabled) {
return;
}
// Sync again if local file has changed and current status is in conflicts
if (this.status === SyncStatus.HasConflicts) {
this.syncPreviewResultPromise?.then(result => {
this.cancel();
this.doSync(result.remoteUserData, result.lastSyncUserData);
});
}
// Otherwise fire change event
else {
this._onDidChangeLocal.fire();
}
}
protected cancel(): void {
if (this.syncPreviewResultPromise) {
this.syncPreviewResultPromise.cancel();
this.syncPreviewResultPromise = null;
}
}
protected abstract readonly conflictsPreviewResource: URI;
}
export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {
constructor(
file: URI,
source: SyncSource,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService,
) {
super(file, source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService);
}
protected hasErrors(content: string): boolean {
const parseErrors: ParseError[] = [];
parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
return parseErrors.length > 0;
}
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
protected getFormattingOptions(): Promise<FormattingOptions> {
if (!this._formattingOptions) {
this._formattingOptions = this.userDataSyncUtilService.resolveFormattingOptions(this.file);
}
return this._formattingOptions;
}
}