mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode 3c6f6af7347d38e87bc6406024e8dcf9e9bce229 (#8962)
* Merge from vscode 3c6f6af7347d38e87bc6406024e8dcf9e9bce229 * skip failing tests * update mac build image
This commit is contained in:
committed by
Karl Burtram
parent
0eaee18dc4
commit
fefe1454de
@@ -1,43 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IAccessibilityService, AccessibilitySupport, CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export abstract class AbstractAccessibilityService extends Disposable implements IAccessibilityService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private _accessibilityModeEnabledContext: IContextKey<boolean>;
|
||||
protected readonly _onDidChangeAccessibilitySupport = new Emitter<void>();
|
||||
readonly onDidChangeAccessibilitySupport: Event<void> = this._onDidChangeAccessibilitySupport.event;
|
||||
|
||||
constructor(
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
this._accessibilityModeEnabledContext = CONTEXT_ACCESSIBILITY_MODE_ENABLED.bindTo(this._contextKeyService);
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('editor.accessibilitySupport')) {
|
||||
this._updateContextKey();
|
||||
}
|
||||
}));
|
||||
this._updateContextKey();
|
||||
this.onDidChangeAccessibilitySupport(() => this._updateContextKey());
|
||||
}
|
||||
|
||||
abstract alwaysUnderlineAccessKeys(): Promise<boolean>;
|
||||
abstract getAccessibilitySupport(): AccessibilitySupport;
|
||||
abstract setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void;
|
||||
|
||||
private _updateContextKey(): void {
|
||||
const detected = this.getAccessibilitySupport() === AccessibilitySupport.Enabled;
|
||||
const config = this._configurationService.getValue('editor.accessibilitySupport');
|
||||
this._accessibilityModeEnabledContext.set(config === 'on' || (config === 'auto' && detected));
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ export const IAccessibilityService = createDecorator<IAccessibilityService>('acc
|
||||
export interface IAccessibilityService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
readonly onDidChangeAccessibilitySupport: Event<void>;
|
||||
readonly onDidChangeScreenReaderOptimized: Event<void>;
|
||||
|
||||
alwaysUnderlineAccessKeys(): Promise<boolean>;
|
||||
getAccessibilitySupport(): AccessibilitySupport;
|
||||
isScreenReaderOptimized(): boolean;
|
||||
setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,22 +3,43 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IAccessibilityService, AccessibilitySupport, CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { AbstractAccessibilityService } from 'vs/platform/accessibility/common/abstractAccessibilityService';
|
||||
|
||||
export class BrowserAccessibilityService extends AbstractAccessibilityService implements IAccessibilityService {
|
||||
|
||||
export class AccessibilityService extends Disposable implements IAccessibilityService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private _accessibilitySupport = AccessibilitySupport.Unknown;
|
||||
private _accessibilityModeEnabledContext: IContextKey<boolean>;
|
||||
protected _accessibilitySupport = AccessibilitySupport.Unknown;
|
||||
protected readonly _onDidChangeScreenReaderOptimized = new Emitter<void>();
|
||||
|
||||
constructor(
|
||||
@IContextKeyService readonly contextKeyService: IContextKeyService,
|
||||
@IConfigurationService readonly configurationService: IConfigurationService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IConfigurationService protected readonly _configurationService: IConfigurationService,
|
||||
) {
|
||||
super(contextKeyService, configurationService);
|
||||
super();
|
||||
this._accessibilityModeEnabledContext = CONTEXT_ACCESSIBILITY_MODE_ENABLED.bindTo(this._contextKeyService);
|
||||
const updateContextKey = () => this._accessibilityModeEnabledContext.set(this.isScreenReaderOptimized());
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('editor.accessibilitySupport')) {
|
||||
updateContextKey();
|
||||
this._onDidChangeScreenReaderOptimized.fire();
|
||||
}
|
||||
}));
|
||||
updateContextKey();
|
||||
this.onDidChangeScreenReaderOptimized(() => updateContextKey());
|
||||
}
|
||||
|
||||
get onDidChangeScreenReaderOptimized(): Event<void> {
|
||||
return this._onDidChangeScreenReaderOptimized.event;
|
||||
}
|
||||
|
||||
isScreenReaderOptimized(): boolean {
|
||||
const config = this._configurationService.getValue('editor.accessibilitySupport');
|
||||
return config === 'on' || (config === 'auto' && this._accessibilitySupport === AccessibilitySupport.Enabled);
|
||||
}
|
||||
|
||||
alwaysUnderlineAccessKeys(): Promise<boolean> {
|
||||
@@ -31,10 +52,6 @@ export class BrowserAccessibilityService extends AbstractAccessibilityService im
|
||||
}
|
||||
|
||||
this._accessibilitySupport = accessibilitySupport;
|
||||
this._onDidChangeAccessibilitySupport.fire();
|
||||
}
|
||||
|
||||
getAccessibilitySupport(): AccessibilitySupport {
|
||||
return this._accessibilitySupport;
|
||||
this._onDidChangeScreenReaderOptimized.fire();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ export const enum MenuId {
|
||||
DataExplorerContext, // {{SQL CARBON EDIT}}
|
||||
DataExplorerAction, // {{SQL CARBON EDIT}}
|
||||
ExplorerWidgetContext, // {{SQL CARBON EDIT}}
|
||||
ViewTitleContext,
|
||||
CommentThreadTitle,
|
||||
CommentThreadActions,
|
||||
CommentTitle,
|
||||
|
||||
@@ -100,9 +100,9 @@ export const enum ConfigurationScope {
|
||||
*/
|
||||
RESOURCE,
|
||||
/**
|
||||
* Resource specific configuration that can also be configured in language specific settings
|
||||
* Resource specific configuration that can be configured in language specific settings
|
||||
*/
|
||||
RESOURCE_LANGUAGE,
|
||||
LANGUAGE_OVERRIDABLE,
|
||||
/**
|
||||
* Machine specific configuration that can also be configured in workspace or folder settings.
|
||||
*/
|
||||
@@ -221,7 +221,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
delete windowSettings.properties[key];
|
||||
break;
|
||||
case ConfigurationScope.RESOURCE:
|
||||
case ConfigurationScope.RESOURCE_LANGUAGE:
|
||||
case ConfigurationScope.LANGUAGE_OVERRIDABLE:
|
||||
delete resourceSettings.properties[key];
|
||||
break;
|
||||
}
|
||||
@@ -373,7 +373,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
case ConfigurationScope.RESOURCE:
|
||||
resourceSettings.properties[key] = properties[key];
|
||||
break;
|
||||
case ConfigurationScope.RESOURCE_LANGUAGE:
|
||||
case ConfigurationScope.LANGUAGE_OVERRIDABLE:
|
||||
resourceSettings.properties[key] = properties[key];
|
||||
this.resourceLanguageSettingsSchema.properties![key] = properties[key];
|
||||
break;
|
||||
|
||||
@@ -173,7 +173,7 @@ export class DialogMainService implements IDialogMainService {
|
||||
|
||||
showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise<OpenDialogReturnValue> {
|
||||
|
||||
function normalizePaths(paths: string[]): string[] {
|
||||
function normalizePaths(paths: string[] | undefined): string[] | undefined {
|
||||
if (paths && paths.length > 0 && isMacintosh) {
|
||||
paths = paths.map(path => normalizeNFC(path)); // normalize paths returned from the OS
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ScanCodeBinding } from 'vs/base/common/scanCode';
|
||||
import { KeybindingParser } from 'vs/base/common/keybindingParser';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { IDriver, IElement, IWindowDriver } from 'vs/platform/driver/common/driver';
|
||||
import { NativeImage } from 'electron';
|
||||
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { IElectronMainService } from 'vs/platform/electron/electron-main/electronMainService';
|
||||
|
||||
@@ -66,7 +67,7 @@ export class Driver implements IDriver, IWindowDriverRegistry {
|
||||
throw new Error('Invalid window');
|
||||
}
|
||||
const webContents = window.win.webContents;
|
||||
const image = await webContents.capturePage();
|
||||
const image = await new Promise<NativeImage>(c => webContents.capturePage(c));
|
||||
return image.toPNG().toString('base64');
|
||||
}
|
||||
|
||||
|
||||
@@ -367,13 +367,15 @@ export class ElectronMainService implements IElectronMainService {
|
||||
//#region Connectivity
|
||||
|
||||
async resolveProxy(windowId: number | undefined, url: string): Promise<string | undefined> {
|
||||
const window = this.windowById(windowId);
|
||||
const session = window?.win?.webContents?.session;
|
||||
if (session) {
|
||||
return session.resolveProxy(url);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const window = this.windowById(windowId);
|
||||
const session = window?.win?.webContents?.session;
|
||||
if (session) {
|
||||
session.resolveProxy(url, proxy => resolve(proxy));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -94,6 +94,8 @@ export interface ParsedArgs {
|
||||
'nolazy'?: boolean;
|
||||
'force-device-scale-factor'?: string;
|
||||
'force-renderer-accessibility'?: boolean;
|
||||
'ignore-certificate-error'?: boolean;
|
||||
'allow-insecure-localhost'?: boolean;
|
||||
}
|
||||
|
||||
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
|
||||
@@ -133,6 +135,7 @@ export interface IEnvironmentService extends IUserHomeProvider {
|
||||
|
||||
// sync resources
|
||||
userDataSyncLogResource: URI;
|
||||
userDataSyncHome: URI;
|
||||
settingsSyncPreviewResource: URI;
|
||||
keybindingsSyncPreviewResource: URI;
|
||||
|
||||
|
||||
@@ -128,6 +128,8 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
|
||||
'nolazy': { type: 'boolean' }, // node inspect
|
||||
'force-device-scale-factor': { type: 'string' },
|
||||
'force-renderer-accessibility': { type: 'boolean' },
|
||||
'ignore-certificate-error': { type: 'boolean' },
|
||||
'allow-insecure-localhost': { type: 'boolean' },
|
||||
'_urls': { type: 'string[]' },
|
||||
|
||||
_: { type: 'string[]' } // main arguments
|
||||
|
||||
@@ -13,7 +13,7 @@ import { parseArgs, ErrorReporter, OPTIONS } from 'vs/platform/environment/node/
|
||||
function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): ParsedArgs {
|
||||
const errorReporter: ErrorReporter = {
|
||||
onUnknownOption: (id) => {
|
||||
console.warn(localize('unknownOption', "Option '{0}' is unknown. Ignoring.", id));
|
||||
console.warn(localize('unknownOption', "Warning: '{0}' is not in the list of known options, but still passed to Electron/Chromium.", id));
|
||||
},
|
||||
onMultipleValues: (id, val) => {
|
||||
console.warn(localize('multipleValues', "Option '{0}' is defined more than once. Using value '{1}.'", id, val));
|
||||
|
||||
@@ -112,10 +112,13 @@ export class EnvironmentService implements IEnvironmentService {
|
||||
get settingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'settings.json'); }
|
||||
|
||||
@memoize
|
||||
get settingsSyncPreviewResource(): URI { return resources.joinPath(this.userRoamingDataHome, '.settings.json'); }
|
||||
get userDataSyncHome(): URI { return resources.joinPath(this.userRoamingDataHome, '.sync'); }
|
||||
|
||||
@memoize
|
||||
get keybindingsSyncPreviewResource(): URI { return resources.joinPath(this.userRoamingDataHome, '.keybindings.json'); }
|
||||
get settingsSyncPreviewResource(): URI { return resources.joinPath(this.userDataSyncHome, 'settings.json'); }
|
||||
|
||||
@memoize
|
||||
get keybindingsSyncPreviewResource(): URI { return resources.joinPath(this.userDataSyncHome, 'keybindings.json'); }
|
||||
|
||||
@memoize
|
||||
get userDataSyncLogResource(): URI { return URI.file(path.join(this.logsPath, 'userDataSync.log')); }
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtensionIdentifier, IGlobalExtensionEnablementService, DISABLED_EXTENSIONS_STORAGE_PATH } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
|
||||
export class GlobalExtensionEnablementService extends Disposable implements IGlobalExtensionEnablementService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private _onDidChangeEnablement = new Emitter<readonly IExtensionIdentifier[]>();
|
||||
readonly onDidChangeEnablement: Event<readonly IExtensionIdentifier[]> = this._onDidChangeEnablement.event;
|
||||
private readonly storageManger: StorageManager;
|
||||
|
||||
constructor(
|
||||
@IStorageService storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this.storageManger = this._register(new StorageManager(storageService));
|
||||
this._register(this.storageManger.onDidChange(extensions => this._onDidChangeEnablement.fire(extensions)));
|
||||
}
|
||||
|
||||
async enableExtension(extension: IExtensionIdentifier): Promise<boolean> {
|
||||
if (this._removeFromDisabledExtensions(extension)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async disableExtension(extension: IExtensionIdentifier): Promise<boolean> {
|
||||
if (this._addToDisabledExtensions(extension)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getDisabledExtensions(): IExtensionIdentifier[] {
|
||||
return this._getExtensions(DISABLED_EXTENSIONS_STORAGE_PATH);
|
||||
}
|
||||
|
||||
async getDisabledExtensionsAsync(): Promise<IExtensionIdentifier[]> {
|
||||
return this.getDisabledExtensions();
|
||||
}
|
||||
|
||||
private _addToDisabledExtensions(identifier: IExtensionIdentifier): boolean {
|
||||
let disabledExtensions = this.getDisabledExtensions();
|
||||
if (disabledExtensions.every(e => !areSameExtensions(e, identifier))) {
|
||||
disabledExtensions.push(identifier);
|
||||
this._setDisabledExtensions(disabledExtensions);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _removeFromDisabledExtensions(identifier: IExtensionIdentifier): boolean {
|
||||
let disabledExtensions = this.getDisabledExtensions();
|
||||
for (let index = 0; index < disabledExtensions.length; index++) {
|
||||
const disabledExtension = disabledExtensions[index];
|
||||
if (areSameExtensions(disabledExtension, identifier)) {
|
||||
disabledExtensions.splice(index, 1);
|
||||
this._setDisabledExtensions(disabledExtensions);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _setDisabledExtensions(disabledExtensions: IExtensionIdentifier[]): void {
|
||||
this._setExtensions(DISABLED_EXTENSIONS_STORAGE_PATH, disabledExtensions);
|
||||
}
|
||||
|
||||
private _getExtensions(storageId: string): IExtensionIdentifier[] {
|
||||
return this.storageManger.get(storageId, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
private _setExtensions(storageId: string, extensions: IExtensionIdentifier[]): void {
|
||||
this.storageManger.set(storageId, extensions, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class StorageManager extends Disposable {
|
||||
|
||||
private storage: { [key: string]: string } = Object.create(null);
|
||||
|
||||
private _onDidChange: Emitter<IExtensionIdentifier[]> = this._register(new Emitter<IExtensionIdentifier[]>());
|
||||
readonly onDidChange: Event<IExtensionIdentifier[]> = this._onDidChange.event;
|
||||
|
||||
constructor(private storageService: IStorageService) {
|
||||
super();
|
||||
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
|
||||
}
|
||||
|
||||
get(key: string, scope: StorageScope): IExtensionIdentifier[] {
|
||||
let value: string;
|
||||
if (scope === StorageScope.GLOBAL) {
|
||||
if (isUndefinedOrNull(this.storage[key])) {
|
||||
this.storage[key] = this._get(key, scope);
|
||||
}
|
||||
value = this.storage[key];
|
||||
} else {
|
||||
value = this._get(key, scope);
|
||||
}
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
||||
set(key: string, value: IExtensionIdentifier[], scope: StorageScope): void {
|
||||
let newValue: string = JSON.stringify(value.map(({ id, uuid }) => (<IExtensionIdentifier>{ id, uuid })));
|
||||
const oldValue = this._get(key, scope);
|
||||
if (oldValue !== newValue) {
|
||||
if (scope === StorageScope.GLOBAL) {
|
||||
if (value.length) {
|
||||
this.storage[key] = newValue;
|
||||
} else {
|
||||
delete this.storage[key];
|
||||
}
|
||||
}
|
||||
this._set(key, value.length ? newValue : undefined, scope);
|
||||
}
|
||||
}
|
||||
|
||||
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
|
||||
if (workspaceStorageChangeEvent.scope === StorageScope.GLOBAL) {
|
||||
if (!isUndefinedOrNull(this.storage[workspaceStorageChangeEvent.key])) {
|
||||
const newValue = this._get(workspaceStorageChangeEvent.key, workspaceStorageChangeEvent.scope);
|
||||
if (newValue !== this.storage[workspaceStorageChangeEvent.key]) {
|
||||
const oldValues = this.get(workspaceStorageChangeEvent.key, workspaceStorageChangeEvent.scope);
|
||||
delete this.storage[workspaceStorageChangeEvent.key];
|
||||
const newValues = this.get(workspaceStorageChangeEvent.key, workspaceStorageChangeEvent.scope);
|
||||
const added = oldValues.filter(oldValue => !newValues.some(newValue => areSameExtensions(oldValue, newValue)));
|
||||
const removed = newValues.filter(newValue => !oldValues.some(oldValue => areSameExtensions(oldValue, newValue)));
|
||||
if (added.length || removed.length) {
|
||||
this._onDidChange.fire([...added, ...removed]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _get(key: string, scope: StorageScope): string {
|
||||
return this.storageService.get(key, scope, '[]');
|
||||
}
|
||||
|
||||
private _set(key: string, value: string | undefined, scope: StorageScope): void {
|
||||
if (value) {
|
||||
this.storageService.store(key, value, scope);
|
||||
} else {
|
||||
this.storageService.remove(key, scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +211,22 @@ export interface IExtensionManagementService {
|
||||
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension>;
|
||||
}
|
||||
|
||||
export const DISABLED_EXTENSIONS_STORAGE_PATH = 'extensionsIdentifiers/disabled';
|
||||
export const ENABLED_EXTENSIONS_STORAGE_PATH = 'extensionsIdentifiers/enabled';
|
||||
export const IGlobalExtensionEnablementService = createDecorator<IGlobalExtensionEnablementService>('IGlobalExtensionEnablementService');
|
||||
|
||||
export interface IGlobalExtensionEnablementService {
|
||||
_serviceBrand: undefined;
|
||||
readonly onDidChangeEnablement: Event<readonly IExtensionIdentifier[]>;
|
||||
|
||||
getDisabledExtensions(): IExtensionIdentifier[];
|
||||
enableExtension(extension: IExtensionIdentifier): Promise<boolean>;
|
||||
disableExtension(extension: IExtensionIdentifier): Promise<boolean>;
|
||||
|
||||
// Async method until storage service is available in shared process
|
||||
getDisabledExtensionsAsync(): Promise<IExtensionIdentifier[]>;
|
||||
}
|
||||
|
||||
export const ExtensionsLabel = localize('extensions', "Extensions");
|
||||
export const ExtensionsChannelId = 'extensions';
|
||||
export const PreferencesLabel = localize('preferences', "Preferences");
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc';
|
||||
@@ -130,3 +130,53 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer
|
||||
return Promise.resolve(this.channel.call('getExtensionsReport'));
|
||||
}
|
||||
}
|
||||
|
||||
export class GlobalExtensionEnablementServiceChannel implements IServerChannel {
|
||||
|
||||
constructor(private readonly service: IGlobalExtensionEnablementService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChangeEnablement': return this.service.onDidChangeEnablement;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'getDisabledExtensionsAsync': return Promise.resolve(this.service.getDisabledExtensions());
|
||||
case 'enableExtension': return this.service.enableExtension(args[0]);
|
||||
case 'disableExtension': return this.service.disableExtension(args[0]);
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
}
|
||||
|
||||
export class GlobalExtensionEnablementServiceClient implements IGlobalExtensionEnablementService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
get onDidChangeEnablement(): Event<IExtensionIdentifier[]> { return this.channel.listen('onDidChangeEnablement'); }
|
||||
|
||||
constructor(private readonly channel: IChannel) {
|
||||
}
|
||||
|
||||
getDisabledExtensionsAsync(): Promise<IExtensionIdentifier[]> {
|
||||
return this.channel.call('getDisabledExtensionsAsync');
|
||||
}
|
||||
|
||||
enableExtension(extension: IExtensionIdentifier): Promise<boolean> {
|
||||
return this.channel.call('enableExtension', [extension]);
|
||||
}
|
||||
|
||||
disableExtension(extension: IExtensionIdentifier): Promise<boolean> {
|
||||
return this.channel.call('disableExtension', [extension]);
|
||||
}
|
||||
|
||||
getDisabledExtensions(): IExtensionIdentifier[] {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -120,4 +120,25 @@ export function getMaliciousExtensionsSet(report: IReportedExtension[]): Set<str
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IBuiltInExtension {
|
||||
name: string;
|
||||
version: string;
|
||||
repo: string;
|
||||
forQualities?: ReadonlyArray<string>;
|
||||
metadata: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the built-in extension JSON data and filters it down to the
|
||||
* extensions built into this product quality.
|
||||
*/
|
||||
export function parseBuiltInExtensions(rawJson: string, productQuality: string | undefined) {
|
||||
const parsed: IBuiltInExtension[] = JSON.parse(rawJson);
|
||||
if (!productQuality) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return parsed.filter(ext => ext.forQualities?.indexOf?.(productQuality) !== -1);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
INSTALL_ERROR_MALICIOUS,
|
||||
INSTALL_ERROR_INCOMPATIBLE
|
||||
} from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion, parseBuiltInExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { localizeManifest } from '../common/extensionNls';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { Limiter, createCancelablePromise, CancelablePromise, Queue } from 'vs/base/common/async';
|
||||
@@ -45,6 +45,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
|
||||
import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
|
||||
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
|
||||
const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser';
|
||||
@@ -132,6 +133,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@optional(IDownloadService) private downloadService: IDownloadService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
) {
|
||||
super();
|
||||
this.systemExtensionsPath = environmentService.builtinExtensionsPath;
|
||||
@@ -971,10 +973,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
|
||||
private getDevSystemExtensionsList(): Promise<string[]> {
|
||||
return pfs.readFile(this.devSystemExtensionsFilePath, 'utf8')
|
||||
.then<string[]>(raw => {
|
||||
const parsed: { name: string }[] = JSON.parse(raw);
|
||||
return parsed.map(({ name }) => name);
|
||||
});
|
||||
.then(data => parseBuiltInExtensions(data, this.productService.quality).map(ext => ext.name));
|
||||
}
|
||||
|
||||
private toNonCancellablePromise<T>(promise: Promise<T>): Promise<T> {
|
||||
|
||||
@@ -30,7 +30,7 @@ import { ITreeEvent, ITreeRenderer, IAsyncDataSource, IDataSource, ITreeMouseEve
|
||||
import { AsyncDataTree, IAsyncDataTreeOptions, CompressibleAsyncDataTree, ITreeCompressionDelegate, ICompressibleAsyncDataTreeOptions } from 'vs/base/browser/ui/tree/asyncDataTree';
|
||||
import { DataTree, IDataTreeOptions } from 'vs/base/browser/ui/tree/dataTree';
|
||||
import { IKeyboardNavigationEventFilter, IAbstractTreeOptions, RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree';
|
||||
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
||||
|
||||
export type ListWidget = List<any> | PagedList<any> | ITree | ObjectTree<any, any> | DataTree<any, any, any> | AsyncDataTree<any, any, any>;
|
||||
|
||||
@@ -58,18 +58,23 @@ export class ListService implements IListService {
|
||||
private disposables = new DisposableStore();
|
||||
private lists: IRegisteredList[] = [];
|
||||
private _lastFocusedWidget: ListWidget | undefined = undefined;
|
||||
private _hasCreatedStyleController: boolean = false;
|
||||
|
||||
get lastFocusedList(): ListWidget | undefined {
|
||||
return this._lastFocusedWidget;
|
||||
}
|
||||
|
||||
constructor(@IThemeService themeService: IThemeService) {
|
||||
// create a shared default tree style sheet for performance reasons
|
||||
const styleController = new DefaultStyleController(createStyleSheet(), '');
|
||||
this.disposables.add(attachListStyler(styleController, themeService));
|
||||
constructor(@IThemeService private readonly _themeService: IThemeService) {
|
||||
}
|
||||
|
||||
register(widget: ListWidget, extraContextKeys?: (IContextKey<boolean>)[]): IDisposable {
|
||||
if (!this._hasCreatedStyleController) {
|
||||
this._hasCreatedStyleController = true;
|
||||
// create a shared default tree style sheet for performance reasons
|
||||
const styleController = new DefaultStyleController(createStyleSheet(), '');
|
||||
this.disposables.add(attachListStyler(styleController, this._themeService));
|
||||
}
|
||||
|
||||
if (this.lists.some(l => l.widget === widget)) {
|
||||
throw new Error('Cannot register the same widget multiple times');
|
||||
}
|
||||
@@ -580,82 +585,6 @@ export interface IResourceResultsNavigationOptions {
|
||||
openOnFocus: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export class TreeResourceNavigator extends Disposable {
|
||||
|
||||
private readonly _openResource = new Emitter<IOpenResourceOptions>();
|
||||
readonly openResource: Event<IOpenResourceOptions> = this._openResource.event;
|
||||
|
||||
constructor(private tree: WorkbenchTree, private options?: IResourceResultsNavigationOptions) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
if (this.options && this.options.openOnFocus) {
|
||||
this._register(this.tree.onDidChangeFocus(e => this.onFocus(e)));
|
||||
}
|
||||
|
||||
this._register(this.tree.onDidChangeSelection(e => this.onSelection(e)));
|
||||
}
|
||||
|
||||
private onFocus({ payload }: any): void {
|
||||
const element = this.tree.getFocus();
|
||||
this.tree.setSelection([element], { fromFocus: true });
|
||||
|
||||
const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent;
|
||||
const isMouseEvent = payload && payload.origin === 'mouse';
|
||||
const isDoubleClick = isMouseEvent && originalEvent && originalEvent.detail === 2;
|
||||
|
||||
const preventOpen = payload && payload.preventOpenOnFocus;
|
||||
if (!preventOpen && (!isMouseEvent || this.tree.openOnSingleClick || isDoubleClick)) {
|
||||
this._openResource.fire({
|
||||
editorOptions: {
|
||||
preserveFocus: true,
|
||||
pinned: false,
|
||||
revealIfVisible: true
|
||||
},
|
||||
sideBySide: false,
|
||||
element,
|
||||
payload
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onSelection({ payload }: any): void {
|
||||
if (payload && payload.fromFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent;
|
||||
const isMouseEvent = payload && payload.origin === 'mouse';
|
||||
const isDoubleClick = isMouseEvent && originalEvent && originalEvent.detail === 2;
|
||||
|
||||
if (!isMouseEvent || this.tree.openOnSingleClick || isDoubleClick) {
|
||||
if (isDoubleClick && originalEvent) {
|
||||
originalEvent.preventDefault(); // focus moves to editor, we need to prevent default
|
||||
}
|
||||
|
||||
const isFromKeyboard = payload && payload.origin === 'keyboard';
|
||||
const sideBySide = (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey || originalEvent.altKey));
|
||||
const preserveFocus = !((isFromKeyboard && (!payload || !payload.preserveFocus)) || isDoubleClick || (payload && payload.focusEditor));
|
||||
this._openResource.fire({
|
||||
editorOptions: {
|
||||
preserveFocus,
|
||||
pinned: isDoubleClick,
|
||||
revealIfVisible: true
|
||||
},
|
||||
sideBySide,
|
||||
element: this.tree.getSelection()[0],
|
||||
payload
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IOpenEvent<T> {
|
||||
editorOptions: IEditorOptions;
|
||||
sideBySide: boolean;
|
||||
@@ -663,9 +592,9 @@ export interface IOpenEvent<T> {
|
||||
browserEvent?: UIEvent;
|
||||
}
|
||||
|
||||
export interface IResourceResultsNavigationOptions2 {
|
||||
openOnFocus?: boolean;
|
||||
openOnSelection?: boolean;
|
||||
export interface ITreeResourceNavigatorOptions {
|
||||
readonly openOnFocus?: boolean;
|
||||
readonly openOnSelection?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectionKeyboardEvent extends KeyboardEvent {
|
||||
@@ -679,16 +608,16 @@ export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: b
|
||||
return e;
|
||||
}
|
||||
|
||||
export class TreeResourceNavigator2<T, TFilterData> extends Disposable {
|
||||
export class TreeResourceNavigator<T, TFilterData> extends Disposable {
|
||||
|
||||
private options: IResourceResultsNavigationOptions2;
|
||||
private options: ITreeResourceNavigatorOptions;
|
||||
|
||||
private readonly _onDidOpenResource = new Emitter<IOpenEvent<T | null>>();
|
||||
readonly onDidOpenResource: Event<IOpenEvent<T | null>> = this._onDidOpenResource.event;
|
||||
|
||||
constructor(
|
||||
private tree: WorkbenchObjectTree<T, TFilterData> | WorkbenchCompressibleObjectTree<T, TFilterData> | WorkbenchDataTree<any, T, TFilterData> | WorkbenchAsyncDataTree<any, T, TFilterData> | WorkbenchCompressibleAsyncDataTree<any, T, TFilterData>,
|
||||
options?: IResourceResultsNavigationOptions2
|
||||
options?: ITreeResourceNavigatorOptions
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -971,7 +900,7 @@ function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTre
|
||||
return automaticKeyboardNavigation;
|
||||
};
|
||||
|
||||
const accessibilityOn = accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled;
|
||||
const accessibilityOn = accessibilityService.isScreenReaderOptimized();
|
||||
const keyboardNavigation = accessibilityOn ? 'simple' : configurationService.getValue<string>(keyboardNavigationSettingKey);
|
||||
const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : getHorizontalScrollingSetting(configurationService);
|
||||
const openOnSingleClick = useSingleClickToOpen(configurationService);
|
||||
@@ -1033,7 +962,7 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
|
||||
const interestingContextKeys = new Set();
|
||||
interestingContextKeys.add(WorkbenchListAutomaticKeyboardNavigationKey);
|
||||
const updateKeyboardNavigation = () => {
|
||||
const accessibilityOn = accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled;
|
||||
const accessibilityOn = accessibilityService.isScreenReaderOptimized();
|
||||
const keyboardNavigation = accessibilityOn ? 'simple' : configurationService.getValue<string>(keyboardNavigationSettingKey);
|
||||
tree.updateOptions({
|
||||
simpleKeyboardNavigation: keyboardNavigation === 'simple',
|
||||
@@ -1086,7 +1015,7 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
|
||||
tree.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() });
|
||||
}
|
||||
}),
|
||||
accessibilityService.onDidChangeAccessibilitySupport(() => updateKeyboardNavigation())
|
||||
accessibilityService.onDidChangeScreenReaderOptimized(() => updateKeyboardNavigation())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export namespace MarkerSeverity {
|
||||
* A structure defining a problem/warning/etc.
|
||||
*/
|
||||
export interface IMarkerData {
|
||||
code?: string;
|
||||
code?: string | { value: string; link: URI };
|
||||
severity: MarkerSeverity;
|
||||
message: string;
|
||||
source?: string;
|
||||
@@ -108,7 +108,7 @@ export interface IMarker {
|
||||
owner: string;
|
||||
resource: URI;
|
||||
severity: MarkerSeverity;
|
||||
code?: string;
|
||||
code?: string | { value: string; link: URI };
|
||||
message: string;
|
||||
source?: string;
|
||||
startLineNumber: number;
|
||||
@@ -140,7 +140,11 @@ export namespace IMarkerData {
|
||||
result.push(emptyString);
|
||||
}
|
||||
if (markerData.code) {
|
||||
result.push(markerData.code.replace('¦', '\¦'));
|
||||
if (typeof markerData.code === 'string') {
|
||||
result.push(markerData.code.replace('¦', '\¦'));
|
||||
} else {
|
||||
result.push(markerData.code.value.replace('¦', '\¦'));
|
||||
}
|
||||
} else {
|
||||
result.push(emptyString);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ export interface TunnelDescription {
|
||||
}
|
||||
export interface TunnelInformation {
|
||||
environmentTunnels?: TunnelDescription[];
|
||||
hideCandidatePorts?: boolean;
|
||||
}
|
||||
|
||||
export interface ResolverResult {
|
||||
|
||||
@@ -58,7 +58,7 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
this._onDidChangeStorage.fire({ key, scope });
|
||||
}
|
||||
|
||||
initialize(payload: IWorkspaceInitializationPayload): Promise<void> {
|
||||
initialize(payload?: IWorkspaceInitializationPayload): Promise<void> {
|
||||
if (!this.initializePromise) {
|
||||
this.initializePromise = this.doInitialize(payload);
|
||||
}
|
||||
@@ -66,12 +66,12 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
return this.initializePromise;
|
||||
}
|
||||
|
||||
private async doInitialize(payload: IWorkspaceInitializationPayload): Promise<void> {
|
||||
private async doInitialize(payload?: IWorkspaceInitializationPayload): Promise<void> {
|
||||
|
||||
// Init all storage locations
|
||||
await Promise.all([
|
||||
this.initializeGlobalStorage(),
|
||||
this.initializeWorkspaceStorage(payload)
|
||||
payload ? this.initializeWorkspaceStorage(payload) : Promise.resolve()
|
||||
]);
|
||||
|
||||
// On some OS we do not get enough time to persist state on shutdown (e.g. when
|
||||
@@ -196,6 +196,10 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
this.getStorage(scope).delete(key);
|
||||
}
|
||||
|
||||
private getStorage(scope: StorageScope): IStorage {
|
||||
return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage);
|
||||
}
|
||||
|
||||
private doFlushWhenIdle(): void {
|
||||
|
||||
// Dispose any previous idle runner
|
||||
@@ -227,15 +231,11 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
|
||||
// Do it
|
||||
await Promise.all([
|
||||
this.getStorage(StorageScope.GLOBAL).close(),
|
||||
this.getStorage(StorageScope.WORKSPACE).close()
|
||||
this.globalStorage.close(),
|
||||
this.workspaceStorage ? this.workspaceStorage.close() : Promise.resolve()
|
||||
]);
|
||||
}
|
||||
|
||||
private getStorage(scope: StorageScope): IStorage {
|
||||
return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage);
|
||||
}
|
||||
|
||||
async logStorage(): Promise<void> {
|
||||
const [workspaceStorage, workspaceStoragePath] = assertAllDefined(this.workspaceStorage, this.workspaceStoragePath);
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface TokenStylingDefaultRule {
|
||||
export interface TokenStylingRule {
|
||||
match(classification: TokenClassification): number;
|
||||
value: TokenStyle;
|
||||
selector: TokenClassification;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,7 +295,8 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
public getTokenStylingRule(selector: TokenClassification, value: TokenStyle): TokenStylingRule {
|
||||
return {
|
||||
match: this.newMatcher(selector),
|
||||
value
|
||||
value,
|
||||
selector
|
||||
};
|
||||
}
|
||||
|
||||
@@ -376,11 +378,11 @@ function registerDefaultClassifications(): void {
|
||||
|
||||
registerTokenType('namespace', nls.localize('namespace', "Style for namespaces."), [['entity.name.namespace']]);
|
||||
|
||||
registerTokenType('type', nls.localize('type', "Style for types."), [['entity.name.type'], ['entity.name.class'], ['support.type'], ['support.class']]);
|
||||
registerTokenType('type', nls.localize('type', "Style for types."), [['entity.name.type'], ['support.type'], ['support.class']]);
|
||||
registerTokenType('struct', nls.localize('struct', "Style for structs."), [['storage.type.struct']], 'type');
|
||||
registerTokenType('class', nls.localize('class', "Style for classes."), [['entity.name.class']], 'type');
|
||||
registerTokenType('interface', nls.localize('interface', "Style for interfaces."), undefined, 'type');
|
||||
registerTokenType('enum', nls.localize('enum', "Style for enums."), undefined, 'type');
|
||||
registerTokenType('class', nls.localize('class', "Style for classes."), [['entity.name.type.class']], 'type');
|
||||
registerTokenType('interface', nls.localize('interface', "Style for interfaces."), [['entity.name.type.interface']], 'type');
|
||||
registerTokenType('enum', nls.localize('enum', "Style for enums."), [['entity.name.type.enum']], 'type');
|
||||
registerTokenType('typeParameter', nls.localize('typeParameter', "Style for type parameters."), undefined, 'type');
|
||||
|
||||
registerTokenType('function', nls.localize('function', "Style for functions"), [['entity.name.function'], ['support.function']]);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isWindows, isMacintosh } from 'vs/base/common/platform';
|
||||
import { ipcMain as ipc, nativeTheme } from 'electron';
|
||||
import { systemPreferences, ipcMain as ipc } from 'electron';
|
||||
import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
@@ -42,14 +42,14 @@ export class ThemeMainService implements IThemeMainService {
|
||||
}
|
||||
|
||||
getBackgroundColor(): string {
|
||||
if (isWindows && nativeTheme.shouldUseInvertedColorScheme) {
|
||||
if (isWindows && systemPreferences.isInvertedColorScheme()) {
|
||||
return DEFAULT_BG_HC_BLACK;
|
||||
}
|
||||
|
||||
let background = this.stateService.getItem<string | null>(THEME_BG_STORAGE_KEY, null);
|
||||
if (!background) {
|
||||
let baseTheme: string;
|
||||
if (isWindows && nativeTheme.shouldUseInvertedColorScheme) {
|
||||
if (isWindows && systemPreferences.isInvertedColorScheme()) {
|
||||
baseTheme = 'hc-black';
|
||||
} else {
|
||||
baseTheme = this.stateService.getItem<string>(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0];
|
||||
|
||||
@@ -4,30 +4,87 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IFileService, IFileContent } from 'vs/platform/files/common/files';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncSource, SyncStatus, IUserData, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { joinPath, dirname } from 'vs/base/common/resources';
|
||||
import { toLocalISOString } from 'vs/base/common/date';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
|
||||
export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
||||
private readonly syncFolder: URI;
|
||||
protected readonly syncFolder: URI;
|
||||
private cleanUpDelayer: ThrottledDelayer<void>;
|
||||
|
||||
private _status: SyncStatus = SyncStatus.Idle;
|
||||
get status(): SyncStatus { return this._status; }
|
||||
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
|
||||
|
||||
protected readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
|
||||
|
||||
protected readonly lastSyncResource: URI;
|
||||
|
||||
constructor(
|
||||
syncSource: SyncSource,
|
||||
readonly source: SyncSource,
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
) {
|
||||
super();
|
||||
this.syncFolder = joinPath(environmentService.userRoamingDataHome, '.sync', syncSource);
|
||||
this.syncFolder = joinPath(environmentService.userDataSyncHome, source);
|
||||
this.lastSyncResource = joinPath(this.syncFolder, `.lastSync${source}.json`);
|
||||
this.cleanUpDelayer = new ThrottledDelayer(50);
|
||||
}
|
||||
|
||||
protected setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
}
|
||||
}
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
return !!lastSyncData;
|
||||
}
|
||||
|
||||
async hasRemoteData(): Promise<boolean> {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
return remoteUserData.content !== null;
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.lastSyncResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
protected async getLastSyncUserData<T extends IUserData>(): Promise<T | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncResource);
|
||||
return JSON.parse(content.value.toString());
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateLastSyncUserData<T extends IUserData>(lastSyncUserData: T): Promise<void> {
|
||||
await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
|
||||
}
|
||||
|
||||
protected getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
|
||||
return this.userDataSyncStoreService.read(this.getRemoteDataResourceKey(), lastSyncData || null, this.source);
|
||||
}
|
||||
|
||||
protected async updateRemoteUserData(content: string, ref: string | null): Promise<string> {
|
||||
return this.userDataSyncStoreService.write(this.getRemoteDataResourceKey(), content, ref, this.source);
|
||||
}
|
||||
|
||||
protected async backupLocal(content: VSBuffer): Promise<void> {
|
||||
const resource = joinPath(this.syncFolder, toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, ''));
|
||||
await this.fileService.writeFile(resource, content);
|
||||
@@ -43,4 +100,40 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract getRemoteDataResourceKey(): string;
|
||||
}
|
||||
|
||||
export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
|
||||
constructor(
|
||||
protected readonly file: URI,
|
||||
readonly source: SyncSource,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
) {
|
||||
super(source, fileService, environmentService, userDataSyncStoreService);
|
||||
this._register(this.fileService.watch(dirname(file)));
|
||||
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(file))(() => this._onDidChangeLocal.fire()));
|
||||
}
|
||||
|
||||
protected async getLocalFileContent(): Promise<IFileContent | null> {
|
||||
try {
|
||||
return await this.fileService.readFile(this.file);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -100,14 +100,8 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
|
||||
|
||||
// Remotely updated extensions
|
||||
for (const key of values(baseToRemote.updated)) {
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
// Update in local always
|
||||
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
|
||||
}
|
||||
|
||||
// Locally added extensions
|
||||
|
||||
@@ -3,29 +3,25 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { Queue } from 'vs/base/common/async';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { localize } from 'vs/nls';
|
||||
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly added: ISyncExtension[];
|
||||
readonly removed: IExtensionIdentifier[];
|
||||
readonly updated: ISyncExtension[];
|
||||
readonly remote: ISyncExtension[] | null;
|
||||
readonly remoteUserData: IUserData | null;
|
||||
readonly remoteUserData: IUserData;
|
||||
readonly skippedExtensions: ISyncExtension[];
|
||||
}
|
||||
|
||||
@@ -33,33 +29,19 @@ interface ILastSyncUserData extends IUserData {
|
||||
skippedExtensions: ISyncExtension[] | undefined;
|
||||
}
|
||||
|
||||
export class ExtensionsSynchroniser extends Disposable implements ISynchroniser {
|
||||
|
||||
private static EXTERNAL_USER_DATA_EXTENSIONS_KEY: string = 'extensions';
|
||||
|
||||
private _status: SyncStatus = SyncStatus.Idle;
|
||||
get status(): SyncStatus { return this._status; }
|
||||
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
|
||||
|
||||
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>;
|
||||
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
this.replaceQueue = this._register(new Queue());
|
||||
this.lastSyncExtensionsResource = joinPath(environmentService.userRoamingDataHome, '.lastSyncExtensions');
|
||||
super(SyncSource.Extensions, fileService, environmentService, userDataSyncStoreService);
|
||||
this._register(
|
||||
Event.debounce(
|
||||
Event.any(
|
||||
@@ -68,12 +50,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
|
||||
() => undefined, 500)(() => this._onDidChangeLocal.fire()));
|
||||
}
|
||||
|
||||
private setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
}
|
||||
}
|
||||
protected getRemoteDataResourceKey(): string { return 'extensions'; }
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
|
||||
@@ -121,7 +98,8 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
|
||||
|
||||
const localExtensions = await this.getLocalExtensions();
|
||||
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions());
|
||||
await this.apply({ added, removed, updated, remote, remoteUserData: null, skippedExtensions: [] });
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
await this.apply({ added, removed, updated, remote, remoteUserData, skippedExtensions: [] }, true);
|
||||
|
||||
this.logService.info('Extensions: Finished pushing extensions.');
|
||||
} finally {
|
||||
@@ -130,18 +108,18 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
|
||||
|
||||
}
|
||||
|
||||
async sync(): Promise<boolean> {
|
||||
async sync(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
|
||||
this.logService.trace('Extensions: Skipping synchronizing extensions as it is disabled.');
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
if (!this.extensionGalleryService.isEnabled()) {
|
||||
this.logService.trace('Extensions: Skipping synchronizing extensions as gallery is disabled.');
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
this.logService.trace('Extensions: Skipping synchronizing extensions as it is running already.');
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace('Extensions: Started synchronizing extensions...');
|
||||
@@ -152,7 +130,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
|
||||
await this.apply(previewResult);
|
||||
} catch (e) {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.Rejected) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('Extensions: Failed to synchronise extensions as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
@@ -160,21 +138,18 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.logService.trace('Extensions: Finised synchronizing extensions.');
|
||||
this.logService.trace('Extensions: Finished synchronizing extensions.');
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
return true;
|
||||
}
|
||||
|
||||
stop(): void { }
|
||||
async stop(): Promise<void> { }
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
return !!lastSyncData;
|
||||
async restart(): Promise<void> {
|
||||
throw new Error('Extensions: Conflicts should not occur');
|
||||
}
|
||||
|
||||
async hasRemoteData(): Promise<boolean> {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
return remoteUserData.content !== null;
|
||||
resolveConflicts(content: string): Promise<void> {
|
||||
throw new Error('Extensions: Conflicts should not occur');
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
@@ -189,30 +164,12 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
|
||||
return false;
|
||||
}
|
||||
|
||||
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 ignoredExtensions = this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
|
||||
const removedExtensions = remoteExtensions.filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier)) && areSameExtensions(e.identifier, identifier));
|
||||
if (removedExtensions.length) {
|
||||
for (const removedExtension of removedExtensions) {
|
||||
remoteExtensions.splice(remoteExtensions.indexOf(removedExtension), 1);
|
||||
}
|
||||
this.logService.info(`Extensions: Removing extension '${identifier.id}' from remote.`);
|
||||
await this.writeToRemote(remoteExtensions, remoteData.ref);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.lastSyncExtensionsResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
async getRemoteContent(): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getPreview(): Promise<ISyncPreviewResult> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const lastSyncData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const lastSyncExtensions: ISyncExtension[] | null = lastSyncData ? JSON.parse(lastSyncData.content!) : null;
|
||||
const skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : [];
|
||||
|
||||
@@ -236,7 +193,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
|
||||
return this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
|
||||
}
|
||||
|
||||
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions }: ISyncPreviewResult): Promise<void> {
|
||||
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
if (!added.length && !removed.length && !updated.length && !remote) {
|
||||
this.logService.trace('Extensions: No changes found during synchronizing extensions.');
|
||||
}
|
||||
@@ -249,13 +206,15 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
|
||||
if (remote) {
|
||||
// update remote
|
||||
this.logService.info('Extensions: Updating remote extensions...');
|
||||
remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null);
|
||||
const content = JSON.stringify(remote);
|
||||
const ref = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
remoteUserData = { ref, content };
|
||||
}
|
||||
|
||||
if (remoteUserData?.content) {
|
||||
if (remoteUserData.content) {
|
||||
// update last sync
|
||||
this.logService.info('Extensions: Updating last synchronised extensions...');
|
||||
await this.updateLastSyncValue({ ...remoteUserData, skippedExtensions });
|
||||
await this.updateLastSyncUserData<ILastSyncUserData>({ ...remoteUserData, skippedExtensions });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +240,11 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
|
||||
try {
|
||||
await this.extensionManagementService.installFromGallery(extension);
|
||||
removeFromSkipped.push(extension.identifier);
|
||||
if (e.enabled) {
|
||||
await this.extensionEnablementService.enableExtension(extension.identifier);
|
||||
} else {
|
||||
await this.extensionEnablementService.disableExtension(extension.identifier);
|
||||
}
|
||||
} catch (error) {
|
||||
addToSkipped.push(e);
|
||||
this.logService.error(error);
|
||||
@@ -308,31 +272,9 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
|
||||
|
||||
private async getLocalExtensions(): Promise<ISyncExtension[]> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const disabledExtensions = await this.extensionEnablementService.getDisabledExtensionsAsync();
|
||||
return installedExtensions
|
||||
.map(({ identifier }) => ({ identifier, enabled: true }));
|
||||
}
|
||||
|
||||
private async getLastSyncUserData(): Promise<ILastSyncUserData | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncExtensionsResource);
|
||||
return JSON.parse(content.value.toString());
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async updateLastSyncValue(lastSyncUserData: ILastSyncUserData): Promise<void> {
|
||||
await this.fileService.writeFile(this.lastSyncExtensionsResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData)));
|
||||
}
|
||||
|
||||
private getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
|
||||
return this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData || null);
|
||||
}
|
||||
|
||||
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 };
|
||||
.map(({ identifier }) => ({ identifier, enabled: !disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier)) }));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,61 +3,42 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, dirname } from 'vs/base/common/resources';
|
||||
import { dirname } from 'vs/base/common/resources';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly local: IGlobalState | undefined;
|
||||
readonly remote: IGlobalState | undefined;
|
||||
readonly remoteUserData: IUserData | null;
|
||||
readonly remoteUserData: IUserData;
|
||||
}
|
||||
|
||||
export class GlobalStateSynchroniser extends Disposable implements ISynchroniser {
|
||||
|
||||
private static EXTERNAL_USER_DATA_GLOBAL_STATE_KEY: string = 'globalState';
|
||||
|
||||
private _status: SyncStatus = SyncStatus.Idle;
|
||||
get status(): SyncStatus { return this._status; }
|
||||
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
|
||||
|
||||
private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
|
||||
|
||||
private readonly lastSyncGlobalStateResource: URI;
|
||||
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
constructor(
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
this.lastSyncGlobalStateResource = joinPath(environmentService.userRoamingDataHome, '.lastSyncGlobalState');
|
||||
super(SyncSource.GlobalState, fileService, environmentService, userDataSyncStoreService);
|
||||
this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
|
||||
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire()));
|
||||
}
|
||||
|
||||
private setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
}
|
||||
}
|
||||
protected getRemoteDataResourceKey(): string { return 'globalState'; }
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
|
||||
@@ -102,7 +83,8 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const remote = await this.getLocalGlobalState();
|
||||
await this.apply({ local: undefined, remote, remoteUserData: null });
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
await this.apply({ local: undefined, remote, remoteUserData }, true);
|
||||
|
||||
this.logService.info('UI State: Finished pushing UI State.');
|
||||
} finally {
|
||||
@@ -111,15 +93,15 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
|
||||
|
||||
}
|
||||
|
||||
async sync(): Promise<boolean> {
|
||||
async sync(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableUIState')) {
|
||||
this.logService.trace('UI State: Skipping synchronizing UI state as it is disabled.');
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
this.logService.trace('UI State: Skipping synchronizing ui state as it is running already.');
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace('UI State: Started synchronizing ui state...');
|
||||
@@ -128,11 +110,10 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
|
||||
try {
|
||||
const result = await this.getPreview();
|
||||
await this.apply(result);
|
||||
this.logService.trace('UI State: Finised synchronizing ui state.');
|
||||
return true;
|
||||
this.logService.trace('UI State: Finished synchronizing ui state.');
|
||||
} catch (e) {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.Rejected) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('UI State: Failed to synchronise ui state as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
@@ -143,16 +124,14 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void { }
|
||||
async stop(): Promise<void> { }
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
return !!lastSyncData;
|
||||
async restart(): Promise<void> {
|
||||
throw new Error('UI State: Conflicts should not occur');
|
||||
}
|
||||
|
||||
async hasRemoteData(): Promise<boolean> {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
return remoteUserData.content !== null;
|
||||
resolveConflicts(content: string): Promise<void> {
|
||||
throw new Error('UI State: Conflicts should not occur');
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
@@ -167,10 +146,8 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
|
||||
return false;
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.lastSyncGlobalStateResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
async getRemoteContent(): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getPreview(): Promise<ISyncPreviewResult> {
|
||||
@@ -187,7 +164,7 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
|
||||
return { local, remote, remoteUserData };
|
||||
}
|
||||
|
||||
private async apply({ local, remote, remoteUserData }: ISyncPreviewResult): Promise<void> {
|
||||
private async apply({ local, remote, remoteUserData }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
if (local) {
|
||||
// update local
|
||||
this.logService.info('UI State: Updating local ui state...');
|
||||
@@ -197,13 +174,15 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
|
||||
if (remote) {
|
||||
// update remote
|
||||
this.logService.info('UI State: Updating remote ui state...');
|
||||
remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null);
|
||||
const content = JSON.stringify(remote);
|
||||
const ref = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
remoteUserData = { ref, content };
|
||||
}
|
||||
|
||||
if (remoteUserData?.content) {
|
||||
if (remoteUserData.content) {
|
||||
// update last sync
|
||||
this.logService.info('UI State: Updating last synchronised ui state...');
|
||||
await this.updateLastSyncValue(remoteUserData);
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,27 +212,4 @@ export class GlobalStateSynchroniser extends Disposable implements ISynchroniser
|
||||
}
|
||||
}
|
||||
|
||||
private async getLastSyncUserData(): Promise<IUserData | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncGlobalStateResource);
|
||||
return JSON.parse(content.value.toString());
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async updateLastSyncValue(remoteUserData: IUserData): Promise<void> {
|
||||
await this.fileService.writeFile(this.lastSyncGlobalStateResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
|
||||
}
|
||||
|
||||
private getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
|
||||
return this.userDataSyncStoreService.read(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, lastSyncData || null);
|
||||
}
|
||||
|
||||
private async writeToRemote(globalState: IGlobalState, ref: string | null): Promise<IUserData> {
|
||||
const content = JSON.stringify(globalState);
|
||||
ref = await this.userDataSyncStoreService.write(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, content, ref);
|
||||
return { content, ref };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -99,16 +99,7 @@ export async function merge(localContent: string, remoteContent: string, baseCon
|
||||
mergeContent = updateKeybindings(mergeContent, command, keybindings, formattingOptions);
|
||||
}
|
||||
|
||||
const hasConflicts = commandsMergeResult.conflicts.size > 0;
|
||||
if (hasConflicts) {
|
||||
mergeContent = `<<<<<<< local${formattingOptions.eol}`
|
||||
+ mergeContent
|
||||
+ `${formattingOptions.eol}=======${formattingOptions.eol}`
|
||||
+ remoteContent
|
||||
+ `${formattingOptions.eol}>>>>>>> remote`;
|
||||
}
|
||||
|
||||
return { mergeContent, hasChanges: true, hasConflicts };
|
||||
return { mergeContent, hasChanges: true, hasConflicts: commandsMergeResult.conflicts.size > 0 };
|
||||
}
|
||||
|
||||
function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompareResult, baseToRemote: ICompareResult): { added: Set<string>, removed: Set<string>, updated: Set<string>, conflicts: Set<string> } {
|
||||
|
||||
@@ -4,23 +4,20 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource, IUserDataSynchroniser } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { parse, ParseError } from 'vs/base/common/json';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, dirname } from 'vs/base/common/resources';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { OS, OperatingSystem } from 'vs/base/common/platform';
|
||||
import { isUndefined } from 'vs/base/common/types';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
|
||||
interface ISyncContent {
|
||||
mac?: string;
|
||||
@@ -31,48 +28,28 @@ interface ISyncContent {
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly remoteUserData: IUserData | null;
|
||||
readonly remoteUserData: IUserData;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
}
|
||||
|
||||
export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISynchroniser {
|
||||
|
||||
private static EXTERNAL_USER_DATA_KEYBINDINGS_KEY: string = 'keybindings';
|
||||
export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
|
||||
|
||||
private _status: SyncStatus = SyncStatus.Idle;
|
||||
get status(): SyncStatus { return this._status; }
|
||||
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
|
||||
|
||||
private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
|
||||
|
||||
private readonly lastSyncKeybindingsResource: URI;
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
) {
|
||||
super(SyncSource.Keybindings, fileService, environmentService);
|
||||
this.lastSyncKeybindingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncKeybindings.json');
|
||||
this._register(this.fileService.watch(dirname(this.environmentService.keybindingsResource)));
|
||||
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.keybindingsResource))(() => this._onDidChangeLocal.fire()));
|
||||
super(environmentService.keybindingsResource, SyncSource.Keybindings, fileService, environmentService, userDataSyncStoreService);
|
||||
}
|
||||
|
||||
private setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
}
|
||||
}
|
||||
protected getRemoteDataResourceKey(): string { return 'keybindings'; }
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
|
||||
@@ -129,15 +106,16 @@ export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISy
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
|
||||
if (fileContent !== null) {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, fileContent.value);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
fileContent,
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: true,
|
||||
remoteUserData: null
|
||||
remoteUserData
|
||||
}));
|
||||
await this.apply();
|
||||
await this.apply(undefined, true);
|
||||
}
|
||||
|
||||
// No local exists to push
|
||||
@@ -152,74 +130,55 @@ export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISy
|
||||
|
||||
}
|
||||
|
||||
async sync(_continue?: boolean): Promise<boolean> {
|
||||
async sync(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
|
||||
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is disabled.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_continue) {
|
||||
this.logService.info('Keybindings: Resumed synchronizing keybindings');
|
||||
return this.continueSync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is running already.');
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace('Keybindings: Started synchronizing keybindings...');
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
try {
|
||||
const result = await this.getPreview();
|
||||
if (result.hasConflicts) {
|
||||
this.logService.info('Keybindings: Detected conflicts while synchronizing keybindings.');
|
||||
this.setStatus(SyncStatus.HasConflicts);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await this.apply();
|
||||
this.logService.trace('Keybindings: Finished synchronizing keybindings...');
|
||||
return true;
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
} catch (e) {
|
||||
this.syncPreviewResultPromise = null;
|
||||
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('Keybindings: Failed to synchronise keybindings as there is a new remote version available. Synchronizing 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('Keybindings: Failed to synchronise keybindings as there is a new local version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return this.doSync();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
async stop(): Promise<void> {
|
||||
if (this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
this.logService.info('Keybindings: Stopped synchronizing keybindings.');
|
||||
}
|
||||
this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
|
||||
await this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
return !!lastSyncData;
|
||||
async restart(): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.syncPreviewResultPromise!.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
await this.doSync();
|
||||
}
|
||||
}
|
||||
|
||||
async hasRemoteData(): Promise<boolean> {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
return remoteUserData.content !== null;
|
||||
async resolveConflicts(content: string): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
try {
|
||||
await this.apply(content, true);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
|
||||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
|
||||
throw new Error('Failed to resolve conflicts as there is a new local version available.');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
@@ -239,29 +198,63 @@ export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISy
|
||||
return false;
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.lastSyncKeybindingsResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
private async continueSync(): Promise<boolean> {
|
||||
if (this.status !== SyncStatus.HasConflicts) {
|
||||
return false;
|
||||
async getRemoteContent(): Promise<string | null> {
|
||||
let content: string | null | undefined = null;
|
||||
if (this.syncPreviewResultPromise) {
|
||||
const preview = await this.syncPreviewResultPromise;
|
||||
content = preview.remoteUserData?.content;
|
||||
} else {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
content = remoteUserData.content;
|
||||
}
|
||||
await this.apply();
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
return true;
|
||||
return content ? this.getKeybindingsContentFromSyncContent(content) : null;
|
||||
}
|
||||
|
||||
private async apply(): Promise<void> {
|
||||
private async doSync(): Promise<void> {
|
||||
try {
|
||||
const result = await this.getPreview();
|
||||
if (result.hasConflicts) {
|
||||
this.logService.info('Keybindings: Detected conflicts while synchronizing keybindings.');
|
||||
this.setStatus(SyncStatus.HasConflicts);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.apply();
|
||||
this.logService.trace('Keybindings: Finished synchronizing keybindings...');
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
} catch (e) {
|
||||
this.syncPreviewResultPromise = null;
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.Rejected) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
|
||||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
|
||||
// Rejected as there is a new local version. Syncing again.
|
||||
this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new local version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async apply(content?: string, forcePush?: boolean): Promise<void> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.fileService.exists(this.environmentService.keybindingsSyncPreviewResource)) {
|
||||
const keybindingsPreivew = await this.fileService.readFile(this.environmentService.keybindingsSyncPreviewResource);
|
||||
const content = keybindingsPreivew.value.toString();
|
||||
if (content === undefined) {
|
||||
if (await this.fileService.exists(this.environmentService.keybindingsSyncPreviewResource)) {
|
||||
const keybindingsPreivew = await this.fileService.readFile(this.environmentService.keybindingsSyncPreviewResource);
|
||||
content = keybindingsPreivew.value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (content !== undefined) {
|
||||
if (this.hasErrors(content)) {
|
||||
const error = new Error(localize('errorInvalidKeybindings', "Unable to sync keybindings. Please resolve conflicts without any errors/warnings and try again."));
|
||||
this.logService.error(error);
|
||||
@@ -274,13 +267,12 @@ export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISy
|
||||
}
|
||||
if (hasLocalChanged) {
|
||||
this.logService.info('Keybindings: Updating local keybindings');
|
||||
await this.updateLocalContent(content, fileContent);
|
||||
await this.updateLocalFileContent(content, fileContent);
|
||||
}
|
||||
if (hasRemoteChanged) {
|
||||
this.logService.info('Keybindings: Updating remote keybindings');
|
||||
let remoteContents = remoteUserData ? remoteUserData.content : (await this.getRemoteUserData()).content;
|
||||
remoteContents = this.updateSyncContent(content, remoteContents);
|
||||
const ref = await this.updateRemoteUserData(remoteContents, remoteUserData ? remoteUserData.ref : null);
|
||||
const remoteContents = this.updateSyncContent(content, remoteUserData.content);
|
||||
const ref = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref);
|
||||
remoteUserData = { ref, content: remoteContents };
|
||||
}
|
||||
if (remoteUserData?.content) {
|
||||
@@ -369,46 +361,6 @@ export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISy
|
||||
return this._formattingOptions;
|
||||
}
|
||||
|
||||
private async getLocalFileContent(): Promise<IFileContent | null> {
|
||||
try {
|
||||
return await this.fileService.readFile(this.environmentService.keybindingsResource);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async updateLocalContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
|
||||
if (oldContent) {
|
||||
// file exists already
|
||||
await this.backupLocal(oldContent.value);
|
||||
await this.fileService.writeFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), oldContent);
|
||||
} else {
|
||||
// file does not exist
|
||||
await this.fileService.createFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), { overwrite: false });
|
||||
}
|
||||
}
|
||||
|
||||
private async getLastSyncUserData(): Promise<IUserData | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncKeybindingsResource);
|
||||
return JSON.parse(content.value.toString());
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async updateLastSyncUserData(remoteUserData: IUserData): Promise<void> {
|
||||
await this.fileService.writeFile(this.lastSyncKeybindingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
|
||||
}
|
||||
|
||||
private async getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
|
||||
return this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData || null);
|
||||
}
|
||||
|
||||
private async updateRemoteUserData(content: string, ref: string | null): Promise<string> {
|
||||
return this.userDataSyncStoreService.write(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, content, ref);
|
||||
}
|
||||
|
||||
private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
|
||||
try {
|
||||
const parsed = <ISyncContent>JSON.parse(syncContent);
|
||||
|
||||
@@ -4,47 +4,95 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { parse, findNodeAtLocation, parseTree, Node } from 'vs/base/common/json';
|
||||
import { setProperty } from 'vs/base/common/jsonEdit';
|
||||
import { parse, JSONVisitor, visit } from 'vs/base/common/json';
|
||||
import { setProperty, withFormatting, applyEdits } from 'vs/base/common/jsonEdit';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { FormattingOptions, Edit, getEOL } from 'vs/base/common/jsonFormatter';
|
||||
import * as contentUtil from 'vs/platform/userDataSync/common/content';
|
||||
import { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { firstIndex } from 'vs/base/common/arrays';
|
||||
|
||||
export interface IMergeResult {
|
||||
localContent: string | null;
|
||||
remoteContent: string | null;
|
||||
hasConflicts: boolean;
|
||||
conflictsSettings: IConflictSetting[];
|
||||
}
|
||||
|
||||
export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string {
|
||||
if (ignoredSettings.length) {
|
||||
const sourceTree = parseSettings(sourceContent);
|
||||
const source = parse(sourceContent);
|
||||
const target = parse(targetContent);
|
||||
const settingsToAdd: INode[] = [];
|
||||
for (const key of ignoredSettings) {
|
||||
targetContent = contentUtil.edit(targetContent, [key], source[key], formattingOptions);
|
||||
const sourceValue = source[key];
|
||||
const targetValue = target[key];
|
||||
|
||||
// Remove in target
|
||||
if (sourceValue === undefined) {
|
||||
targetContent = contentUtil.edit(targetContent, [key], undefined, formattingOptions);
|
||||
}
|
||||
|
||||
// Update in target
|
||||
else if (targetValue !== undefined) {
|
||||
targetContent = contentUtil.edit(targetContent, [key], sourceValue, formattingOptions);
|
||||
}
|
||||
|
||||
else {
|
||||
settingsToAdd.push(findSettingNode(key, sourceTree)!);
|
||||
}
|
||||
}
|
||||
|
||||
settingsToAdd.sort((a, b) => a.startOffset - b.startOffset);
|
||||
settingsToAdd.forEach(s => targetContent = addSetting(s.setting!.key, sourceContent, targetContent, formattingOptions));
|
||||
}
|
||||
return targetContent;
|
||||
}
|
||||
|
||||
export function merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[], resolvedConflicts: { key: string, value: any | undefined }[], formattingOptions: FormattingOptions): { mergeContent: string, hasChanges: boolean, conflicts: IConflictSetting[] } {
|
||||
const local = parse(localContent);
|
||||
const remote = parse(remoteContent);
|
||||
const base = baseContent ? parse(baseContent) : null;
|
||||
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
|
||||
export function merge(originalLocalContent: string, originalRemoteContent: string, baseContent: string | null, ignoredSettings: string[], resolvedConflicts: { key: string, value: any | undefined }[], formattingOptions: FormattingOptions): IMergeResult {
|
||||
|
||||
const localToRemote = compare(local, remote, ignored);
|
||||
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
|
||||
// No changes found between local and remote.
|
||||
return { mergeContent: localContent, hasChanges: false, conflicts: [] };
|
||||
const localContentWithoutIgnoredSettings = updateIgnoredSettings(originalLocalContent, originalRemoteContent, ignoredSettings, formattingOptions);
|
||||
const localForwarded = baseContent !== localContentWithoutIgnoredSettings;
|
||||
const remoteForwarded = baseContent !== originalRemoteContent;
|
||||
|
||||
/* no changes */
|
||||
if (!localForwarded && !remoteForwarded) {
|
||||
return { conflictsSettings: [], localContent: null, remoteContent: null, hasConflicts: false };
|
||||
}
|
||||
|
||||
/* local has changed and remote has not */
|
||||
if (localForwarded && !remoteForwarded) {
|
||||
return { conflictsSettings: [], localContent: null, remoteContent: localContentWithoutIgnoredSettings, hasConflicts: false };
|
||||
}
|
||||
|
||||
/* remote has changed and local has not */
|
||||
if (remoteForwarded && !localForwarded) {
|
||||
return { conflictsSettings: [], localContent: updateIgnoredSettings(originalRemoteContent, originalLocalContent, ignoredSettings, formattingOptions), remoteContent: null, hasConflicts: false };
|
||||
}
|
||||
|
||||
/* remote and local has changed */
|
||||
|
||||
let localContent = originalLocalContent;
|
||||
let remoteContent = originalRemoteContent;
|
||||
const local = parse(originalLocalContent);
|
||||
const remote = parse(originalRemoteContent);
|
||||
const base = baseContent ? parse(baseContent) : null;
|
||||
|
||||
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
|
||||
const localToRemote = compare(local, remote, ignored);
|
||||
const baseToLocal = compare(base, local, ignored);
|
||||
const baseToRemote = compare(base, remote, ignored);
|
||||
|
||||
const conflicts: Map<string, IConflictSetting> = new Map<string, IConflictSetting>();
|
||||
const handledConflicts: Set<string> = new Set<string>();
|
||||
const baseToLocal = base ? compare(base, local, ignored) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
const baseToRemote = base ? compare(base, remote, ignored) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
let mergeContent = localContent;
|
||||
|
||||
const handleConflict = (conflictKey: string): void => {
|
||||
handledConflicts.add(conflictKey);
|
||||
const resolvedConflict = resolvedConflicts.filter(({ key }) => key === conflictKey)[0];
|
||||
if (resolvedConflict) {
|
||||
mergeContent = contentUtil.edit(mergeContent, [conflictKey], resolvedConflict.value, formattingOptions);
|
||||
localContent = contentUtil.edit(localContent, [conflictKey], resolvedConflict.value, formattingOptions);
|
||||
remoteContent = contentUtil.edit(remoteContent, [conflictKey], resolvedConflict.value, formattingOptions);
|
||||
} else {
|
||||
conflicts.set(conflictKey, { key: conflictKey, localValue: local[conflictKey], remoteValue: remote[conflictKey] });
|
||||
}
|
||||
@@ -52,10 +100,14 @@ export function merge(localContent: string, remoteContent: string, baseContent:
|
||||
|
||||
// Removed settings in Local
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
// Got updated in remote
|
||||
// Conflict - Got updated in remote.
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
// Also remove in remote
|
||||
else {
|
||||
remoteContent = contentUtil.edit(remoteContent, [key], undefined, formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
// Removed settings in Remote
|
||||
@@ -63,41 +115,13 @@ export function merge(localContent: string, remoteContent: string, baseContent:
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got updated in local
|
||||
// Conflict - Got updated in local
|
||||
if (baseToLocal.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
} else {
|
||||
mergeContent = contentUtil.edit(mergeContent, [key], undefined, formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
// Added settings in Local
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in remote
|
||||
if (baseToRemote.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Added settings in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions);
|
||||
// Also remove in locals
|
||||
else {
|
||||
localContent = contentUtil.edit(localContent, [key], undefined, formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +136,8 @@ export function merge(localContent: string, remoteContent: string, baseContent:
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
remoteContent = contentUtil.edit(remoteContent, [key], local[key], formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,74 +153,425 @@ export function merge(localContent: string, remoteContent: string, baseContent:
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions);
|
||||
localContent = contentUtil.edit(localContent, [key], remote[key], formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (conflicts.size > 0) {
|
||||
const conflictNodes: { key: string, node: Node | undefined }[] = [];
|
||||
const tree = parseTree(mergeContent);
|
||||
const eol = formattingOptions.eol!;
|
||||
for (const { key } of values(conflicts)) {
|
||||
const node = findNodeAtLocation(tree, [key]);
|
||||
conflictNodes.push({ key, node });
|
||||
// Added settings in Local
|
||||
for (const key of values(baseToLocal.added)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
conflictNodes.sort((a, b) => {
|
||||
if (a.node && b.node) {
|
||||
return b.node.offset - a.node.offset;
|
||||
}
|
||||
return a.node ? 1 : -1;
|
||||
});
|
||||
const lastNode = tree.children ? tree.children[tree.children.length - 1] : undefined;
|
||||
for (const { key, node } of conflictNodes) {
|
||||
const remoteEdit = setProperty(`{${eol}\t${eol}}`, [key], remote[key], { tabSize: 4, insertSpaces: false, eol: eol })[0];
|
||||
const remoteContent = remoteEdit ? `${remoteEdit.content.substring(remoteEdit.offset + remoteEdit.length + 1)},${eol}` : '';
|
||||
if (node) {
|
||||
// Updated in Local and Remote with different value
|
||||
const localStartOffset = contentUtil.getLineStartOffset(mergeContent, eol, node.parent!.offset);
|
||||
const localEndOffset = contentUtil.getLineEndOffset(mergeContent, eol, node.offset + node.length);
|
||||
mergeContent = mergeContent.substring(0, localStartOffset)
|
||||
+ `<<<<<<< local${eol}`
|
||||
+ mergeContent.substring(localStartOffset, localEndOffset)
|
||||
+ `${eol}=======${eol}${remoteContent}>>>>>>> remote`
|
||||
+ mergeContent.substring(localEndOffset);
|
||||
} else {
|
||||
// Removed in Local, but updated in Remote
|
||||
if (lastNode) {
|
||||
const localStartOffset = contentUtil.getLineEndOffset(mergeContent, eol, lastNode.offset + lastNode.length);
|
||||
mergeContent = mergeContent.substring(0, localStartOffset)
|
||||
+ `${eol}<<<<<<< local${eol}=======${eol}${remoteContent}>>>>>>> remote`
|
||||
+ mergeContent.substring(localStartOffset);
|
||||
} else {
|
||||
const localStartOffset = tree.offset + 1;
|
||||
mergeContent = mergeContent.substring(0, localStartOffset)
|
||||
+ `${eol}<<<<<<< local${eol}=======${eol}${remoteContent}>>>>>>> remote${eol}`
|
||||
+ mergeContent.substring(localStartOffset);
|
||||
}
|
||||
// Got added in remote
|
||||
if (baseToRemote.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
remoteContent = addSetting(key, localContent, remoteContent, formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
return { mergeContent, hasChanges: true, conflicts: values(conflicts) };
|
||||
// Added settings in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
if (handledConflicts.has(key)) {
|
||||
continue;
|
||||
}
|
||||
// Got added in local
|
||||
if (baseToLocal.added.has(key)) {
|
||||
// Has different value
|
||||
if (localToRemote.updated.has(key)) {
|
||||
handleConflict(key);
|
||||
}
|
||||
} else {
|
||||
localContent = addSetting(key, remoteContent, localContent, formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
const hasConflicts = conflicts.size > 0 || !areSame(localContent, remoteContent, ignoredSettings);
|
||||
const hasLocalChanged = hasConflicts || !areSame(localContent, originalLocalContent, []);
|
||||
const hasRemoteChanged = hasConflicts || !areSame(remoteContent, originalRemoteContent, []);
|
||||
return { localContent: hasLocalChanged ? localContent : null, remoteContent: hasRemoteChanged ? remoteContent : null, conflictsSettings: values(conflicts), hasConflicts };
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>, ignored: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
const fromKeys = Object.keys(from).filter(key => !ignored.has(key));
|
||||
export function areSame(localContent: string, remoteContent: string, ignoredSettings: string[]): boolean {
|
||||
if (localContent === remoteContent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const local = parse(localContent);
|
||||
const remote = parse(remoteContent);
|
||||
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
|
||||
const localTree = parseSettings(localContent).filter(node => !(node.setting && ignored.has(node.setting.key)));
|
||||
const remoteTree = parseSettings(remoteContent).filter(node => !(node.setting && ignored.has(node.setting.key)));
|
||||
|
||||
if (localTree.length !== remoteTree.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let index = 0; index < localTree.length; index++) {
|
||||
const localNode = localTree[index];
|
||||
const remoteNode = remoteTree[index];
|
||||
if (localNode.setting && remoteNode.setting) {
|
||||
if (localNode.setting.key !== remoteNode.setting.key) {
|
||||
return false;
|
||||
}
|
||||
if (!objects.equals(local[localNode.setting.key], remote[localNode.setting.key])) {
|
||||
return false;
|
||||
}
|
||||
} else if (!localNode.setting && !remoteNode.setting) {
|
||||
if (localNode.value !== remoteNode.value) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<any> | null, to: IStringDictionary<any>, ignored: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
const fromKeys = from ? Object.keys(from).filter(key => !ignored.has(key)) : [];
|
||||
const toKeys = Object.keys(to).filter(key => !ignored.has(key));
|
||||
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 key of fromKeys) {
|
||||
if (removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const value1 = from[key];
|
||||
const value2 = to[key];
|
||||
if (!objects.equals(value1, value2)) {
|
||||
updated.add(key);
|
||||
if (from) {
|
||||
for (const key of fromKeys) {
|
||||
if (removed.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const value1 = from[key];
|
||||
const value2 = to[key];
|
||||
if (!objects.equals(value1, value2)) {
|
||||
updated.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
export function addSetting(key: string, sourceContent: string, targetContent: string, formattingOptions: FormattingOptions): string {
|
||||
const source = parse(sourceContent);
|
||||
const sourceTree = parseSettings(sourceContent);
|
||||
const targetTree = parseSettings(targetContent);
|
||||
const insertLocation = getInsertLocation(key, sourceTree, targetTree);
|
||||
return insertAtLocation(targetContent, key, source[key], insertLocation, targetTree, formattingOptions);
|
||||
}
|
||||
|
||||
interface InsertLocation {
|
||||
index: number,
|
||||
insertAfter: boolean;
|
||||
}
|
||||
|
||||
function getInsertLocation(key: string, sourceTree: INode[], targetTree: INode[]): InsertLocation {
|
||||
|
||||
const sourceNodeIndex = firstIndex(sourceTree, (node => node.setting?.key === key));
|
||||
|
||||
const sourcePreviousNode: INode = sourceTree[sourceNodeIndex - 1];
|
||||
if (sourcePreviousNode) {
|
||||
/*
|
||||
Previous node in source is a setting.
|
||||
Find the same setting in the target.
|
||||
Insert it after that setting
|
||||
*/
|
||||
if (sourcePreviousNode.setting) {
|
||||
const targetPreviousSetting = findSettingNode(sourcePreviousNode.setting.key, targetTree);
|
||||
if (targetPreviousSetting) {
|
||||
/* Insert after target's previous setting */
|
||||
return { index: targetTree.indexOf(targetPreviousSetting), insertAfter: true };
|
||||
}
|
||||
}
|
||||
/* Previous node in source is a comment */
|
||||
else {
|
||||
const sourcePreviousSettingNode = findPreviousSettingNode(sourceNodeIndex, sourceTree);
|
||||
/*
|
||||
Source has a setting defined before the setting to be added.
|
||||
Find the same previous setting in the target.
|
||||
If found, insert before its next setting so that comments are retrieved.
|
||||
Otherwise, insert at the end.
|
||||
*/
|
||||
if (sourcePreviousSettingNode) {
|
||||
const targetPreviousSetting = findSettingNode(sourcePreviousSettingNode.setting!.key, targetTree);
|
||||
if (targetPreviousSetting) {
|
||||
const targetNextSetting = findNextSettingNode(targetTree.indexOf(targetPreviousSetting), targetTree);
|
||||
const sourceCommentNodes = findNodesBetween(sourceTree, sourcePreviousSettingNode, sourceTree[sourceNodeIndex]);
|
||||
if (targetNextSetting) {
|
||||
const targetCommentNodes = findNodesBetween(targetTree, targetPreviousSetting, targetNextSetting);
|
||||
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes, targetCommentNodes);
|
||||
if (targetCommentNode) {
|
||||
return { index: targetTree.indexOf(targetCommentNode), insertAfter: true }; /* Insert after comment */
|
||||
} else {
|
||||
return { index: targetTree.indexOf(targetNextSetting), insertAfter: false }; /* Insert before target next setting */
|
||||
}
|
||||
} else {
|
||||
const targetCommentNodes = findNodesBetween(targetTree, targetPreviousSetting, targetTree[targetTree.length - 1]);
|
||||
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes, targetCommentNodes);
|
||||
if (targetCommentNode) {
|
||||
return { index: targetTree.indexOf(targetCommentNode), insertAfter: true }; /* Insert after comment */
|
||||
} else {
|
||||
return { index: targetTree.length - 1, insertAfter: true }; /* Insert at the end */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sourceNextNode = sourceTree[sourceNodeIndex + 1];
|
||||
if (sourceNextNode) {
|
||||
/*
|
||||
Next node in source is a setting.
|
||||
Find the same setting in the target.
|
||||
Insert it before that setting
|
||||
*/
|
||||
if (sourceNextNode.setting) {
|
||||
const targetNextSetting = findSettingNode(sourceNextNode.setting.key, targetTree);
|
||||
if (targetNextSetting) {
|
||||
/* Insert before target's next setting */
|
||||
return { index: targetTree.indexOf(targetNextSetting), insertAfter: false };
|
||||
}
|
||||
}
|
||||
/* Next node in source is a comment */
|
||||
else {
|
||||
const sourceNextSettingNode = findNextSettingNode(sourceNodeIndex, sourceTree);
|
||||
/*
|
||||
Source has a setting defined after the setting to be added.
|
||||
Find the same next setting in the target.
|
||||
If found, insert after its previous setting so that comments are retrieved.
|
||||
Otherwise, insert at the beginning.
|
||||
*/
|
||||
if (sourceNextSettingNode) {
|
||||
const targetNextSetting = findSettingNode(sourceNextSettingNode.setting!.key, targetTree);
|
||||
if (targetNextSetting) {
|
||||
const targetPreviousSetting = findPreviousSettingNode(targetTree.indexOf(targetNextSetting), targetTree);
|
||||
const sourceCommentNodes = findNodesBetween(sourceTree, sourceTree[sourceNodeIndex], sourceNextSettingNode);
|
||||
if (targetPreviousSetting) {
|
||||
const targetCommentNodes = findNodesBetween(targetTree, targetPreviousSetting, targetNextSetting);
|
||||
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes.reverse(), targetCommentNodes.reverse());
|
||||
if (targetCommentNode) {
|
||||
return { index: targetTree.indexOf(targetCommentNode), insertAfter: false }; /* Insert before comment */
|
||||
} else {
|
||||
return { index: targetTree.indexOf(targetPreviousSetting), insertAfter: true }; /* Insert after target previous setting */
|
||||
}
|
||||
} else {
|
||||
const targetCommentNodes = findNodesBetween(targetTree, targetTree[0], targetNextSetting);
|
||||
const targetCommentNode = findLastMatchingTargetCommentNode(sourceCommentNodes.reverse(), targetCommentNodes.reverse());
|
||||
if (targetCommentNode) {
|
||||
return { index: targetTree.indexOf(targetCommentNode), insertAfter: false }; /* Insert before comment */
|
||||
} else {
|
||||
return { index: 0, insertAfter: false }; /* Insert at the beginning */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Insert at the end */
|
||||
return { index: targetTree.length - 1, insertAfter: true };
|
||||
}
|
||||
|
||||
function insertAtLocation(content: string, key: string, value: any, location: InsertLocation, tree: INode[], formattingOptions: FormattingOptions): string {
|
||||
let edits: Edit[];
|
||||
/* Insert at the end */
|
||||
if (location.index === -1) {
|
||||
edits = setProperty(content, [key], value, formattingOptions);
|
||||
} else {
|
||||
edits = getEditToInsertAtLocation(content, key, value, location, tree, formattingOptions).map(edit => withFormatting(content, edit, formattingOptions)[0]);
|
||||
}
|
||||
return applyEdits(content, edits);
|
||||
}
|
||||
|
||||
function getEditToInsertAtLocation(content: string, key: string, value: any, location: InsertLocation, tree: INode[], formattingOptions: FormattingOptions): Edit[] {
|
||||
const newProperty = `${JSON.stringify(key)}: ${JSON.stringify(value)}`;
|
||||
const eol = getEOL(formattingOptions, content);
|
||||
const node = tree[location.index];
|
||||
|
||||
if (location.insertAfter) {
|
||||
|
||||
/* Insert after a setting */
|
||||
if (node.setting) {
|
||||
return [{ offset: node.endOffset, length: 0, content: ',' + newProperty }];
|
||||
}
|
||||
|
||||
/*
|
||||
Insert after a comment and before a setting (or)
|
||||
Insert between comments and there is a setting after
|
||||
*/
|
||||
if (tree[location.index + 1] &&
|
||||
(tree[location.index + 1].setting || findNextSettingNode(location.index, tree))) {
|
||||
return [{ offset: node.endOffset, length: 0, content: eol + newProperty + ',' }];
|
||||
}
|
||||
|
||||
/* Insert after the comment at the end */
|
||||
const edits = [{ offset: node.endOffset, length: 0, content: eol + newProperty }];
|
||||
const previousSettingNode = findPreviousSettingNode(location.index, tree);
|
||||
if (previousSettingNode && !previousSettingNode.setting!.hasCommaSeparator) {
|
||||
edits.splice(0, 0, { offset: previousSettingNode.endOffset, length: 0, content: ',' });
|
||||
}
|
||||
return edits;
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
/* Insert before a setting */
|
||||
if (node.setting) {
|
||||
return [{ offset: node.startOffset, length: 0, content: newProperty + ',' }];
|
||||
}
|
||||
|
||||
/* Insert before a comment */
|
||||
const content = (tree[location.index - 1] && !tree[location.index - 1].setting /* previous node is comment */ ? eol : '')
|
||||
+ newProperty
|
||||
+ (findNextSettingNode(location.index, tree) ? ',' : '')
|
||||
+ eol;
|
||||
return [{ offset: node.startOffset, length: 0, content }];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function findSettingNode(key: string, tree: INode[]): INode | undefined {
|
||||
return tree.filter(node => node.setting?.key === key)[0];
|
||||
}
|
||||
|
||||
function findPreviousSettingNode(index: number, tree: INode[]): INode | undefined {
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
if (tree[i].setting) {
|
||||
return tree[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findNextSettingNode(index: number, tree: INode[]): INode | undefined {
|
||||
for (let i = index + 1; i < tree.length; i++) {
|
||||
if (tree[i].setting) {
|
||||
return tree[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findNodesBetween(nodes: INode[], from: INode, till: INode): INode[] {
|
||||
const fromIndex = nodes.indexOf(from);
|
||||
const tillIndex = nodes.indexOf(till);
|
||||
return nodes.filter((node, index) => fromIndex < index && index < tillIndex);
|
||||
}
|
||||
|
||||
function findLastMatchingTargetCommentNode(sourceComments: INode[], targetComments: INode[]): INode | undefined {
|
||||
if (sourceComments.length && targetComments.length) {
|
||||
let index = 0;
|
||||
for (; index < targetComments.length && index < sourceComments.length; index++) {
|
||||
if (sourceComments[index].value !== targetComments[index].value) {
|
||||
return targetComments[index - 1];
|
||||
}
|
||||
}
|
||||
return targetComments[index - 1];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface INode {
|
||||
readonly startOffset: number;
|
||||
readonly endOffset: number;
|
||||
readonly value: string;
|
||||
readonly setting?: {
|
||||
readonly key: string;
|
||||
readonly hasCommaSeparator: boolean;
|
||||
};
|
||||
readonly comment?: string;
|
||||
}
|
||||
|
||||
function parseSettings(content: string): INode[] {
|
||||
const nodes: INode[] = [];
|
||||
let hierarchyLevel = -1;
|
||||
let startOffset: number;
|
||||
let key: string;
|
||||
|
||||
const visitor: JSONVisitor = {
|
||||
onObjectBegin: (offset: number) => {
|
||||
hierarchyLevel++;
|
||||
},
|
||||
onObjectProperty: (name: string, offset: number, length: number) => {
|
||||
if (hierarchyLevel === 0) {
|
||||
// this is setting key
|
||||
startOffset = offset;
|
||||
key = name;
|
||||
}
|
||||
},
|
||||
onObjectEnd: (offset: number, length: number) => {
|
||||
hierarchyLevel--;
|
||||
if (hierarchyLevel === 0) {
|
||||
nodes.push({
|
||||
startOffset,
|
||||
endOffset: offset + length,
|
||||
value: content.substring(startOffset, offset + length),
|
||||
setting: {
|
||||
key,
|
||||
hasCommaSeparator: false
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onArrayBegin: (offset: number, length: number) => {
|
||||
hierarchyLevel++;
|
||||
},
|
||||
onArrayEnd: (offset: number, length: number) => {
|
||||
hierarchyLevel--;
|
||||
if (hierarchyLevel === 0) {
|
||||
nodes.push({
|
||||
startOffset,
|
||||
endOffset: offset + length,
|
||||
value: content.substring(startOffset, offset + length),
|
||||
setting: {
|
||||
key,
|
||||
hasCommaSeparator: false
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onLiteralValue: (value: any, offset: number, length: number) => {
|
||||
if (hierarchyLevel === 0) {
|
||||
nodes.push({
|
||||
startOffset,
|
||||
endOffset: offset + length,
|
||||
value: content.substring(startOffset, offset + length),
|
||||
setting: {
|
||||
key,
|
||||
hasCommaSeparator: false
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onSeparator: (sep: string, offset: number, length: number) => {
|
||||
if (hierarchyLevel === 0) {
|
||||
if (sep === ',') {
|
||||
const node = nodes.pop();
|
||||
nodes.push({
|
||||
startOffset: node!.startOffset,
|
||||
endOffset: node!.endOffset,
|
||||
value: node!.value,
|
||||
setting: {
|
||||
key: node!.setting!.key,
|
||||
hasCommaSeparator: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
onComment: (offset: number, length: number) => {
|
||||
if (hierarchyLevel === 0) {
|
||||
nodes.push({
|
||||
startOffset: offset,
|
||||
endOffset: offset + length,
|
||||
value: content.substring(offset, offset + length),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(content, visitor);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, SyncStatus, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { parse, ParseError } from 'vs/base/common/json';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, dirname } from 'vs/base/common/resources';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
@@ -22,59 +20,45 @@ import * as arrays from 'vs/base/common/arrays';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { isEmptyObject } from 'vs/base/common/types';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly remoteUserData: IUserData | null;
|
||||
readonly remoteUserData: IUserData;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly conflicts: IConflictSetting[];
|
||||
readonly remoteContent: string | null;
|
||||
readonly hasConflicts: boolean;
|
||||
readonly conflictSettings: IConflictSetting[];
|
||||
}
|
||||
|
||||
export class SettingsSynchroniser extends AbstractSynchroniser implements ISettingsSyncService {
|
||||
export class SettingsSynchroniser extends AbstractFileSynchroniser implements ISettingsSyncService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private static EXTERNAL_USER_DATA_SETTINGS_KEY: string = 'settings';
|
||||
|
||||
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
|
||||
|
||||
private _status: SyncStatus = SyncStatus.Idle;
|
||||
get status(): SyncStatus { return this._status; }
|
||||
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
|
||||
|
||||
private _conflicts: IConflictSetting[] = [];
|
||||
get conflicts(): IConflictSetting[] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<IConflictSetting[]> = this._register(new Emitter<IConflictSetting[]>());
|
||||
readonly onDidChangeConflicts: Event<IConflictSetting[]> = this._onDidChangeConflicts.event;
|
||||
|
||||
private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
|
||||
|
||||
private readonly lastSyncSettingsResource: URI;
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super(SyncSource.Settings, fileService, environmentService);
|
||||
this.lastSyncSettingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncSettings.json');
|
||||
this._register(this.fileService.watch(dirname(this.environmentService.settingsResource)));
|
||||
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.settingsResource))(() => this._onDidChangeLocal.fire()));
|
||||
super(environmentService.settingsResource, SyncSource.Settings, fileService, environmentService, userDataSyncStoreService);
|
||||
}
|
||||
|
||||
private setStatus(status: SyncStatus): void {
|
||||
if (this._status !== status) {
|
||||
this._status = status;
|
||||
this._onDidChangStatus.fire(status);
|
||||
}
|
||||
if (this._status !== SyncStatus.HasConflicts) {
|
||||
protected getRemoteDataResourceKey(): string { return 'settings'; }
|
||||
|
||||
protected setStatus(status: SyncStatus): void {
|
||||
super.setStatus(status);
|
||||
if (this.status !== SyncStatus.HasConflicts) {
|
||||
this.setConflicts([]);
|
||||
}
|
||||
}
|
||||
@@ -110,11 +94,13 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content));
|
||||
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
conflicts: [],
|
||||
hasConflicts: false,
|
||||
conflictSettings: [],
|
||||
fileContent,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false,
|
||||
remoteUserData
|
||||
remoteContent: content,
|
||||
remoteUserData,
|
||||
}));
|
||||
|
||||
await this.apply();
|
||||
@@ -150,16 +136,19 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
// Remove ignored settings
|
||||
const content = updateIgnoredSettings(fileContent.value.toString(), '{}', getIgnoredSettings(this.configurationService), formatUtils);
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content));
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
conflicts: [],
|
||||
conflictSettings: [],
|
||||
hasConflicts: false,
|
||||
fileContent,
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: true,
|
||||
remoteUserData: null
|
||||
remoteContent: content,
|
||||
remoteUserData,
|
||||
}));
|
||||
|
||||
await this.apply();
|
||||
await this.apply(undefined, true);
|
||||
}
|
||||
|
||||
// No local exists to push
|
||||
@@ -173,20 +162,15 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
}
|
||||
}
|
||||
|
||||
async sync(_continue?: boolean): Promise<boolean> {
|
||||
async sync(): Promise<void> {
|
||||
if (!this.configurationService.getValue<boolean>('sync.enableSettings')) {
|
||||
this.logService.trace('Settings: Skipping synchronizing settings as it is disabled.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_continue) {
|
||||
this.logService.info('Settings: Resumed synchronizing settings');
|
||||
return this.continueSync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status !== SyncStatus.Idle) {
|
||||
this.logService.trace('Settings: Skipping synchronizing settings as it is running already.');
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.trace('Settings: Started synchronizing settings...');
|
||||
@@ -194,26 +178,16 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
return this.doSync([]);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
async stop(): Promise<void> {
|
||||
if (this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
this.logService.info('Settings: Stopped synchronizing settings.');
|
||||
}
|
||||
this.fileService.del(this.environmentService.settingsSyncPreviewResource);
|
||||
await this.fileService.del(this.environmentService.settingsSyncPreviewResource);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
|
||||
async hasPreviouslySynced(): Promise<boolean> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
return !!lastSyncData;
|
||||
}
|
||||
|
||||
async hasRemoteData(): Promise<boolean> {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
return remoteUserData.content !== null;
|
||||
}
|
||||
|
||||
async hasLocalData(): Promise<boolean> {
|
||||
try {
|
||||
const localFileContent = await this.getLocalFileContent();
|
||||
@@ -233,7 +207,43 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
return false;
|
||||
}
|
||||
|
||||
async resolveConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void> {
|
||||
async getRemoteContent(): Promise<string | null> {
|
||||
let content: string | null | undefined = null;
|
||||
if (this.syncPreviewResultPromise) {
|
||||
const preview = await this.syncPreviewResultPromise;
|
||||
content = preview.remoteUserData?.content;
|
||||
} else {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
content = remoteUserData.content;
|
||||
}
|
||||
return content !== undefined ? content : null;
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.syncPreviewResultPromise!.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
await this.doSync([]);
|
||||
}
|
||||
}
|
||||
|
||||
async resolveConflicts(content: string): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
try {
|
||||
await this.apply(content, true);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
|
||||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
|
||||
throw new Error('New local version available.');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
this.syncPreviewResultPromise!.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
@@ -241,36 +251,30 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
}
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
try {
|
||||
await this.fileService.del(this.lastSyncSettingsResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
private async doSync(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<boolean> {
|
||||
private async doSync(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void> {
|
||||
try {
|
||||
const result = await this.getPreview(resolvedConflicts);
|
||||
if (result.conflicts.length) {
|
||||
if (result.hasConflicts) {
|
||||
this.logService.info('Settings: Detected conflicts while synchronizing settings.');
|
||||
this.setStatus(SyncStatus.HasConflicts);
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.apply();
|
||||
this.logService.trace('Settings: Finished synchronizing settings.');
|
||||
return true;
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
} catch (e) {
|
||||
this.syncPreviewResultPromise = null;
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.Rejected) {
|
||||
// Rejected as there is a new remote version. Syncing again,
|
||||
this.logService.info('Settings: Failed to synchronise settings as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
if (e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) {
|
||||
if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) ||
|
||||
(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
|
||||
// Rejected as there is a new local version. Syncing again.
|
||||
this.logService.info('Settings: Failed to synchronise settings as there is a new local version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
@@ -279,22 +283,20 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
}
|
||||
}
|
||||
|
||||
private async continueSync(): Promise<boolean> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
await this.apply();
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async apply(): Promise<void> {
|
||||
private async apply(content?: string, forcePush?: boolean): Promise<void> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.fileService.exists(this.environmentService.settingsSyncPreviewResource)) {
|
||||
const settingsPreivew = await this.fileService.readFile(this.environmentService.settingsSyncPreviewResource);
|
||||
const content = settingsPreivew.value.toString();
|
||||
if (content === undefined) {
|
||||
if (await this.fileService.exists(this.environmentService.settingsSyncPreviewResource)) {
|
||||
const settingsPreivew = await this.fileService.readFile(this.environmentService.settingsSyncPreviewResource);
|
||||
content = settingsPreivew.value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (content !== undefined) {
|
||||
|
||||
if (this.hasErrors(content)) {
|
||||
const error = new Error(localize('errorInvalidSettings', "Unable to sync settings. Please resolve conflicts without any errors/warnings and try again."));
|
||||
this.logService.error(error);
|
||||
@@ -307,18 +309,18 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
}
|
||||
if (hasLocalChanged) {
|
||||
this.logService.info('Settings: Updating local settings');
|
||||
await this.writeToLocal(content, fileContent);
|
||||
await this.updateLocalFileContent(content, fileContent);
|
||||
}
|
||||
if (hasRemoteChanged) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const remoteContent = remoteUserData?.content ? updateIgnoredSettings(content, remoteUserData.content, getIgnoredSettings(this.configurationService, content), formatUtils) : content;
|
||||
const remoteContent = updateIgnoredSettings(content, remoteUserData.content || '{}', getIgnoredSettings(this.configurationService, content), formatUtils);
|
||||
this.logService.info('Settings: Updating remote settings');
|
||||
const ref = await this.writeToRemote(remoteContent, remoteUserData ? remoteUserData.ref : null);
|
||||
const ref = await this.updateRemoteUserData(remoteContent, forcePush ? null : remoteUserData.ref);
|
||||
remoteUserData = { ref, content };
|
||||
}
|
||||
if (remoteUserData?.content) {
|
||||
if (remoteUserData.content) {
|
||||
this.logService.info('Settings: Updating last synchronised sttings');
|
||||
await this.updateLastSyncValue(remoteUserData);
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
}
|
||||
|
||||
// Delete the preview
|
||||
@@ -346,15 +348,16 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
private async generatePreview(resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise<ISyncPreviewResult> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncData);
|
||||
const remoteContent: string | null = remoteUserData.content;
|
||||
// Get file content last to get the latest
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let conflicts: IConflictSetting[] = [];
|
||||
let previewContent = null;
|
||||
let hasConflicts: boolean = false;
|
||||
let conflictSettings: IConflictSetting[] = [];
|
||||
let previewContent: string | null = null;
|
||||
let remoteContent: string | null = null;
|
||||
|
||||
if (remoteContent) {
|
||||
if (remoteUserData.content) {
|
||||
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
|
||||
|
||||
// No action when there are errors
|
||||
@@ -362,20 +365,16 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
this.logService.error('Settings: Unable to sync settings as there are errors/warning in settings file.');
|
||||
}
|
||||
|
||||
else if (!lastSyncData // First time sync
|
||||
|| lastSyncData.content !== localContent // Local has forwarded
|
||||
|| lastSyncData.content !== remoteContent // Remote has forwarded
|
||||
) {
|
||||
else {
|
||||
this.logService.trace('Settings: Merging remote settings with local settings...');
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const result = merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formatUtils);
|
||||
// Sync only if there are changes
|
||||
if (result.hasChanges) {
|
||||
hasLocalChanged = result.mergeContent !== localContent;
|
||||
hasRemoteChanged = result.mergeContent !== remoteContent;
|
||||
conflicts = result.conflicts;
|
||||
previewContent = result.mergeContent;
|
||||
}
|
||||
const result = merge(localContent, remoteUserData.content, lastSyncData ? lastSyncData.content : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formatUtils);
|
||||
hasConflicts = result.hasConflicts;
|
||||
hasLocalChanged = result.localContent !== null;
|
||||
hasRemoteChanged = result.remoteContent !== null;
|
||||
conflictSettings = result.conflictsSettings;
|
||||
remoteContent = result.remoteContent;
|
||||
previewContent = result.localContent || result.remoteContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,14 +383,15 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
this.logService.info('Settings: Remote settings does not exist. Synchronizing settings for the first time.');
|
||||
hasRemoteChanged = true;
|
||||
previewContent = fileContent.value.toString();
|
||||
remoteContent = fileContent.value.toString();
|
||||
}
|
||||
|
||||
if (previewContent && !token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(previewContent));
|
||||
}
|
||||
|
||||
this.setConflicts(conflicts);
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, conflicts };
|
||||
this.setConflicts(conflictSettings);
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, remoteContent, conflictSettings, hasConflicts };
|
||||
}
|
||||
|
||||
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
|
||||
@@ -402,46 +402,6 @@ export class SettingsSynchroniser extends AbstractSynchroniser implements ISetti
|
||||
return this._formattingOptions;
|
||||
}
|
||||
|
||||
private async getLastSyncUserData(): Promise<IUserData | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.lastSyncSettingsResource);
|
||||
return JSON.parse(content.value.toString());
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async getLocalFileContent(): Promise<IFileContent | null> {
|
||||
try {
|
||||
return await this.fileService.readFile(this.environmentService.settingsResource);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private getRemoteUserData(lastSyncData?: IUserData | null): Promise<IUserData> {
|
||||
return this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, lastSyncData || null);
|
||||
}
|
||||
|
||||
private async writeToRemote(content: string, ref: string | null): Promise<string> {
|
||||
return this.userDataSyncStoreService.write(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, content, ref);
|
||||
}
|
||||
|
||||
private async writeToLocal(newContent: string, oldContent: IFileContent | null): Promise<void> {
|
||||
if (oldContent) {
|
||||
// file exists already
|
||||
await this.backupLocal(oldContent.value);
|
||||
await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), oldContent);
|
||||
} else {
|
||||
// file does not exist
|
||||
await this.fileService.createFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), { overwrite: false });
|
||||
}
|
||||
}
|
||||
|
||||
private async updateLastSyncValue(remoteUserData: IUserData): Promise<void> {
|
||||
await this.fileService.writeFile(this.lastSyncSettingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function getIgnoredSettings(configurationService: IConfigurationService, settingsContent?: string): string[] {
|
||||
|
||||
@@ -4,16 +4,20 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAuthTokenService, IUserDataAutoSyncService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAuthTokenService, IUserDataAutoSyncService, IUserDataSyncUtilService, UserDataSyncError, UserDataSyncErrorCode, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
|
||||
export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private enabled: boolean = false;
|
||||
private successiveFailures: number = 0;
|
||||
|
||||
private readonly _onError: Emitter<{ code: UserDataSyncErrorCode, source?: SyncSource }> = this._register(new Emitter<{ code: UserDataSyncErrorCode, source?: SyncSource }>());
|
||||
readonly onError: Event<{ code: UserDataSyncErrorCode, source?: SyncSource }> = this._onError.event;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@@ -30,20 +34,21 @@ export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncSer
|
||||
}
|
||||
|
||||
private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise<void> {
|
||||
const enabled = await this.isSyncEnabled();
|
||||
const enabled = await this.isAutoSyncEnabled();
|
||||
if (this.enabled === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enabled = enabled;
|
||||
if (this.enabled) {
|
||||
this.logService.info('Syncing configuration started');
|
||||
this.logService.info('Auto sync started');
|
||||
this.sync(true, auto);
|
||||
return;
|
||||
} else {
|
||||
this.successiveFailures = 0;
|
||||
if (stopIfDisabled) {
|
||||
this.userDataSyncService.stop();
|
||||
this.logService.info('Syncing configuration stopped.');
|
||||
this.logService.info('Auto sync stopped.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,13 +64,23 @@ export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncSer
|
||||
await this.userDataSyncUtilService.updateConfigurationValue('sync.enable', false);
|
||||
return;
|
||||
}
|
||||
if (this.userDataSyncService.status !== SyncStatus.Idle) {
|
||||
this.logService.info('Skipped auto sync as sync is happening');
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.userDataSyncService.sync();
|
||||
this.successiveFailures = 0;
|
||||
} catch (e) {
|
||||
this.successiveFailures++;
|
||||
this.logService.error(e);
|
||||
this._onError.fire(e instanceof UserDataSyncError ? { code: e.code, source: e.source } : { code: UserDataSyncErrorCode.Unknown });
|
||||
}
|
||||
if (this.successiveFailures > 5) {
|
||||
this._onError.fire({ code: UserDataSyncErrorCode.TooManyFailures });
|
||||
}
|
||||
if (loop) {
|
||||
await timeout(1000 * 60 * 5); // Loop sync for every 5 min.
|
||||
await timeout(1000 * 60 * 5 * (this.successiveFailures + 1)); // Loop sync for every (successive failures count + 1) times 5 mins interval.
|
||||
this.sync(loop, true);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +92,7 @@ export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncSer
|
||||
return !hasRemote && hasPreviouslySynced;
|
||||
}
|
||||
|
||||
private async isSyncEnabled(): Promise<boolean> {
|
||||
private async isAutoSyncEnabled(): Promise<boolean> {
|
||||
return this.configurationService.getValue<boolean>('sync.enable')
|
||||
&& this.userDataSyncService.status !== SyncStatus.Uninitialized
|
||||
&& !!(await this.userDataAuthTokenService.getToken());
|
||||
|
||||
@@ -18,6 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
|
||||
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
|
||||
|
||||
@@ -122,15 +123,18 @@ export interface IUserData {
|
||||
content: string | null;
|
||||
}
|
||||
|
||||
export enum UserDataSyncStoreErrorCode {
|
||||
export enum UserDataSyncErrorCode {
|
||||
TooLarge = 'TooLarge',
|
||||
Unauthroized = 'Unauthroized',
|
||||
Rejected = 'Rejected',
|
||||
Unknown = 'Unknown'
|
||||
Unknown = 'Unknown',
|
||||
TooManyFailures = 'TooManyFailures',
|
||||
ConnectionRefused = 'ConnectionRefused'
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreError extends Error {
|
||||
export class UserDataSyncError extends Error {
|
||||
|
||||
constructor(message: string, public readonly code: UserDataSyncStoreErrorCode) {
|
||||
constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly source?: SyncSource) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@@ -138,22 +142,20 @@ export class UserDataSyncStoreError extends Error {
|
||||
|
||||
export interface IUserDataSyncStore {
|
||||
url: string;
|
||||
name: string;
|
||||
account: string;
|
||||
authenticationProviderId: string;
|
||||
}
|
||||
|
||||
export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined {
|
||||
const value = configurationService.getValue<IUserDataSyncStore>(CONFIGURATION_SYNC_STORE_KEY);
|
||||
return value && value.url && value.name && value.account && value.authenticationProviderId ? value : undefined;
|
||||
return value && value.url && value.authenticationProviderId ? value : undefined;
|
||||
}
|
||||
|
||||
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
|
||||
export interface IUserDataSyncStoreService {
|
||||
_serviceBrand: undefined;
|
||||
readonly userDataSyncStore: IUserDataSyncStore | undefined;
|
||||
read(key: string, oldValue: IUserData | null): Promise<IUserData>;
|
||||
write(key: string, content: string, ref: string | null): Promise<string>;
|
||||
read(key: string, oldValue: IUserData | null, source?: SyncSource): Promise<IUserData>;
|
||||
write(key: string, content: string, ref: string | null, source?: SyncSource): Promise<string>;
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -172,7 +174,7 @@ export const enum SyncSource {
|
||||
Settings = 'Settings',
|
||||
Keybindings = 'Keybindings',
|
||||
Extensions = 'Extensions',
|
||||
UIState = 'UI State'
|
||||
GlobalState = 'GlobalState'
|
||||
}
|
||||
|
||||
export const enum SyncStatus {
|
||||
@@ -188,14 +190,21 @@ export interface ISynchroniser {
|
||||
readonly onDidChangeLocal: Event<void>;
|
||||
pull(): Promise<void>;
|
||||
push(): Promise<void>;
|
||||
sync(_continue?: boolean): Promise<boolean>;
|
||||
stop(): void;
|
||||
sync(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
restart(): Promise<void>;
|
||||
hasPreviouslySynced(): Promise<boolean>
|
||||
hasRemoteData(): Promise<boolean>;
|
||||
hasLocalData(): Promise<boolean>;
|
||||
resetLocal(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IUserDataSynchroniser extends ISynchroniser {
|
||||
readonly source: SyncSource;
|
||||
getRemoteContent(): Promise<string | null>;
|
||||
resolveConflicts(content: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
|
||||
export interface IUserDataSyncService extends ISynchroniser {
|
||||
_serviceBrand: any;
|
||||
@@ -203,12 +212,14 @@ export interface IUserDataSyncService extends ISynchroniser {
|
||||
isFirstTimeSyncAndHasUserData(): Promise<boolean>;
|
||||
reset(): Promise<void>;
|
||||
resetLocal(): Promise<void>;
|
||||
removeExtension(identifier: IExtensionIdentifier): Promise<void>;
|
||||
getRemoteContent(source: SyncSource): Promise<string | null>;
|
||||
resolveConflictsAndContinueSync(content: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
|
||||
export interface IUserDataAutoSyncService {
|
||||
_serviceBrand: any;
|
||||
onError: Event<{ code: UserDataSyncErrorCode, source?: SyncSource }>;
|
||||
triggerAutoSync(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -218,7 +229,6 @@ export interface IUserDataSyncUtilService {
|
||||
updateConfigurationValue(key: string, value: any): Promise<void>;
|
||||
resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>>;
|
||||
resolveFormattingOptions(resource: URI): Promise<FormattingOptions>;
|
||||
ignoreExtensionsToSync(extensionIdentifiers: IExtensionIdentifier[]): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataAuthTokenService = createDecorator<IUserDataAuthTokenService>('IUserDataAuthTokenService');
|
||||
@@ -242,11 +252,19 @@ export interface IConflictSetting {
|
||||
}
|
||||
|
||||
export const ISettingsSyncService = createDecorator<ISettingsSyncService>('ISettingsSyncService');
|
||||
export interface ISettingsSyncService extends ISynchroniser {
|
||||
export interface ISettingsSyncService extends IUserDataSynchroniser {
|
||||
_serviceBrand: any;
|
||||
readonly onDidChangeConflicts: Event<IConflictSetting[]>;
|
||||
readonly conflicts: IConflictSetting[];
|
||||
resolveConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void>;
|
||||
resolveSettingsConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise<void>;
|
||||
}
|
||||
|
||||
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
|
||||
|
||||
export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync';
|
||||
export function toRemoteContentResource(source: SyncSource): URI {
|
||||
return URI.from({ scheme: USER_DATA_SYNC_SCHEME, path: `${source}/remoteContent` });
|
||||
}
|
||||
export function getSyncSourceFromRemoteContentResource(uri: URI): SyncSource | undefined {
|
||||
return [SyncSource.Settings, SyncSource.Keybindings, SyncSource.Extensions, SyncSource.GlobalState].filter(source => isEqual(uri, toRemoteContentResource(source)))[0];
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService, I
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import type { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
|
||||
export class UserDataSyncChannel implements IServerChannel {
|
||||
|
||||
@@ -25,18 +24,20 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'sync': return this.service.sync(args[0]);
|
||||
case 'sync': return this.service.sync();
|
||||
case 'resolveConflictsAndContinueSync': return this.service.resolveConflictsAndContinueSync(args[0]);
|
||||
case 'pull': return this.service.pull();
|
||||
case 'push': return this.service.push();
|
||||
case '_getInitialStatus': return Promise.resolve(this.service.status);
|
||||
case 'getConflictsSource': return Promise.resolve(this.service.conflictsSource);
|
||||
case 'removeExtension': return this.service.removeExtension(args[0]);
|
||||
case 'stop': this.service.stop(); return Promise.resolve();
|
||||
case 'restart': return this.service.restart().then(() => this.service.status);
|
||||
case 'reset': return this.service.reset();
|
||||
case 'resetLocal': return this.service.resetLocal();
|
||||
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
|
||||
case 'hasRemoteData': return this.service.hasRemoteData();
|
||||
case 'hasLocalData': return this.service.hasLocalData();
|
||||
case 'getRemoteContent': return this.service.getRemoteContent(args[0]);
|
||||
case 'isFirstTimeSyncAndHasUserData': return this.service.isFirstTimeSyncAndHasUserData();
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
@@ -58,9 +59,11 @@ export class SettingsSyncChannel implements IServerChannel {
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'sync': return this.service.sync(args[0]);
|
||||
case 'sync': return this.service.sync();
|
||||
case 'resolveConflicts': return this.service.resolveConflicts(args[0]);
|
||||
case 'pull': return this.service.pull();
|
||||
case 'push': return this.service.push();
|
||||
case 'restart': return this.service.restart().then(() => this.service.status);
|
||||
case '_getInitialStatus': return Promise.resolve(this.service.status);
|
||||
case '_getInitialConflicts': return Promise.resolve(this.service.conflicts);
|
||||
case 'stop': this.service.stop(); return Promise.resolve();
|
||||
@@ -68,7 +71,8 @@ export class SettingsSyncChannel implements IServerChannel {
|
||||
case 'hasPreviouslySynced': return this.service.hasPreviouslySynced();
|
||||
case 'hasRemoteData': return this.service.hasRemoteData();
|
||||
case 'hasLocalData': return this.service.hasLocalData();
|
||||
case 'resolveConflicts': return this.service.resolveConflicts(args[0]);
|
||||
case 'resolveSettingsConflicts': return this.service.resolveSettingsConflicts(args[0]);
|
||||
case 'getRemoteContent': return this.service.getRemoteContent();
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
@@ -79,6 +83,9 @@ export class UserDataAutoSyncChannel implements IServerChannel {
|
||||
constructor(private readonly service: IUserDataAutoSyncService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onError': return this.service.onError;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
@@ -122,7 +129,6 @@ export class UserDataSycnUtilServiceChannel implements IServerChannel {
|
||||
case 'resolveUserKeybindings': return this.service.resolveUserBindings(args[0]);
|
||||
case 'resolveFormattingOptions': return this.service.resolveFormattingOptions(URI.revive(args[0]));
|
||||
case 'updateConfigurationValue': return this.service.updateConfigurationValue(args[0], args[1]);
|
||||
case 'ignoreExtensionsToSync': return this.service.ignoreExtensionsToSync(args[0]);
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
@@ -147,9 +153,5 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
|
||||
return this.channel.call('updateConfigurationValue', [key, value]);
|
||||
}
|
||||
|
||||
async ignoreExtensionsToSync(extensionIdentifiers: IExtensionIdentifier[]): Promise<void> {
|
||||
return this.channel.call('ignoreExtensionsToSync', [extensionIdentifiers]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,22 +3,27 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSynchroniser } 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 { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync';
|
||||
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
|
||||
import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
type SyncConflictsClassification = {
|
||||
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly synchronisers: ISynchroniser[];
|
||||
private readonly synchronisers: IUserDataSynchroniser[];
|
||||
|
||||
private _status: SyncStatus = SyncStatus.Uninitialized;
|
||||
get status(): SyncStatus { return this._status; }
|
||||
@@ -40,6 +45,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
@ISettingsSyncService private readonly settingsSynchroniser: ISettingsSyncService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
) {
|
||||
super();
|
||||
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
|
||||
@@ -88,31 +94,57 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
}
|
||||
|
||||
async sync(_continue?: boolean): Promise<boolean> {
|
||||
async sync(): Promise<void> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
if (!(await this.userDataAuthTokenService.getToken())) {
|
||||
throw new Error('Not Authenticated. Please sign in to start sync.');
|
||||
}
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
throw new Error(localize('resolve conflicts', "Please resolve conflicts before resuming sync."));
|
||||
}
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
if (!await synchroniser.sync(_continue)) {
|
||||
return false;
|
||||
await synchroniser.sync();
|
||||
// do not continue if synchroniser has conflicts
|
||||
if (synchroniser.status === SyncStatus.HasConflicts) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
async resolveConflictsAndContinueSync(content: string): Promise<void> {
|
||||
const synchroniser = this.getSynchroniserInConflicts();
|
||||
if (!synchroniser) {
|
||||
throw new Error(localize('no synchroniser with conflicts', "No conflicts detected."));
|
||||
}
|
||||
await synchroniser.resolveConflicts(content);
|
||||
if (synchroniser.status !== SyncStatus.HasConflicts) {
|
||||
await this.sync();
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
synchroniser.stop();
|
||||
await synchroniser.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
const synchroniser = this.getSynchroniserInConflicts();
|
||||
if (!synchroniser) {
|
||||
throw new Error(localize('no synchroniser with conflicts', "No conflicts detected."));
|
||||
}
|
||||
await synchroniser.restart();
|
||||
if (synchroniser.status !== SyncStatus.HasConflicts) {
|
||||
await this.sync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +193,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return false;
|
||||
}
|
||||
|
||||
async getRemoteContent(source: SyncSource): Promise<string | null> {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (synchroniser.source === source) {
|
||||
return synchroniser.getRemoteContent();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async isFirstTimeSyncAndHasUserData(): Promise<boolean> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
@@ -211,18 +252,21 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.logService.info('Completed resetting local cache');
|
||||
}
|
||||
|
||||
removeExtension(identifier: IExtensionIdentifier): Promise<void> {
|
||||
return this.extensionsSynchroniser.removeExtension(identifier);
|
||||
}
|
||||
|
||||
private updateStatus(): void {
|
||||
this._conflictsSource = this.computeConflictsSource();
|
||||
this.setStatus(this.computeStatus());
|
||||
}
|
||||
|
||||
private setStatus(status: SyncStatus): void {
|
||||
const status = this.computeStatus();
|
||||
if (this._status !== status) {
|
||||
const oldStatus = this._status;
|
||||
const oldConflictsSource = this._conflictsSource;
|
||||
this._conflictsSource = this.computeConflictsSource();
|
||||
this._status = status;
|
||||
if (status === SyncStatus.HasConflicts) {
|
||||
// Log to telemetry when there is a sync conflict
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsDetected', { source: this._conflictsSource! });
|
||||
}
|
||||
if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) {
|
||||
// Log to telemetry when conflicts are resolved
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsResolved', { source: oldConflictsSource! });
|
||||
}
|
||||
this._onDidChangeStatus.fire(status);
|
||||
}
|
||||
}
|
||||
@@ -245,6 +289,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return synchroniser ? this.getSyncSource(synchroniser) : null;
|
||||
}
|
||||
|
||||
private getSynchroniserInConflicts(): IUserDataSynchroniser | null {
|
||||
const synchroniser = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0];
|
||||
return synchroniser || null;
|
||||
}
|
||||
|
||||
private getSyncSource(synchroniser: ISynchroniser): SyncSource {
|
||||
if (synchroniser instanceof SettingsSynchroniser) {
|
||||
return SyncSource.Settings;
|
||||
@@ -255,7 +304,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
if (synchroniser instanceof ExtensionsSynchroniser) {
|
||||
return SyncSource.Extensions;
|
||||
}
|
||||
return SyncSource.UIState;
|
||||
return SyncSource.GlobalState;
|
||||
}
|
||||
|
||||
private onDidChangeAuthTokenStatus(token: string | undefined): void {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, } from 'vs/base/common/lifecycle';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncStoreErrorCode, UserDataSyncStoreError, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IRequestService, asText, isSuccess } from 'vs/platform/request/common/request';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
@@ -27,7 +27,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
this.userDataSyncStore = getUserDataSyncStore(configurationService);
|
||||
}
|
||||
|
||||
async read(key: string, oldValue: IUserData | null): Promise<IUserData> {
|
||||
async read(key: string, oldValue: IUserData | null, source?: SyncSource): Promise<IUserData> {
|
||||
if (!this.userDataSyncStore) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
headers['If-None-Match'] = oldValue.ref;
|
||||
}
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
|
||||
const context = await this.request({ type: 'GET', url, headers }, source, CancellationToken.None);
|
||||
|
||||
if (context.res.statusCode === 304) {
|
||||
// There is no new value. Hence return the old value.
|
||||
@@ -59,7 +59,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
return { ref, content };
|
||||
}
|
||||
|
||||
async write(key: string, data: string, ref: string | null): Promise<string> {
|
||||
async write(key: string, data: string, ref: string | null, source?: SyncSource): Promise<string> {
|
||||
if (!this.userDataSyncStore) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
@@ -70,12 +70,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
headers['If-Match'] = ref;
|
||||
}
|
||||
|
||||
const context = await this.request({ type: 'POST', url, data, headers }, CancellationToken.None);
|
||||
|
||||
if (context.res.statusCode === 412) {
|
||||
// There is a new value. Throw Rejected Error
|
||||
throw new UserDataSyncStoreError('New data exists', UserDataSyncStoreErrorCode.Rejected);
|
||||
}
|
||||
const context = await this.request({ type: 'POST', url, data, headers }, source, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new Error('Server returned ' + context.res.statusCode);
|
||||
@@ -96,14 +91,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource').toString();
|
||||
const headers: IHeaders = { 'Content-Type': 'text/plain' };
|
||||
|
||||
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
|
||||
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new Error('Server returned ' + context.res.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
private async request(options: IRequestOptions, source: SyncSource | undefined, token: CancellationToken): Promise<IRequestContext> {
|
||||
const authToken = await this.authTokenService.getToken();
|
||||
if (!authToken) {
|
||||
throw new Error('No Auth Token Available.');
|
||||
@@ -111,15 +106,30 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
options.headers = options.headers || {};
|
||||
options.headers['authorization'] = `Bearer ${authToken}`;
|
||||
|
||||
const context = await this.requestService.request(options, token);
|
||||
let context;
|
||||
|
||||
try {
|
||||
context = await this.requestService.request(options, token);
|
||||
} catch (e) {
|
||||
throw new UserDataSyncError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, source);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 401) {
|
||||
// Throw Unauthorized Error
|
||||
throw new UserDataSyncStoreError('Unauthorized', UserDataSyncStoreErrorCode.Unauthroized);
|
||||
throw new UserDataSyncError(`Request '${options.url?.toString()}' is not authorized.`, UserDataSyncErrorCode.Unauthroized, source);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 412) {
|
||||
// There is a new value. Throw Rejected Error
|
||||
throw new UserDataSyncError(`${options.type} request '${options.url?.toString()}' failed with precondition. There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.Rejected, source);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 413) {
|
||||
// Throw Too Large Payload Error
|
||||
throw new UserDataSyncError(`${options.type} request '${options.url?.toString()}' failed because data is too large.`, UserDataSyncErrorCode.TooLarge, source);
|
||||
}
|
||||
|
||||
return context;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -438,11 +438,6 @@ suite('KeybindingsMerge - No Conflicts', () => {
|
||||
assert.equal(actual.mergeContent, expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
suite('KeybindingsMerge - Conflicts', () => {
|
||||
|
||||
test('merge when local and remote with one entry but different value', async () => {
|
||||
const localContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
|
||||
@@ -450,23 +445,13 @@ suite('KeybindingsMerge - Conflicts', () => {
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`<<<<<<< local
|
||||
[
|
||||
`[
|
||||
{
|
||||
"key": "alt+d",
|
||||
"command": "a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
}
|
||||
]
|
||||
=======
|
||||
[
|
||||
{
|
||||
"key": "alt+c",
|
||||
"command": "a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
}
|
||||
]
|
||||
>>>>>>> remote`);
|
||||
]`);
|
||||
});
|
||||
|
||||
test('merge when local and remote with different keybinding', async () => {
|
||||
@@ -482,8 +467,7 @@ suite('KeybindingsMerge - Conflicts', () => {
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`<<<<<<< local
|
||||
[
|
||||
`[
|
||||
{
|
||||
"key": "alt+d",
|
||||
"command": "a",
|
||||
@@ -494,21 +478,7 @@ suite('KeybindingsMerge - Conflicts', () => {
|
||||
"command": "-a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
}
|
||||
]
|
||||
=======
|
||||
[
|
||||
{
|
||||
"key": "alt+c",
|
||||
"command": "a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
},
|
||||
{
|
||||
"key": "alt+a",
|
||||
"command": "-a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
}
|
||||
]
|
||||
>>>>>>> remote`);
|
||||
]`);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in local but updated in remote', async () => {
|
||||
@@ -519,17 +489,7 @@ suite('KeybindingsMerge - Conflicts', () => {
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`<<<<<<< local
|
||||
[]
|
||||
=======
|
||||
[
|
||||
{
|
||||
"key": "alt+c",
|
||||
"command": "a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
}
|
||||
]
|
||||
>>>>>>> remote`);
|
||||
`[]`);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in local but updated in remote and a new entry is added in local', async () => {
|
||||
@@ -540,22 +500,12 @@ suite('KeybindingsMerge - Conflicts', () => {
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`<<<<<<< local
|
||||
[
|
||||
`[
|
||||
{
|
||||
"key": "alt+b",
|
||||
"command": "b"
|
||||
}
|
||||
]
|
||||
=======
|
||||
[
|
||||
{
|
||||
"key": "alt+c",
|
||||
"command": "a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
}
|
||||
]
|
||||
>>>>>>> remote`);
|
||||
]`);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in remote but updated in local', async () => {
|
||||
@@ -566,17 +516,13 @@ suite('KeybindingsMerge - Conflicts', () => {
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`<<<<<<< local
|
||||
[
|
||||
`[
|
||||
{
|
||||
"key": "alt+c",
|
||||
"command": "a",
|
||||
"when": "editorTextFocus && !editorReadonly"
|
||||
}
|
||||
]
|
||||
=======
|
||||
[]
|
||||
>>>>>>> remote`);
|
||||
]`);
|
||||
});
|
||||
|
||||
test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => {
|
||||
@@ -587,8 +533,7 @@ suite('KeybindingsMerge - Conflicts', () => {
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`<<<<<<< local
|
||||
[
|
||||
`[
|
||||
{
|
||||
"key": "alt+c",
|
||||
"command": "a",
|
||||
@@ -598,15 +543,7 @@ suite('KeybindingsMerge - Conflicts', () => {
|
||||
"key": "alt+b",
|
||||
"command": "b"
|
||||
}
|
||||
]
|
||||
=======
|
||||
[
|
||||
{
|
||||
"key": "alt+b",
|
||||
"command": "b"
|
||||
}
|
||||
]
|
||||
>>>>>>> remote`);
|
||||
]`);
|
||||
});
|
||||
|
||||
test('merge when local and remote has moved forwareded with conflicts', async () => {
|
||||
@@ -639,8 +576,7 @@ suite('KeybindingsMerge - Conflicts', () => {
|
||||
assert.ok(actual.hasChanges);
|
||||
assert.ok(actual.hasConflicts);
|
||||
assert.equal(actual.mergeContent,
|
||||
`<<<<<<< local
|
||||
[
|
||||
`[
|
||||
{
|
||||
"key": "alt+d",
|
||||
"command": "-f"
|
||||
@@ -671,42 +607,11 @@ suite('KeybindingsMerge - Conflicts', () => {
|
||||
"command": "g",
|
||||
"when": "context2"
|
||||
}
|
||||
]
|
||||
=======
|
||||
[
|
||||
{
|
||||
"key": "alt+a",
|
||||
"command": "f"
|
||||
},
|
||||
{
|
||||
"key": "cmd+c",
|
||||
"command": "-c"
|
||||
},
|
||||
{
|
||||
"key": "cmd+d",
|
||||
"command": "d"
|
||||
},
|
||||
{
|
||||
"key": "alt+d",
|
||||
"command": "-f"
|
||||
},
|
||||
{
|
||||
"key": "alt+c",
|
||||
"command": "c",
|
||||
"when": "context1"
|
||||
},
|
||||
{
|
||||
"key": "alt+g",
|
||||
"command": "g",
|
||||
"when": "context2"
|
||||
}
|
||||
]
|
||||
>>>>>>> remote`);
|
||||
]`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
async function mergeKeybindings(localContent: string, remoteContent: string, baseContent: string | null) {
|
||||
const userDataSyncUtilService = new MockUserDataSyncUtilService();
|
||||
const formattingOptions = await userDataSyncUtilService.resolveFormattingOptions();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
||||
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
|
||||
import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window';
|
||||
import { ipcMain as ipc, screen, BrowserWindow, MessageBoxOptions, Display, app, nativeTheme } from 'electron';
|
||||
import { ipcMain as ipc, screen, BrowserWindow, systemPreferences, MessageBoxOptions, Display, app } from 'electron';
|
||||
import { parseLineAndColumnAware } from 'vs/code/node/paths';
|
||||
import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -226,13 +226,16 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
|
||||
// React to HC color scheme changes (Windows)
|
||||
if (isWindows) {
|
||||
nativeTheme.on('updated', () => {
|
||||
if (nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors) {
|
||||
const onHighContrastChange = () => {
|
||||
if (systemPreferences.isInvertedColorScheme() || systemPreferences.isHighContrastColorScheme()) {
|
||||
this.sendToAll('vscode:enterHighContrast');
|
||||
} else {
|
||||
this.sendToAll('vscode:leaveHighContrast');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
systemPreferences.on('inverted-color-scheme-changed', () => onHighContrastChange());
|
||||
systemPreferences.on('high-contrast-color-scheme-changed', () => onHighContrastChange());
|
||||
}
|
||||
|
||||
// When a window looses focus, save all windows state. This allows to
|
||||
|
||||
Reference in New Issue
Block a user