mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-10 02:02:35 -05:00
Merge from vscode 718331d6f3ebd1b571530ab499edb266ddd493d5
This commit is contained in:
@@ -8,7 +8,7 @@ import { SyncDescriptor0, createSyncDescriptor } from 'vs/platform/instantiation
|
||||
import { IConstructorSignature2, createDecorator, BrandedService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindings, KeybindingsRegistry, IKeybindingRule } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { ICommandService, CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands';
|
||||
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
@@ -56,66 +56,78 @@ export function isISubmenuItem(item: IMenuItem | ISubmenuItem): item is ISubmenu
|
||||
return (item as ISubmenuItem).submenu !== undefined;
|
||||
}
|
||||
|
||||
export const enum MenuId {
|
||||
CommandPalette,
|
||||
DebugBreakpointsContext,
|
||||
DebugCallStackContext,
|
||||
DebugConsoleContext,
|
||||
DebugVariablesContext,
|
||||
DebugWatchContext,
|
||||
DebugToolBar,
|
||||
EditorContext,
|
||||
EditorContextPeek,
|
||||
EditorTitle,
|
||||
EditorTitleContext,
|
||||
EmptyEditorGroupContext,
|
||||
ExplorerContext,
|
||||
ExtensionContext,
|
||||
GlobalActivity,
|
||||
MenubarAppearanceMenu,
|
||||
MenubarDebugMenu,
|
||||
MenubarEditMenu,
|
||||
MenubarFileMenu,
|
||||
MenubarGoMenu,
|
||||
MenubarHelpMenu,
|
||||
MenubarLayoutMenu,
|
||||
MenubarNewBreakpointMenu,
|
||||
MenubarPreferencesMenu,
|
||||
MenubarRecentMenu,
|
||||
MenubarSelectionMenu,
|
||||
MenubarSwitchEditorMenu,
|
||||
MenubarSwitchGroupMenu,
|
||||
MenubarTerminalMenu,
|
||||
MenubarViewMenu,
|
||||
OpenEditorsContext,
|
||||
ProblemsPanelContext,
|
||||
SCMChangeContext,
|
||||
SCMResourceContext,
|
||||
SCMResourceFolderContext,
|
||||
SCMResourceGroupContext,
|
||||
SCMSourceControl,
|
||||
SCMTitle,
|
||||
SearchContext,
|
||||
StatusBarWindowIndicatorMenu,
|
||||
TouchBarContext,
|
||||
TitleBarContext,
|
||||
TunnelContext,
|
||||
TunnelInline,
|
||||
TunnelTitle,
|
||||
ViewItemContext,
|
||||
ViewTitle,
|
||||
ObjectExplorerItemContext, // {{SQL CARBON EDIT}}
|
||||
NotebookToolbar, // {{SQL CARBON EDIT}}
|
||||
DataExplorerContext, // {{SQL CARBON EDIT}}
|
||||
DataExplorerAction, // {{SQL CARBON EDIT}}
|
||||
ExplorerWidgetContext, // {{SQL CARBON EDIT}}
|
||||
ViewTitleContext,
|
||||
CommentThreadTitle,
|
||||
CommentThreadActions,
|
||||
CommentTitle,
|
||||
CommentActions,
|
||||
BulkEditTitle,
|
||||
BulkEditContext,
|
||||
export class MenuId {
|
||||
|
||||
private static _idPool = 0;
|
||||
|
||||
static readonly CommandPalette = new MenuId('CommandPalette');
|
||||
static readonly DebugBreakpointsContext = new MenuId('DebugBreakpointsContext');
|
||||
static readonly DebugCallStackContext = new MenuId('DebugCallStackContext');
|
||||
static readonly DebugConsoleContext = new MenuId('DebugConsoleContext');
|
||||
static readonly DebugVariablesContext = new MenuId('DebugVariablesContext');
|
||||
static readonly DebugWatchContext = new MenuId('DebugWatchContext');
|
||||
static readonly DebugToolBar = new MenuId('DebugToolBar');
|
||||
static readonly EditorContext = new MenuId('EditorContext');
|
||||
static readonly EditorContextPeek = new MenuId('EditorContextPeek');
|
||||
static readonly EditorTitle = new MenuId('EditorTitle');
|
||||
static readonly EditorTitleContext = new MenuId('EditorTitleContext');
|
||||
static readonly EmptyEditorGroupContext = new MenuId('EmptyEditorGroupContext');
|
||||
static readonly ExplorerContext = new MenuId('ExplorerContext');
|
||||
static readonly ExtensionContext = new MenuId('ExtensionContext');
|
||||
static readonly GlobalActivity = new MenuId('GlobalActivity');
|
||||
static readonly MenubarAppearanceMenu = new MenuId('MenubarAppearanceMenu');
|
||||
static readonly MenubarDebugMenu = new MenuId('MenubarDebugMenu');
|
||||
static readonly MenubarEditMenu = new MenuId('MenubarEditMenu');
|
||||
static readonly MenubarFileMenu = new MenuId('MenubarFileMenu');
|
||||
static readonly MenubarGoMenu = new MenuId('MenubarGoMenu');
|
||||
static readonly MenubarHelpMenu = new MenuId('MenubarHelpMenu');
|
||||
static readonly MenubarLayoutMenu = new MenuId('MenubarLayoutMenu');
|
||||
static readonly MenubarNewBreakpointMenu = new MenuId('MenubarNewBreakpointMenu');
|
||||
static readonly MenubarPreferencesMenu = new MenuId('MenubarPreferencesMenu');
|
||||
static readonly MenubarRecentMenu = new MenuId('MenubarRecentMenu');
|
||||
static readonly MenubarSelectionMenu = new MenuId('MenubarSelectionMenu');
|
||||
static readonly MenubarSwitchEditorMenu = new MenuId('MenubarSwitchEditorMenu');
|
||||
static readonly MenubarSwitchGroupMenu = new MenuId('MenubarSwitchGroupMenu');
|
||||
static readonly MenubarTerminalMenu = new MenuId('MenubarTerminalMenu');
|
||||
static readonly MenubarViewMenu = new MenuId('MenubarViewMenu');
|
||||
static readonly OpenEditorsContext = new MenuId('OpenEditorsContext');
|
||||
static readonly ProblemsPanelContext = new MenuId('ProblemsPanelContext');
|
||||
static readonly SCMChangeContext = new MenuId('SCMChangeContext');
|
||||
static readonly SCMResourceContext = new MenuId('SCMResourceContext');
|
||||
static readonly SCMResourceFolderContext = new MenuId('SCMResourceFolderContext');
|
||||
static readonly SCMResourceGroupContext = new MenuId('SCMResourceGroupContext');
|
||||
static readonly SCMSourceControl = new MenuId('SCMSourceControl');
|
||||
static readonly SCMTitle = new MenuId('SCMTitle');
|
||||
static readonly SearchContext = new MenuId('SearchContext');
|
||||
static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu');
|
||||
static readonly TouchBarContext = new MenuId('TouchBarContext');
|
||||
static readonly TitleBarContext = new MenuId('TitleBarContext');
|
||||
static readonly TunnelContext = new MenuId('TunnelContext');
|
||||
static readonly TunnelInline = new MenuId('TunnelInline');
|
||||
static readonly TunnelTitle = new MenuId('TunnelTitle');
|
||||
static readonly ViewItemContext = new MenuId('ViewItemContext');
|
||||
static readonly ViewTitle = new MenuId('ViewTitle');
|
||||
static readonly ViewTitleContext = new MenuId('ViewTitleContext');
|
||||
static readonly CommentThreadTitle = new MenuId('CommentThreadTitle');
|
||||
static readonly CommentThreadActions = new MenuId('CommentThreadActions');
|
||||
static readonly CommentTitle = new MenuId('CommentTitle');
|
||||
static readonly CommentActions = new MenuId('CommentActions');
|
||||
static readonly BulkEditTitle = new MenuId('BulkEditTitle');
|
||||
static readonly BulkEditContext = new MenuId('BulkEditContext');
|
||||
static readonly ObjectExplorerItemContext = new MenuId('ObjectExplorerItemContext'); // {{SQL CARBON EDIT}}
|
||||
static readonly NotebookToolbar = new MenuId('NotebookToolbar'); // {{SQL CARBON EDIT}}
|
||||
static readonly DataExplorerContext = new MenuId('DataExplorerContext'); // {{SQL CARBON EDIT}}
|
||||
static readonly DataExplorerAction = new MenuId('DataExplorerAction'); // {{SQL CARBON EDIT}}
|
||||
static readonly ExplorerWidgetContext = new MenuId('ExplorerWidgetContext'); // {{SQL CARBON EDIT}}
|
||||
|
||||
|
||||
readonly id: number;
|
||||
readonly _debugName: string;
|
||||
|
||||
constructor(debugName: string) {
|
||||
this.id = MenuId._idPool++;
|
||||
this._debugName = debugName;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IMenuActionOptions {
|
||||
@@ -151,7 +163,7 @@ export interface IMenuRegistry {
|
||||
export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
|
||||
|
||||
private readonly _commands = new Map<string, ICommandAction>();
|
||||
private readonly _menuItems = new Map<number, Array<IMenuItem | ISubmenuItem>>();
|
||||
private readonly _menuItems = new Map<MenuId, Array<IMenuItem | ISubmenuItem>>();
|
||||
private readonly _onDidChangeMenu = new Emitter<MenuId>();
|
||||
|
||||
readonly onDidChangeMenu: Event<MenuId> = this._onDidChangeMenu.event;
|
||||
@@ -356,9 +368,27 @@ export class SyncActionDescriptor {
|
||||
type OneOrN<T> = T | T[];
|
||||
|
||||
export interface IAction2Options extends ICommandAction {
|
||||
|
||||
/**
|
||||
* Shorthand to add this command to the command palette
|
||||
*/
|
||||
f1?: boolean;
|
||||
|
||||
/**
|
||||
* One or many menu items.
|
||||
*/
|
||||
menu?: OneOrN<{ id: MenuId } & Omit<IMenuItem, 'command'>>;
|
||||
keybinding?: Omit<IKeybindingRule, 'id'>;
|
||||
|
||||
/**
|
||||
* One keybinding.
|
||||
*/
|
||||
keybinding?: OneOrN<Omit<IKeybindingRule, 'id'>>;
|
||||
|
||||
/**
|
||||
* Metadata about this command, used for API commands or when
|
||||
* showing keybindings that have no other UX.
|
||||
*/
|
||||
description?: ICommandHandlerDescription;
|
||||
}
|
||||
|
||||
export abstract class Action2 {
|
||||
@@ -369,12 +399,15 @@ export abstract class Action2 {
|
||||
export function registerAction2(ctor: { new(): Action2 }): IDisposable {
|
||||
const disposables = new DisposableStore();
|
||||
const action = new ctor();
|
||||
|
||||
// command
|
||||
disposables.add(CommandsRegistry.registerCommand({
|
||||
id: action.desc.id,
|
||||
handler: (accessor, ...args) => action.run(accessor, ...args),
|
||||
description: undefined,
|
||||
description: action.desc.description,
|
||||
}));
|
||||
|
||||
// menu
|
||||
if (Array.isArray(action.desc.menu)) {
|
||||
for (let item of action.desc.menu) {
|
||||
disposables.add(MenuRegistry.appendMenuItem(item.id, { command: action.desc, ...item }));
|
||||
@@ -382,12 +415,20 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable {
|
||||
} else if (action.desc.menu) {
|
||||
disposables.add(MenuRegistry.appendMenuItem(action.desc.menu.id, { command: action.desc, ...action.desc.menu }));
|
||||
}
|
||||
|
||||
if (action.desc.f1) {
|
||||
disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: action.desc, ...action.desc }));
|
||||
}
|
||||
|
||||
if (action.desc.keybinding) {
|
||||
// keybinding
|
||||
if (Array.isArray(action.desc.keybinding)) {
|
||||
for (let item of action.desc.keybinding) {
|
||||
KeybindingsRegistry.registerKeybindingRule({
|
||||
...item,
|
||||
id: action.desc.id,
|
||||
when: ContextKeyExpr.and(action.desc.precondition, item.when)
|
||||
});
|
||||
}
|
||||
} else if (action.desc.keybinding) {
|
||||
KeybindingsRegistry.registerKeybindingRule({
|
||||
...action.desc.keybinding,
|
||||
id: action.desc.id,
|
||||
|
||||
@@ -28,7 +28,7 @@ suite('MenuService', function () {
|
||||
|
||||
setup(function () {
|
||||
menuService = new MenuService(NullCommandService);
|
||||
testMenuId = Math.PI;
|
||||
testMenuId = new MenuId('testo');
|
||||
disposables.clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export class ConfigurationModel implements IConfigurationModel {
|
||||
contents[key] = contentsForKey;
|
||||
}
|
||||
|
||||
return new ConfigurationModel(contents);
|
||||
return new ConfigurationModel(contents, this.keys, this.overrides);
|
||||
}
|
||||
|
||||
merge(...others: ConfigurationModel[]): ConfigurationModel {
|
||||
@@ -516,7 +516,7 @@ export class Configuration {
|
||||
const currentFolderConfiguration = this.folderConfigurations.get(resource);
|
||||
const { added, updated, removed, overrides } = compare(currentFolderConfiguration, folderConfiguration);
|
||||
let keys = [...added, ...updated, ...removed];
|
||||
if (keys.length) {
|
||||
if (keys.length || !currentFolderConfiguration) {
|
||||
this.updateFolderConfiguration(resource, folderConfiguration);
|
||||
}
|
||||
return { keys, overrides };
|
||||
|
||||
@@ -394,7 +394,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
const resourceLanguagePropertiesSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
description: nls.localize('overrideSettings.defaultDescription', "Configure editor settings to be overridden for a language."),
|
||||
errorMessage: 'Unknown Identifier. Use language identifiers',
|
||||
errorMessage: nls.localize('overrideSettings.errorMessage', "This setting does not support per-language configuration."),
|
||||
$ref: resourceLanguageSettingsSchemaId,
|
||||
default: this.defaultOverridesConfigurationNode.properties![overrideIdentifierProperty]?.default
|
||||
};
|
||||
|
||||
@@ -561,7 +561,7 @@ export class ContextKeyNotRegexExpr implements ContextKeyExpr {
|
||||
|
||||
export class ContextKeyAndExpr implements ContextKeyExpr {
|
||||
|
||||
public static create(_expr: Array<ContextKeyExpr | null | undefined>): ContextKeyExpr | undefined {
|
||||
public static create(_expr: ReadonlyArray<ContextKeyExpr | null | undefined>): ContextKeyExpr | undefined {
|
||||
const expr = ContextKeyAndExpr._normalizeArr(_expr);
|
||||
if (expr.length === 0) {
|
||||
return undefined;
|
||||
@@ -621,32 +621,29 @@ export class ContextKeyAndExpr implements ContextKeyExpr {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _normalizeArr(arr: Array<ContextKeyExpr | null | undefined>): ContextKeyExpr[] {
|
||||
let expr: ContextKeyExpr[] = [];
|
||||
private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpr | null | undefined>): ContextKeyExpr[] {
|
||||
const expr: ContextKeyExpr[] = [];
|
||||
|
||||
if (arr) {
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
let e: ContextKeyExpr | null | undefined = arr[i];
|
||||
if (!e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e instanceof ContextKeyAndExpr) {
|
||||
expr = expr.concat(e.expr);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e instanceof ContextKeyOrExpr) {
|
||||
// Not allowed, because we don't have parens!
|
||||
throw new Error(`It is not allowed to have an or expression here due to lack of parens! For example "a && (b||c)" is not supported, use "(a&&b) || (a&&c)" instead.`);
|
||||
}
|
||||
|
||||
expr.push(e);
|
||||
for (const e of arr) {
|
||||
if (!e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
expr.sort(cmp);
|
||||
if (e instanceof ContextKeyAndExpr) {
|
||||
expr.push(...e.expr);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e instanceof ContextKeyOrExpr) {
|
||||
// Not allowed, because we don't have parens!
|
||||
throw new Error(`It is not allowed to have an or expression here due to lack of parens! For example "a && (b||c)" is not supported, use "(a&&b) || (a&&c)" instead.`);
|
||||
}
|
||||
|
||||
expr.push(e);
|
||||
}
|
||||
|
||||
expr.sort(cmp);
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
@@ -677,7 +674,7 @@ export class ContextKeyAndExpr implements ContextKeyExpr {
|
||||
|
||||
export class ContextKeyOrExpr implements ContextKeyExpr {
|
||||
|
||||
public static create(_expr: Array<ContextKeyExpr | null | undefined>): ContextKeyExpr | undefined {
|
||||
public static create(_expr: ReadonlyArray<ContextKeyExpr | null | undefined>): ContextKeyExpr | undefined {
|
||||
const expr = ContextKeyOrExpr._normalizeArr(_expr);
|
||||
if (expr.length === 0) {
|
||||
return undefined;
|
||||
@@ -721,7 +718,7 @@ export class ContextKeyOrExpr implements ContextKeyExpr {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static _normalizeArr(arr: Array<ContextKeyExpr | null | undefined>): ContextKeyExpr[] {
|
||||
private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpr | null | undefined>): ContextKeyExpr[] {
|
||||
let expr: ContextKeyExpr[] = [];
|
||||
|
||||
if (arr) {
|
||||
|
||||
@@ -173,7 +173,7 @@ export class DialogMainService implements IDialogMainService {
|
||||
|
||||
showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise<OpenDialogReturnValue> {
|
||||
|
||||
function normalizePaths(paths: string[] | undefined): string[] | undefined {
|
||||
function normalizePaths(paths: string[]): string[] {
|
||||
if (paths && paths.length > 0 && isMacintosh) {
|
||||
paths = paths.map(path => normalizeNFC(path)); // normalize paths returned from the OS
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ 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';
|
||||
|
||||
@@ -67,7 +66,7 @@ export class Driver implements IDriver, IWindowDriverRegistry {
|
||||
throw new Error('Invalid window');
|
||||
}
|
||||
const webContents = window.win.webContents;
|
||||
const image = await new Promise<NativeImage>(c => webContents.capturePage(c));
|
||||
const image = await webContents.capturePage();
|
||||
return image.toPNG().toString('base64');
|
||||
}
|
||||
|
||||
|
||||
@@ -215,6 +215,21 @@ export interface ITextEditorSelection {
|
||||
readonly endColumn?: number;
|
||||
}
|
||||
|
||||
export const enum TextEditorSelectionRevealType {
|
||||
/**
|
||||
* Option to scroll vertically or horizontally as necessary and reveal a range centered vertically.
|
||||
*/
|
||||
Center = 0,
|
||||
/**
|
||||
* Option to scroll vertically or horizontally as necessary and reveal a range centered vertically only if it lies outside the viewport.
|
||||
*/
|
||||
CenterIfOutsideViewport = 1,
|
||||
/**
|
||||
* Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top.
|
||||
*/
|
||||
NearTop = 2,
|
||||
}
|
||||
|
||||
export interface ITextEditorOptions extends IEditorOptions {
|
||||
|
||||
/**
|
||||
@@ -228,7 +243,8 @@ export interface ITextEditorOptions extends IEditorOptions {
|
||||
readonly viewState?: object;
|
||||
|
||||
/**
|
||||
* Option to scroll vertically or horizontally as necessary and reveal a range centered vertically only if it lies outside the viewport.
|
||||
* Option to control the text editor selection reveal type.
|
||||
* Defaults to TextEditorSelectionRevealType.Center
|
||||
*/
|
||||
readonly revealInCenterIfOutsideViewport?: boolean;
|
||||
readonly selectionRevealType?: TextEditorSelectionRevealType;
|
||||
}
|
||||
|
||||
@@ -157,15 +157,6 @@ export class ElectronMainService implements IElectronMainService {
|
||||
}
|
||||
}
|
||||
|
||||
async isWindowFocused(windowId: number | undefined): Promise<boolean> {
|
||||
const window = this.windowById(windowId);
|
||||
if (window) {
|
||||
return window.win.isFocused();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async focusWindow(windowId: number | undefined, options?: { windowId?: number; }): Promise<void> {
|
||||
if (options && typeof options.windowId === 'number') {
|
||||
windowId = options.windowId;
|
||||
@@ -367,15 +358,13 @@ export class ElectronMainService implements IElectronMainService {
|
||||
//#region Connectivity
|
||||
|
||||
async resolveProxy(windowId: number | undefined, url: string): Promise<string | 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();
|
||||
}
|
||||
});
|
||||
const window = this.windowById(windowId);
|
||||
const session = window?.win?.webContents?.session;
|
||||
if (session) {
|
||||
return session.resolveProxy(url);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -43,7 +43,6 @@ export interface IElectronService {
|
||||
unmaximizeWindow(): Promise<void>;
|
||||
minimizeWindow(): Promise<void>;
|
||||
|
||||
isWindowFocused(): Promise<boolean>;
|
||||
focusWindow(options?: { windowId?: number }): Promise<void>;
|
||||
|
||||
// Dialogs
|
||||
|
||||
@@ -14,8 +14,8 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private _onDidChangeEnablement = new Emitter<readonly IExtensionIdentifier[]>();
|
||||
readonly onDidChangeEnablement: Event<readonly IExtensionIdentifier[]> = this._onDidChangeEnablement.event;
|
||||
private _onDidChangeEnablement = new Emitter<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }>();
|
||||
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }> = this._onDidChangeEnablement.event;
|
||||
private readonly storageManger: StorageManager;
|
||||
|
||||
constructor(
|
||||
@@ -23,18 +23,20 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo
|
||||
) {
|
||||
super();
|
||||
this.storageManger = this._register(new StorageManager(storageService));
|
||||
this._register(this.storageManger.onDidChange(extensions => this._onDidChangeEnablement.fire(extensions)));
|
||||
this._register(this.storageManger.onDidChange(extensions => this._onDidChangeEnablement.fire({ extensions, source: 'storage' })));
|
||||
}
|
||||
|
||||
async enableExtension(extension: IExtensionIdentifier): Promise<boolean> {
|
||||
async enableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean> {
|
||||
if (this._removeFromDisabledExtensions(extension)) {
|
||||
this._onDidChangeEnablement.fire({ extensions: [extension], source });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async disableExtension(extension: IExtensionIdentifier): Promise<boolean> {
|
||||
async disableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean> {
|
||||
if (this._addToDisabledExtensions(extension)) {
|
||||
this._onDidChangeEnablement.fire({ extensions: [extension], source });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -217,11 +217,11 @@ export const IGlobalExtensionEnablementService = createDecorator<IGlobalExtensio
|
||||
|
||||
export interface IGlobalExtensionEnablementService {
|
||||
_serviceBrand: undefined;
|
||||
readonly onDidChangeEnablement: Event<readonly IExtensionIdentifier[]>;
|
||||
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }>;
|
||||
|
||||
getDisabledExtensions(): IExtensionIdentifier[];
|
||||
enableExtension(extension: IExtensionIdentifier): Promise<boolean>;
|
||||
disableExtension(extension: IExtensionIdentifier): Promise<boolean>;
|
||||
enableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean>;
|
||||
disableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean>;
|
||||
|
||||
// Async method until storage service is available in shared process
|
||||
getDisabledExtensionsAsync(): Promise<IExtensionIdentifier[]>;
|
||||
|
||||
@@ -156,7 +156,7 @@ export class GlobalExtensionEnablementServiceClient implements IGlobalExtensionE
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
get onDidChangeEnablement(): Event<IExtensionIdentifier[]> { return this.channel.listen('onDidChangeEnablement'); }
|
||||
get onDidChangeEnablement(): Event<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }> { return this.channel.listen('onDidChangeEnablement'); }
|
||||
|
||||
constructor(private readonly channel: IChannel) {
|
||||
}
|
||||
|
||||
@@ -24,4 +24,4 @@ suite('Extension Identifier Pattern', () => {
|
||||
assert.equal(false, regEx.test('publ_isher.name'));
|
||||
assert.equal(false, regEx.test('publisher._name'));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -752,7 +752,22 @@ export class FileService extends Disposable implements IFileService {
|
||||
// Create directories as needed
|
||||
for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
|
||||
directory = joinPath(directory, directoriesToCreate[i]);
|
||||
await provider.mkdir(directory);
|
||||
|
||||
try {
|
||||
await provider.mkdir(directory);
|
||||
} catch (error) {
|
||||
if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileExists) {
|
||||
// For mkdirp() we tolerate that the mkdir() call fails
|
||||
// in case the folder already exists. This follows node.js
|
||||
// own implementation of fs.mkdir({ recursive: true }) and
|
||||
// reduces the chances of race conditions leading to errors
|
||||
// if multiple calls try to create the same folders
|
||||
// As such, we only throw an error here if it is other than
|
||||
// the fact that the file already exists.
|
||||
// (see also https://github.com/microsoft/vscode/issues/89834)
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -804,7 +804,7 @@ export interface IFilesConfiguration {
|
||||
eol: string;
|
||||
enableTrash: boolean;
|
||||
hotExit: string;
|
||||
preventSaveConflicts: boolean;
|
||||
saveConflictResolution: 'askUser' | 'overwriteFileOnDisk';
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -76,10 +76,10 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
async stat(resource: URI): Promise<IStat> {
|
||||
try {
|
||||
const { stat, isSymbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
|
||||
const { stat, symbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
|
||||
|
||||
return {
|
||||
type: this.toType(stat, isSymbolicLink),
|
||||
type: this.toType(stat, symbolicLink),
|
||||
ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time
|
||||
mtime: stat.mtime.getTime(),
|
||||
size: stat.size
|
||||
@@ -115,12 +115,28 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
}
|
||||
}
|
||||
|
||||
private toType(entry: Stats | Dirent, isSymbolicLink = entry.isSymbolicLink()): FileType {
|
||||
if (isSymbolicLink) {
|
||||
return FileType.SymbolicLink | (entry.isDirectory() ? FileType.Directory : FileType.File);
|
||||
private toType(entry: Stats | Dirent, symbolicLink?: { dangling: boolean }): FileType {
|
||||
|
||||
// Signal file type by checking for file / directory, except:
|
||||
// - symbolic links pointing to non-existing files are FileType.Unknown
|
||||
// - files that are neither file nor directory are FileType.Unknown
|
||||
let type: FileType;
|
||||
if (symbolicLink?.dangling) {
|
||||
type = FileType.Unknown;
|
||||
} else if (entry.isFile()) {
|
||||
type = FileType.File;
|
||||
} else if (entry.isDirectory()) {
|
||||
type = FileType.Directory;
|
||||
} else {
|
||||
type = FileType.Unknown;
|
||||
}
|
||||
|
||||
return entry.isFile() ? FileType.File : entry.isDirectory() ? FileType.Directory : FileType.Unknown;
|
||||
// Always signal symbolic link as file type additionally
|
||||
if (symbolicLink) {
|
||||
type |= FileType.SymbolicLink;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -35,14 +35,14 @@ export class FileWatcher extends Disposable {
|
||||
|
||||
private async startWatching(): Promise<void> {
|
||||
try {
|
||||
const { stat, isSymbolicLink } = await statLink(this.path);
|
||||
const { stat, symbolicLink } = await statLink(this.path);
|
||||
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pathToWatch = this.path;
|
||||
if (isSymbolicLink) {
|
||||
if (symbolicLink) {
|
||||
try {
|
||||
pathToWatch = await realpath(pathToWatch);
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { isEqual, isEqualOrParent } from 'vs/base/common/extpath';
|
||||
import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files';
|
||||
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||
// eslint-disable-next-line code-import-patterns
|
||||
import { toResource } from 'vs/base/test/common/utils';
|
||||
|
||||
suite('Files', () => {
|
||||
@@ -19,7 +19,7 @@ import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, File
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { isLinux, isWindows } from 'vs/base/common/platform';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { isEqual, joinPath } from 'vs/base/common/resources';
|
||||
import { VSBuffer, VSBufferReadable, streamToBufferReadableStream, VSBufferReadableStream, bufferToReadable, bufferToStream, streamToBuffer } from 'vs/base/common/buffer';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
|
||||
@@ -440,17 +440,22 @@ suite('Disk File Service', function () {
|
||||
assert.equal(resolved.isSymbolicLink, true);
|
||||
});
|
||||
|
||||
test('resolve - invalid symbolic link does not break', async () => {
|
||||
test('resolve - symbolic link pointing to non-existing file does not break', async () => {
|
||||
if (isWindows) {
|
||||
return; // not reliable on windows
|
||||
}
|
||||
|
||||
const link = URI.file(join(testDir, 'foo'));
|
||||
await symlink(link.fsPath, join(testDir, 'bar'));
|
||||
await symlink(join(testDir, 'foo'), join(testDir, 'bar'));
|
||||
|
||||
const resolved = await service.resolve(URI.file(testDir));
|
||||
assert.equal(resolved.isDirectory, true);
|
||||
assert.equal(resolved.children!.length, 8);
|
||||
assert.equal(resolved.children!.length, 9);
|
||||
|
||||
const resolvedLink = resolved.children?.filter(child => child.name === 'bar' && child.isSymbolicLink)[0];
|
||||
assert.ok(resolvedLink);
|
||||
|
||||
assert.ok(!resolvedLink?.isDirectory);
|
||||
assert.ok(!resolvedLink?.isFile);
|
||||
});
|
||||
|
||||
test('deleteFile', async () => {
|
||||
@@ -479,6 +484,52 @@ suite('Disk File Service', function () {
|
||||
assert.equal((<FileOperationError>error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('deleteFile - symbolic link (exists)', async () => {
|
||||
if (isWindows) {
|
||||
return; // not reliable on windows
|
||||
}
|
||||
|
||||
const target = URI.file(join(testDir, 'lorem.txt'));
|
||||
const link = URI.file(join(testDir, 'lorem.txt-linked'));
|
||||
await symlink(target.fsPath, link.fsPath);
|
||||
|
||||
const source = await service.resolve(link);
|
||||
|
||||
let event: FileOperationEvent;
|
||||
disposables.add(service.onAfterOperation(e => event = e));
|
||||
|
||||
await service.del(source.resource);
|
||||
|
||||
assert.equal(existsSync(source.resource.fsPath), false);
|
||||
|
||||
assert.ok(event!);
|
||||
assert.equal(event!.resource.fsPath, link.fsPath);
|
||||
assert.equal(event!.operation, FileOperation.DELETE);
|
||||
|
||||
assert.equal(existsSync(target.fsPath), true); // target the link pointed to is never deleted
|
||||
});
|
||||
|
||||
test('deleteFile - symbolic link (pointing to non-existing file)', async () => {
|
||||
if (isWindows) {
|
||||
return; // not reliable on windows
|
||||
}
|
||||
|
||||
const target = URI.file(join(testDir, 'foo'));
|
||||
const link = URI.file(join(testDir, 'bar'));
|
||||
await symlink(target.fsPath, link.fsPath);
|
||||
|
||||
let event: FileOperationEvent;
|
||||
disposables.add(service.onAfterOperation(e => event = e));
|
||||
|
||||
await service.del(link);
|
||||
|
||||
assert.equal(existsSync(link.fsPath), false);
|
||||
|
||||
assert.ok(event!);
|
||||
assert.equal(event!.resource.fsPath, link.fsPath);
|
||||
assert.equal(event!.operation, FileOperation.DELETE);
|
||||
});
|
||||
|
||||
test('deleteFolder (recursive)', async () => {
|
||||
let event: FileOperationEvent;
|
||||
disposables.add(service.onAfterOperation(e => event = e));
|
||||
@@ -1867,6 +1918,47 @@ suite('Disk File Service', function () {
|
||||
assert.ok(!error);
|
||||
});
|
||||
|
||||
test('writeFile - no error when writing to same non-existing folder multiple times different new files', async () => {
|
||||
const newFolder = URI.file(join(testDir, 'some', 'new', 'folder'));
|
||||
|
||||
const file1 = joinPath(newFolder, 'file-1');
|
||||
const file2 = joinPath(newFolder, 'file-2');
|
||||
const file3 = joinPath(newFolder, 'file-3');
|
||||
|
||||
// this essentially verifies that the mkdirp logic implemented
|
||||
// in the file service is able to receive multiple requests for
|
||||
// the same folder and will not throw errors if another racing
|
||||
// call succeeded first.
|
||||
const newContent = 'Updates to the small file';
|
||||
await Promise.all([
|
||||
service.writeFile(file1, VSBuffer.fromString(newContent)),
|
||||
service.writeFile(file2, VSBuffer.fromString(newContent)),
|
||||
service.writeFile(file3, VSBuffer.fromString(newContent))
|
||||
]);
|
||||
|
||||
assert.ok(service.exists(file1));
|
||||
assert.ok(service.exists(file2));
|
||||
assert.ok(service.exists(file3));
|
||||
});
|
||||
|
||||
test('writeFile - error when writing to folder that is a file', async () => {
|
||||
const existingFile = URI.file(join(testDir, 'my-file'));
|
||||
|
||||
await service.createFile(existingFile);
|
||||
|
||||
const newFile = joinPath(existingFile, 'file-1');
|
||||
|
||||
let error;
|
||||
const newContent = 'Updates to the small file';
|
||||
try {
|
||||
await service.writeFile(newFile, VSBuffer.fromString(newContent));
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
assert.ok(error);
|
||||
});
|
||||
|
||||
const runWatchTests = isLinux;
|
||||
|
||||
(runWatchTests ? test : test.skip)('watch - file', done => {
|
||||
|
||||
@@ -807,6 +807,14 @@ export class WorkbenchDataTree<TInput, T, TFilterData = void> extends DataTree<T
|
||||
this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService);
|
||||
this.disposables.add(this.internals);
|
||||
}
|
||||
|
||||
updateOptions(options: IWorkbenchAsyncDataTreeOptions<T, TFilterData> = {}): void {
|
||||
super.updateOptions(options);
|
||||
|
||||
if (options.overrideStyles) {
|
||||
this.internals.updateStyleOverrides(options.overrideStyles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWorkbenchAsyncDataTreeOptions<T, TFilterData> extends IAsyncDataTreeOptions<T, TFilterData> {
|
||||
@@ -839,6 +847,14 @@ export class WorkbenchAsyncDataTree<TInput, T, TFilterData = void> extends Async
|
||||
this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService);
|
||||
this.disposables.add(this.internals);
|
||||
}
|
||||
|
||||
updateOptions(options: IWorkbenchAsyncDataTreeOptions<T, TFilterData> = {}): void {
|
||||
super.updateOptions(options);
|
||||
|
||||
if (options.overrideStyles) {
|
||||
this.internals.updateStyleOverrides(options.overrideStyles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWorkbenchCompressibleAsyncDataTreeOptions<T, TFilterData> extends ICompressibleAsyncDataTreeOptions<T, TFilterData> {
|
||||
@@ -936,15 +952,16 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
|
||||
private hasMultiSelection: IContextKey<boolean>;
|
||||
private _useAltAsMultipleSelectionModifier: boolean;
|
||||
private disposables: IDisposable[] = [];
|
||||
private styler: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
tree: WorkbenchObjectTree<T, TFilterData> | CompressibleObjectTree<T, TFilterData> | WorkbenchDataTree<TInput, T, TFilterData> | WorkbenchAsyncDataTree<TInput, T, TFilterData> | WorkbenchCompressibleAsyncDataTree<TInput, T, TFilterData>,
|
||||
private tree: WorkbenchObjectTree<T, TFilterData> | CompressibleObjectTree<T, TFilterData> | WorkbenchDataTree<TInput, T, TFilterData> | WorkbenchAsyncDataTree<TInput, T, TFilterData> | WorkbenchCompressibleAsyncDataTree<TInput, T, TFilterData>,
|
||||
options: IAbstractTreeOptions<T, TFilterData> | IAsyncDataTreeOptions<T, TFilterData>,
|
||||
getAutomaticKeyboardNavigation: () => boolean | undefined,
|
||||
overrideStyles: IColorMapping | undefined,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IListService listService: IListService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IThemeService private themeService: IThemeService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IAccessibilityService accessibilityService: IAccessibilityService,
|
||||
) {
|
||||
@@ -970,10 +987,11 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
|
||||
});
|
||||
};
|
||||
|
||||
this.updateStyleOverrides(overrideStyles);
|
||||
|
||||
this.disposables.push(
|
||||
this.contextKeyService,
|
||||
(listService as ListService).register(tree),
|
||||
overrideStyles ? attachListStyler(tree, themeService, overrideStyles) : Disposable.None,
|
||||
tree.onDidChangeSelection(() => {
|
||||
const selection = tree.getSelection();
|
||||
const focus = tree.getFocus();
|
||||
@@ -1023,8 +1041,15 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
|
||||
return this._useAltAsMultipleSelectionModifier;
|
||||
}
|
||||
|
||||
updateStyleOverrides(overrideStyles?: IColorMapping): void {
|
||||
dispose(this.styler);
|
||||
this.styler = overrideStyles ? attachListStyler(this.tree, this.themeService, overrideStyles) : Disposable.None;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
dispose(this.styler);
|
||||
this.styler = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
src/vs/platform/log/browser/log.ts
Normal file
33
src/vs/platform/log/browser/log.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ILogService, DEFAULT_LOG_LEVEL, LogLevel, LogServiceAdapter } from 'vs/platform/log/common/log';
|
||||
|
||||
interface IAutomatedWindow {
|
||||
codeAutomationLog(type: string, args: any[]): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A logger that is used when VSCode is running in the web with
|
||||
* an automation such as playwright. We expect a global codeAutomationLog
|
||||
* to be defined that we can use to log to.
|
||||
*/
|
||||
export class ConsoleLogInAutomationService extends LogServiceAdapter implements ILogService {
|
||||
|
||||
declare codeAutomationLog: any;
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
|
||||
super({ consoleLog: (type, args) => this.consoleLog(type, args) }, logLevel);
|
||||
}
|
||||
|
||||
private consoleLog(type: string, args: any[]): void {
|
||||
const automatedWindow = window as unknown as IAutomatedWindow;
|
||||
if (typeof automatedWindow.codeAutomationLog === 'function') {
|
||||
automatedWindow.codeAutomationLog(type, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,48 +213,48 @@ export class ConsoleLogService extends AbstractLogService implements ILogService
|
||||
}
|
||||
}
|
||||
|
||||
export class ConsoleLogInMainService extends AbstractLogService implements ILogService {
|
||||
export class LogServiceAdapter extends AbstractLogService implements ILogService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(private readonly client: LoggerChannelClient, logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
|
||||
constructor(private readonly adapter: { consoleLog: (type: string, args: any[]) => void }, logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
|
||||
super();
|
||||
this.setLevel(logLevel);
|
||||
}
|
||||
|
||||
trace(message: string, ...args: any[]): void {
|
||||
if (this.getLevel() <= LogLevel.Trace) {
|
||||
this.client.consoleLog('trace', [this.extractMessage(message), ...args]);
|
||||
this.adapter.consoleLog('trace', [this.extractMessage(message), ...args]);
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, ...args: any[]): void {
|
||||
if (this.getLevel() <= LogLevel.Debug) {
|
||||
this.client.consoleLog('debug', [this.extractMessage(message), ...args]);
|
||||
this.adapter.consoleLog('debug', [this.extractMessage(message), ...args]);
|
||||
}
|
||||
}
|
||||
|
||||
info(message: string, ...args: any[]): void {
|
||||
if (this.getLevel() <= LogLevel.Info) {
|
||||
this.client.consoleLog('info', [this.extractMessage(message), ...args]);
|
||||
this.adapter.consoleLog('info', [this.extractMessage(message), ...args]);
|
||||
}
|
||||
}
|
||||
|
||||
warn(message: string | Error, ...args: any[]): void {
|
||||
if (this.getLevel() <= LogLevel.Warning) {
|
||||
this.client.consoleLog('warn', [this.extractMessage(message), ...args]);
|
||||
this.adapter.consoleLog('warn', [this.extractMessage(message), ...args]);
|
||||
}
|
||||
}
|
||||
|
||||
error(message: string | Error, ...args: any[]): void {
|
||||
if (this.getLevel() <= LogLevel.Error) {
|
||||
this.client.consoleLog('error', [this.extractMessage(message), ...args]);
|
||||
this.adapter.consoleLog('error', [this.extractMessage(message), ...args]);
|
||||
}
|
||||
}
|
||||
|
||||
critical(message: string | Error, ...args: any[]): void {
|
||||
if (this.getLevel() <= LogLevel.Critical) {
|
||||
this.client.consoleLog('critical', [this.extractMessage(message), ...args]);
|
||||
this.adapter.consoleLog('critical', [this.extractMessage(message), ...args]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +275,15 @@ export class ConsoleLogInMainService extends AbstractLogService implements ILogS
|
||||
}
|
||||
}
|
||||
|
||||
export class ConsoleLogInMainService extends LogServiceAdapter implements ILogService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(client: LoggerChannelClient, logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
|
||||
super({ consoleLog: (type, args) => client.consoleLog(type, args) }, logLevel);
|
||||
}
|
||||
}
|
||||
|
||||
export class MultiplexLogService extends AbstractLogService implements ILogService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
|
||||
@@ -60,7 +60,11 @@ export class LoggerChannelClient {
|
||||
}
|
||||
|
||||
setLevel(level: LogLevel): void {
|
||||
this.channel.call('setLevel', level);
|
||||
LoggerChannelClient.setLevel(this.channel, level);
|
||||
}
|
||||
|
||||
public static setLevel(channel: IChannel, level: LogLevel): Promise<void> {
|
||||
return channel.call('setLevel', level);
|
||||
}
|
||||
|
||||
consoleLog(severity: string, args: string[]): void {
|
||||
|
||||
@@ -500,7 +500,7 @@ export class Menubar {
|
||||
}).length > 0;
|
||||
|
||||
if (!success) {
|
||||
this.workspacesHistoryMainService.removeFromRecentlyOpened([revivedUri]);
|
||||
this.workspacesHistoryMainService.removeRecentlyOpened([revivedUri]);
|
||||
}
|
||||
}
|
||||
}, false));
|
||||
|
||||
@@ -64,7 +64,7 @@ export interface INeverShowAgainOptions {
|
||||
isSecondary?: boolean;
|
||||
|
||||
/**
|
||||
* Wether to persist the choice in the current workspace or for all workspaces. By
|
||||
* Whether to persist the choice in the current workspace or for all workspaces. By
|
||||
* default it will be persisted for all workspaces.
|
||||
*/
|
||||
scope?: NeverShowAgainScope;
|
||||
@@ -192,7 +192,7 @@ export interface IPromptChoice {
|
||||
isSecondary?: boolean;
|
||||
|
||||
/**
|
||||
* Wether to keep the notification open after the choice was selected
|
||||
* Whether to keep the notification open after the choice was selected
|
||||
* by the user. By default, will close the notification upon click.
|
||||
*/
|
||||
keepOpen?: boolean;
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface IProgressService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
withProgress<R = any>(options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, task: (progress: IProgress<IProgressStep>) => Promise<R>, onDidCancel?: () => void): Promise<R>;
|
||||
withProgress<R = any>(options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, task: (progress: IProgress<IProgressStep>) => Promise<R>, onDidCancel?: (choice?: number) => void): Promise<R>;
|
||||
}
|
||||
|
||||
export interface IProgressIndicator {
|
||||
|
||||
@@ -5,260 +5,14 @@
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInputButton, IInputBox, QuickPickInput } from 'vs/base/parts/quickinput/common/quickInput';
|
||||
|
||||
export interface IQuickPickItem {
|
||||
type?: 'item';
|
||||
id?: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
detail?: string;
|
||||
iconClasses?: string[];
|
||||
buttons?: IQuickInputButton[];
|
||||
picked?: boolean;
|
||||
alwaysShow?: boolean;
|
||||
}
|
||||
|
||||
export interface IQuickPickSeparator {
|
||||
type: 'separator';
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IKeyMods {
|
||||
readonly ctrlCmd: boolean;
|
||||
readonly alt: boolean;
|
||||
}
|
||||
|
||||
export interface IQuickNavigateConfiguration {
|
||||
keybindings: ResolvedKeybinding[];
|
||||
}
|
||||
|
||||
export interface IPickOptions<T extends IQuickPickItem> {
|
||||
|
||||
/**
|
||||
* an optional string to show as placeholder in the input box to guide the user what she picks on
|
||||
*/
|
||||
placeHolder?: string;
|
||||
|
||||
/**
|
||||
* an optional flag to include the description when filtering the picks
|
||||
*/
|
||||
matchOnDescription?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to include the detail when filtering the picks
|
||||
*/
|
||||
matchOnDetail?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to filter the picks based on label. Defaults to true.
|
||||
*/
|
||||
matchOnLabel?: boolean;
|
||||
|
||||
/**
|
||||
* an option flag to control whether focus is always automatically brought to a list item. Defaults to true.
|
||||
*/
|
||||
autoFocusOnList?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to not close the picker on focus lost
|
||||
*/
|
||||
ignoreFocusLost?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to make this picker multi-select
|
||||
*/
|
||||
canPickMany?: boolean;
|
||||
|
||||
/**
|
||||
* enables quick navigate in the picker to open an element without typing
|
||||
*/
|
||||
quickNavigate?: IQuickNavigateConfiguration;
|
||||
|
||||
/**
|
||||
* a context key to set when this picker is active
|
||||
*/
|
||||
contextKey?: string;
|
||||
|
||||
/**
|
||||
* an optional property for the item to focus initially.
|
||||
*/
|
||||
activeItem?: Promise<T> | T;
|
||||
|
||||
onKeyMods?: (keyMods: IKeyMods) => void;
|
||||
onDidFocus?: (entry: T) => void;
|
||||
onDidTriggerItemButton?: (context: IQuickPickItemButtonContext<T>) => void;
|
||||
}
|
||||
|
||||
export interface IInputOptions {
|
||||
|
||||
/**
|
||||
* the value to prefill in the input box
|
||||
*/
|
||||
value?: string;
|
||||
|
||||
/**
|
||||
* the selection of value, default to the whole word
|
||||
*/
|
||||
valueSelection?: [number, number];
|
||||
|
||||
/**
|
||||
* the text to display underneath the input box
|
||||
*/
|
||||
prompt?: string;
|
||||
|
||||
/**
|
||||
* an optional string to show as placeholder in the input box to guide the user what to type
|
||||
*/
|
||||
placeHolder?: string;
|
||||
|
||||
/**
|
||||
* set to true to show a password prompt that will not show the typed value
|
||||
*/
|
||||
password?: boolean;
|
||||
|
||||
ignoreFocusLost?: boolean;
|
||||
|
||||
/**
|
||||
* an optional function that is used to validate user input.
|
||||
*/
|
||||
validateInput?: (input: string) => Promise<string | null | undefined>;
|
||||
}
|
||||
|
||||
export interface IQuickInput {
|
||||
|
||||
title: string | undefined;
|
||||
|
||||
description: string | undefined;
|
||||
|
||||
step: number | undefined;
|
||||
|
||||
totalSteps: number | undefined;
|
||||
|
||||
enabled: boolean;
|
||||
|
||||
contextKey: string | undefined;
|
||||
|
||||
busy: boolean;
|
||||
|
||||
ignoreFocusOut: boolean;
|
||||
|
||||
show(): void;
|
||||
|
||||
hide(): void;
|
||||
|
||||
onDidHide: Event<void>;
|
||||
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
|
||||
|
||||
value: string;
|
||||
|
||||
placeholder: string | undefined;
|
||||
|
||||
readonly onDidChangeValue: Event<string>;
|
||||
|
||||
readonly onDidAccept: Event<void>;
|
||||
|
||||
ok: boolean;
|
||||
|
||||
readonly onDidCustom: Event<void>;
|
||||
|
||||
customButton: boolean;
|
||||
|
||||
customLabel: string | undefined;
|
||||
|
||||
customHover: string | undefined;
|
||||
|
||||
buttons: ReadonlyArray<IQuickInputButton>;
|
||||
|
||||
readonly onDidTriggerButton: Event<IQuickInputButton>;
|
||||
|
||||
readonly onDidTriggerItemButton: Event<IQuickPickItemButtonEvent<T>>;
|
||||
|
||||
items: ReadonlyArray<T | IQuickPickSeparator>;
|
||||
|
||||
canSelectMany: boolean;
|
||||
|
||||
matchOnDescription: boolean;
|
||||
|
||||
matchOnDetail: boolean;
|
||||
|
||||
matchOnLabel: boolean;
|
||||
|
||||
sortByLabel: boolean;
|
||||
|
||||
autoFocusOnList: boolean;
|
||||
|
||||
quickNavigate: IQuickNavigateConfiguration | undefined;
|
||||
|
||||
activeItems: ReadonlyArray<T>;
|
||||
|
||||
readonly onDidChangeActive: Event<T[]>;
|
||||
|
||||
selectedItems: ReadonlyArray<T>;
|
||||
|
||||
readonly onDidChangeSelection: Event<T[]>;
|
||||
|
||||
readonly keyMods: IKeyMods;
|
||||
|
||||
valueSelection: Readonly<[number, number]> | undefined;
|
||||
|
||||
validationMessage: string | undefined;
|
||||
|
||||
inputHasFocus(): boolean;
|
||||
}
|
||||
|
||||
export interface IInputBox extends IQuickInput {
|
||||
|
||||
value: string;
|
||||
|
||||
valueSelection: Readonly<[number, number]> | undefined;
|
||||
|
||||
placeholder: string | undefined;
|
||||
|
||||
password: boolean;
|
||||
|
||||
readonly onDidChangeValue: Event<string>;
|
||||
|
||||
readonly onDidAccept: Event<void>;
|
||||
|
||||
buttons: ReadonlyArray<IQuickInputButton>;
|
||||
|
||||
readonly onDidTriggerButton: Event<IQuickInputButton>;
|
||||
|
||||
prompt: string | undefined;
|
||||
|
||||
validationMessage: string | undefined;
|
||||
}
|
||||
|
||||
export interface IQuickInputButton {
|
||||
/** iconPath or iconClass required */
|
||||
iconPath?: { dark: URI; light?: URI; };
|
||||
/** iconPath or iconClass required */
|
||||
iconClass?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export interface IQuickPickItemButtonEvent<T extends IQuickPickItem> {
|
||||
button: IQuickInputButton;
|
||||
item: T;
|
||||
}
|
||||
|
||||
export interface IQuickPickItemButtonContext<T extends IQuickPickItem> extends IQuickPickItemButtonEvent<T> {
|
||||
removeItem(): void;
|
||||
}
|
||||
export { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods } from 'vs/base/parts/quickinput/common/quickInput';
|
||||
|
||||
export const IQuickInputService = createDecorator<IQuickInputService>('quickInputService');
|
||||
|
||||
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
export type QuickPickInput<T = IQuickPickItem> = T | IQuickPickSeparator;
|
||||
|
||||
export interface IQuickInputService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
@@ -1,202 +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 { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, IFileChange, IStat, IWatchOptions, FileOpenOptions, IFileSystemProviderWithFileReadWriteCapability, FileWriteOptions, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, FileReadStreamOptions, IFileSystemProviderWithOpenReadWriteCloseCapability } from 'vs/platform/files/common/files';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { newWriteableStream, ReadableStreamEvents, ReadableStreamEventPayload } from 'vs/base/common/stream';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
|
||||
export const REMOTE_FILE_SYSTEM_CHANNEL_NAME = 'remotefilesystem';
|
||||
|
||||
export interface IFileChangeDto {
|
||||
resource: UriComponents;
|
||||
type: FileChangeType;
|
||||
}
|
||||
|
||||
export class RemoteFileSystemProvider extends Disposable implements
|
||||
IFileSystemProviderWithFileReadWriteCapability,
|
||||
IFileSystemProviderWithOpenReadWriteCloseCapability,
|
||||
IFileSystemProviderWithFileReadStreamCapability,
|
||||
IFileSystemProviderWithFileFolderCopyCapability {
|
||||
|
||||
private readonly session: string = generateUuid();
|
||||
|
||||
private readonly _onDidChange = this._register(new Emitter<readonly IFileChange[]>());
|
||||
readonly onDidChangeFile = this._onDidChange.event;
|
||||
|
||||
private _onDidWatchErrorOccur = this._register(new Emitter<string>());
|
||||
readonly onDidErrorOccur = this._onDidWatchErrorOccur.event;
|
||||
|
||||
private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());
|
||||
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
|
||||
|
||||
private _capabilities!: FileSystemProviderCapabilities;
|
||||
get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }
|
||||
|
||||
constructor(private readonly channel: IChannel, environment: Promise<IRemoteAgentEnvironment | null>) {
|
||||
super();
|
||||
|
||||
this.setCaseSensitive(true);
|
||||
environment.then(remoteAgentEnvironment => this.setCaseSensitive(!!(remoteAgentEnvironment && remoteAgentEnvironment.os === OperatingSystem.Linux)));
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.channel.listen<IFileChangeDto[] | string>('filechange', [this.session])(eventsOrError => {
|
||||
if (Array.isArray(eventsOrError)) {
|
||||
const events = eventsOrError;
|
||||
this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type })));
|
||||
} else {
|
||||
const error = eventsOrError;
|
||||
this._onDidWatchErrorOccur.fire(error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
setCaseSensitive(isCaseSensitive: boolean) {
|
||||
let capabilities = (
|
||||
FileSystemProviderCapabilities.FileReadWrite
|
||||
| FileSystemProviderCapabilities.FileOpenReadWriteClose
|
||||
| FileSystemProviderCapabilities.FileReadStream
|
||||
| FileSystemProviderCapabilities.FileFolderCopy
|
||||
);
|
||||
|
||||
if (isCaseSensitive) {
|
||||
capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
}
|
||||
|
||||
this._capabilities = capabilities;
|
||||
this._onDidChangeCapabilities.fire(undefined);
|
||||
}
|
||||
|
||||
// --- forwarding calls
|
||||
|
||||
stat(resource: URI): Promise<IStat> {
|
||||
return this.channel.call('stat', [resource]);
|
||||
}
|
||||
|
||||
open(resource: URI, opts: FileOpenOptions): Promise<number> {
|
||||
return this.channel.call('open', [resource, opts]);
|
||||
}
|
||||
|
||||
close(fd: number): Promise<void> {
|
||||
return this.channel.call('close', [fd]);
|
||||
}
|
||||
|
||||
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]);
|
||||
|
||||
// copy back the data that was written into the buffer on the remote
|
||||
// side. we need to do this because buffers are not referenced by
|
||||
// pointer, but only by value and as such cannot be directly written
|
||||
// to from the other process.
|
||||
data.set(bytes.buffer.slice(0, bytesRead), offset);
|
||||
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
async readFile(resource: URI): Promise<Uint8Array> {
|
||||
const buff = <VSBuffer>await this.channel.call('readFile', [resource]);
|
||||
|
||||
return buff.buffer;
|
||||
}
|
||||
|
||||
readFileStream(resource: URI, opts: FileReadStreamOptions, token?: CancellationToken): ReadableStreamEvents<Uint8Array> {
|
||||
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
|
||||
|
||||
// Reading as file stream goes through an event to the remote side
|
||||
const listener = this.channel.listen<ReadableStreamEventPayload<VSBuffer>>('readFileStream', [resource, opts])(dataOrErrorOrEnd => {
|
||||
|
||||
// data
|
||||
if (dataOrErrorOrEnd instanceof VSBuffer) {
|
||||
stream.write(dataOrErrorOrEnd.buffer);
|
||||
}
|
||||
|
||||
// end or error
|
||||
else {
|
||||
if (dataOrErrorOrEnd === 'end') {
|
||||
stream.end();
|
||||
} else {
|
||||
|
||||
// Since we receive data through a IPC channel, it is likely
|
||||
// that the error was not serialized, or only partially. To
|
||||
// ensure our API use is correct, we convert the data to an
|
||||
// error here to forward it properly.
|
||||
let error = dataOrErrorOrEnd;
|
||||
if (!(error instanceof Error)) {
|
||||
error = new Error(toErrorMessage(error));
|
||||
}
|
||||
|
||||
stream.end(error);
|
||||
}
|
||||
|
||||
// Signal to the remote side that we no longer listen
|
||||
listener.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
// Support cancellation
|
||||
if (token) {
|
||||
Event.once(token.onCancellationRequested)(() => {
|
||||
|
||||
// Ensure to end the stream properly with an error
|
||||
// to indicate the cancellation.
|
||||
stream.end(canceled());
|
||||
|
||||
// Ensure to dispose the listener upon cancellation. This will
|
||||
// bubble through the remote side as event and allows to stop
|
||||
// reading the file.
|
||||
listener.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);
|
||||
}
|
||||
|
||||
writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]);
|
||||
}
|
||||
|
||||
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||
return this.channel.call('delete', [resource, opts]);
|
||||
}
|
||||
|
||||
mkdir(resource: URI): Promise<void> {
|
||||
return this.channel.call('mkdir', [resource]);
|
||||
}
|
||||
|
||||
readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
return this.channel.call('readdir', [resource]);
|
||||
}
|
||||
|
||||
rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this.channel.call('rename', [resource, target, opts]);
|
||||
}
|
||||
|
||||
copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this.channel.call('copy', [resource, target, opts]);
|
||||
}
|
||||
|
||||
watch(resource: URI, opts: IWatchOptions): IDisposable {
|
||||
const req = Math.random();
|
||||
this.channel.call('watch', [this.session, req, resource, opts]);
|
||||
|
||||
return toDisposable(() => this.channel.call('unwatch', [this.session, req]));
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,11 @@ export class RequestChannelClient {
|
||||
constructor(private readonly channel: IChannel) { }
|
||||
|
||||
async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
const [res, buffer] = await this.channel.call<RequestResponse>('request', [options]);
|
||||
return RequestChannelClient.request(this.channel, options, token);
|
||||
}
|
||||
|
||||
static async request(channel: IChannel, options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
const [res, buffer] = await channel.call<RequestResponse>('request', [options]);
|
||||
return { res, stream: bufferToStream(buffer) };
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
private doFlushWhenIdle(): void {
|
||||
|
||||
// Dispose any previous idle runner
|
||||
this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable);
|
||||
dispose(this.runWhenIdleDisposable);
|
||||
|
||||
// Run when idle
|
||||
this.runWhenIdleDisposable = runWhenIdle(() => {
|
||||
@@ -180,7 +180,8 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable);
|
||||
dispose(this.runWhenIdleDisposable);
|
||||
this.runWhenIdleDisposable = undefined;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
private doFlushWhenIdle(): void {
|
||||
|
||||
// Dispose any previous idle runner
|
||||
this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable);
|
||||
dispose(this.runWhenIdleDisposable);
|
||||
|
||||
// Run when idle
|
||||
this.runWhenIdleDisposable = runWhenIdle(() => {
|
||||
@@ -224,7 +224,8 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
|
||||
// Stop periodic scheduler and idle runner as we now collect state normally
|
||||
this.periodicFlushScheduler.dispose();
|
||||
this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable);
|
||||
dispose(this.runWhenIdleDisposable);
|
||||
this.runWhenIdleDisposable = undefined;
|
||||
|
||||
// Signal as event so that clients can still store data
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
|
||||
|
||||
@@ -426,6 +426,7 @@ export const minimapFindMatch = registerColor('minimap.findMatchHighlight', { li
|
||||
export const minimapSelection = registerColor('minimap.selectionHighlight', { light: '#ADD6FF', dark: '#264F78', hc: '#ffffff' }, nls.localize('minimapSelectionHighlight', 'Minimap marker color for the editor selection.'), true);
|
||||
export const minimapError = registerColor('minimap.errorHighlight', { dark: new Color(new RGBA(255, 18, 18, 0.7)), light: new Color(new RGBA(255, 18, 18, 0.7)), hc: new Color(new RGBA(255, 50, 50, 1)) }, nls.localize('minimapError', 'Minimap marker color for errors.'));
|
||||
export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.'));
|
||||
export const minimapBackground = registerColor('minimap.background', { dark: null, light: null, hc: null }, nls.localize('minimapBackground', "Minimap background color."));
|
||||
|
||||
export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon."));
|
||||
export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningForeground }, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon."));
|
||||
|
||||
@@ -79,6 +79,13 @@ export function getThemeTypeSelector(type: ThemeType): string {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITokenStyle {
|
||||
readonly foreground?: number;
|
||||
readonly bold?: boolean;
|
||||
readonly underline?: boolean;
|
||||
readonly italic?: boolean;
|
||||
}
|
||||
|
||||
export interface ITheme {
|
||||
readonly type: ThemeType;
|
||||
|
||||
@@ -99,7 +106,7 @@ export interface ITheme {
|
||||
/**
|
||||
* Returns the token style for a given classification. The result uses the <code>MetadataConsts</code> format
|
||||
*/
|
||||
getTokenStyleMetadata(type: string, modifiers: string[]): number | undefined;
|
||||
getTokenStyleMetadata(type: string, modifiers: string[]): ITokenStyle | undefined;
|
||||
|
||||
/**
|
||||
* List of all colors used with tokens. <code>getTokenStyleMetadata</code> references the colors by index into this list.
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
|
||||
|
||||
export const TOKEN_TYPE_WILDCARD = '*';
|
||||
export const TOKEN_TYPE_WILDCARD_NUM = -1;
|
||||
|
||||
// qualified string [type|*](.modifier)*
|
||||
export type TokenClassificationString = string;
|
||||
@@ -21,16 +20,17 @@ export type TokenClassificationString = string;
|
||||
export const typeAndModifierIdPattern = '^\\w+[-_\\w+]*$';
|
||||
export const fontStylePattern = '^(\\s*(-?italic|-?bold|-?underline))*\\s*$';
|
||||
|
||||
export interface TokenClassification {
|
||||
type: number;
|
||||
modifiers: number;
|
||||
export interface TokenSelector {
|
||||
match(type: string, modifiers: string[]): number;
|
||||
readonly selectorString: string;
|
||||
}
|
||||
|
||||
export interface TokenTypeOrModifierContribution {
|
||||
readonly num: number;
|
||||
readonly id: string;
|
||||
readonly superType?: string;
|
||||
readonly description: string;
|
||||
readonly deprecationMessage: string | undefined;
|
||||
readonly deprecationMessage?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,15 +96,13 @@ export interface TokenStyleDefaults {
|
||||
}
|
||||
|
||||
export interface TokenStylingDefaultRule {
|
||||
match(classification: TokenClassification): number;
|
||||
selector: TokenClassification;
|
||||
selector: TokenSelector;
|
||||
defaults: TokenStyleDefaults;
|
||||
}
|
||||
|
||||
export interface TokenStylingRule {
|
||||
match(classification: TokenClassification): number;
|
||||
value: TokenStyle;
|
||||
selector: TokenClassification;
|
||||
style: TokenStyle;
|
||||
selector: TokenSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,33 +122,37 @@ export interface ITokenClassificationRegistry {
|
||||
/**
|
||||
* Register a token type to the registry.
|
||||
* @param id The TokenType id as used in theme description files
|
||||
* @description the description
|
||||
* @param description the description
|
||||
*/
|
||||
registerTokenType(id: string, description: string): void;
|
||||
registerTokenType(id: string, description: string, superType?: string, deprecationMessage?: string): void;
|
||||
|
||||
/**
|
||||
* Register a token modifier to the registry.
|
||||
* @param id The TokenModifier id as used in theme description files
|
||||
* @description the description
|
||||
* @param description the description
|
||||
*/
|
||||
registerTokenModifier(id: string, description: string): void;
|
||||
|
||||
getTokenClassification(type: string, modifiers: string[]): TokenClassification | undefined;
|
||||
|
||||
getTokenStylingRule(classification: TokenClassification, value: TokenStyle): TokenStylingRule;
|
||||
/**
|
||||
* Parses a token selector from a selector string.
|
||||
* @param selectorString selector string in the form (*|type)(.modifier)*
|
||||
* @returns the parsesd selector
|
||||
* @throws an error if the string is not a valid selector
|
||||
*/
|
||||
parseTokenSelector(selectorString: string): TokenSelector;
|
||||
|
||||
/**
|
||||
* Register a TokenStyle default to the registry.
|
||||
* @param selector The rule selector
|
||||
* @param defaults The default values
|
||||
*/
|
||||
registerTokenStyleDefault(selector: TokenClassification, defaults: TokenStyleDefaults): void;
|
||||
registerTokenStyleDefault(selector: TokenSelector, defaults: TokenStyleDefaults): void;
|
||||
|
||||
/**
|
||||
* Deregister a TokenStyle default to the registry.
|
||||
* @param selector The rule selector
|
||||
*/
|
||||
deregisterTokenStyleDefault(selector: TokenClassification): void;
|
||||
deregisterTokenStyleDefault(selector: TokenSelector): void;
|
||||
|
||||
/**
|
||||
* Deregister a TokenType from the registry.
|
||||
@@ -196,6 +198,8 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
|
||||
private tokenStylingDefaultRules: TokenStylingDefaultRule[] = [];
|
||||
|
||||
private typeHierarchy: { [id: string]: string[] };
|
||||
|
||||
private tokenStylingSchema: IJSONSchema & { properties: IJSONSchemaMap } = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
@@ -232,20 +236,23 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
constructor() {
|
||||
this.tokenTypeById = {};
|
||||
this.tokenModifierById = {};
|
||||
|
||||
this.tokenTypeById[TOKEN_TYPE_WILDCARD] = { num: TOKEN_TYPE_WILDCARD_NUM, id: TOKEN_TYPE_WILDCARD, description: '', deprecationMessage: undefined };
|
||||
this.typeHierarchy = {};
|
||||
}
|
||||
|
||||
public registerTokenType(id: string, description: string, deprecationMessage?: string): void {
|
||||
public registerTokenType(id: string, description: string, superType?: string, deprecationMessage?: string): void {
|
||||
if (!id.match(typeAndModifierIdPattern)) {
|
||||
throw new Error('Invalid token type id.');
|
||||
}
|
||||
if (superType && !superType.match(typeAndModifierIdPattern)) {
|
||||
throw new Error('Invalid token super type id.');
|
||||
}
|
||||
|
||||
const num = this.currentTypeNumber++;
|
||||
let tokenStyleContribution: TokenTypeOrModifierContribution = { num, id, description, deprecationMessage };
|
||||
let tokenStyleContribution: TokenTypeOrModifierContribution = { num, id, superType, description, deprecationMessage };
|
||||
this.tokenTypeById[id] = tokenStyleContribution;
|
||||
|
||||
this.tokenStylingSchema.properties[id] = getStylingSchemeEntry(description, deprecationMessage);
|
||||
this.typeHierarchy = {};
|
||||
}
|
||||
|
||||
public registerTokenModifier(id: string, description: string, deprecationMessage?: string): void {
|
||||
@@ -261,56 +268,52 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
this.tokenStylingSchema.properties[`*.${id}`] = getStylingSchemeEntry(description, deprecationMessage);
|
||||
}
|
||||
|
||||
public getTokenClassification(type: string, modifiers: string[]): TokenClassification | undefined {
|
||||
const tokenTypeDesc = this.tokenTypeById[type];
|
||||
if (!tokenTypeDesc) {
|
||||
return undefined;
|
||||
public parseTokenSelector(selectorString: string): TokenSelector {
|
||||
const [selectorType, ...selectorModifiers] = selectorString.split('.');
|
||||
|
||||
if (!selectorType) {
|
||||
return {
|
||||
match: () => -1,
|
||||
selectorString
|
||||
};
|
||||
}
|
||||
let allModifierBits = 0;
|
||||
for (const modifier of modifiers) {
|
||||
const tokenModifierDesc = this.tokenModifierById[modifier];
|
||||
if (tokenModifierDesc) {
|
||||
allModifierBits |= tokenModifierDesc.num;
|
||||
}
|
||||
}
|
||||
return { type: tokenTypeDesc.num, modifiers: allModifierBits };
|
||||
}
|
||||
|
||||
|
||||
private newMatcher(selector: TokenClassification) {
|
||||
const score = getTokenStylingScore(selector);
|
||||
return (classification: TokenClassification) => {
|
||||
const selectorType = selector.type;
|
||||
if (selectorType !== TOKEN_TYPE_WILDCARD_NUM && selectorType !== classification.type) {
|
||||
return -1;
|
||||
}
|
||||
const selectorModifier = selector.modifiers;
|
||||
if ((classification.modifiers & selectorModifier) !== selectorModifier) {
|
||||
return -1;
|
||||
}
|
||||
return score;
|
||||
};
|
||||
}
|
||||
|
||||
public getTokenStylingRule(selector: TokenClassification, value: TokenStyle): TokenStylingRule {
|
||||
return {
|
||||
match: this.newMatcher(selector),
|
||||
value,
|
||||
selector
|
||||
match: (type: string, modifiers: string[]) => {
|
||||
let score = 0;
|
||||
if (selectorType !== TOKEN_TYPE_WILDCARD) {
|
||||
const hierarchy = this.getTypeHierarchy(type);
|
||||
const level = hierarchy.indexOf(selectorType);
|
||||
if (level === -1) {
|
||||
return -1;
|
||||
}
|
||||
score = 100 - level;
|
||||
}
|
||||
// all selector modifiers must be present
|
||||
for (const selectorModifier of selectorModifiers) {
|
||||
if (modifiers.indexOf(selectorModifier) === -1) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return score + selectorModifiers.length * 100;
|
||||
},
|
||||
selectorString
|
||||
};
|
||||
}
|
||||
|
||||
public registerTokenStyleDefault(selector: TokenClassification, defaults: TokenStyleDefaults): void {
|
||||
this.tokenStylingDefaultRules.push({ selector, match: this.newMatcher(selector), defaults });
|
||||
public registerTokenStyleDefault(selector: TokenSelector, defaults: TokenStyleDefaults): void {
|
||||
this.tokenStylingDefaultRules.push({ selector, defaults });
|
||||
}
|
||||
|
||||
public deregisterTokenStyleDefault(classification: TokenClassification): void {
|
||||
this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => !(r.selector.type === classification.type && r.selector.modifiers === classification.modifiers));
|
||||
public deregisterTokenStyleDefault(selector: TokenSelector): void {
|
||||
const selectorString = selector.selectorString;
|
||||
this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => r.selector.selectorString !== selectorString);
|
||||
}
|
||||
|
||||
public deregisterTokenType(id: string): void {
|
||||
delete this.tokenTypeById[id];
|
||||
delete this.tokenStylingSchema.properties[id];
|
||||
this.typeHierarchy = {};
|
||||
}
|
||||
|
||||
public deregisterTokenModifier(id: string): void {
|
||||
@@ -334,6 +337,19 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
return this.tokenStylingDefaultRules;
|
||||
}
|
||||
|
||||
private getTypeHierarchy(typeId: string): string[] {
|
||||
let hierarchy = this.typeHierarchy[typeId];
|
||||
if (!hierarchy) {
|
||||
this.typeHierarchy[typeId] = hierarchy = [typeId];
|
||||
let type = this.tokenTypeById[typeId];
|
||||
while (type && type.superType) {
|
||||
hierarchy.push(type.superType);
|
||||
type = this.tokenTypeById[type.superType];
|
||||
}
|
||||
}
|
||||
return hierarchy;
|
||||
}
|
||||
|
||||
|
||||
public toString() {
|
||||
let sorter = (a: string, b: string) => {
|
||||
@@ -357,16 +373,23 @@ platform.Registry.add(Extensions.TokenClassificationContribution, tokenClassific
|
||||
registerDefaultClassifications();
|
||||
|
||||
function registerDefaultClassifications(): void {
|
||||
function registerTokenType(id: string, description: string, scopesToProbe: ProbeScope[] = [], extendsTC?: string, deprecationMessage?: string): string {
|
||||
tokenClassificationRegistry.registerTokenType(id, description, deprecationMessage);
|
||||
|
||||
if (scopesToProbe || extendsTC) {
|
||||
const classification = tokenClassificationRegistry.getTokenClassification(id, []);
|
||||
tokenClassificationRegistry.registerTokenStyleDefault(classification!, { scopesToProbe, light: extendsTC, dark: extendsTC, hc: extendsTC });
|
||||
function registerTokenType(id: string, description: string, scopesToProbe: ProbeScope[] = [], superType?: string, deprecationMessage?: string): string {
|
||||
tokenClassificationRegistry.registerTokenType(id, description, superType, deprecationMessage);
|
||||
if (scopesToProbe) {
|
||||
registerTokenStyleDefault(id, scopesToProbe);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function registerTokenStyleDefault(selectorString: string, scopesToProbe: ProbeScope[]) {
|
||||
try {
|
||||
const selector = tokenClassificationRegistry.parseTokenSelector(selectorString);
|
||||
tokenClassificationRegistry.registerTokenStyleDefault(selector, { scopesToProbe });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// default token types
|
||||
|
||||
registerTokenType('comment', nls.localize('comment', "Style for comments."), [['comment']]);
|
||||
@@ -383,16 +406,17 @@ function registerDefaultClassifications(): void {
|
||||
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('typeParameter', nls.localize('typeParameter', "Style for type parameters."), [['entity.name.type', 'meta.type.parameters']], 'type');
|
||||
|
||||
registerTokenType('function', nls.localize('function', "Style for functions"), [['entity.name.function'], ['support.function']]);
|
||||
registerTokenType('member', nls.localize('member', "Style for member"), [['entity.name.function'], ['support.function']]);
|
||||
registerTokenType('macro', nls.localize('macro', "Style for macros."), undefined, 'function');
|
||||
registerTokenType('member', nls.localize('member', "Style for member"), [['entity.name.function.member'], ['support.function']]);
|
||||
registerTokenType('macro', nls.localize('macro', "Style for macros."), [['entity.name.other.preprocessor.macro']], 'function');
|
||||
|
||||
registerTokenType('variable', nls.localize('variable', "Style for variables."), [['variable'], ['entity.name.variable']]);
|
||||
registerTokenType('constant', nls.localize('constant', "Style for constants."), undefined, 'variable');
|
||||
registerTokenType('parameter', nls.localize('parameter', "Style for parameters."), undefined, 'variable');
|
||||
registerTokenType('property', nls.localize('property', "Style for properties."), undefined, 'variable');
|
||||
registerTokenType('variable', nls.localize('variable', "Style for variables."), [['variable.other.readwrite'], ['entity.name.variable']]);
|
||||
registerTokenType('parameter', nls.localize('parameter', "Style for parameters."), [['variable.parameter']], 'variable');
|
||||
registerTokenType('property', nls.localize('property', "Style for properties."), [['variable.other.property']], 'variable');
|
||||
registerTokenType('enumMember', nls.localize('enumMember', "Style for enum members."), [['variable.other.enummember']], 'variable');
|
||||
registerTokenType('event', nls.localize('event', "Style for events."), [['variable.other.event']], 'variable');
|
||||
|
||||
registerTokenType('label', nls.localize('labels', "Style for labels. "), undefined);
|
||||
|
||||
@@ -406,22 +430,15 @@ function registerDefaultClassifications(): void {
|
||||
tokenClassificationRegistry.registerTokenModifier('modification', nls.localize('modification', "Style to use for write accesses."), undefined);
|
||||
tokenClassificationRegistry.registerTokenModifier('async', nls.localize('async', "Style to use for symbols that are async."), undefined);
|
||||
tokenClassificationRegistry.registerTokenModifier('readonly', nls.localize('readonly', "Style to use for symbols that are readonly."), undefined);
|
||||
|
||||
|
||||
registerTokenStyleDefault('variable.readonly', [['variable.other.constant']]);
|
||||
}
|
||||
|
||||
export function getTokenClassificationRegistry(): ITokenClassificationRegistry {
|
||||
return tokenClassificationRegistry;
|
||||
}
|
||||
|
||||
function bitCount(u: number) {
|
||||
// https://blogs.msdn.microsoft.com/jeuge/2005/06/08/bit-fiddling-3/
|
||||
const uCount = u - ((u >> 1) & 0o33333333333) - ((u >> 2) & 0o11111111111);
|
||||
return ((uCount + (uCount >> 3)) & 0o30707070707) % 63;
|
||||
}
|
||||
|
||||
function getTokenStylingScore(classification: TokenClassification) {
|
||||
return bitCount(classification.modifiers) + ((classification.type !== TOKEN_TYPE_WILDCARD_NUM) ? 1 : 0);
|
||||
}
|
||||
|
||||
function getStylingSchemeEntry(description?: string, deprecationMessage?: string): IJSONSchema {
|
||||
return {
|
||||
description,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isWindows, isMacintosh } from 'vs/base/common/platform';
|
||||
import { systemPreferences, ipcMain as ipc } from 'electron';
|
||||
import { ipcMain as ipc, nativeTheme } 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 && systemPreferences.isInvertedColorScheme()) {
|
||||
if (isWindows && nativeTheme.shouldUseInvertedColorScheme) {
|
||||
return DEFAULT_BG_HC_BLACK;
|
||||
}
|
||||
|
||||
let background = this.stateService.getItem<string | null>(THEME_BG_STORAGE_KEY, null);
|
||||
if (!background) {
|
||||
let baseTheme: string;
|
||||
if (isWindows && systemPreferences.isInvertedColorScheme()) {
|
||||
if (isWindows && nativeTheme.shouldUseInvertedColorScheme) {
|
||||
baseTheme = 'hc-black';
|
||||
} else {
|
||||
baseTheme = this.stateService.getItem<string>(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0];
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IThemeService, ITheme, DARK, IIconTheme } from 'vs/platform/theme/common/themeService';
|
||||
import { IThemeService, ITheme, DARK, IIconTheme, ITokenStyle } from 'vs/platform/theme/common/themeService';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
|
||||
export class TestTheme implements ITheme {
|
||||
@@ -24,7 +24,7 @@ export class TestTheme implements ITheme {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getTokenStyleMetadata(type: string, modifiers: string[]): number | undefined {
|
||||
getTokenStyleMetadata(type: string, modifiers: string[]): ITokenStyle | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
|
||||
async hasRemoteData(): Promise<boolean> {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncData);
|
||||
return remoteUserData.content !== null;
|
||||
}
|
||||
|
||||
@@ -77,8 +78,8 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
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 getRemoteUserData(lastSyncData: IUserData | null): Promise<IUserData> {
|
||||
return this.userDataSyncStoreService.read(this.getRemoteDataResourceKey(), lastSyncData, this.source);
|
||||
}
|
||||
|
||||
protected async updateRemoteUserData(content: string, ref: string | null): Promise<string> {
|
||||
|
||||
@@ -23,6 +23,7 @@ interface ISyncPreviewResult {
|
||||
readonly remote: ISyncExtension[] | null;
|
||||
readonly remoteUserData: IUserData;
|
||||
readonly skippedExtensions: ISyncExtension[];
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
}
|
||||
|
||||
interface ILastSyncUserData extends IUserData {
|
||||
@@ -44,9 +45,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
super(SyncSource.Extensions, fileService, environmentService, userDataSyncStoreService);
|
||||
this._register(
|
||||
Event.debounce(
|
||||
Event.any(
|
||||
Event.any<any>(
|
||||
Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)),
|
||||
Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error))),
|
||||
Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)),
|
||||
this.extensionEnablementService.onDidChangeEnablement),
|
||||
() => undefined, 500)(() => this._onDidChangeLocal.fire()));
|
||||
}
|
||||
|
||||
@@ -64,13 +66,14 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
this.logService.info('Extensions: Started pulling extensions...');
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
|
||||
if (remoteUserData.content !== null) {
|
||||
const localExtensions = await this.getLocalExtensions();
|
||||
const remoteExtensions: ISyncExtension[] = JSON.parse(remoteUserData.content);
|
||||
const { added, updated, remote } = merge(localExtensions, remoteExtensions, [], [], this.getIgnoredExtensions());
|
||||
await this.apply({ added, removed: [], updated, remote, remoteUserData, skippedExtensions: [] });
|
||||
await this.apply({ added, removed: [], updated, remote, remoteUserData, skippedExtensions: [], lastSyncUserData });
|
||||
}
|
||||
|
||||
// No remote exists to pull
|
||||
@@ -98,8 +101,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
|
||||
const localExtensions = await this.getLocalExtensions();
|
||||
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions());
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
await this.apply({ added, removed, updated, remote, remoteUserData, skippedExtensions: [] }, true);
|
||||
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
await this.apply({ added, removed, updated, remote, remoteUserData, skippedExtensions: [], lastSyncUserData }, true);
|
||||
|
||||
this.logService.info('Extensions: Finished pushing extensions.');
|
||||
} finally {
|
||||
@@ -132,7 +136,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
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('Extensions: Failed to synchronise extensions as there is a new remote version available. Synchronizing again...');
|
||||
this.logService.info('Extensions: Failed to synchronize extensions as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
@@ -148,7 +152,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
throw new Error('Extensions: Conflicts should not occur');
|
||||
}
|
||||
|
||||
resolveConflicts(content: string): Promise<void> {
|
||||
accept(content: string): Promise<void> {
|
||||
throw new Error('Extensions: Conflicts should not occur');
|
||||
}
|
||||
|
||||
@@ -169,11 +173,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
}
|
||||
|
||||
private async getPreview(): Promise<ISyncPreviewResult> {
|
||||
const lastSyncData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const lastSyncExtensions: ISyncExtension[] | null = lastSyncData ? JSON.parse(lastSyncData.content!) : null;
|
||||
const skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : [];
|
||||
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? JSON.parse(lastSyncUserData.content!) : null;
|
||||
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
|
||||
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncData);
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const remoteExtensions: ISyncExtension[] = remoteUserData.content ? JSON.parse(remoteUserData.content) : null;
|
||||
|
||||
const localExtensions = await this.getLocalExtensions();
|
||||
@@ -181,40 +185,44 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
if (remoteExtensions) {
|
||||
this.logService.trace('Extensions: Merging remote extensions with local extensions...');
|
||||
} else {
|
||||
this.logService.info('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.');
|
||||
this.logService.trace('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.');
|
||||
}
|
||||
|
||||
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions());
|
||||
|
||||
return { added, removed, updated, remote, skippedExtensions, remoteUserData };
|
||||
return { added, removed, updated, remote, skippedExtensions, remoteUserData, lastSyncUserData };
|
||||
}
|
||||
|
||||
private getIgnoredExtensions() {
|
||||
return this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
|
||||
}
|
||||
|
||||
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
if (!added.length && !removed.length && !updated.length && !remote) {
|
||||
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
|
||||
const hasChanges = added.length || removed.length || updated.length || remote;
|
||||
|
||||
if (!hasChanges) {
|
||||
this.logService.trace('Extensions: No changes found during synchronizing extensions.');
|
||||
}
|
||||
|
||||
if (added.length || removed.length || updated.length) {
|
||||
this.logService.info('Extensions: Updating local extensions...');
|
||||
skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions);
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
// update remote
|
||||
this.logService.info('Extensions: Updating remote extensions...');
|
||||
this.logService.trace('Extensions: Updating remote extensions...');
|
||||
const content = JSON.stringify(remote);
|
||||
const ref = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
remoteUserData = { ref, content };
|
||||
this.logService.info('Extensions: Updated remote extensions');
|
||||
}
|
||||
|
||||
if (remoteUserData.content) {
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
// update last sync
|
||||
this.logService.info('Extensions: Updating last synchronised extensions...');
|
||||
this.logService.trace('Extensions: Updating last synchronized extensions...');
|
||||
await this.updateLastSyncUserData<ILastSyncUserData>({ ...remoteUserData, skippedExtensions });
|
||||
this.logService.info('Extensions: Updated last synchronized extensions');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,29 +234,56 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const extensionsToRemove = installedExtensions.filter(({ identifier }) => removed.some(r => areSameExtensions(identifier, r)));
|
||||
await Promise.all(extensionsToRemove.map(async extensionToRemove => {
|
||||
this.logService.info('Extensions: Removing local extension.', extensionToRemove.identifier.id);
|
||||
this.logService.trace('Extensions: Uninstalling local extension...', extensionToRemove.identifier.id);
|
||||
await this.extensionManagementService.uninstall(extensionToRemove);
|
||||
this.logService.info('Extensions: Uninstalled local extension.', extensionToRemove.identifier.id);
|
||||
removeFromSkipped.push(extensionToRemove.identifier);
|
||||
}));
|
||||
}
|
||||
|
||||
if (added.length || updated.length) {
|
||||
await Promise.all([...added, ...updated].map(async e => {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const installedExtension = installedExtensions.filter(installed => areSameExtensions(installed.identifier, e.identifier))[0];
|
||||
|
||||
// Builtin Extension: Sync only enablement state
|
||||
if (installedExtension && installedExtension.type === ExtensionType.System) {
|
||||
if (e.enabled) {
|
||||
this.logService.trace('Extensions: Enabling extension...', e.identifier.id);
|
||||
await this.extensionEnablementService.enableExtension(e.identifier);
|
||||
this.logService.info('Extensions: Enabled extension', e.identifier.id);
|
||||
} else {
|
||||
this.logService.trace('Extensions: Disabling extension...', e.identifier.id);
|
||||
await this.extensionEnablementService.disableExtension(e.identifier);
|
||||
this.logService.info('Extensions: Disabled extension.', e.identifier.id);
|
||||
}
|
||||
removeFromSkipped.push(e.identifier);
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier, e.version);
|
||||
if (extension) {
|
||||
this.logService.info('Extensions: Installing local extension.', e.identifier.id, extension.version);
|
||||
try {
|
||||
await this.extensionManagementService.installFromGallery(extension);
|
||||
removeFromSkipped.push(extension.identifier);
|
||||
if (e.enabled) {
|
||||
this.logService.trace('Extensions: Enabling extension...', e.identifier.id, extension.version);
|
||||
await this.extensionEnablementService.enableExtension(extension.identifier);
|
||||
this.logService.info('Extensions: Enabled extension', e.identifier.id, extension.version);
|
||||
} else {
|
||||
this.logService.trace('Extensions: Disabling extension...', e.identifier.id, extension.version);
|
||||
await this.extensionEnablementService.disableExtension(extension.identifier);
|
||||
this.logService.info('Extensions: Disabled extension', e.identifier.id, extension.version);
|
||||
}
|
||||
// Install only if the extension does not exist
|
||||
if (!installedExtension || installedExtension.manifest.version !== extension.version) {
|
||||
this.logService.trace('Extensions: Installing extension...', e.identifier.id, extension.version);
|
||||
await this.extensionManagementService.installFromGallery(extension);
|
||||
this.logService.info('Extensions: Installed extension.', e.identifier.id, extension.version);
|
||||
removeFromSkipped.push(extension.identifier);
|
||||
}
|
||||
} catch (error) {
|
||||
addToSkipped.push(e);
|
||||
this.logService.error(error);
|
||||
this.logService.info(localize('skip extension', "Skipping synchronising extension {0}", extension.displayName || extension.identifier.id));
|
||||
this.logService.info(localize('skip extension', "Skipped synchronizing extension {0}", extension.displayName || extension.identifier.id));
|
||||
}
|
||||
} else {
|
||||
addToSkipped.push(e);
|
||||
@@ -271,7 +306,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
}
|
||||
|
||||
private async getLocalExtensions(): Promise<ISyncExtension[]> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const disabledExtensions = await this.extensionEnablementService.getDisabledExtensionsAsync();
|
||||
return installedExtensions
|
||||
.map(({ identifier }) => ({ identifier, enabled: !disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier)) }));
|
||||
|
||||
@@ -22,6 +22,7 @@ interface ISyncPreviewResult {
|
||||
readonly local: IGlobalState | undefined;
|
||||
readonly remote: IGlobalState | undefined;
|
||||
readonly remoteUserData: IUserData;
|
||||
readonly lastSyncUserData: IUserData | null;
|
||||
}
|
||||
|
||||
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
@@ -52,11 +53,12 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.info('UI State: Started pulling ui state...');
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
|
||||
if (remoteUserData.content !== null) {
|
||||
const local: IGlobalState = JSON.parse(remoteUserData.content);
|
||||
await this.apply({ local, remote: undefined, remoteUserData });
|
||||
await this.apply({ local, remote: undefined, remoteUserData, lastSyncUserData });
|
||||
}
|
||||
|
||||
// No remote exists to pull
|
||||
@@ -83,8 +85,9 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const remote = await this.getLocalGlobalState();
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
await this.apply({ local: undefined, remote, remoteUserData }, true);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
await this.apply({ local: undefined, remote, remoteUserData, lastSyncUserData }, true);
|
||||
|
||||
this.logService.info('UI State: Finished pushing UI State.');
|
||||
} finally {
|
||||
@@ -115,7 +118,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
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('UI State: Failed to synchronise ui state as there is a new remote version available. Synchronizing again...');
|
||||
this.logService.info('UI State: Failed to synchronize ui state as there is a new remote version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
@@ -130,7 +133,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
throw new Error('UI State: Conflicts should not occur');
|
||||
}
|
||||
|
||||
resolveConflicts(content: string): Promise<void> {
|
||||
accept(content: string): Promise<void> {
|
||||
throw new Error('UI State: Conflicts should not occur');
|
||||
}
|
||||
|
||||
@@ -151,38 +154,54 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
}
|
||||
|
||||
private async getPreview(): Promise<ISyncPreviewResult> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const lastSyncGlobalState = lastSyncData && lastSyncData.content ? JSON.parse(lastSyncData.content) : null;
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const lastSyncGlobalState = lastSyncUserData && lastSyncUserData.content ? JSON.parse(lastSyncUserData.content) : null;
|
||||
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.content ? JSON.parse(remoteUserData.content) : null;
|
||||
|
||||
const localGloablState = await this.getLocalGlobalState();
|
||||
|
||||
if (remoteGlobalState) {
|
||||
this.logService.trace('UI State: Merging remote ui state with local ui state...');
|
||||
} else {
|
||||
this.logService.trace('UI State: Remote ui state does not exist. Synchronizing ui state for the first time.');
|
||||
}
|
||||
|
||||
const { local, remote } = merge(localGloablState, remoteGlobalState, lastSyncGlobalState);
|
||||
|
||||
return { local, remote, remoteUserData };
|
||||
return { local, remote, remoteUserData, lastSyncUserData };
|
||||
}
|
||||
|
||||
private async apply({ local, remote, remoteUserData }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
private async apply({ local, remote, remoteUserData, lastSyncUserData }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
|
||||
const hasChanges = local || remote;
|
||||
|
||||
if (!hasChanges) {
|
||||
this.logService.trace('UI State: No changes found during synchronizing ui state.');
|
||||
}
|
||||
|
||||
if (local) {
|
||||
// update local
|
||||
this.logService.info('UI State: Updating local ui state...');
|
||||
this.logService.trace('UI State: Updating local ui state...');
|
||||
await this.writeLocalGlobalState(local);
|
||||
this.logService.info('UI State: Updated local ui state');
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
// update remote
|
||||
this.logService.info('UI State: Updating remote ui state...');
|
||||
this.logService.trace('UI State: Updating remote ui state...');
|
||||
const content = JSON.stringify(remote);
|
||||
const ref = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
this.logService.info('UI State: Updated remote ui state');
|
||||
remoteUserData = { ref, content };
|
||||
}
|
||||
|
||||
if (remoteUserData.content) {
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
// update last sync
|
||||
this.logService.info('UI State: Updating last synchronised ui state...');
|
||||
this.logService.trace('UI State: Updating last synchronized ui state...');
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
this.logService.info('UI State: Updated last synchronized ui state');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ interface ISyncContent {
|
||||
interface ISyncPreviewResult {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly remoteUserData: IUserData;
|
||||
readonly lastSyncUserData: IUserData | null;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
@@ -63,7 +64,8 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
this.logService.info('Keybindings: Started pulling keybindings...');
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const remoteContent = remoteUserData.content !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null;
|
||||
|
||||
if (remoteContent !== null) {
|
||||
@@ -74,7 +76,8 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false,
|
||||
remoteUserData
|
||||
remoteUserData,
|
||||
lastSyncUserData
|
||||
}));
|
||||
await this.apply();
|
||||
}
|
||||
@@ -106,14 +109,16 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
|
||||
if (fileContent !== null) {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, fileContent.value);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
fileContent,
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: true,
|
||||
remoteUserData
|
||||
remoteUserData,
|
||||
lastSyncUserData
|
||||
}));
|
||||
await this.apply(undefined, true);
|
||||
}
|
||||
@@ -151,7 +156,7 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
if (this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
this.logService.info('Keybindings: Stopped synchronizing keybindings.');
|
||||
this.logService.trace('Keybindings: Stopped synchronizing keybindings.');
|
||||
}
|
||||
await this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
@@ -165,16 +170,15 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
}
|
||||
}
|
||||
|
||||
async resolveConflicts(content: string): Promise<void> {
|
||||
async accept(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 new UserDataSyncError('Failed to resolve conflicts as there is a new local version available.', UserDataSyncErrorCode.NewLocal);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@@ -204,7 +208,8 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
const preview = await this.syncPreviewResultPromise;
|
||||
content = preview.remoteUserData?.content;
|
||||
} else {
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncData);
|
||||
content = remoteUserData.content;
|
||||
}
|
||||
return content ? this.getKeybindingsContentFromSyncContent(content) : null;
|
||||
@@ -229,13 +234,13 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
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...');
|
||||
this.logService.info('Keybindings: Failed to synchronize 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...');
|
||||
this.logService.info('Keybindings: Failed to synchronize keybindings as there is a new local version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
@@ -247,6 +252,8 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
return;
|
||||
}
|
||||
|
||||
let { fileContent, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
|
||||
|
||||
if (content === undefined) {
|
||||
if (await this.fileService.exists(this.environmentService.keybindingsSyncPreviewResource)) {
|
||||
const keybindingsPreivew = await this.fileService.readFile(this.environmentService.keybindingsSyncPreviewResource);
|
||||
@@ -261,24 +268,22 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
throw error;
|
||||
}
|
||||
|
||||
let { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
this.logService.trace('Keybindings: No changes found during synchronizing keybindings.');
|
||||
}
|
||||
|
||||
if (hasLocalChanged) {
|
||||
this.logService.info('Keybindings: Updating local keybindings');
|
||||
this.logService.trace('Keybindings: Updating local keybindings...');
|
||||
await this.updateLocalFileContent(content, fileContent);
|
||||
this.logService.info('Keybindings: Updated local keybindings');
|
||||
}
|
||||
|
||||
if (hasRemoteChanged) {
|
||||
this.logService.info('Keybindings: Updating remote keybindings');
|
||||
this.logService.trace('Keybindings: Updating remote keybindings...');
|
||||
const remoteContents = this.updateSyncContent(content, remoteUserData.content);
|
||||
const ref = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref);
|
||||
remoteUserData = { ref, content: remoteContents };
|
||||
}
|
||||
if (remoteUserData?.content) {
|
||||
this.logService.info('Keybindings: Updating last synchronised keybindings');
|
||||
const lastSyncContent = this.updateSyncContent(content, null);
|
||||
await this.updateLastSyncUserData({ ref: remoteUserData.ref, content: lastSyncContent });
|
||||
this.logService.info('Keybindings: Updated remote keybindings');
|
||||
}
|
||||
|
||||
// Delete the preview
|
||||
@@ -287,6 +292,13 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
this.logService.trace('Keybindings: No changes found during synchronizing keybindings.');
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== undefined || fileContent !== null)) {
|
||||
this.logService.trace('Keybindings: Updating last synchronized keybindings...');
|
||||
const lastSyncContent = this.updateSyncContent(content !== undefined ? content : fileContent!.value.toString(), null);
|
||||
await this.updateLastSyncUserData({ ref: remoteUserData.ref, content: lastSyncContent });
|
||||
this.logService.info('Keybindings: Updated last synchronized keybindings');
|
||||
}
|
||||
|
||||
this.syncPreviewResultPromise = null;
|
||||
}
|
||||
|
||||
@@ -304,9 +316,9 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
}
|
||||
|
||||
private async generatePreview(token: CancellationToken): Promise<ISyncPreviewResult> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const lastSyncContent = lastSyncData && lastSyncData.content ? this.getKeybindingsContentFromSyncContent(lastSyncData.content) : null;
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncData);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const lastSyncContent = lastSyncUserData && lastSyncUserData.content ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.content) : null;
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const remoteContent = remoteUserData.content ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null;
|
||||
// Get file content last to get the latest
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
@@ -319,7 +331,7 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
const localContent: string = fileContent ? fileContent.value.toString() : '[]';
|
||||
if (this.hasErrors(localContent)) {
|
||||
this.logService.error('Keybindings: Unable to sync keybindings as there are errors/warning in keybindings file.');
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
return { fileContent, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
if (!lastSyncContent // First time sync
|
||||
@@ -341,7 +353,7 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
|
||||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.info('Keybindings: Remote keybindings does not exist. Synchronizing keybindings for the first time.');
|
||||
this.logService.trace('Keybindings: Remote keybindings does not exist. Synchronizing keybindings for the first time.');
|
||||
hasRemoteChanged = true;
|
||||
previewContent = fileContent.value.toString();
|
||||
}
|
||||
@@ -350,7 +362,7 @@ export class KeybindingsSynchroniser extends AbstractFileSynchroniser implements
|
||||
await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(previewContent));
|
||||
}
|
||||
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
return { fileContent, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
|
||||
}
|
||||
|
||||
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
|
||||
|
||||
@@ -10,8 +10,10 @@ import { values } from 'vs/base/common/map';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
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 { IConflictSetting, DEFAULT_IGNORED_SETTINGS } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { firstIndex } from 'vs/base/common/arrays';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
|
||||
export interface IMergeResult {
|
||||
localContent: string | null;
|
||||
@@ -20,6 +22,30 @@ export interface IMergeResult {
|
||||
conflictsSettings: IConflictSetting[];
|
||||
}
|
||||
|
||||
export function getIgnoredSettings(configurationService: IConfigurationService, settingsContent?: string): string[] {
|
||||
let value: string[] = [];
|
||||
if (settingsContent) {
|
||||
const setting = parse(settingsContent);
|
||||
if (setting) {
|
||||
value = setting['sync.ignoredSettings'];
|
||||
}
|
||||
} else {
|
||||
value = configurationService.getValue<string[]>('sync.ignoredSettings');
|
||||
}
|
||||
const added: string[] = [], removed: string[] = [];
|
||||
if (Array.isArray(value)) {
|
||||
for (const key of value) {
|
||||
if (startsWith(key, '-')) {
|
||||
removed.push(key.substring(1));
|
||||
} else {
|
||||
added.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...DEFAULT_IGNORED_SETTINGS, ...added].filter(setting => removed.indexOf(setting) === -1);
|
||||
}
|
||||
|
||||
|
||||
export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string {
|
||||
if (ignoredSettings.length) {
|
||||
const sourceTree = parseSettings(sourceContent);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
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 { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, 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';
|
||||
@@ -12,22 +12,22 @@ 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { updateIgnoredSettings, merge } from 'vs/platform/userDataSync/common/settingsMerge';
|
||||
import { updateIgnoredSettings, merge, getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { isEmptyObject } from 'vs/base/common/types';
|
||||
import { isEmptyObject, isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { AbstractFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly remoteUserData: IUserData;
|
||||
readonly lastSyncUserData: IUserData | null;
|
||||
readonly content: string | null;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly remoteContent: string | null;
|
||||
readonly hasConflicts: boolean;
|
||||
readonly conflictSettings: IConflictSetting[];
|
||||
}
|
||||
@@ -84,23 +84,23 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
this.logService.info('Settings: Started pulling settings...');
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const remoteUserData = await this.getRemoteUserData();
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
|
||||
if (remoteUserData.content !== null) {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Update ignored settings
|
||||
// Update ignored settings from local file content
|
||||
const content = updateIgnoredSettings(remoteUserData.content, fileContent ? fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils);
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content));
|
||||
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
hasConflicts: false,
|
||||
conflictSettings: [],
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false,
|
||||
remoteContent: content,
|
||||
remoteUserData,
|
||||
hasConflicts: false,
|
||||
conflictSettings: [],
|
||||
}));
|
||||
|
||||
await this.apply();
|
||||
@@ -135,20 +135,21 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// 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();
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
conflictSettings: [],
|
||||
hasConflicts: false,
|
||||
fileContent,
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: true,
|
||||
remoteContent: content,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasRemoteChanged: true,
|
||||
hasLocalChanged: false,
|
||||
hasConflicts: false,
|
||||
conflictSettings: [],
|
||||
}));
|
||||
|
||||
await this.apply(undefined, true);
|
||||
await this.apply(true);
|
||||
}
|
||||
|
||||
// No local exists to push
|
||||
@@ -182,7 +183,7 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
if (this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise.cancel();
|
||||
this.syncPreviewResultPromise = null;
|
||||
this.logService.info('Settings: Stopped synchronizing settings.');
|
||||
this.logService.trace('Settings: Stopped synchronizing settings.');
|
||||
}
|
||||
await this.fileService.del(this.environmentService.settingsSyncPreviewResource);
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
@@ -207,15 +208,21 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
return false;
|
||||
}
|
||||
|
||||
async getRemoteContent(): Promise<string | null> {
|
||||
async getRemoteContent(preview?: boolean): 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();
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncData);
|
||||
content = remoteUserData.content;
|
||||
}
|
||||
if (preview && !isUndefinedOrNull(content)) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// remove ignored settings from the remote content for preview
|
||||
content = updateIgnoredSettings(content, '{}', getIgnoredSettings(this.configurationService), formatUtils);
|
||||
}
|
||||
return content !== undefined ? content : null;
|
||||
}
|
||||
|
||||
@@ -227,16 +234,20 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
}
|
||||
}
|
||||
|
||||
async resolveConflicts(content: string): Promise<void> {
|
||||
async accept(content: string): Promise<void> {
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
try {
|
||||
await this.apply(content, true);
|
||||
const preview = await this.syncPreviewResultPromise!;
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Add ignored settings from local file content
|
||||
content = updateIgnoredSettings(content, preview.fileContent ? preview.fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(async () => ({ ...preview, content }));
|
||||
await this.apply(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 new UserDataSyncError('Failed to resolve conflicts as there is a new local version available.', UserDataSyncErrorCode.NewLocal);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@@ -270,32 +281,27 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
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('Settings: Failed to synchronise settings as there is a new remote version available. Synchronizing again...');
|
||||
this.logService.info('Settings: Failed to synchronize settings 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('Settings: Failed to synchronise settings as there is a new local version available. Synchronizing again...');
|
||||
this.logService.info('Settings: Failed to synchronize settings as there is a new local version available. Synchronizing again...');
|
||||
return this.sync();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async apply(content?: string, forcePush?: boolean): Promise<void> {
|
||||
private async apply(forcePush?: boolean): Promise<void> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (content === undefined) {
|
||||
if (await this.fileService.exists(this.environmentService.settingsSyncPreviewResource)) {
|
||||
const settingsPreivew = await this.fileService.readFile(this.environmentService.settingsSyncPreviewResource);
|
||||
content = settingsPreivew.value.toString();
|
||||
}
|
||||
}
|
||||
let { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
|
||||
|
||||
if (content !== undefined) {
|
||||
if (content !== null) {
|
||||
|
||||
if (this.hasErrors(content)) {
|
||||
const error = new Error(localize('errorInvalidSettings', "Unable to sync settings. Please resolve conflicts without any errors/warnings and try again."));
|
||||
@@ -303,25 +309,20 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
throw error;
|
||||
}
|
||||
|
||||
let { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
this.logService.trace('Settings: No changes found during synchronizing settings.');
|
||||
}
|
||||
if (hasLocalChanged) {
|
||||
this.logService.info('Settings: Updating local settings');
|
||||
this.logService.trace('Settings: Updating local settings...');
|
||||
await this.updateLocalFileContent(content, fileContent);
|
||||
this.logService.info('Settings: Updated local settings');
|
||||
}
|
||||
if (hasRemoteChanged) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const remoteContent = updateIgnoredSettings(content, remoteUserData.content || '{}', getIgnoredSettings(this.configurationService, content), formatUtils);
|
||||
this.logService.info('Settings: Updating remote settings');
|
||||
const ref = await this.updateRemoteUserData(remoteContent, forcePush ? null : remoteUserData.ref);
|
||||
// Update ignored settings from remote
|
||||
content = updateIgnoredSettings(content, remoteUserData.content || '{}', getIgnoredSettings(this.configurationService, content), formatUtils);
|
||||
this.logService.trace('Settings: Updating remote settings...');
|
||||
const ref = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref);
|
||||
this.logService.info('Settings: Updated remote settings');
|
||||
remoteUserData = { ref, content };
|
||||
}
|
||||
if (remoteUserData.content) {
|
||||
this.logService.info('Settings: Updating last synchronised sttings');
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
}
|
||||
|
||||
// Delete the preview
|
||||
await this.fileService.del(this.environmentService.settingsSyncPreviewResource);
|
||||
@@ -329,6 +330,12 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
this.logService.trace('Settings: No changes found during synchronizing settings.');
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
this.logService.trace('Settings: Updating last synchronized settings...');
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
this.logService.info('Settings: Updated last synchronized settings');
|
||||
}
|
||||
|
||||
this.syncPreviewResultPromise = null;
|
||||
}
|
||||
|
||||
@@ -346,16 +353,17 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
}
|
||||
|
||||
private async generatePreview(resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise<ISyncPreviewResult> {
|
||||
const lastSyncData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncData);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
// Get file content last to get the latest
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
|
||||
let content: string | null = null;
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
let conflictSettings: IConflictSetting[] = [];
|
||||
let previewContent: string | null = null;
|
||||
let remoteContent: string | null = null;
|
||||
|
||||
if (remoteUserData.content) {
|
||||
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
|
||||
@@ -367,31 +375,30 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
|
||||
else {
|
||||
this.logService.trace('Settings: Merging remote settings with local settings...');
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const result = merge(localContent, remoteUserData.content, lastSyncData ? lastSyncData.content : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formatUtils);
|
||||
hasConflicts = result.hasConflicts;
|
||||
const result = merge(localContent, remoteUserData.content, lastSyncUserData ? lastSyncUserData.content : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formatUtils);
|
||||
content = result.localContent || result.remoteContent;
|
||||
hasLocalChanged = result.localContent !== null;
|
||||
hasRemoteChanged = result.remoteContent !== null;
|
||||
hasConflicts = result.hasConflicts;
|
||||
conflictSettings = result.conflictsSettings;
|
||||
remoteContent = result.remoteContent;
|
||||
previewContent = result.localContent || result.remoteContent;
|
||||
}
|
||||
}
|
||||
|
||||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.info('Settings: Remote settings does not exist. Synchronizing settings for the first time.');
|
||||
this.logService.trace('Settings: Remote settings does not exist. Synchronizing settings for the first time.');
|
||||
content = fileContent.value.toString();
|
||||
hasRemoteChanged = true;
|
||||
previewContent = fileContent.value.toString();
|
||||
remoteContent = fileContent.value.toString();
|
||||
}
|
||||
|
||||
if (previewContent && !token.isCancellationRequested) {
|
||||
if (content && !token.isCancellationRequested) {
|
||||
// Remove the ignored settings from the preview.
|
||||
const previewContent = updateIgnoredSettings(content, '{}', getIgnoredSettings(this.configurationService), formatUtils);
|
||||
await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(previewContent));
|
||||
}
|
||||
|
||||
this.setConflicts(conflictSettings);
|
||||
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, remoteContent, conflictSettings, hasConflicts };
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, conflictSettings, hasConflicts };
|
||||
}
|
||||
|
||||
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
|
||||
@@ -403,26 +410,3 @@ export class SettingsSynchroniser extends AbstractFileSynchroniser implements IS
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function getIgnoredSettings(configurationService: IConfigurationService, settingsContent?: string): string[] {
|
||||
let value: string[] = [];
|
||||
if (settingsContent) {
|
||||
const setting = parse(settingsContent);
|
||||
if (setting) {
|
||||
value = setting['sync.ignoredSettings'];
|
||||
}
|
||||
} else {
|
||||
value = configurationService.getValue<string[]>('sync.ignoredSettings');
|
||||
}
|
||||
const added: string[] = [], removed: string[] = [];
|
||||
if (Array.isArray(value)) {
|
||||
for (const key of value) {
|
||||
if (startsWith(key, '-')) {
|
||||
removed.push(key.substring(1));
|
||||
} else {
|
||||
added.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...DEFAULT_IGNORED_SETTINGS, ...added].filter(setting => removed.indexOf(setting) === -1);
|
||||
}
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { timeout, Delayer } from 'vs/base/common/async';
|
||||
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, UserDataSyncError, UserDataSyncErrorCode, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
|
||||
export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncService {
|
||||
export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private enabled: boolean = false;
|
||||
private successiveFailures: number = 0;
|
||||
private readonly syncDelayer: Delayer<void>;
|
||||
|
||||
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;
|
||||
@@ -28,6 +29,7 @@ export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncSer
|
||||
) {
|
||||
super();
|
||||
this.updateEnablement(false, true);
|
||||
this.syncDelayer = this._register(new Delayer<void>(0));
|
||||
this._register(Event.any<any>(userDataAuthTokenService.onDidChangeToken)(() => this.updateEnablement(true, true)));
|
||||
this._register(Event.any<any>(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true, true)));
|
||||
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.enable'))(() => this.updateEnablement(true, false)));
|
||||
@@ -41,14 +43,14 @@ export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncSer
|
||||
|
||||
this.enabled = enabled;
|
||||
if (this.enabled) {
|
||||
this.logService.info('Auto sync started');
|
||||
this.logService.info('Auto Sync: Started');
|
||||
this.sync(true, auto);
|
||||
return;
|
||||
} else {
|
||||
this.successiveFailures = 0;
|
||||
this.resetFailures();
|
||||
if (stopIfDisabled) {
|
||||
this.userDataSyncService.stop();
|
||||
this.logService.info('Auto sync stopped.');
|
||||
this.logService.info('Auto Sync: stopped.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,29 +62,29 @@ export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncSer
|
||||
if (auto) {
|
||||
if (await this.isTurnedOffEverywhere()) {
|
||||
// Turned off everywhere. Reset & Stop Sync.
|
||||
this.logService.info('Auto Sync: Turning off sync as it is turned off everywhere.');
|
||||
await this.userDataSyncService.resetLocal();
|
||||
await this.userDataSyncUtilService.updateConfigurationValue('sync.enable', false);
|
||||
return;
|
||||
}
|
||||
if (this.userDataSyncService.status !== SyncStatus.Idle) {
|
||||
this.logService.info('Skipped auto sync as sync is happening');
|
||||
this.logService.trace('Auto Sync: Skipped once as it is syncing already');
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.userDataSyncService.sync();
|
||||
this.successiveFailures = 0;
|
||||
this.resetFailures();
|
||||
} 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 * (this.successiveFailures + 1)); // Loop sync for every (successive failures count + 1) times 5 mins interval.
|
||||
await timeout(1000 * 60 * 5);
|
||||
this.sync(loop, true);
|
||||
}
|
||||
} else {
|
||||
this.logService.trace('Auto Sync: Not syncing as it is disabled.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +100,21 @@ export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncSer
|
||||
&& !!(await this.userDataAuthTokenService.getToken());
|
||||
}
|
||||
|
||||
triggerAutoSync(): Promise<void> {
|
||||
return this.sync(false, true);
|
||||
private resetFailures(): void {
|
||||
this.successiveFailures = 0;
|
||||
}
|
||||
|
||||
async triggerAutoSync(): Promise<void> {
|
||||
if (this.enabled) {
|
||||
return this.syncDelayer.trigger(() => {
|
||||
this.logService.info('Auto Sync: Triggerred.');
|
||||
return this.sync(false, true);
|
||||
}, this.successiveFailures
|
||||
? 1000 * 1 * Math.min(this.successiveFailures, 60) /* Delay by number of seconds as number of failures up to 1 minute */
|
||||
: 1000);
|
||||
} else {
|
||||
this.syncDelayer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -124,22 +124,38 @@ export interface IUserData {
|
||||
}
|
||||
|
||||
export enum UserDataSyncErrorCode {
|
||||
TooLarge = 'TooLarge',
|
||||
Unauthroized = 'Unauthroized',
|
||||
Unauthorized = 'Unauthorized',
|
||||
Forbidden = 'Forbidden',
|
||||
ConnectionRefused = 'ConnectionRefused',
|
||||
Rejected = 'Rejected',
|
||||
TooLarge = 'TooLarge',
|
||||
NoRef = 'NoRef',
|
||||
NewLocal = 'NewLocal',
|
||||
Unknown = 'Unknown',
|
||||
TooManyFailures = 'TooManyFailures',
|
||||
ConnectionRefused = 'ConnectionRefused'
|
||||
}
|
||||
|
||||
export class UserDataSyncError extends Error {
|
||||
|
||||
constructor(message: string, public readonly code: UserDataSyncErrorCode, public readonly source?: SyncSource) {
|
||||
super(message);
|
||||
this.name = `${this.code} (UserDataSyncError) ${this.source}`;
|
||||
}
|
||||
|
||||
static toUserDataSyncError(error: Error): UserDataSyncError {
|
||||
if (error instanceof UserDataSyncStoreError) {
|
||||
return error;
|
||||
}
|
||||
const match = /^(.+) \(UserDataSyncError\) (.+)?$/.exec(error.name);
|
||||
if (match && match[1]) {
|
||||
return new UserDataSyncError(error.message, <UserDataSyncErrorCode>match[1], <SyncSource>match[2]);
|
||||
}
|
||||
return new UserDataSyncError(error.message, UserDataSyncErrorCode.Unknown);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreError extends UserDataSyncError { }
|
||||
|
||||
export interface IUserDataSyncStore {
|
||||
url: string;
|
||||
authenticationProviderId: string;
|
||||
@@ -201,8 +217,8 @@ export interface ISynchroniser {
|
||||
|
||||
export interface IUserDataSynchroniser extends ISynchroniser {
|
||||
readonly source: SyncSource;
|
||||
getRemoteContent(): Promise<string | null>;
|
||||
resolveConflicts(content: string): Promise<void>;
|
||||
getRemoteContent(preivew?: boolean): Promise<string | null>;
|
||||
accept(content: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
|
||||
@@ -212,14 +228,14 @@ export interface IUserDataSyncService extends ISynchroniser {
|
||||
isFirstTimeSyncAndHasUserData(): Promise<boolean>;
|
||||
reset(): Promise<void>;
|
||||
resetLocal(): Promise<void>;
|
||||
getRemoteContent(source: SyncSource): Promise<string | null>;
|
||||
resolveConflictsAndContinueSync(content: string): Promise<void>;
|
||||
getRemoteContent(source: SyncSource, preview: boolean): Promise<string | null>;
|
||||
accept(source: SyncSource, content: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
|
||||
export interface IUserDataAutoSyncService {
|
||||
_serviceBrand: any;
|
||||
onError: Event<{ code: UserDataSyncErrorCode, source?: SyncSource }>;
|
||||
readonly onError: Event<{ code: UserDataSyncErrorCode, source?: SyncSource }>;
|
||||
triggerAutoSync(): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'sync': return this.service.sync();
|
||||
case 'resolveConflictsAndContinueSync': return this.service.resolveConflictsAndContinueSync(args[0]);
|
||||
case 'accept': return this.service.accept(args[0], args[1]);
|
||||
case 'pull': return this.service.pull();
|
||||
case 'push': return this.service.push();
|
||||
case '_getInitialStatus': return Promise.resolve(this.service.status);
|
||||
@@ -37,7 +37,7 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
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 'getRemoteContent': return this.service.getRemoteContent(args[0], args[1]);
|
||||
case 'isFirstTimeSyncAndHasUserData': return this.service.isFirstTimeSyncAndHasUserData();
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
@@ -60,7 +60,7 @@ export class SettingsSyncChannel implements IServerChannel {
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'sync': return this.service.sync();
|
||||
case 'resolveConflicts': return this.service.resolveConflicts(args[0]);
|
||||
case 'accept': return this.service.accept(args[0]);
|
||||
case 'pull': return this.service.pull();
|
||||
case 'push': return this.service.push();
|
||||
case 'restart': return this.service.restart().then(() => this.service.status);
|
||||
@@ -72,7 +72,7 @@ export class SettingsSyncChannel implements IServerChannel {
|
||||
case 'hasRemoteData': return this.service.hasRemoteData();
|
||||
case 'hasLocalData': return this.service.hasLocalData();
|
||||
case 'resolveSettingsConflicts': return this.service.resolveSettingsConflicts(args[0]);
|
||||
case 'getRemoteContent': return this.service.getRemoteContent();
|
||||
case 'getRemoteContent': return this.service.getRemoteContent(args[0]);
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSynchroniser } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode } 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 { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
|
||||
@@ -19,6 +18,10 @@ type SyncConflictsClassification = {
|
||||
source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
type SyncErrorClassification = {
|
||||
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
|
||||
|
||||
_serviceBrand: any;
|
||||
@@ -73,7 +76,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
try {
|
||||
await synchroniser.pull();
|
||||
} catch (e) {
|
||||
this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`);
|
||||
this.handleSyncError(e, synchroniser.source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +92,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
try {
|
||||
await synchroniser.push();
|
||||
} catch (e) {
|
||||
this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`);
|
||||
this.handleSyncError(e, synchroniser.source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,25 +107,25 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
if (this.status === SyncStatus.HasConflicts) {
|
||||
throw new Error(localize('resolve conflicts', "Please resolve conflicts before resuming sync."));
|
||||
}
|
||||
const startTime = new Date().getTime();
|
||||
this.logService.trace('Started Syncing...');
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
await synchroniser.sync();
|
||||
// do not continue if synchroniser has conflicts
|
||||
if (synchroniser.status === SyncStatus.HasConflicts) {
|
||||
return;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`);
|
||||
this.handleSyncError(e, synchroniser.source);
|
||||
}
|
||||
}
|
||||
this.logService.trace(`Finished Syncing. Took ${new Date().getTime() - startTime}ms`);
|
||||
}
|
||||
|
||||
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);
|
||||
async accept(source: SyncSource, content: string): Promise<void> {
|
||||
const synchroniser = this.getSynchroniser(source);
|
||||
await synchroniser.accept(content);
|
||||
if (synchroniser.status !== SyncStatus.HasConflicts) {
|
||||
await this.sync();
|
||||
}
|
||||
@@ -193,10 +196,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return false;
|
||||
}
|
||||
|
||||
async getRemoteContent(source: SyncSource): Promise<string | null> {
|
||||
async getRemoteContent(source: SyncSource, preview: boolean): Promise<string | null> {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (synchroniser.source === source) {
|
||||
return synchroniser.getRemoteContent();
|
||||
return synchroniser.getRemoteContent(preview);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -246,7 +249,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
try {
|
||||
await synchroniser.resetLocal();
|
||||
} catch (e) {
|
||||
this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`);
|
||||
this.logService.error(`${synchroniser.source}: ${toErrorMessage(e)}`);
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
this.logService.info('Completed resetting local cache');
|
||||
@@ -284,9 +288,21 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
private handleSyncError(e: Error, source: SyncSource): void {
|
||||
if (e instanceof UserDataSyncStoreError) {
|
||||
switch (e.code) {
|
||||
case UserDataSyncErrorCode.TooLarge:
|
||||
this.telemetryService.publicLog2<{ source: string }, SyncErrorClassification>('sync/errorTooLarge', { source });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
this.logService.error(e);
|
||||
this.logService.error(`${source}: ${toErrorMessage(e)}`);
|
||||
}
|
||||
|
||||
private computeConflictsSource(): SyncSource | null {
|
||||
const synchroniser = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0];
|
||||
return synchroniser ? this.getSyncSource(synchroniser) : null;
|
||||
return synchroniser ? synchroniser.source : null;
|
||||
}
|
||||
|
||||
private getSynchroniserInConflicts(): IUserDataSynchroniser | null {
|
||||
@@ -294,17 +310,13 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return synchroniser || null;
|
||||
}
|
||||
|
||||
private getSyncSource(synchroniser: ISynchroniser): SyncSource {
|
||||
if (synchroniser instanceof SettingsSynchroniser) {
|
||||
return SyncSource.Settings;
|
||||
private getSynchroniser(source: SyncSource): IUserDataSynchroniser {
|
||||
switch (source) {
|
||||
case SyncSource.Settings: return this.settingsSynchroniser;
|
||||
case SyncSource.Keybindings: return this.keybindingsSynchroniser;
|
||||
case SyncSource.Extensions: return this.extensionsSynchroniser;
|
||||
case SyncSource.GlobalState: return this.globalStateSynchroniser;
|
||||
}
|
||||
if (synchroniser instanceof KeybindingsSynchroniser) {
|
||||
return SyncSource.Keybindings;
|
||||
}
|
||||
if (synchroniser instanceof ExtensionsSynchroniser) {
|
||||
return SyncSource.Extensions;
|
||||
}
|
||||
return SyncSource.GlobalState;
|
||||
}
|
||||
|
||||
private onDidChangeAuthTokenStatus(token: string | undefined): void {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, } from 'vs/base/common/lifecycle';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService, SyncSource, UserDataSyncStoreError, IUserDataSyncLogService } 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';
|
||||
@@ -22,6 +22,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@IUserDataAuthTokenService private readonly authTokenService: IUserDataAuthTokenService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super();
|
||||
this.userDataSyncStore = getUserDataSyncStore(configurationService);
|
||||
@@ -48,12 +49,12 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new Error('Server returned ' + context.res.statusCode);
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, source);
|
||||
}
|
||||
|
||||
const ref = context.res.headers['etag'];
|
||||
if (!ref) {
|
||||
throw new Error('Server did not return the ref');
|
||||
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, source);
|
||||
}
|
||||
const content = await asText(context);
|
||||
return { ref, content };
|
||||
@@ -73,12 +74,12 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
const context = await this.request({ type: 'POST', url, data, headers }, source, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new Error('Server returned ' + context.res.statusCode);
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, source);
|
||||
}
|
||||
|
||||
const newRef = context.res.headers['etag'];
|
||||
if (!newRef) {
|
||||
throw new Error('Server did not return the ref');
|
||||
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, source);
|
||||
}
|
||||
return newRef;
|
||||
}
|
||||
@@ -94,39 +95,42 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new Error('Server returned ' + context.res.statusCode);
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
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.');
|
||||
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, source);
|
||||
}
|
||||
options.headers = options.headers || {};
|
||||
options.headers['authorization'] = `Bearer ${authToken}`;
|
||||
|
||||
let context;
|
||||
this.logService.trace('Sending request to server', { url: options.url, headers: { ...options.headers, ...{ authorization: undefined } } });
|
||||
|
||||
let context;
|
||||
try {
|
||||
context = await this.requestService.request(options, token);
|
||||
this.logService.trace('Request finished', { url: options.url, status: context.res.statusCode });
|
||||
} catch (e) {
|
||||
throw new UserDataSyncError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, source);
|
||||
throw new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, source);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 401) {
|
||||
// Throw Unauthorized Error
|
||||
throw new UserDataSyncError(`Request '${options.url?.toString()}' is not authorized.`, UserDataSyncErrorCode.Unauthroized, source);
|
||||
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source);
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 403) {
|
||||
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' is Forbidden (403).`, UserDataSyncErrorCode.Forbidden, 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);
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). 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);
|
||||
throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge, source);
|
||||
}
|
||||
|
||||
return context;
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
import { IUserDataSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IElectronService } from 'vs/platform/electron/node/electron';
|
||||
import { UserDataAutoSync as BaseUserDataAutoSync } from 'vs/platform/userDataSync/common/userDataAutoSync';
|
||||
import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export class UserDataAutoSync extends BaseUserDataAutoSync {
|
||||
export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncService userDataSyncService: IUserDataSyncService,
|
||||
@@ -1201,7 +1201,7 @@ suite('SettingsMerge - Add Setting', () => {
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
test('Insert before a setting and before a comment at the begining', () => {
|
||||
test('Insert before a setting and before a comment at the beginning', () => {
|
||||
|
||||
const sourceContent = `
|
||||
{
|
||||
|
||||
@@ -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, systemPreferences, MessageBoxOptions, Display, app } from 'electron';
|
||||
import { ipcMain as ipc, screen, BrowserWindow, MessageBoxOptions, Display, app, nativeTheme } 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';
|
||||
@@ -29,7 +29,7 @@ import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFi
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getComparisonKey, isEqual, normalizePath, originalFSPath, hasTrailingPathSeparator, removeTrailingPathSeparator } from 'vs/base/common/resources';
|
||||
import { getComparisonKey, isEqual, normalizePath, originalFSPath, removeTrailingPathSeparator } from 'vs/base/common/resources';
|
||||
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/platform/windows/electron-main/windowsStateStorage';
|
||||
import { getWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
@@ -226,16 +226,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
|
||||
// React to HC color scheme changes (Windows)
|
||||
if (isWindows) {
|
||||
const onHighContrastChange = () => {
|
||||
if (systemPreferences.isInvertedColorScheme() || systemPreferences.isHighContrastColorScheme()) {
|
||||
nativeTheme.on('updated', () => {
|
||||
if (nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors) {
|
||||
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
|
||||
@@ -1062,9 +1059,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
uri = normalizePath(uri);
|
||||
|
||||
// remove trailing slash
|
||||
if (hasTrailingPathSeparator(uri)) {
|
||||
uri = removeTrailingPathSeparator(uri);
|
||||
}
|
||||
uri = removeTrailingPathSeparator(uri);
|
||||
|
||||
// File
|
||||
if (isFileToOpen(toOpen)) {
|
||||
@@ -1195,7 +1190,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
}
|
||||
} catch (error) {
|
||||
const fileUri = URI.file(candidate);
|
||||
this.workspacesHistoryMainService.removeFromRecentlyOpened([fileUri]); // since file does not seem to exist anymore, remove from recent
|
||||
this.workspacesHistoryMainService.removeRecentlyOpened([fileUri]); // since file does not seem to exist anymore, remove from recent
|
||||
|
||||
// assume this is a file that does not yet exist
|
||||
if (options?.ignoreFileNotFound) {
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface IWorkspacesService {
|
||||
// History
|
||||
readonly onRecentlyOpenedChange: CommonEvent<void>;
|
||||
addRecentlyOpened(recents: IRecent[]): Promise<void>;
|
||||
removeFromRecentlyOpened(workspaces: URI[]): Promise<void>;
|
||||
removeRecentlyOpened(workspaces: URI[]): Promise<void>;
|
||||
clearRecentlyOpened(): Promise<void>;
|
||||
getRecentlyOpened(): Promise<IRecentlyOpened>;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface IWorkspacesHistoryMainService {
|
||||
|
||||
addRecentlyOpened(recents: IRecent[]): void;
|
||||
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened;
|
||||
removeFromRecentlyOpened(paths: URI[]): void;
|
||||
removeRecentlyOpened(paths: URI[]): void;
|
||||
clearRecentlyOpened(): void;
|
||||
|
||||
updateWindowsJumpList(): void;
|
||||
@@ -148,7 +148,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
}
|
||||
}
|
||||
|
||||
removeFromRecentlyOpened(toRemove: URI[]): void {
|
||||
removeRecentlyOpened(toRemove: URI[]): void {
|
||||
const keep = (recent: IRecent) => {
|
||||
const uri = location(recent);
|
||||
for (const r of toRemove) {
|
||||
@@ -344,7 +344,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
}
|
||||
}
|
||||
}
|
||||
this.removeFromRecentlyOpened(toRemove);
|
||||
this.removeRecentlyOpened(toRemove);
|
||||
|
||||
// Add entries
|
||||
jumpList.push({
|
||||
|
||||
@@ -63,8 +63,8 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
|
||||
return this.workspacesHistoryMainService.addRecentlyOpened(recents);
|
||||
}
|
||||
|
||||
async removeFromRecentlyOpened(windowId: number, paths: URI[]): Promise<void> {
|
||||
return this.workspacesHistoryMainService.removeFromRecentlyOpened(paths);
|
||||
async removeRecentlyOpened(windowId: number, paths: URI[]): Promise<void> {
|
||||
return this.workspacesHistoryMainService.removeRecentlyOpened(paths);
|
||||
}
|
||||
|
||||
async clearRecentlyOpened(windowId: number): Promise<void> {
|
||||
|
||||
@@ -11,14 +11,93 @@ import * as pfs from 'vs/base/node/pfs';
|
||||
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
|
||||
import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { normalizeDriveLetter } from 'vs/base/common/labels';
|
||||
import { dirname, joinPath } from 'vs/base/common/resources';
|
||||
import { TestBackupMainService, TestDialogMainService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
|
||||
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
|
||||
import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
|
||||
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
||||
|
||||
export class TestDialogMainService implements IDialogMainService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
pickFileFolder(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<string[] | undefined> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
pickFolder(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<string[] | undefined> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
pickFile(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<string[] | undefined> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
pickWorkspace(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<string[] | undefined> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
showMessageBox(options: Electron.MessageBoxOptions, window?: Electron.BrowserWindow | undefined): Promise<Electron.MessageBoxReturnValue> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
showSaveDialog(options: Electron.SaveDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<Electron.SaveDialogReturnValue> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
showOpenDialog(options: Electron.OpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<Electron.OpenDialogReturnValue> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class TestBackupMainService implements IBackupMainService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
isHotExitEnabled(): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getWorkspaceBackups(): IWorkspaceBackupInfo[] {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getFolderBackupPaths(): URI[] {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
registerWorkspaceBackupSync(workspace: IWorkspaceBackupInfo, migrateFrom?: string | undefined): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
registerFolderBackupSync(folderUri: URI): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
registerEmptyWindowBackupSync(backupFolder?: string | undefined, remoteAuthority?: string | undefined): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
unregisterFolderBackupSync(folderUri: URI): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
unregisterEmptyWindowBackupSync(backupFolder: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
suite('WorkspacesMainService', () => {
|
||||
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesservice');
|
||||
|
||||
Reference in New Issue
Block a user