diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 4860d158671..47e10a2c34e 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -144,6 +144,33 @@ export abstract class AbstractSynchroniser extends Disposable { } } + async replace(uri: URI): Promise { + const content = await this.resolveContent(uri); + if (!content) { + return false; + } + + const syncData = this.parseSyncData(content); + if (!syncData) { + return false; + } + + await this.stop(); + + try { + this.logService.trace(`${this.syncResourceLogLabel}: Started resetting ${this.resource.toLowerCase()}...`); + this.setStatus(SyncStatus.Syncing); + const lastSyncUserData = await this.getLastSyncUserData(); + const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData); + await this.performReplace(syncData, remoteUserData, lastSyncUserData); + this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`); + } finally { + this.setStatus(SyncStatus.Idle); + } + + return true; + } + private async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise { if (lastSyncUserData) { @@ -323,6 +350,7 @@ export abstract class AbstractSynchroniser extends Disposable { protected abstract readonly version: number; protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; + protected abstract performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; } diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index ae3e0ba4da8..0aa08c4aa3a 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -200,6 +200,18 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return SyncStatus.Idle; } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + const localExtensions = await this.getLocalExtensions(); + const syncExtensions = this.parseExtensions(syncData); + const { added, updated, removed } = merge(localExtensions, syncExtensions, localExtensions, [], this.getIgnoredExtensions()); + + await this.apply({ + added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData, + hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0, + hasRemoteChanged: true + }); + } + protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? this.parseExtensions(remoteUserData.syncData) : null; const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? this.parseExtensions(lastSyncUserData.syncData!) : null; diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index ca00b220b5b..ad2acedf184 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -13,7 +13,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { edit } from 'vs/platform/userDataSync/common/content'; import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; import { parse } from 'vs/base/common/json'; -import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; @@ -200,6 +200,18 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return SyncStatus.Idle; } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + const localUserData = await this.getLocalGlobalState(); + const syncGlobalState: IGlobalState = JSON.parse(syncData.content); + const { local, skipped } = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService); + await this.apply({ + local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData, + skippedStorageKeys: skipped, + hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0, + hasRemoteChanged: true + }); + } + protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 9d3c69444c9..b2d71b4cfac 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -16,7 +16,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { isUndefined } from 'vs/base/common/types'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources'; @@ -212,6 +212,24 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + const content = this.getKeybindingsContentFromSyncContent(syncData.content); + + if (content !== null) { + const fileContent = await this.getLocalFileContent(); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + fileContent, + remoteUserData, + lastSyncUserData, + content, + hasConflicts: false, + hasLocalChanged: true, + hasRemoteChanged: true, + })); + await this.apply(); + } + } + private async apply(forcePush?: boolean): Promise { if (!this.syncPreviewResultPromise) { return; diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 33b7eae171a..4a7b0151ba3 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -14,7 +14,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CancellationToken } from 'vs/base/common/cancellation'; import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge'; import { edit } from 'vs/platform/userDataSync/common/content'; -import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -257,6 +257,27 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { } } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + const settingsSyncContent = this.parseSettingsSyncContent(syncData.content); + if (settingsSyncContent) { + const fileContent = await this.getLocalFileContent(); + const formatUtils = await this.getFormattingOptions(); + const ignoredSettings = await this.getIgnoredSettings(); + const content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + fileContent, + remoteUserData, + lastSyncUserData, + content, + hasLocalChanged: true, + hasRemoteChanged: true, + hasConflicts: false, + })); + + await this.apply(); + } + } + private async apply(forcePush?: boolean): Promise { if (!this.syncPreviewResultPromise) { return; diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index b8d4a194b8c..8871c78c7cd 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -256,6 +256,19 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + const local = await this.getSnippetsFileContents(); + const localSnippets = this.toSnippetsContents(local); + const snippets = this.parseSnippets(syncData); + const { added, updated, removed } = merge(localSnippets, snippets, localSnippets); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, + hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0, + hasRemoteChanged: true + })); + await this.apply(); + } + protected getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { if (!this.syncPreviewResultPromise) { this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token)); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index e158e4dce61..a12fe37c347 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -275,6 +275,7 @@ export interface IUserDataSynchroniser { pull(): Promise; push(): Promise; sync(manifest: IUserDataManifest | null): Promise; + replace(uri: URI): Promise; stop(): Promise; getSyncPreview(): Promise @@ -330,6 +331,7 @@ export interface IUserDataSyncService { pull(): Promise; sync(): Promise; stop(): Promise; + replace(uri: URI): Promise; reset(): Promise; resetLocal(): Promise; @@ -380,3 +382,13 @@ export function getSyncResourceFromLocalPreview(localPreview: URI, environmentSe localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme }); return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0]; } + +export function getSyncAreaLabel(source: SyncResource): string { + switch (source) { + case SyncResource.Settings: return localize('settings', "Settings"); + case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); + case SyncResource.Snippets: return localize('snippets', "User Snippets"); + case SyncResource.Extensions: return localize('extensions', "Extensions"); + case SyncResource.GlobalState: return localize('ui state label', "UI State"); + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 1aeeecf920f..45643cde05f 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -33,6 +33,7 @@ export class UserDataSyncChannel implements IServerChannel { case 'pull': return this.service.pull(); case 'sync': return this.service.sync(); case 'stop': this.service.stop(); return Promise.resolve(); + case 'replace': return this.service.replace(URI.revive(args[0])); case 'reset': return this.service.reset(); case 'resetLocal': return this.service.resetLocal(); case 'isFirstTimeSyncWithMerge': return this.service.isFirstTimeSyncWithMerge(); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index bd150093aab..ea1397a9667 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -162,6 +162,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } + async replace(uri: URI): Promise { + await this.checkEnablement(); + for (const synchroniser of this.synchronisers) { + if (await synchroniser.replace(uri)) { + return; + } + } + } + async stop(): Promise { await this.checkEnablement(); if (this.status === SyncStatus.Idle) { diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 340b95c0f24..08241ecb441 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { Barrier } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -39,6 +39,8 @@ class TestSynchroniser extends AbstractSynchroniser { return this.syncResult.status || SyncStatus.Idle; } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { } + async apply(ref: string): Promise { ref = await this.userDataSyncStoreService.write(this.resource, '', ref); await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index ba8bcf7c7e7..be2be3ec4f2 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -30,7 +30,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CONTEXT_SYNC_STATE, IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, CONTEXT_SYNC_ENABLEMENT, - SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview + SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview, getSyncAreaLabel } from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -54,16 +54,6 @@ const CONTEXT_CONFLICTS_SOURCES = new RawContextKey('conflictsSources', type ConfigureSyncQuickPickItem = { id: SyncResource, label: string, description?: string }; -function getSyncAreaLabel(source: SyncResource): string { - switch (source) { - case SyncResource.Settings: return localize('settings', "Settings"); - case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); - case SyncResource.Snippets: return localize('snippets', "User Snippets"); - case SyncResource.Extensions: return localize('extensions', "Extensions"); - case SyncResource.GlobalState: return localize('ui state label', "UI State"); - } -} - type SyncConflictsClassification = { source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; action?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts index 6b027b310ff..ff99e211245 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts @@ -10,18 +10,19 @@ import { localize } from 'vs/nls'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle, CONTEXT_SYNC_STATE, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; +import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle, CONTEXT_SYNC_STATE, SyncStatus, getSyncAreaLabel } from 'vs/platform/userDataSync/common/userDataSync'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { FolderThemeIcon } from 'vs/platform/theme/common/themeService'; import { fromNow } from 'vs/base/common/date'; -import { pad, uppercaseFirstLetter } from 'vs/base/common/strings'; +import { pad } from 'vs/base/common/strings'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { Codicon } from 'vs/base/common/codicons'; import { CONTEXT_ACCOUNT_STATE } from 'vs/workbench/contrib/userDataSync/browser/userDataSync'; import { AccountStatus } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAccount'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; export const VIEW_CONTAINER_ID = 'workbench.view.sync'; const CONTEXT_ENABLE_VIEWS = new RawContextKey(`showUserDataSyncViews`, false); @@ -132,8 +133,37 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { }); } async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { + const { resource } = <{ resource: string }>JSON.parse(handle.$treeItemHandle); const editorService = accessor.get(IEditorService); - await editorService.openEditor({ resource: URI.parse(handle.$treeItemHandle) }); + await editorService.openEditor({ resource: URI.parse(resource) }); + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.sync.replaceCurrent`, + title: localize('workbench.actions.sync.replaceCurrent', "Download..."), + icon: { id: 'codicon/cloud-download' }, + menu: { + id: MenuId.ViewItemContext, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i)), + group: 'inline', + }, + }); + } + async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { + const dialogService = accessor.get(IDialogService); + const userDataSyncService = accessor.get(IUserDataSyncService); + const { resource, syncResource } = <{ resource: string, syncResource: SyncResource }>JSON.parse(handle.$treeItemHandle); + const result = await dialogService.confirm({ + message: localize('confirm replace', "Would you like to replace your current {0} with selected?", getSyncAreaLabel(syncResource)), + type: 'info', + title: localize('preferences sync', "Preferences Sync") + }); + if (result.confirmed) { + return userDataSyncService.replace(URI.parse(resource)); + } } }); @@ -179,23 +209,24 @@ class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider { return ALL_SYNC_RESOURCES.map(resourceKey => ({ handle: resourceKey, collapsibleState: TreeItemCollapsibleState.Collapsed, - label: { label: uppercaseFirstLetter(resourceKey) }, + label: { label: getSyncAreaLabel(resourceKey) }, themeIcon: FolderThemeIcon, })); } - const resourceKey = ALL_SYNC_RESOURCES.filter(key => key === element.handle)[0] as SyncResource; - if (resourceKey) { - const refHandles = this.remote ? await this.userDataSyncService.getRemoteSyncResourceHandles(resourceKey) : await this.userDataSyncService.getLocalSyncResourceHandles(resourceKey); + const syncResource = ALL_SYNC_RESOURCES.filter(key => key === element.handle)[0] as SyncResource; + if (syncResource) { + const refHandles = this.remote ? await this.userDataSyncService.getRemoteSyncResourceHandles(syncResource) : await this.userDataSyncService.getLocalSyncResourceHandles(syncResource); return refHandles.map(({ uri, created }) => { + const handle = JSON.stringify({ resource: uri.toString(), syncResource }); return { - handle: uri.toString(), + handle, collapsibleState: TreeItemCollapsibleState.Collapsed, label: { label: label(new Date(created)) }, description: fromNow(created, true), resourceUri: uri, - resource: resourceKey, + resource: syncResource, resourceHandle: { uri, created }, - contextValue: `sync-resource-${resourceKey}` + contextValue: `sync-resource-${syncResource}` }; }); } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index 3cfea3362a7..e53ff702420 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -77,6 +77,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('stop'); } + replace(uri: URI): Promise { + return this.channel.call('replace', [uri]); + } + reset(): Promise { return this.channel.call('reset'); }