提交 578c4c0a 编写于 作者: S Sandeep Somavarapu

Merge branch 'sandy081/extensionsSync'

......@@ -4,18 +4,28 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError } from 'vs/platform/files/common/files';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter, Event } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
import { ThrottledDelayer } from 'vs/base/common/async';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { keys, values } from 'vs/base/common/map';
import { startsWith } from 'vs/base/common/strings';
import { IFileService } from 'vs/platform/files/common/files';
import { Queue } from 'vs/base/common/async';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export interface ISyncPreviewResult {
readonly added: ISyncExtension[];
readonly removed: ISyncExtension[];
readonly updated: ISyncExtension[];
readonly remote: ISyncExtension[] | null;
}
export class ExtensionsSynchroniser extends Disposable implements ISynchroniser {
......@@ -26,26 +36,30 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
private readonly throttledDelayer: ThrottledDelayer<void>;
private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
private readonly lastSyncExtensionsResource: URI;
private readonly replaceQueue: Queue<void>;
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService private readonly fileService: IFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@ILogService private readonly logService: ILogService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
this.lastSyncExtensionsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncExtensions');
this.throttledDelayer = this._register(new ThrottledDelayer<void>(500));
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.settingsResource))(() => this.throttledDelayer.trigger(() => this.onDidChangeSettings())));
}
private async onDidChangeSettings(): Promise<void> {
this.replaceQueue = this._register(new Queue());
this.lastSyncExtensionsResource = joinPath(environmentService.userRoamingDataHome, '.lastSyncExtensions');
this._register(
Event.debounce(
Event.any(
Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)),
Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error))),
() => undefined, 500)(() => this._onDidChangeLocal.fire()));
}
private setStatus(status: SyncStatus): void {
......@@ -56,6 +70,10 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
}
async sync(): Promise<boolean> {
const syncExtensions = this.configurationService.getValue<boolean>('userConfiguration.syncExtensions');
if (syncExtensions === false) {
return false;
}
if (this.status !== SyncStatus.Idle) {
return false;
......@@ -69,133 +87,222 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
this.setStatus(SyncStatus.Idle);
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
// Rejected as there is a new remote version. Syncing again,
this.logService.info('Failed to Synchronise settings as there is a new remote version available. Synchronising again...');
return this.sync();
}
if (e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) {
// Rejected as there is a new local version. Syncing again.
this.logService.info('Failed to Synchronise settings as there is a new local version available. Synchronising again...');
this.logService.info('Failed to Synchronise extensions as there is a new remote version available. Synchronising again...');
return this.sync();
}
throw e;
}
this.setStatus(SyncStatus.Idle);
return true;
}
async getRemoteExtensions(): Promise<ISyncExtension[]> {
const remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, null);
return remoteData.content ? JSON.parse(remoteData.content) : [];
}
removeExtension(identifier: IExtensionIdentifier): Promise<void> {
return this.replaceQueue.queue(async () => {
const remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, null);
const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : [];
const removedExtensions = remoteExtensions.filter(e => areSameExtensions(e.identifier, identifier));
if (removedExtensions.length) {
for (const removedExtension of removedExtensions) {
remoteExtensions.splice(remoteExtensions.indexOf(removedExtension), 1);
}
await this.writeToRemote(remoteExtensions, remoteData.ref);
}
});
}
private async doSync(): Promise<void> {
const lastSyncData = await this.getLastSyncUserData();
const remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData);
let remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData);
const lastSyncExtensions: ISyncExtension[] = lastSyncData ? JSON.parse(lastSyncData.content!) : null;
const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : null;
const localExtensions = await this.getLocalExtensions();
// First time sync to remote
if (localExtensions && !remoteExtensions) {
this.logService.trace('Settings Sync: Remote contents does not exist. So sync with settings file.');
// Return local extensions
return;
const { added, removed, updated, remote } = this.merge(localExtensions, remoteExtensions, lastSyncExtensions);
// update local
await this.updateLocalExtensions(added, removed, updated);
if (remote) {
// update remote
remoteData = await this.writeToRemote(remote, remoteData.ref);
}
if (localExtensions && remoteExtensions) {
const localToRemote = this.compare(localExtensions, remoteExtensions);
if (localToRemote.added.length === 0 && localToRemote.removed.length === 0 && localToRemote.updated.length === 0) {
// No changes found between local and remote.
return;
}
// update last sync
await this.updateLastSyncValue(remoteData);
}
const baseToLocal = lastSyncExtensions ? this.compare(lastSyncExtensions, localExtensions) : { added: localExtensions.map(({ identifier }) => identifier), removed: [], updated: [] };
const baseToRemote = lastSyncExtensions ? this.compare(lastSyncExtensions, remoteExtensions) : { added: remoteExtensions.map(({ identifier }) => identifier), removed: [], updated: [] };
// Locally added extensions
for (const localAdded of baseToLocal.added) {
// Got added in remote
if (baseToRemote.added.some(added => areSameExtensions(added, localAdded))) {
// Is different from local to remote
if (localToRemote.updated.some(updated => areSameExtensions(updated, localAdded))) {
// update it in local
}
} else {
// add to remote
}
/**
* Merge Strategy:
* - If remote does not exist, merge with local (First time sync)
* - Overwrite local with remote changes. Removed, Added, Updated.
* - Update remote with those local extension which are newly added or updated or removed and untouched in remote.
*/
private merge(localExtensions: ISyncExtension[], remoteExtensions: ISyncExtension[] | null, lastSyncExtensions: ISyncExtension[] | null): { added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], remote: ISyncExtension[] | null } {
// First time sync
if (!remoteExtensions) {
return { added: [], removed: [], updated: [], remote: localExtensions };
}
const uuids: Map<string, string> = new Map<string, string>();
const addUUID = (identifier: IExtensionIdentifier) => { if (identifier.uuid) { uuids.set(identifier.id.toLowerCase(), identifier.uuid); } };
localExtensions.forEach(({ identifier }) => addUUID(identifier));
remoteExtensions.forEach(({ identifier }) => addUUID(identifier));
if (lastSyncExtensions) {
lastSyncExtensions.forEach(({ identifier }) => addUUID(identifier));
}
const addExtensionToMap = (map: Map<string, ISyncExtension>, extension: ISyncExtension) => {
const uuid = extension.identifier.uuid || uuids.get(extension.identifier.id.toLowerCase());
const key = uuid ? `uuid:${uuid}` : `id:${extension.identifier.id.toLowerCase()}`;
map.set(key, extension);
return map;
};
const localExtensionsMap = localExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const newRemoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()) : null;
const localToRemote = this.compare(localExtensionsMap, remoteExtensionsMap);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { added: [], removed: [], updated: [], remote: null };
}
const added: ISyncExtension[] = [];
const removed: IExtensionIdentifier[] = [];
const updated: ISyncExtension[] = [];
const baseToLocal = lastSyncExtensionsMap ? this.compare(lastSyncExtensionsMap, localExtensionsMap) : { added: keys(localExtensionsMap).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemote = lastSyncExtensionsMap ? this.compare(lastSyncExtensionsMap, remoteExtensionsMap) : { added: keys(remoteExtensionsMap).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const massageSyncExtension = (extension: ISyncExtension, key: string): ISyncExtension => {
return {
identifier: {
id: extension.identifier.id,
uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined
},
enabled: extension.enabled,
version: extension.version
};
};
// Remotely removed extension.
for (const key of baseToRemote.removed.keys()) {
const e = localExtensionsMap.get(key);
if (e) {
removed.push(e.identifier);
}
}
// Remotely added extension
for (const remoteAdded of baseToRemote.added) {
// Got added in local
if (baseToLocal.added.some(added => areSameExtensions(added, remoteAdded))) {
// Is different from local to remote
if (localToRemote.updated.some(updated => areSameExtensions(updated, remoteAdded))) {
// update it in local
}
} else {
// add to local
// Remotely added extension
for (const key of baseToRemote.added.keys()) {
// Got added in local
if (baseToLocal.added.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
} else {
// Add to local
added.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
}
// Locally updated extensions
for (const localUpdated of baseToLocal.updated) {
// If updated in remote
if (baseToRemote.updated.some(updated => areSameExtensions(updated, localUpdated))) {
// Is different from local to remote
if (localToRemote.updated.some(updated => areSameExtensions(updated, localUpdated))) {
// update it in local
}
// Remotely updated extensions
for (const key of baseToRemote.updated.keys()) {
// If updated in local
if (baseToLocal.updated.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
// update it in local
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
}
}
// Remotely updated extensions
for (const remoteUpdated of baseToRemote.updated) {
// If updated in local
if (baseToLocal.updated.some(updated => areSameExtensions(updated, remoteUpdated))) {
// Is different from local to remote
if (localToRemote.updated.some(updated => areSameExtensions(updated, remoteUpdated))) {
// update it in local
}
}
// Locally added extensions
for (const key of baseToLocal.added.keys()) {
// Not there in remote
if (!baseToRemote.added.has(key)) {
newRemoteExtensionsMap.set(key, massageSyncExtension(localExtensionsMap.get(key)!, key));
}
}
// Locally removed extensions
for (const localRemoved of baseToLocal.removed) {
// If not updated in remote
if (!baseToRemote.updated.some(updated => areSameExtensions(updated, localRemoved))) {
// remove it from remote
}
// Locally updated extensions
for (const key of baseToLocal.updated.keys()) {
// If removed in remote
if (baseToRemote.removed.has(key)) {
continue;
}
// Remote removed extensions
for (const remoteRemoved of baseToRemote.removed) {
// If not updated in local
if (!baseToLocal.updated.some(updated => areSameExtensions(updated, remoteRemoved))) {
// remove it from local
}
// If not updated in remote
if (!baseToRemote.updated.has(key)) {
newRemoteExtensionsMap.set(key, massageSyncExtension(localExtensionsMap.get(key)!, key));
}
}
// Locally removed extensions
for (const key of baseToLocal.removed.keys()) {
// If not updated in remote
if (!baseToRemote.updated.has(key)) {
newRemoteExtensionsMap.delete(key);
}
}
const remoteChanges = this.compare(remoteExtensionsMap, newRemoteExtensionsMap);
const remote = remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0 ? values(newRemoteExtensionsMap) : null;
return { added, removed, updated, remote };
}
private compare(from: ISyncExtension[], to: ISyncExtension[]): { added: IExtensionIdentifier[], removed: IExtensionIdentifier[], updated: IExtensionIdentifier[] } {
const added = to.filter(toExtension => from.every(fromExtension => !areSameExtensions(fromExtension.identifier, toExtension.identifier))).map(({ identifier }) => identifier);
const removed = from.filter(fromExtension => to.every(toExtension => !areSameExtensions(toExtension.identifier, fromExtension.identifier))).map(({ identifier }) => identifier);
const updated: IExtensionIdentifier[] = [];
private compare(from: Map<string, ISyncExtension>, to: Map<string, ISyncExtension>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = keys(from);
const toKeys = keys(to);
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
for (const fromExtension of from) {
if (removed.some(identifier => areSameExtensions(identifier, fromExtension.identifier))) {
for (const key of fromKeys) {
if (removed.has(key)) {
continue;
}
const toExtension = to.filter(e => areSameExtensions(e.identifier, fromExtension.identifier))[0];
if (
fromExtension.enabled !== toExtension.enabled
const fromExtension = from.get(key)!;
const toExtension = to.get(key);
if (!toExtension
|| fromExtension.enabled !== toExtension.enabled
|| fromExtension.version !== toExtension.version
) {
updated.push(fromExtension.identifier);
updated.add(key);
}
}
return { added, removed, updated };
}
private async updateLocalExtensions(added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[]): Promise<void> {
if (removed.length) {
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
const extensionsToRemove = installedExtensions.filter(({ identifier }) => removed.some(r => areSameExtensions(identifier, r)));
await Promise.all(extensionsToRemove.map(e => this.extensionManagementService.uninstall(e)));
}
if (added.length || updated.length) {
await Promise.all([...added, ...updated].map(async e => {
const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier, e.version);
if (extension) {
await this.extensionManagementService.installFromGallery(extension);
}
}));
}
}
private async getLocalExtensions(): Promise<ISyncExtension[]> {
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
return installedExtensions.map(({ identifier }) => ({ identifier, enabled: true }));
......@@ -210,11 +317,13 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
}
}
protected async writeToRemote(content: string, ref: string | null): Promise<string> {
return this.userDataSyncStoreService.write(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, content, ref);
private async writeToRemote(extensions: ISyncExtension[], ref: string | null): Promise<IUserData> {
const content = JSON.stringify(extensions);
ref = await this.userDataSyncStoreService.write(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, content, ref);
return { content, ref };
}
protected async updateLastSyncValue(remoteUserData: IUserData): Promise<void> {
private async updateLastSyncValue(remoteUserData: IUserData): Promise<void> {
await this.fileService.writeFile(this.lastSyncExtensionsResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
}
......
......@@ -6,6 +6,7 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
export interface IUserData {
ref: string;
......@@ -73,6 +74,9 @@ export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUser
export interface IUserDataSyncService extends ISynchroniser {
_serviceBrand: any;
readonly conflictsSource: SyncSource | null;
getRemoteExtensions(): Promise<ISyncExtension[]>;
removeExtension(identifier: IExtensionIdentifier): Promise<void>;
}
export const ISettingsMergeService = createDecorator<ISettingsMergeService>('ISettingsMergeService');
......@@ -84,3 +88,5 @@ export interface ISettingsMergeService {
merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }>;
}
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
......@@ -24,6 +24,8 @@ export class UserDataSyncChannel implements IServerChannel {
case 'sync': return this.service.sync(args[0]);
case '_getInitialStatus': return Promise.resolve(this.service.status);
case 'getConflictsSource': return Promise.resolve(this.service.conflictsSource);
case 'getRemoteExtensions': return this.service.getRemoteExtensions();
case 'removeExtension': return this.service.removeExtension(args[0]);
}
throw new Error('Invalid call');
}
......
......@@ -3,13 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
import { Emitter, Event } from 'vs/base/common/event';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { timeout } from 'vs/base/common/async';
import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
......@@ -27,14 +29,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
private _conflictsSource: SyncSource | null = null;
get conflictsSource(): SyncSource | null { return this._conflictsSource; }
private readonly settingsSynchroniser: SettingsSynchroniser;
private readonly extensionsSynchroniser: ExtensionsSynchroniser;
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this.synchronisers = [
this.instantiationService.createInstance(SettingsSynchroniser)
];
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser));
this.synchronisers = [this.settingsSynchroniser, this.extensionsSynchroniser];
this.updateStatus();
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal));
......@@ -52,6 +57,14 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return true;
}
getRemoteExtensions(): Promise<ISyncExtension[]> {
return this.extensionsSynchroniser.getRemoteExtensions();
}
removeExtension(identifier: IExtensionIdentifier): Promise<void> {
return this.extensionsSynchroniser.removeExtension(identifier);
}
private updateStatus(): void {
this._conflictsSource = this.computeConflictsSource();
this.setStatus(this.computeStatus());
......
......@@ -1610,6 +1610,29 @@ export class ShowRecommendedExtensionsAction extends Action {
}
}
export class ShowSyncedExtensionsAction extends Action {
static readonly ID = 'workbench.extensions.action.listSyncedExtensions';
static LABEL = localize('showSyncedExtensions', "Show My Accoount Extensions");
constructor(
id: string,
label: string,
@IViewletService private readonly viewletService: IViewletService
) {
super(id, label, undefined, true);
}
run(): Promise<void> {
return this.viewletService.openViewlet(VIEWLET_ID, true)
.then(viewlet => viewlet as IExtensionsViewlet)
.then(viewlet => {
viewlet.search('@myaccount ');
viewlet.focus();
});
}
}
export class InstallWorkspaceRecommendedExtensionsAction extends Action {
static readonly ID = 'workbench.extensions.action.installWorkspaceRecommendedExtensions';
......
......@@ -22,12 +22,12 @@ import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, AutoUpdate
import {
ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowDisabledExtensionsAction,
ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction,
EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction
EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, ShowSyncedExtensionsAction
} from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView, BuiltInThemesExtensionsView, BuiltInBasicsExtensionsView, ServerExtensionsView, DefaultRecommendedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews';
import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInExtensionsView, BuiltInThemesExtensionsView, BuiltInBasicsExtensionsView, ServerExtensionsView, DefaultRecommendedExtensionsView, SyncedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews';
import { OpenGlobalSettingsAction } from 'vs/workbench/contrib/preferences/browser/preferencesActions';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
......@@ -58,6 +58,7 @@ import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsView
import { RemoteNameContext } from 'vs/workbench/browser/contextkeys';
import { ILabelService } from 'vs/platform/label/common/label';
import { MementoObject } from 'vs/workbench/common/memento';
import { SyncStatus, CONTEXT_SYNC_STATE } from 'vs/platform/userDataSync/common/userDataSync';
const NonEmptyWorkspaceContext = new RawContextKey<boolean>('nonEmptyWorkspace', false);
const DefaultViewsContext = new RawContextKey<boolean>('defaultExtensionViews', true);
......@@ -70,6 +71,7 @@ const HasInstalledExtensionsContext = new RawContextKey<boolean>('hasInstalledEx
const SearchBuiltInExtensionsContext = new RawContextKey<boolean>('searchBuiltInExtensions', false);
const RecommendedExtensionsContext = new RawContextKey<boolean>('recommendedExtensions', false);
const DefaultRecommendedExtensionsContext = new RawContextKey<boolean>('defaultRecommendedExtensions', false);
const SyncedExtensionsContext = new RawContextKey<boolean>('syncedExtensions', false);
const viewIdNameMappings: { [id: string]: string } = {
'extensions.listView': localize('marketPlace', "Marketplace"),
'extensions.enabledExtensionList': localize('enabledExtensions', "Enabled"),
......@@ -83,6 +85,7 @@ const viewIdNameMappings: { [id: string]: string } = {
'extensions.builtInExtensionsList': localize('builtInExtensions', "Features"),
'extensions.builtInThemesExtensionsList': localize('builtInThemesExtensions', "Themes"),
'extensions.builtInBasicsExtensionsList': localize('builtInBasicsExtensions', "Programming Languages"),
'extensions.syncedExtensionsList': localize('syncedExtensions', "My Account"),
};
export class ExtensionsViewletViewsContribution implements IWorkbenchContribution {
......@@ -108,6 +111,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
viewDescriptors.push(this.createDefaultRecommendedExtensionsListViewDescriptor());
viewDescriptors.push(this.createOtherRecommendedExtensionsListViewDescriptor());
viewDescriptors.push(this.createWorkspaceRecommendedExtensionsListViewDescriptor());
viewDescriptors.push(this.createSyncedExtensionsViewDescriptor());
if (this.extensionManagementServerService.localExtensionManagementServer) {
viewDescriptors.push(...this.createExtensionsViewDescriptorsForServer(this.extensionManagementServerService.localExtensionManagementServer));
......@@ -310,6 +314,17 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
weight: 100
};
}
private createSyncedExtensionsViewDescriptor(): IViewDescriptor {
const id = 'extensions.syncedExtensionsList';
return {
id,
name: viewIdNameMappings[id],
ctorDescriptor: { ctor: SyncedExtensionsView },
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.has('config.userConfiguration.enableSync'), ContextKeyExpr.has('syncedExtensions')),
weight: 100
};
}
}
export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensionsViewlet {
......@@ -326,6 +341,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
private hasInstalledExtensionsContextKey: IContextKey<boolean>;
private searchBuiltInExtensionsContextKey: IContextKey<boolean>;
private recommendedExtensionsContextKey: IContextKey<boolean>;
private syncedExtensionsContextKey: IContextKey<boolean>;
private defaultRecommendedExtensionsContextKey: IContextKey<boolean>;
private searchDelayer: Delayer<void>;
......@@ -367,6 +383,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService);
this.defaultRecommendedExtensionsContextKey = DefaultRecommendedExtensionsContext.bindTo(contextKeyService);
this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue<boolean>(ShowRecommendationsOnlyOnDemandKey));
this.syncedExtensionsContextKey = SyncedExtensionsContext.bindTo(contextKeyService);
this._register(this.viewletService.onDidViewletOpen(this.onViewletOpen, this));
this.searchViewletState = this.getMemento(StorageScope.WORKSPACE);
......@@ -466,6 +483,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
this.instantiationService.createInstance(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, ShowBuiltInExtensionsAction.LABEL),
this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL),
this.instantiationService.createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL),
this.instantiationService.createInstance(ShowSyncedExtensionsAction, ShowSyncedExtensionsAction.ID, ShowSyncedExtensionsAction.LABEL),
new Separator(),
this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Sort By: Install Count"), this.onSearchChange, 'installs'),
this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Sort By: Rating"), this.onSearchChange, 'rating'),
......@@ -513,6 +531,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
private doSearch(): Promise<void> {
const value = this.normalizedQuery();
const isSyncedExtensionsQuery = ExtensionsListView.isSyncedExtensionsQuery(value);
const isRecommendedExtensionsQuery = ExtensionsListView.isRecommendedExtensionsQuery(value);
this.searchInstalledExtensionsContextKey.set(ExtensionsListView.isInstalledExtensionsQuery(value));
this.searchOutdatedExtensionsContextKey.set(ExtensionsListView.isOutdatedExtensionsQuery(value));
......@@ -520,7 +539,8 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
this.searchDisabledExtensionsContextKey.set(ExtensionsListView.isDisabledExtensionsQuery(value));
this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value));
this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery);
this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery);
this.syncedExtensionsContextKey.set(isSyncedExtensionsQuery);
this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery && !isSyncedExtensionsQuery);
this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY);
this.defaultViewsContextKey.set(!value);
......
......@@ -47,6 +47,7 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async
import { IProductService } from 'vs/platform/product/common/productService';
import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync';
class ExtensionsViewState extends Disposable implements IExtensionsViewState {
......@@ -103,6 +104,7 @@ export class ExtensionsListView extends ViewletPanel {
@IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService,
@IProductService protected readonly productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
) {
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService);
this.server = options.server;
......@@ -439,6 +441,8 @@ export class ExtensionsListView extends ViewletPanel {
return this.getAllRecommendationsModel(query, options, token);
} else if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {
return this.getRecommendationsModel(query, options, token);
} else if (ExtensionsListView.isSyncedExtensionsQuery(query.value)) {
return this.getSyncedExtensionsModel(query, options, token);
}
if (/\bcurated:([^\s]+)\b/.test(query.value)) {
......@@ -685,6 +689,23 @@ export class ExtensionsListView extends ViewletPanel {
.then(result => this.getPagedModel(result));
}
private async getSyncedExtensionsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const syncedExtensions = await this.userDataSyncService.getRemoteExtensions();
if (!syncedExtensions.length) {
return this.showEmptyModel();
}
const ids: string[] = [], names: string[] = [];
for (const installed of syncedExtensions) {
if (installed.identifier.uuid) {
ids.push(installed.identifier.uuid);
} else {
names.push(installed.identifier.id);
}
}
const pager = await this.extensionsWorkbenchService.queryGallery({ ids, names, pageSize: ids.length }, token);
return this.getPagedModel(pager || []);
}
// Sorts the firstPage of the pager in the same order as given array of extension ids
private sortFirstPage(pager: IPager<IExtension>, ids: string[]) {
ids = ids.map(x => x.toLowerCase());
......@@ -824,6 +845,10 @@ export class ExtensionsListView extends ViewletPanel {
return /@recommended:keymaps/i.test(query);
}
static isSyncedExtensionsQuery(query: string): boolean {
return /@myaccount/i.test(query);
}
focus(): void {
super.focus();
if (!this.list) {
......@@ -859,10 +884,11 @@ export class ServerExtensionsView extends ExtensionsListView {
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IProductService productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService
@IContextKeyService contextKeyService: IContextKeyService,
@IUserDataSyncService userDataSyncService: IUserDataSyncService
) {
options.server = server;
super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService);
super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService, userDataSyncService);
this._register(onDidChangeTitle(title => this.updateTitle(title)));
}
......@@ -918,6 +944,14 @@ export class BuiltInBasicsExtensionsView extends ExtensionsListView {
}
}
export class SyncedExtensionsView extends ExtensionsListView {
async show(query: string): Promise<IPagedModel<IExtension>> {
query = query || '@myaccount';
return ExtensionsListView.isSyncedExtensionsQuery(query) ? super.show(query) : this.showEmptyModel();
}
}
export class DefaultRecommendedExtensionsView extends ExtensionsListView {
private readonly recommendedExtensionsQuery = '@recommended:all';
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IUserDataSyncService, SyncStatus, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, SyncStatus, SyncSource, CONTEXT_SYNC_STATE } from 'vs/platform/userDataSync/common/userDataSync';
import { localize } from 'vs/nls';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
......@@ -13,7 +13,7 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { MenuRegistry, MenuId, IMenuItem } from 'vs/platform/actions/common/actions';
import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity';
import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
......@@ -30,8 +30,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { isWeb } from 'vs/base/common/platform';
import { UserDataAutoSync } from 'vs/platform/userDataSync/common/userDataSyncService';
const CONTEXT_SYNC_STATE = new RawContextKey<string>('userDataSyncStatus', SyncStatus.Uninitialized);
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
.registerConfiguration({
id: 'userConfiguration',
......@@ -44,6 +42,12 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
description: localize('userConfiguration.enableSync', "When enabled, synchronises User Configuration: Settings, Keybindings, Extensions & Snippets."),
default: true,
scope: ConfigurationScope.APPLICATION
},
'userConfiguration.syncExtensions': {
type: 'boolean',
description: localize('userConfiguration.syncExtensions', "When enabled extensions are synchronised while synchronising user configuration."),
default: true,
scope: ConfigurationScope.APPLICATION
}
}
});
......
......@@ -3,12 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SyncStatus, SyncSource, IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync';
import { SyncStatus, SyncSource, IUserDataSyncService, ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
......@@ -41,6 +42,14 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.channel.call('sync', [_continue]);
}
getRemoteExtensions(): Promise<ISyncExtension[]> {
return this.channel.call('getRemoteExtensions');
}
removeExtension(identifier: IExtensionIdentifier): Promise<void> {
return this.channel.call('removeExtension', [identifier]);
}
private async updateStatus(status: SyncStatus): Promise<void> {
this._conflictsSource = await this.channel.call<SyncSource>('getConflictsSource');
this._status = status;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册