Merge from vscode 718331d6f3ebd1b571530ab499edb266ddd493d5

This commit is contained in:
ADS Merger
2020-02-08 04:50:58 +00:00
parent 8c61538a27
commit 2af13c18d2
752 changed files with 16458 additions and 10063 deletions

View File

@@ -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,

View File

@@ -28,7 +28,7 @@ suite('MenuService', function () {
setup(function () {
menuService = new MenuService(NullCommandService);
testMenuId = Math.PI;
testMenuId = new MenuId('testo');
disposables.clear();
});

View File

@@ -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 };

View File

@@ -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
};

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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');
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -43,7 +43,6 @@ export interface IElectronService {
unmaximizeWindow(): Promise<void>;
minimizeWindow(): Promise<void>;
isWindowFocused(): Promise<boolean>;
focusWindow(options?: { windowId?: number }): Promise<void>;
// Dialogs

View File

@@ -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;

View File

@@ -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[]>;

View File

@@ -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) {
}

View File

@@ -24,4 +24,4 @@ suite('Extension Identifier Pattern', () => {
assert.equal(false, regEx.test('publ_isher.name'));
assert.equal(false, regEx.test('publisher._name'));
});
});
});

View File

@@ -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;
}
}
}
}

View File

@@ -804,7 +804,7 @@ export interface IFilesConfiguration {
eol: string;
enableTrash: boolean;
hotExit: string;
preventSaveConflicts: boolean;
saveConflictResolution: 'askUser' | 'overwriteFileOnDisk';
};
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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', () => {

View File

@@ -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 => {

View File

@@ -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;
}
}

View 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);
}
}
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -500,7 +500,7 @@ export class Menubar {
}).length > 0;
if (!success) {
this.workspacesHistoryMainService.removeFromRecentlyOpened([revivedUri]);
this.workspacesHistoryMainService.removeRecentlyOpened([revivedUri]);
}
}
}, false));

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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]));
}
}

View File

@@ -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) };
}

View File

@@ -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();
}

View File

@@ -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 });

View File

@@ -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."));

View File

@@ -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.

View File

@@ -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,

View File

@@ -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];

View File

@@ -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;
}

View File

@@ -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> {

View File

@@ -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)) }));

View File

@@ -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');
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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>;
}

View File

@@ -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');
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 = `
{

View File

@@ -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) {

View File

@@ -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>;
}

View File

@@ -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({

View File

@@ -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> {

View File

@@ -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');