@@ -7,10 +7,13 @@ import { Disposable } from 'vs/base/common/lifecycle';
77import { IFileService , IFileContent , FileChangesEvent , FileOperationResult , FileOperationError } from 'vs/platform/files/common/files' ;
88import { VSBuffer } from 'vs/base/common/buffer' ;
99import { URI } from 'vs/base/common/uri' ;
10- import { SyncResource , SyncStatus , IUserData , IUserDataSyncStoreService , UserDataSyncErrorCode , UserDataSyncError , IUserDataSyncLogService , IUserDataSyncUtilService , IUserDataSyncEnablementService , IUserDataSyncBackupStoreService , Conflict , ISyncResourceHandle , USER_DATA_SYNC_SCHEME , ISyncPreviewResult , IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync' ;
10+ import {
11+ SyncResource , SyncStatus , IUserData , IUserDataSyncStoreService , UserDataSyncErrorCode , UserDataSyncError , IUserDataSyncLogService , IUserDataSyncUtilService ,
12+ IUserDataSyncEnablementService , IUserDataSyncBackupStoreService , Conflict , ISyncResourceHandle , USER_DATA_SYNC_SCHEME , ISyncPreview , IUserDataManifest , ISyncData , IRemoteUserData
13+ } from 'vs/platform/userDataSync/common/userDataSync' ;
1114import { IEnvironmentService } from 'vs/platform/environment/common/environment' ;
1215import { joinPath , dirname , isEqual , basename } from 'vs/base/common/resources' ;
13- import { CancelablePromise , RunOnceScheduler } from 'vs/base/common/async' ;
16+ import { CancelablePromise , RunOnceScheduler , createCancelablePromise } from 'vs/base/common/async' ;
1417import { Emitter , Event } from 'vs/base/common/event' ;
1518import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry' ;
1619import { ParseError , parse } from 'vs/base/common/json' ;
@@ -23,22 +26,12 @@ import { uppercaseFirstLetter } from 'vs/base/common/strings';
2326import { equals } from 'vs/base/common/arrays' ;
2427import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId' ;
2528import { IStorageService } from 'vs/platform/storage/common/storage' ;
29+ import { CancellationToken } from 'vs/base/common/cancellation' ;
2630
2731type SyncSourceClassification = {
2832 source ?: { classification : 'SystemMetaData' , purpose : 'FeatureInsight' , isMeasurement : true } ;
2933} ;
3034
31- export interface IRemoteUserData {
32- ref : string ;
33- syncData : ISyncData | null ;
34- }
35-
36- export interface ISyncData {
37- version : number ;
38- machineId ?: string ;
39- content : string ;
40- }
41-
4235function isSyncData ( thing : any ) : thing is ISyncData {
4336 if ( thing
4437 && ( thing . version !== undefined && typeof thing . version === 'number' )
@@ -58,9 +51,10 @@ function isSyncData(thing: any): thing is ISyncData {
5851 return false ;
5952}
6053
61-
6254export abstract class AbstractSynchroniser extends Disposable {
6355
56+ private syncPreviewPromise : CancelablePromise < ISyncPreview > | null = null ;
57+
6458 protected readonly syncFolder : URI ;
6559 private readonly currentMachineIdPromise : Promise < string > ;
6660
@@ -100,18 +94,33 @@ export abstract class AbstractSynchroniser extends Disposable {
10094 this . currentMachineIdPromise = getServiceMachineId ( environmentService , fileService , storageService ) ;
10195 }
10296
97+ protected isEnabled ( ) : boolean { return this . userDataSyncEnablementService . isResourceEnabled ( this . resource ) ; }
98+
10399 protected async triggerLocalChange ( ) : Promise < void > {
104100 if ( this . isEnabled ( ) ) {
105101 this . localChangeTriggerScheduler . schedule ( ) ;
106102 }
107103 }
108104
109105 protected async doTriggerLocalChange ( ) : Promise < void > {
110- this . logService . trace ( `${ this . syncResourceLogLabel } : Checking for local changes...` ) ;
111- const lastSyncUserData = await this . getLastSyncUserData ( ) ;
112- const hasRemoteChanged = lastSyncUserData ? ( await this . generatePreview ( lastSyncUserData , lastSyncUserData ) ) . hasRemoteChanged : true ;
113- if ( hasRemoteChanged ) {
114- this . _onDidChangeLocal . fire ( ) ;
106+
107+ // Sync again if current status is in conflicts
108+ if ( this . status === SyncStatus . HasConflicts ) {
109+ this . logService . info ( `${ this . syncResourceLogLabel } : In conflicts state and local change detected. Syncing again...` ) ;
110+ const preview = await this . syncPreviewPromise ! ;
111+ this . syncPreviewPromise = null ;
112+ const status = await this . performSync ( preview . remoteUserData , preview . lastSyncUserData ) ;
113+ this . setStatus ( status ) ;
114+ }
115+
116+ // Check if local change causes remote change
117+ else {
118+ this . logService . trace ( `${ this . syncResourceLogLabel } : Checking for local changes...` ) ;
119+ const lastSyncUserData = await this . getLastSyncUserData ( ) ;
120+ const hasRemoteChanged = lastSyncUserData ? ( await this . generatePreview ( lastSyncUserData , lastSyncUserData , CancellationToken . None ) ) . hasRemoteChanged : true ;
121+ if ( hasRemoteChanged ) {
122+ this . _onDidChangeLocal . fire ( ) ;
123+ }
115124 }
116125 }
117126
@@ -141,8 +150,6 @@ export abstract class AbstractSynchroniser extends Disposable {
141150 }
142151 }
143152
144- protected isEnabled ( ) : boolean { return this . userDataSyncEnablementService . isResourceEnabled ( this . resource ) ; }
145-
146153 async sync ( manifest : IUserDataManifest | null ) : Promise < void > {
147154 if ( ! this . isEnabled ( ) ) {
148155 if ( this . status !== SyncStatus . Idle ) {
@@ -168,7 +175,7 @@ export abstract class AbstractSynchroniser extends Disposable {
168175
169176 let status : SyncStatus = SyncStatus . Idle ;
170177 try {
171- status = await this . doSync ( remoteUserData , lastSyncUserData ) ;
178+ status = await this . performSync ( remoteUserData , lastSyncUserData ) ;
172179 if ( status === SyncStatus . HasConflicts ) {
173180 this . logService . info ( `${ this . syncResourceLogLabel } : Detected conflicts while synchronizing ${ this . resource . toLowerCase ( ) } .` ) ;
174181 } else if ( status === SyncStatus . Idle ) {
@@ -224,28 +231,33 @@ export abstract class AbstractSynchroniser extends Disposable {
224231 return this . getRemoteUserData ( lastSyncUserData ) ;
225232 }
226233
227- async getSyncPreview ( ) : Promise < ISyncPreviewResult > {
228- if ( ! this . isEnabled ( ) ) {
229- return { hasLocalChanged : false , hasRemoteChanged : false , isLastSyncFromCurrentMachine : false } ;
234+ async generateSyncPreview ( ) : Promise < ISyncPreview | null > {
235+ if ( this . isEnabled ( ) ) {
236+ const lastSyncUserData = await this . getLastSyncUserData ( ) ;
237+ const remoteUserData = await this . getRemoteUserData ( lastSyncUserData ) ;
238+ return this . generatePreview ( remoteUserData , lastSyncUserData , CancellationToken . None ) ;
230239 }
231-
232- const lastSyncUserData = await this . getLastSyncUserData ( ) ;
233- const remoteUserData = await this . getRemoteUserData ( lastSyncUserData ) ;
234- return this . generatePreview ( remoteUserData , lastSyncUserData ) ;
240+ return null ;
235241 }
236242
237- protected async doSync ( remoteUserData : IRemoteUserData , lastSyncUserData : IRemoteUserData | null ) : Promise < SyncStatus > {
243+ private async performSync ( remoteUserData : IRemoteUserData , lastSyncUserData : IRemoteUserData | null ) : Promise < SyncStatus > {
238244 if ( remoteUserData . syncData && remoteUserData . syncData . version > this . version ) {
239245 // current version is not compatible with cloud version
240246 this . telemetryService . publicLog2 < { source : string } , SyncSourceClassification > ( 'sync/incompatible' , { source : this . resource } ) ;
241247 throw new UserDataSyncError ( localize ( { key : 'incompatible' , comment : [ 'This is an error while syncing a resource that its local version is not compatible with its remote version.' ] } , "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}" , this . resource , this . version , remoteUserData . syncData . version ) , UserDataSyncErrorCode . Incompatible , this . resource ) ;
242248 }
249+
243250 try {
244- const status = await this . performSync ( remoteUserData , lastSyncUserData ) ;
245- return status ;
251+ return await this . doSync ( remoteUserData , lastSyncUserData ) ;
246252 } catch ( e ) {
247253 if ( e instanceof UserDataSyncError ) {
248254 switch ( e . code ) {
255+
256+ case UserDataSyncErrorCode . LocalPreconditionFailed :
257+ // Rejected as there is a new local version. Syncing again...
258+ this . logService . info ( `${ this . syncResourceLogLabel } : Failed to synchronize ${ this . syncResourceLogLabel } as there is a new local version available. Synchronizing again...` ) ;
259+ return this . performSync ( remoteUserData , lastSyncUserData ) ;
260+
249261 case UserDataSyncErrorCode . PreconditionFailed :
250262 // Rejected as there is a new remote version. Syncing again...
251263 this . logService . info ( `${ this . syncResourceLogLabel } : Failed to synchronize as there is a new remote version available. Synchronizing again...` ) ;
@@ -257,13 +269,69 @@ export abstract class AbstractSynchroniser extends Disposable {
257269 // and one of them successfully updated remote and last sync state.
258270 lastSyncUserData = await this . getLastSyncUserData ( ) ;
259271
260- return this . doSync ( remoteUserData , lastSyncUserData ) ;
272+ return this . performSync ( remoteUserData , lastSyncUserData ) ;
261273 }
262274 }
263275 throw e ;
264276 }
265277 }
266278
279+ protected async doSync ( remoteUserData : IRemoteUserData , lastSyncUserData : IRemoteUserData | null ) : Promise < SyncStatus > {
280+ try {
281+ // generate or use existing preview
282+ if ( ! this . syncPreviewPromise ) {
283+ this . syncPreviewPromise = createCancelablePromise ( token => this . generatePreview ( remoteUserData , lastSyncUserData , token ) ) ;
284+ }
285+ const preview = await this . syncPreviewPromise ;
286+
287+ if ( preview . hasConflicts ) {
288+ return SyncStatus . HasConflicts ;
289+ }
290+
291+ // apply preview
292+ await this . applyPreview ( preview ) ;
293+
294+ // reset preview
295+ this . syncPreviewPromise = null ;
296+
297+ return SyncStatus . Idle ;
298+ } catch ( error ) {
299+
300+ // reset preview on error
301+ this . syncPreviewPromise = null ;
302+
303+ throw error ;
304+ }
305+ }
306+
307+ protected async getSyncPreviewInProgress ( ) : Promise < ISyncPreview | null > {
308+ return this . syncPreviewPromise ? this . syncPreviewPromise : null ;
309+ }
310+
311+ async acceptConflict ( conflictUri : URI , conflictContent : string ) : Promise < void > {
312+ let preview = await this . getSyncPreviewInProgress ( ) ;
313+
314+ if ( ! preview || ! preview . hasConflicts ) {
315+ return ;
316+ }
317+
318+
319+ this . syncPreviewPromise = createCancelablePromise ( token => this . updatePreviewWithConflict ( preview ! , conflictUri , conflictContent , token ) ) ;
320+ preview = await this . syncPreviewPromise ;
321+
322+ if ( ! preview . hasConflicts ) {
323+
324+ // apply preview
325+ await this . applyPreview ( preview ) ;
326+
327+ // reset preview
328+ this . syncPreviewPromise = null ;
329+
330+ this . setStatus ( SyncStatus . Idle ) ;
331+ }
332+
333+ }
334+
267335 async hasPreviouslySynced ( ) : Promise < boolean > {
268336 const lastSyncData = await this . getLastSyncUserData ( ) ;
269337 return ! ! lastSyncData ;
@@ -394,26 +462,29 @@ export abstract class AbstractSynchroniser extends Disposable {
394462 return this . userDataSyncBackupStoreService . backup ( this . resource , JSON . stringify ( syncData ) ) ;
395463 }
396464
397- abstract stop ( ) : Promise < void > ;
465+ async stop ( ) : Promise < void > {
466+ this . logService . info ( `${ this . syncResourceLogLabel } : Stopped synchronizing ${ this . resource . toLowerCase ( ) } .` ) ;
467+ if ( this . syncPreviewPromise ) {
468+ this . syncPreviewPromise . cancel ( ) ;
469+ this . syncPreviewPromise = null ;
470+ }
471+ this . setStatus ( SyncStatus . Idle ) ;
472+ }
398473
399474 protected abstract readonly version : number ;
400- protected abstract performSync ( remoteUserData : IRemoteUserData , lastSyncUserData : IRemoteUserData | null ) : Promise < SyncStatus > ;
401475 protected abstract performReplace ( syncData : ISyncData , remoteUserData : IRemoteUserData , lastSyncUserData : IRemoteUserData | null ) : Promise < void > ;
402- protected abstract generatePreview ( remoteUserData : IRemoteUserData , lastSyncUserData : IRemoteUserData | null ) : Promise < ISyncPreviewResult > ;
476+ protected abstract generatePreview ( remoteUserData : IRemoteUserData , lastSyncUserData : IRemoteUserData | null , token : CancellationToken ) : Promise < ISyncPreview > ;
477+ protected abstract updatePreviewWithConflict ( preview : ISyncPreview , conflictResource : URI , content : string , token : CancellationToken ) : Promise < ISyncPreview > ;
478+ protected abstract applyPreview ( preview : ISyncPreview ) : Promise < void > ;
403479}
404480
405- export interface IFileSyncPreviewResult extends ISyncPreviewResult {
481+ export interface IFileSyncPreview extends ISyncPreview {
406482 readonly fileContent : IFileContent | null ;
407- readonly remoteUserData : IRemoteUserData ;
408- readonly lastSyncUserData : IRemoteUserData | null ;
409483 readonly content : string | null ;
410- readonly hasConflicts : boolean ;
411484}
412485
413486export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
414487
415- protected syncPreviewResultPromise : CancelablePromise < IFileSyncPreviewResult > | null = null ;
416-
417488 constructor (
418489 protected readonly file : URI ,
419490 resource : SyncResource ,
@@ -433,23 +504,21 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
433504 }
434505
435506 async stop ( ) : Promise < void > {
436- this . cancel ( ) ;
437- this . logService . info ( `${ this . syncResourceLogLabel } : Stopped synchronizing ${ this . resource . toLowerCase ( ) } .` ) ;
507+ await super . stop ( ) ;
438508 try {
439509 await this . fileService . del ( this . localPreviewResource ) ;
440510 } catch ( e ) { /* ignore */ }
441- this . setStatus ( SyncStatus . Idle ) ;
442511 }
443512
444513 protected async getConflictContent ( conflictResource : URI ) : Promise < string | null > {
445514 if ( isEqual ( this . remotePreviewResource , conflictResource ) || isEqual ( this . localPreviewResource , conflictResource ) ) {
446- if ( this . syncPreviewResultPromise ) {
447- const result = await this . syncPreviewResultPromise ;
515+ const syncPreview = await this . getSyncPreviewInProgress ( ) ;
516+ if ( syncPreview ) {
448517 if ( isEqual ( this . remotePreviewResource , conflictResource ) ) {
449- return result . remoteUserData && result . remoteUserData . syncData ? result . remoteUserData . syncData . content : null ;
518+ return syncPreview . remoteUserData && syncPreview . remoteUserData . syncData ? syncPreview . remoteUserData . syncData . content : null ;
450519 }
451520 if ( isEqual ( this . localPreviewResource , conflictResource ) ) {
452- return result . fileContent ? result . fileContent . value . toString ( ) : null ;
521+ return ( syncPreview as IFileSyncPreview ) . fileContent ? ( syncPreview as IFileSyncPreview ) . fileContent ! . value . toString ( ) : null ;
453522 }
454523 }
455524 }
@@ -487,35 +556,12 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
487556 if ( ! e . contains ( this . file ) ) {
488557 return ;
489558 }
490-
491- if ( ! this . isEnabled ( ) ) {
492- return ;
493- }
494-
495- // Sync again if local file has changed and current status is in conflicts
496- if ( this . status === SyncStatus . HasConflicts ) {
497- this . syncPreviewResultPromise ?. then ( result => {
498- this . cancel ( ) ;
499- this . doSync ( result . remoteUserData , result . lastSyncUserData ) . then ( status => this . setStatus ( status ) ) ;
500- } ) ;
501- }
502-
503- // Otherwise fire change event
504- else {
505- this . triggerLocalChange ( ) ;
506- }
507-
508- }
509-
510- protected cancel ( ) : void {
511- if ( this . syncPreviewResultPromise ) {
512- this . syncPreviewResultPromise . cancel ( ) ;
513- this . syncPreviewResultPromise = null ;
514- }
559+ this . triggerLocalChange ( ) ;
515560 }
516561
517562 protected abstract readonly localPreviewResource : URI ;
518563 protected abstract readonly remotePreviewResource : URI ;
564+ protected abstract applyPreview ( preview : IFileSyncPreview ) : Promise < void > ;
519565}
520566
521567export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroniser {
0 commit comments