chore(vscode): update to 1.53.2

These conflicts will be resolved in the following commits. We do it this way so
that PR review is possible.
This commit is contained in:
Joe Previte
2021-02-25 11:27:27 -07:00
1900 changed files with 83066 additions and 64589 deletions

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-action-bar .action-item.menu-entry .action-label {
background-image: var(--menu-entry-icon-light);
display: inline-flex;
}
.vs-dark .monaco-action-bar .action-item.menu-entry .action-label,
.hc-black .monaco-action-bar .action-item.menu-entry .action-label {
background-image: var(--menu-entry-icon-dark);
}

View File

@@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createCSSRule, asCSSUrl, ModifierKeyEmitter } from 'vs/base/browser/dom';
import 'vs/css!./menuEntryActionViewItem';
import { asCSSUrl, ModifierKeyEmitter } from 'vs/base/browser/dom';
import { domEvent } from 'vs/base/browser/event';
import { IAction, Separator } from 'vs/base/common/actions';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { IDisposable, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { ICommandAction, IMenu, IMenuActionOptions, MenuItemAction, SubmenuItemAction, Icon } from 'vs/platform/actions/common/actions';
@@ -17,6 +17,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
import { isWindows, isLinux } from 'vs/base/common/platform';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable {
const groups = menu.getActions(options);
@@ -44,8 +45,7 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemActio
}
function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemAction | SubmenuItemAction>]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {
for (let tuple of groups) {
let [group, actions] = tuple;
for (let [group, actions] of groups) {
if (useAlternativeActions) {
actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a);
}
@@ -66,10 +66,6 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemActi
}
}
const ids = new IdGenerator('menu-item-action-item-icon-');
const ICON_PATH_TO_CSS_RULES = new Map<string /* path*/, string /* CSS rule */>();
export class MenuEntryActionViewItem extends ActionViewItem {
private _wantsAltCommand: boolean = false;
@@ -85,7 +81,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
this._altKey = ModifierKeyEmitter.getInstance();
}
protected get _commandAction(): IAction {
protected get _commandAction(): MenuItemAction {
return this._wantsAltCommand && (<MenuItemAction>this._action).alt || this._action;
}
@@ -93,12 +89,14 @@ export class MenuEntryActionViewItem extends ActionViewItem {
event.preventDefault();
event.stopPropagation();
this.actionRunner.run(this._commandAction, this._context)
.then(undefined, err => this._notificationService.error(err));
this.actionRunner
.run(this._commandAction, this._context)
.catch(err => this._notificationService.error(err));
}
render(container: HTMLElement): void {
super.render(container);
container.classList.add('menu-entry');
this._updateItemClass(this._action.item);
@@ -167,46 +165,39 @@ export class MenuEntryActionViewItem extends ActionViewItem {
private _updateItemClass(item: ICommandAction): void {
this._itemClassDispose.value = undefined;
const { element, label } = this;
if (!element || !label) {
return;
}
const icon = this._commandAction.checked && (item.toggled as { icon?: Icon })?.icon ? (item.toggled as { icon: Icon }).icon : item.icon;
if (!icon) {
return;
}
if (ThemeIcon.isThemeIcon(icon)) {
// theme icons
const iconClass = ThemeIcon.asClassName(icon);
if (this.label && iconClass) {
this.label.classList.add(...iconClass.split(' '));
this._itemClassDispose.value = toDisposable(() => {
if (this.label) {
this.label.classList.remove(...iconClass.split(' '));
}
});
label.classList.add(...iconClass.split(' '));
this._itemClassDispose.value = toDisposable(() => {
label.classList.remove(...iconClass.split(' '));
});
} else {
// icon path/url
if (icon.light) {
label.style.setProperty('--menu-entry-icon-light', asCSSUrl(icon.light));
}
} else if (icon) {
// icon path
let iconClass: string;
if (icon.dark?.scheme) {
const iconPathMapKey = icon.dark.toString();
if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) {
iconClass = ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!;
} else {
iconClass = ids.nextId();
createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(icon.light || icon.dark)}`);
createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(icon.dark)}`);
ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass);
}
if (this.label) {
this.label.classList.add('icon', ...iconClass.split(' '));
this._itemClassDispose.value = toDisposable(() => {
if (this.label) {
this.label.classList.remove('icon', ...iconClass.split(' '));
}
});
}
if (icon.dark) {
label.style.setProperty('--menu-entry-icon-dark', asCSSUrl(icon.dark));
}
label.classList.add('icon');
this._itemClassDispose.value = toDisposable(() => {
label.classList.remove('icon');
label.style.removeProperty('--menu-entry-icon-light');
label.style.removeProperty('--menu-entry-icon-dark');
});
}
}
}
@@ -215,29 +206,41 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem {
constructor(
action: SubmenuItemAction,
@INotificationService _notificationService: INotificationService,
@IContextMenuService _contextMenuService: IContextMenuService
@IContextMenuService contextMenuService: IContextMenuService
) {
let classNames: string | string[] | undefined;
super(action, { getActions: () => action.actions }, contextMenuService, {
menuAsChild: true,
classNames: ThemeIcon.isThemeIcon(action.item.icon) ? ThemeIcon.asClassName(action.item.icon) : undefined,
});
}
if (action.item.icon) {
if (ThemeIcon.isThemeIcon(action.item.icon)) {
classNames = ThemeIcon.asClassName(action.item.icon)!;
} else if (action.item.icon.dark?.scheme) {
const iconPathMapKey = action.item.icon.dark.toString();
if (ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) {
classNames = ['icon', ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!];
} else {
const className = ids.nextId();
classNames = ['icon', className];
createCSSRule(`.icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.light || action.item.icon.dark)}`);
createCSSRule(`.vs-dark .icon.${className}, .hc-black .icon.${className}`, `background-image: ${asCSSUrl(action.item.icon.dark)}`);
ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, className);
render(container: HTMLElement): void {
super.render(container);
if (this.element) {
container.classList.add('menu-entry');
const { icon } = (<SubmenuItemAction>this._action).item;
if (icon && !ThemeIcon.isThemeIcon(icon)) {
this.element.classList.add('icon');
if (icon.light) {
this.element.style.setProperty('--menu-entry-icon-light', asCSSUrl(icon.light));
}
if (icon.dark) {
this.element.style.setProperty('--menu-entry-icon-dark', asCSSUrl(icon.dark));
}
}
}
super(action, action.actions, _contextMenuService, { classNames: classNames, menuAsChild: true });
}
}
/**
* Creates action view items for menu actions or submenu actions.
*/
export function createActionViewItem(instaService: IInstantiationService, action: IAction): undefined | MenuEntryActionViewItem | SubmenuEntryActionViewItem {
if (action instanceof MenuItemAction) {
return instaService.createInstance(MenuEntryActionViewItem, action);
} else if (action instanceof SubmenuItemAction) {
return instaService.createInstance(SubmenuEntryActionViewItem, action);
} else {
return undefined;
}
}

View File

@@ -16,22 +16,36 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { UriDto } from 'vs/base/common/types';
import { Iterable } from 'vs/base/common/iterator';
import { LinkedList } from 'vs/base/common/linkedList';
import { CSSIcon } from 'vs/base/common/codicons';
export interface ILocalizedString {
/**
* The localized value of the string.
*/
value: string;
/**
* The original (non localized value of the string)
*/
original: string;
}
export interface ICommandActionTitle extends ILocalizedString {
/**
* The title with a mnemonic designation. && precedes the mnemonic.
*/
mnemonicTitle?: string;
}
export type Icon = { dark?: URI; light?: URI; } | ThemeIcon;
export interface ICommandAction {
id: string;
title: string | ILocalizedString;
title: string | ICommandActionTitle;
category?: string | ILocalizedString;
tooltip?: string | ILocalizedString;
tooltip?: string;
icon?: Icon;
precondition?: ContextKeyExpression;
toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString };
toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string };
}
export type ISerializableCommandAction = UriDto<ICommandAction>;
@@ -45,7 +59,7 @@ export interface IMenuItem {
}
export interface ISubmenuItem {
title: string | ILocalizedString;
title: string | ICommandActionTitle;
submenu: MenuId;
icon?: Icon;
when?: ContextKeyExpression;
@@ -112,6 +126,7 @@ export class MenuId {
static readonly TunnelInline = new MenuId('TunnelInline');
static readonly TunnelTitle = new MenuId('TunnelTitle');
static readonly ViewItemContext = new MenuId('ViewItemContext');
static readonly ViewContainerTitle = new MenuId('ViewContainerTitle');
static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext');
static readonly ViewTitle = new MenuId('ViewTitle');
static readonly ViewTitleContext = new MenuId('ViewTitleContext');
@@ -132,6 +147,7 @@ export class MenuId {
static readonly TimelineTitle = new MenuId('TimelineTitle');
static readonly TimelineTitleContext = new MenuId('TimelineTitleContext');
static readonly AccountsContext = new MenuId('AccountsContext');
static readonly PanelTitle = new MenuId('PanelTitle');
readonly id: number;
readonly _debugName: string;
@@ -148,7 +164,7 @@ export interface IMenuActionOptions {
}
export interface IMenu extends IDisposable {
readonly onDidChange: Event<IMenu | undefined>;
readonly onDidChange: Event<IMenu>;
getActions(options?: IMenuActionOptions): [string, Array<MenuItemAction | SubmenuItemAction>][];
}
@@ -334,61 +350,71 @@ export class SubmenuItemAction extends SubmenuAction {
}
}
export class MenuItemAction extends ExecuteCommandAction {
// implements IAction, does NOT extend Action, so that no one
// subscribes to events of Action or modified properties
export class MenuItemAction implements IAction {
readonly item: ICommandAction;
readonly alt: MenuItemAction | undefined;
private _options: IMenuActionOptions;
private readonly _options: IMenuActionOptions | undefined;
readonly id: string;
readonly label: string;
readonly tooltip: string;
readonly class: string | undefined;
readonly enabled: boolean;
readonly checked: boolean;
constructor(
item: ICommandAction,
alt: ICommandAction | undefined,
options: IMenuActionOptions,
options: IMenuActionOptions | undefined,
@IContextKeyService contextKeyService: IContextKeyService,
@ICommandService commandService: ICommandService
@ICommandService private _commandService: ICommandService
) {
typeof item.title === 'string' ? super(item.id, item.title, commandService) : super(item.id, item.title.value, commandService);
this._cssClass = undefined;
this._enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition);
this._tooltip = item.tooltip ? typeof item.tooltip === 'string' ? item.tooltip : item.tooltip.value : undefined;
this.id = item.id;
this.label = typeof item.title === 'string' ? item.title : item.title.value;
this.tooltip = item.tooltip ?? '';
this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition);
this.checked = false;
if (item.toggled) {
const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as {
condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString
};
this._checked = contextKeyService.contextMatchesRules(toggled.condition);
if (this._checked && toggled.tooltip) {
this._tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value;
this.checked = contextKeyService.contextMatchesRules(toggled.condition);
if (this.checked && toggled.tooltip) {
this.tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value;
}
}
this._options = options || {};
this.item = item;
this.alt = alt ? new MenuItemAction(alt, undefined, this._options, contextKeyService, commandService) : undefined;
this.alt = alt ? new MenuItemAction(alt, undefined, options, contextKeyService, _commandService) : undefined;
this._options = options;
if (ThemeIcon.isThemeIcon(item.icon)) {
this.class = CSSIcon.asClassName(item.icon);
}
}
dispose(): void {
if (this.alt) {
this.alt.dispose();
}
super.dispose();
// there is NOTHING to dispose and the MenuItemAction should
// never have anything to dispose as it is a convenience type
// to bridge into the rendering world.
}
run(...args: any[]): Promise<any> {
let runArgs: any[] = [];
if (this._options.arg) {
if (this._options?.arg) {
runArgs = [...runArgs, this._options.arg];
}
if (this._options.shouldForwardArgs) {
if (this._options?.shouldForwardArgs) {
runArgs = [...runArgs, ...args];
}
return super.run(...runArgs);
return this._commandService.executeCommand(this.id, ...runArgs);
}
}

View File

@@ -3,11 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RunOnceScheduler } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IMenu, IMenuActionOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction, ILocalizedString } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextKeyService, IContextKeyChangeEvent, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
export class MenuService implements IMenuService {
@@ -29,9 +30,11 @@ type MenuItemGroup = [string, Array<IMenuItem | ISubmenuItem>];
class Menu implements IMenu {
private readonly _onDidChange = new Emitter<IMenu | undefined>();
private readonly _dispoables = new DisposableStore();
private readonly _onDidChange = new Emitter<IMenu>();
readonly onDidChange: Event<IMenu> = this._onDidChange.event;
private _menuGroups: MenuItemGroup[] = [];
private _contextKeys: Set<string> = new Set();
@@ -45,19 +48,23 @@ class Menu implements IMenu {
// rebuild this menu whenever the menu registry reports an
// event for this MenuId
this._dispoables.add(Event.debounce(
Event.filter(MenuRegistry.onDidChangeMenu, set => set.has(this._id)),
() => { },
50
)(this._build, this));
const rebuildMenuSoon = new RunOnceScheduler(() => this._build(), 50);
this._dispoables.add(rebuildMenuSoon);
this._dispoables.add(MenuRegistry.onDidChangeMenu(e => {
if (e.has(_id)) {
rebuildMenuSoon.schedule();
}
}));
// when context keys change we need to check if the menu also
// has changed
this._dispoables.add(Event.debounce<IContextKeyChangeEvent, boolean>(
this._contextKeyService.onDidChangeContext,
(last, event) => last || event.affectsSome(this._contextKeys),
50
)(e => e && this._onDidChange.fire(undefined), this));
const fireChangeSoon = new RunOnceScheduler(() => this._onDidChange.fire(this), 50);
this._dispoables.add(fireChangeSoon);
this._dispoables.add(_contextKeyService.onDidChangeContext(e => {
if (e.affectsSome(this._contextKeys)) {
fireChangeSoon.schedule();
}
}));
}
dispose(): void {
@@ -88,25 +95,22 @@ class Menu implements IMenu {
// keep keys for eventing
Menu._fillInKbExprKeys(item.when, this._contextKeys);
// keep precondition keys for event if applicable
if (isIMenuItem(item) && item.command.precondition) {
Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys);
}
// keep toggled keys for event if applicable
if (isIMenuItem(item) && item.command.toggled) {
const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled;
Menu._fillInKbExprKeys(toggledExpression, this._contextKeys);
if (isIMenuItem(item)) {
// keep precondition keys for event if applicable
if (item.command.precondition) {
Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys);
}
// keep toggled keys for event if applicable
if (item.command.toggled) {
const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled;
Menu._fillInKbExprKeys(toggledExpression, this._contextKeys);
}
}
}
this._onDidChange.fire(this);
}
get onDidChange(): Event<IMenu | undefined> {
return this._onDidChange.event;
}
getActions(options: IMenuActionOptions): [string, Array<MenuItemAction | SubmenuItemAction>][] {
getActions(options?: IMenuActionOptions): [string, Array<MenuItemAction | SubmenuItemAction>][] {
const result: [string, Array<MenuItemAction | SubmenuItemAction>][] = [];
for (let group of this._menuGroups) {
const [id, items] = group;

View File

@@ -20,39 +20,14 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/
import { ConsoleLogMainService } from 'vs/platform/log/common/log';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { createHash } from 'crypto';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { Schemas } from 'vs/base/common/network';
import { isEqual } from 'vs/base/common/resources';
suite('BackupMainService', () => {
flakySuite('BackupMainService', () => {
function assertEqualUris(actual: URI[], expected: URI[]) {
assert.deepEqual(actual.map(a => a.toString()), expected.map(a => a.toString()));
}
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupservice');
const backupHome = path.join(parentDir, 'Backups');
const backupWorkspacesPath = path.join(backupHome, 'workspaces.json');
const environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS));
class TestBackupMainService extends BackupMainService {
constructor(backupHome: string, backupWorkspacesPath: string, configService: TestConfigurationService) {
super(environmentService, configService, new ConsoleLogMainService());
this.backupHome = backupHome;
this.workspacesJsonPath = backupWorkspacesPath;
}
toBackupPath(arg: URI | string): string {
const id = arg instanceof URI ? super.getFolderHash(arg) : arg;
return path.join(this.backupHome, id);
}
getFolderHash(folderUri: URI): string {
return super.getFolderHash(folderUri);
}
assert.deepStrictEqual(actual.map(a => a.toString()), expected.map(a => a.toString()));
}
function toWorkspace(path: string): IWorkspaceIdentifier {
@@ -79,20 +54,23 @@ suite('BackupMainService', () => {
};
}
async function ensureFolderExists(uri: URI): Promise<void> {
function ensureFolderExists(uri: URI): Promise<void> {
if (!fs.existsSync(uri.fsPath)) {
fs.mkdirSync(uri.fsPath);
}
const backupFolder = service.toBackupPath(uri);
await createBackupFolder(backupFolder);
return createBackupFolder(backupFolder);
}
async function ensureWorkspaceExists(workspace: IWorkspaceIdentifier): Promise<IWorkspaceIdentifier> {
if (!fs.existsSync(workspace.configPath.fsPath)) {
await pfs.writeFile(workspace.configPath.fsPath, 'Hello');
}
const backupFolder = service.toBackupPath(workspace.id);
await createBackupFolder(backupFolder);
return workspace;
}
@@ -111,29 +89,52 @@ suite('BackupMainService', () => {
const fooFile = URI.file(platform.isWindows ? 'C:\\foo' : '/foo');
const barFile = URI.file(platform.isWindows ? 'C:\\bar' : '/bar');
const existingTestFolder1 = URI.file(path.join(parentDir, 'folder1'));
let service: TestBackupMainService;
let service: BackupMainService & { toBackupPath(arg: URI | string): string, getFolderHash(folderUri: URI): string };
let configService: TestConfigurationService;
setup(async () => {
let environmentService: EnvironmentMainService;
let testDir: string;
let backupHome: string;
let backupWorkspacesPath: string;
let existingTestFolder1: URI;
setup(async () => {
testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupmainservice');
backupHome = path.join(testDir, 'Backups');
backupWorkspacesPath = path.join(backupHome, 'workspaces.json');
existingTestFolder1 = URI.file(path.join(testDir, 'folder1'));
environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS));
// Delete any existing backups completely and then re-create it.
await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
await pfs.mkdirp(backupHome);
configService = new TestConfigurationService();
service = new TestBackupMainService(backupHome, backupWorkspacesPath, configService);
service = new class TestBackupMainService extends BackupMainService {
constructor() {
super(environmentService, configService, new ConsoleLogMainService());
this.backupHome = backupHome;
this.workspacesJsonPath = backupWorkspacesPath;
}
toBackupPath(arg: URI | string): string {
const id = arg instanceof URI ? super.getFolderHash(arg) : arg;
return path.join(this.backupHome, id);
}
getFolderHash(folderUri: URI): string {
return super.getFolderHash(folderUri);
}
};
return service.initialize();
});
teardown(() => {
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
return pfs.rimraf(testDir);
});
test('service validates backup workspaces on startup and cleans up (folder workspaces)', async function () {
this.timeout(1000 * 10); // increase timeout for this test
// 1) backup workspace path does not exist
service.registerFolderBackupSync(fooFile);
@@ -170,22 +171,21 @@ suite('BackupMainService', () => {
fs.mkdirSync(service.toBackupPath(barFile));
fs.mkdirSync(fileBackups);
service.registerFolderBackupSync(fooFile);
assert.equal(service.getFolderBackupPaths().length, 1);
assert.equal(service.getEmptyWindowBackupPaths().length, 0);
assert.strictEqual(service.getFolderBackupPaths().length, 1);
assert.strictEqual(service.getEmptyWindowBackupPaths().length, 0);
fs.writeFileSync(path.join(fileBackups, 'backup.txt'), '');
await service.initialize();
assert.equal(service.getFolderBackupPaths().length, 0);
assert.equal(service.getEmptyWindowBackupPaths().length, 1);
assert.strictEqual(service.getFolderBackupPaths().length, 0);
assert.strictEqual(service.getEmptyWindowBackupPaths().length, 1);
});
test('service validates backup workspaces on startup and cleans up (root workspaces)', async function () {
this.timeout(1000 * 10); // increase timeout for this test
// 1) backup workspace path does not exist
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath));
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath));
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
// 2) backup workspace path exists with empty contents within
fs.mkdirSync(service.toBackupPath(fooFile));
@@ -193,7 +193,7 @@ suite('BackupMainService', () => {
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath));
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath));
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
@@ -205,7 +205,7 @@ suite('BackupMainService', () => {
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath));
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath));
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
@@ -216,12 +216,12 @@ suite('BackupMainService', () => {
fs.mkdirSync(service.toBackupPath(barFile));
fs.mkdirSync(fileBackups);
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath));
assert.equal(service.getWorkspaceBackups().length, 1);
assert.equal(service.getEmptyWindowBackupPaths().length, 0);
assert.strictEqual(service.getWorkspaceBackups().length, 1);
assert.strictEqual(service.getEmptyWindowBackupPaths().length, 0);
fs.writeFileSync(path.join(fileBackups, 'backup.txt'), '');
await service.initialize();
assert.equal(service.getWorkspaceBackups().length, 0);
assert.equal(service.getEmptyWindowBackupPaths().length, 1);
assert.strictEqual(service.getWorkspaceBackups().length, 0);
assert.strictEqual(service.getEmptyWindowBackupPaths().length, 1);
});
test('service supports to migrate backup data from another location', () => {
@@ -237,7 +237,7 @@ suite('BackupMainService', () => {
assert.ok(!fs.existsSync(backupPathToMigrate));
const emptyBackups = service.getEmptyWindowBackupPaths();
assert.equal(0, emptyBackups.length);
assert.strictEqual(0, emptyBackups.length);
});
test('service backup migration makes sure to preserve existing backups', () => {
@@ -258,8 +258,8 @@ suite('BackupMainService', () => {
assert.ok(!fs.existsSync(backupPathToMigrate));
const emptyBackups = service.getEmptyWindowBackupPaths();
assert.equal(1, emptyBackups.length);
assert.equal(1, fs.readdirSync(path.join(backupHome, emptyBackups[0].backupFolder!)).length);
assert.strictEqual(1, emptyBackups.length);
assert.strictEqual(1, fs.readdirSync(path.join(backupHome, emptyBackups[0].backupFolder!)).length);
});
suite('loadSync', () => {
@@ -315,121 +315,120 @@ suite('BackupMainService', () => {
});
test('getWorkspaceBackups() should return [] when workspaces.json doesn\'t exist', () => {
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
});
test('getWorkspaceBackups() should return [] when workspaces.json is not properly formed JSON', async () => {
fs.writeFileSync(backupWorkspacesPath, '');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{]');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, 'foo');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
});
test('getWorkspaceBackups() should return [] when folderWorkspaces in workspaces.json is absent', async () => {
fs.writeFileSync(backupWorkspacesPath, '{}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
});
test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array', async () => {
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": ["bar"]}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": []}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": "bar"}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":"foo"}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":1}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
});
test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array', async () => {
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": ["bar"]}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": []}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": "bar"}}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":"foo"}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":1}');
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
});
test('getWorkspaceBackups() should return [] when files.hotExit = "onExitAndWindowClose"', async () => {
const upperFooPath = fooFile.fsPath.toUpperCase();
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath));
assert.equal(service.getWorkspaceBackups().length, 1);
assert.strictEqual(service.getWorkspaceBackups().length, 1);
assertEqualUris(service.getWorkspaceBackups().map(r => r.workspace.configPath), [URI.file(upperFooPath)]);
configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE);
await service.initialize();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.deepStrictEqual(service.getWorkspaceBackups(), []);
});
test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json doesn\'t exist', () => {
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
});
test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON', async () => {
fs.writeFileSync(backupWorkspacesPath, '');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{]');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, 'foo');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
});
test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', async () => {
fs.writeFileSync(backupWorkspacesPath, '{}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
});
test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', async function () {
this.timeout(5000);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{}}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": ["bar"]}}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": []}}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": "bar"}}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":"foo"}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":1}');
await service.initialize();
assert.deepEqual(service.getEmptyWindowBackupPaths(), []);
assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []);
});
});
@@ -448,7 +447,7 @@ suite('BackupMainService', () => {
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
test('should ignore duplicates on Windows and Mac (folder workspace)', async () => {
@@ -464,14 +463,13 @@ suite('BackupMainService', () => {
await service.initialize();
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
test('should ignore duplicates on Windows and Mac (root workspace)', async () => {
const workspacePath = path.join(parentDir, 'Foo.code-workspace');
const workspacePath1 = path.join(parentDir, 'FOO.code-workspace');
const workspacePath2 = path.join(parentDir, 'foo.code-workspace');
const workspacePath = path.join(testDir, 'Foo.code-workspace');
const workspacePath1 = path.join(testDir, 'FOO.code-workspace');
const workspacePath2 = path.join(testDir, 'foo.code-workspace');
const workspace1 = await ensureWorkspaceExists(toWorkspace(workspacePath));
const workspace2 = await ensureWorkspaceExists(toWorkspace(workspacePath1));
@@ -487,11 +485,11 @@ suite('BackupMainService', () => {
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.equal(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1);
assert.strictEqual(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1);
if (platform.isLinux) {
assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString(), URI.file(workspacePath1).toString(), URI.file(workspacePath2).toString()]);
assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString(), URI.file(workspacePath1).toString(), URI.file(workspacePath2).toString()]);
} else {
assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString()], 'should return the first duplicated entry');
assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [URI.file(workspacePath).toString()], 'should return the first duplicated entry');
}
});
});
@@ -503,7 +501,7 @@ suite('BackupMainService', () => {
assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]);
assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]);
});
test('should persist paths to workspaces.json (root workspace)', async () => {
@@ -513,15 +511,15 @@ suite('BackupMainService', () => {
service.registerWorkspaceBackupSync(ws2);
assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [fooFile, barFile]);
assert.equal(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id);
assert.equal(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id);
assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id);
assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id);
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]);
assert.equal(ws1.workspace.id, json.rootURIWorkspaces[0].id);
assert.equal(ws2.workspace.id, json.rootURIWorkspaces[1].id);
assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]);
assert.strictEqual(ws1.workspace.id, json.rootURIWorkspaces[0].id);
assert.strictEqual(ws2.workspace.id, json.rootURIWorkspaces[1].id);
});
});
@@ -531,7 +529,7 @@ suite('BackupMainService', () => {
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]);
assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]);
});
test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (root workspace)', async () => {
@@ -541,7 +539,7 @@ suite('BackupMainService', () => {
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]);
assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]);
});
suite('removeBackupPathSync', () => {
@@ -552,12 +550,12 @@ suite('BackupMainService', () => {
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]);
assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]);
service.unregisterFolderBackupSync(barFile);
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json2.folderURIWorkspaces, []);
assert.deepStrictEqual(json2.folderURIWorkspaces, []);
});
test('should remove folder workspaces from workspaces.json (root workspace)', async () => {
@@ -569,12 +567,12 @@ suite('BackupMainService', () => {
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]);
assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]);
service.unregisterWorkspaceBackupSync(ws2.workspace);
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json2.rootURIWorkspaces, []);
assert.deepStrictEqual(json2.rootURIWorkspaces, []);
});
test('should remove empty workspaces from workspaces.json', async () => {
@@ -584,12 +582,12 @@ suite('BackupMainService', () => {
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]);
assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]);
service.unregisterEmptyWindowBackupSync('bar');
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json2.emptyWorkspaceInfos, []);
assert.deepStrictEqual(json2.emptyWorkspaceInfos, []);
});
test('should fail gracefully when removing a path that doesn\'t exist', async () => {
@@ -603,24 +601,18 @@ suite('BackupMainService', () => {
service.unregisterEmptyWindowBackupSync('test');
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
});
suite('getWorkspaceHash', () => {
test('should ignore case on Windows and Mac', () => {
// Skip test on Linux
if (platform.isLinux) {
return;
}
(platform.isLinux ? test.skip : test)('should ignore case on Windows and Mac', () => {
if (platform.isMacintosh) {
assert.equal(service.getFolderHash(URI.file('/foo')), service.getFolderHash(URI.file('/FOO')));
assert.strictEqual(service.getFolderHash(URI.file('/foo')), service.getFolderHash(URI.file('/FOO')));
}
if (platform.isWindows) {
assert.equal(service.getFolderHash(URI.file('c:\\foo')), service.getFolderHash(URI.file('C:\\FOO')));
assert.strictEqual(service.getFolderHash(URI.file('c:\\foo')), service.getFolderHash(URI.file('C:\\FOO')));
}
});
});
@@ -631,9 +623,9 @@ suite('BackupMainService', () => {
service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
if (platform.isLinux) {
assert.equal(service.getFolderBackupPaths().length, 2);
assert.strictEqual(service.getFolderBackupPaths().length, 2);
} else {
assert.equal(service.getFolderBackupPaths().length, 1);
assert.strictEqual(service.getFolderBackupPaths().length, 1);
}
});
@@ -642,9 +634,9 @@ suite('BackupMainService', () => {
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath.toUpperCase()));
if (platform.isLinux) {
assert.equal(service.getWorkspaceBackups().length, 2);
assert.strictEqual(service.getWorkspaceBackups().length, 2);
} else {
assert.equal(service.getWorkspaceBackups().length, 1);
assert.strictEqual(service.getWorkspaceBackups().length, 1);
}
});
@@ -653,16 +645,16 @@ suite('BackupMainService', () => {
// same case
service.registerFolderBackupSync(fooFile);
service.unregisterFolderBackupSync(fooFile);
assert.equal(service.getFolderBackupPaths().length, 0);
assert.strictEqual(service.getFolderBackupPaths().length, 0);
// mixed case
service.registerFolderBackupSync(fooFile);
service.unregisterFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
if (platform.isLinux) {
assert.equal(service.getFolderBackupPaths().length, 1);
assert.strictEqual(service.getFolderBackupPaths().length, 1);
} else {
assert.equal(service.getFolderBackupPaths().length, 0);
assert.strictEqual(service.getFolderBackupPaths().length, 0);
}
});
});
@@ -674,7 +666,7 @@ suite('BackupMainService', () => {
const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath);
const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo);
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0);
try {
await pfs.mkdirp(path.join(folderBackupPath, Schemas.file));
@@ -683,13 +675,13 @@ suite('BackupMainService', () => {
// ignore - folder might exist already
}
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0);
fs.writeFileSync(path.join(folderBackupPath, Schemas.file, '594a4a9d82a277a899d4713a5b08f504'), '');
fs.writeFileSync(path.join(workspaceBackupPath, Schemas.untitled, '594a4a9d82a277a899d4713a5b08f504'), '');
const dirtyWorkspaces = await service.getDirtyWorkspaces();
assert.equal(dirtyWorkspaces.length, 2);
assert.strictEqual(dirtyWorkspaces.length, 2);
let found = 0;
for (const dirtyWorkpspace of dirtyWorkspaces) {
@@ -704,7 +696,7 @@ suite('BackupMainService', () => {
}
}
assert.equal(found, 2);
assert.strictEqual(found, 2);
});
});
});

View File

@@ -16,7 +16,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { IFileService } from 'vs/platform/files/common/files';
import { dirname } from 'vs/base/common/resources';
import { IExtUri } from 'vs/base/common/resources';
export class ConfigurationModel implements IConfigurationModel {
@@ -348,11 +348,12 @@ export class UserSettings extends Disposable {
constructor(
private readonly userSettingsResource: URI,
private readonly scopes: ConfigurationScope[] | undefined,
extUri: IExtUri,
private readonly fileService: IFileService
) {
super();
this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes);
this._register(this.fileService.watch(dirname(this.userSettingsResource)));
this._register(this.fileService.watch(extUri.dirname(this.userSettingsResource)));
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire()));
}

View File

@@ -112,6 +112,13 @@ export interface IConfigurationPropertySchema extends IJSONSchema {
scope?: ConfigurationScope;
included?: boolean;
tags?: string[];
/**
* When enabled this setting is ignored during sync and user can override this.
*/
ignoreSync?: boolean;
/**
* When enabled this setting is ignored during sync and user cannot override this.
*/
disallowSyncIgnore?: boolean;
enumItemLabels?: string[];
}

View File

@@ -12,6 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { RunOnceScheduler } from 'vs/base/common/async';
import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable {
@@ -29,7 +30,7 @@ export class ConfigurationService extends Disposable implements IConfigurationSe
fileService: IFileService
) {
super();
this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, fileService));
this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, extUriBiasedIgnorePathCase, fileService));
this.configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel());
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50));

View File

@@ -3,9 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event';
import { Emitter, PauseableEmitter } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/map';
import { distinct } from 'vs/base/common/objects';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
@@ -244,12 +244,14 @@ class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent {
}
export abstract class AbstractContextKeyService implements IContextKeyService {
public _serviceBrand: undefined;
declare _serviceBrand: undefined;
protected _isDisposed: boolean;
protected _onDidChangeContext = new PauseableEmitter<IContextKeyChangeEvent>({ merge: input => new CompositeContextKeyChangeEvent(input) });
protected _myContextId: number;
protected _onDidChangeContext = new PauseableEmitter<IContextKeyChangeEvent>({ merge: input => new CompositeContextKeyChangeEvent(input) });
readonly onDidChangeContext = this._onDidChangeContext.event;
constructor(myContextId: number) {
this._isDisposed = false;
this._myContextId = myContextId;
@@ -268,9 +270,6 @@ export abstract class AbstractContextKeyService implements IContextKeyService {
return new ContextKey(this, key, defaultValue);
}
public get onDidChangeContext(): Event<IContextKeyChangeEvent> {
return this._onDidChangeContext.event;
}
bufferChangeEvents(callback: Function): void {
this._onDidChangeContext.pause();
@@ -371,6 +370,7 @@ export class ContextKeyService extends AbstractContextKeyService implements ICon
}
public dispose(): void {
this._onDidChangeContext.dispose();
this._isDisposed = true;
this._toDispose.dispose();
}
@@ -407,34 +407,34 @@ class ScopedContextKeyService extends AbstractContextKeyService {
private _parent: AbstractContextKeyService;
private _domNode: IContextKeyServiceTarget | undefined;
private _parentChangeListener: IDisposable | undefined;
private readonly _parentChangeListener = new MutableDisposable();
constructor(parent: AbstractContextKeyService, domNode?: IContextKeyServiceTarget) {
super(parent.createChildContext());
this._parent = parent;
this.updateParentChangeListener();
this._updateParentChangeListener();
if (domNode) {
this._domNode = domNode;
if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
console.error('Element already has context attribute');
let extraInfo = '';
if ((this._domNode as HTMLElement).classList) {
extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(', ');
}
console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`);
}
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
}
}
private updateParentChangeListener(): void {
if (this._parentChangeListener) {
this._parentChangeListener.dispose();
}
this._parentChangeListener = this._parent.onDidChangeContext(e => {
// Forward parent events to this listener. Parent will change.
this._onDidChangeContext.fire(e);
});
private _updateParentChangeListener(): void {
// Forward parent events to this listener. Parent will change.
this._parentChangeListener.value = this._parent.onDidChangeContext(this._onDidChangeContext.fire, this._onDidChangeContext);
}
public dispose(): void {
this._onDidChangeContext.dispose();
this._isDisposed = true;
this._parent.disposeContext(this._myContextId);
this._parentChangeListener?.dispose();
@@ -444,10 +444,6 @@ class ScopedContextKeyService extends AbstractContextKeyService {
}
}
public get onDidChangeContext(): Event<IContextKeyChangeEvent> {
return this._onDidChangeContext.event;
}
public getContextValuesContainer(contextId: number): Context {
if (this._isDisposed) {
return NullContext.INSTANCE;
@@ -473,7 +469,7 @@ class ScopedContextKeyService extends AbstractContextKeyService {
const thisContainer = this._parent.getContextValuesContainer(this._myContextId);
const oldAllValues = thisContainer.collectAllValues();
this._parent = parentContextKeyService;
this.updateParentChangeListener();
this._updateParentChangeListener();
const newParentContainer = this._parent.getContextValuesContainer(this._parent.contextId);
thisContainer.updateParent(newParentContainer);

View File

@@ -1278,11 +1278,11 @@ export class RawContextKey<T> extends ContextKeyDefinedExpr {
return ContextKeyExpr.not(this.key);
}
public isEqualTo(value: string): ContextKeyExpression {
public isEqualTo(value: any): ContextKeyExpression {
return ContextKeyExpr.equals(this.key, value);
}
public notEqualsTo(value: string): ContextKeyExpression {
public notEqualsTo(value: any): ContextKeyExpression {
return ContextKeyExpr.notEquals(this.key, value);
}
}

View File

@@ -34,10 +34,10 @@ suite('ContextKeyService', () => {
assert.ok(e.affectsSome(new Set(['testC'])), 'testC changed');
assert.ok(!e.affectsSome(new Set(['testD'])), 'testD did not change');
assert.equal(child.getContextKeyValue('testA'), 3);
assert.equal(child.getContextKeyValue('testB'), undefined);
assert.equal(child.getContextKeyValue('testC'), 4);
assert.equal(child.getContextKeyValue('testD'), 0);
assert.strictEqual(child.getContextKeyValue('testA'), 3);
assert.strictEqual(child.getContextKeyValue('testB'), undefined);
assert.strictEqual(child.getContextKeyValue('testC'), 4);
assert.strictEqual(child.getContextKeyValue('testD'), 0);
} catch (err) {
reject(err);
return;

View File

@@ -67,7 +67,7 @@ suite('ContextKeyExpr', () => {
function testExpression(expr: string, expected: boolean): void {
// console.log(expr + ' ' + expected);
let rules = ContextKeyExpr.deserialize(expr);
assert.equal(rules!.evaluate(context), expected, expr);
assert.strictEqual(rules!.evaluate(context), expected, expr);
}
function testBatch(expr: string, value: any): void {
/* eslint-disable eqeqeq */
@@ -153,17 +153,17 @@ suite('ContextKeyExpr', () => {
test('ContextKeyInExpr', () => {
const ainb = ContextKeyExpr.deserialize('a in b')!;
assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true);
assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true);
assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false);
assert.equal(ainb.evaluate(createContext({ 'a': 3 })), false);
assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false);
assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true);
assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false);
assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false);
assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true);
assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true);
assert.equal(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 3 })), false);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true);
assert.strictEqual(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false);
});
test('issue #106524: distributing AND should normalize', () => {
@@ -184,13 +184,13 @@ suite('ContextKeyExpr', () => {
ContextKeyExpr.has('c')
)
);
assert.equal(actual!.equals(expected!), true);
assert.strictEqual(actual!.equals(expected!), true);
});
test('Greater, GreaterEquals, Smaller, SmallerEquals evaluate', () => {
function checkEvaluate(expr: string, ctx: any, expected: any): void {
const _expr = ContextKeyExpr.deserialize(expr)!;
assert.equal(_expr.evaluate(createContext(ctx)), expected);
assert.strictEqual(_expr.evaluate(createContext(ctx)), expected);
}
checkEvaluate('a>1', {}, false);
@@ -236,7 +236,7 @@ suite('ContextKeyExpr', () => {
function checkNegate(expr: string, expected: string): void {
const a = ContextKeyExpr.deserialize(expr)!;
const b = a.negate();
assert.equal(b.serialize(), expected);
assert.strictEqual(b.serialize(), expected);
}
checkNegate('a>1', 'a <= 1');

View File

@@ -158,7 +158,7 @@ export class ContextMenuHandler {
}
private onDidActionRun(e: IRunEvent): void {
if (e.error && this.notificationService) {
if (e.error) {
this.notificationService.error(e.error);
}
}

View File

@@ -5,7 +5,6 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { IRemoteConsoleLog } from 'vs/base/common/console';
import { IProcessEnvironment } from 'vs/base/common/platform';
export const IExtensionHostDebugService = createDecorator<IExtensionHostDebugService>('extensionHostDebugService');
@@ -16,11 +15,6 @@ export interface IAttachSessionEvent {
port: number;
}
export interface ILogToSessionEvent {
sessionId: string;
log: IRemoteConsoleLog;
}
export interface ITerminateSessionEvent {
sessionId: string;
subId?: string;
@@ -50,9 +44,6 @@ export interface IExtensionHostDebugService {
attachSession(sessionId: string, port: number, subId?: string): void;
readonly onAttachSession: Event<IAttachSessionEvent>;
logToSession(sessionId: string, log: IRemoteConsoleLog): void;
readonly onLogToSession: Event<ILogToSessionEvent>;
terminateSession(sessionId: string, subId?: string): void;
readonly onTerminateSession: Event<ITerminateSessionEvent>;

View File

@@ -4,9 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
import { Event, Emitter } from 'vs/base/common/event';
import { IRemoteConsoleLog } from 'vs/base/common/console';
import { Disposable } from 'vs/base/common/lifecycle';
import { IProcessEnvironment } from 'vs/base/common/platform';
@@ -17,7 +16,6 @@ export class ExtensionHostDebugBroadcastChannel<TContext> implements IServerChan
private readonly _onCloseEmitter = new Emitter<ICloseSessionEvent>();
private readonly _onReloadEmitter = new Emitter<IReloadSessionEvent>();
private readonly _onTerminateEmitter = new Emitter<ITerminateSessionEvent>();
private readonly _onLogToEmitter = new Emitter<ILogToSessionEvent>();
private readonly _onAttachEmitter = new Emitter<IAttachSessionEvent>();
call(ctx: TContext, command: string, arg?: any): Promise<any> {
@@ -28,8 +26,6 @@ export class ExtensionHostDebugBroadcastChannel<TContext> implements IServerChan
return Promise.resolve(this._onReloadEmitter.fire({ sessionId: arg[0] }));
case 'terminate':
return Promise.resolve(this._onTerminateEmitter.fire({ sessionId: arg[0] }));
case 'log':
return Promise.resolve(this._onLogToEmitter.fire({ sessionId: arg[0], log: arg[1] }));
case 'attach':
return Promise.resolve(this._onAttachEmitter.fire({ sessionId: arg[0], port: arg[1], subId: arg[2] }));
}
@@ -44,8 +40,6 @@ export class ExtensionHostDebugBroadcastChannel<TContext> implements IServerChan
return this._onReloadEmitter.event;
case 'terminate':
return this._onTerminateEmitter.event;
case 'log':
return this._onLogToEmitter.event;
case 'attach':
return this._onAttachEmitter.event;
}
@@ -85,14 +79,6 @@ export class ExtensionHostDebugChannelClient extends Disposable implements IExte
return this.channel.listen('attach');
}
logToSession(sessionId: string, log: IRemoteConsoleLog): void {
this.channel.call('log', [sessionId, log]);
}
get onLogToSession(): Event<ILogToSessionEvent> {
return this.channel.listen('log');
}
terminateSession(sessionId: string, subId?: string): void {
this.channel.call('terminate', [sessionId, subId]);
}

View File

@@ -8,8 +8,7 @@ import { IProcessEnvironment } from 'vs/base/common/platform';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { createServer, AddressInfo } from 'net';
import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { OpenContext } from 'vs/platform/windows/node/window';
import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows';
export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends ExtensionHostDebugBroadcastChannel<TContext> {

View File

@@ -314,10 +314,10 @@ export class DiagnosticsService implements IDiagnosticsService {
if (isLinux) {
systemInfo.linuxEnv = {
desktopSession: process.env.DESKTOP_SESSION,
xdgSessionDesktop: process.env.XDG_SESSION_DESKTOP,
xdgCurrentDesktop: process.env.XDG_CURRENT_DESKTOP,
xdgSessionType: process.env.XDG_SESSION_TYPE
desktopSession: process.env['DESKTOP_SESSION'],
xdgSessionDesktop: process.env['XDG_SESSION_DESKTOP'],
xdgCurrentDesktop: process.env['XDG_CURRENT_DESKTOP'],
xdgSessionType: process.env['XDG_SESSION_TYPE']
};
}

View File

@@ -181,6 +181,7 @@ export interface IDialogOptions {
cancelId?: number;
detail?: string;
checkbox?: ICheckbox;
useCustom?: boolean;
}
export interface IInput {

View File

@@ -30,6 +30,7 @@ export interface NativeParsedArgs {
'prof-startup'?: boolean;
'prof-startup-prefix'?: string;
'prof-append-timers'?: string;
'prof-v8-extensions'?: boolean;
verbose?: boolean;
trace?: boolean;
'trace-category-filter'?: string;

View File

@@ -55,8 +55,8 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'extra-builtin-extensions-dir': { type: 'string[]', cat: 'o', description: 'Path to an extra builtin extension directory.' },
'extra-extensions-dir': { type: 'string[]', cat: 'o', description: 'Path to an extra user extension directory.' },
'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") },
'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") },
'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") },
'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") },
'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions.") },
'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. The identifier of an extension is always `${publisher}.${name}`. Use `--force` argument to update to latest version. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") },
'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") },
'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") },
@@ -68,6 +68,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup") },
'prof-append-timers': { type: 'string' },
'prof-startup-prefix': { type: 'string' },
'prof-v8-extensions': { type: 'boolean' },
'disable-extensions': { type: 'boolean', deprecates: 'disableExtensions', cat: 't', description: localize('disableExtensions', "Disable all installed extensions.") },
'disable-extension': { type: 'string[]', cat: 't', args: 'extension-id', description: localize('disableExtension', "Disable an extension.") },
'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] },
@@ -151,10 +152,6 @@ export function parseArgs<T>(args: string[], options: OptionDescriptions<T>, err
const string: string[] = [];
const boolean: string[] = [];
for (let optionId in options) {
if (optionId[0] === '_') {
continue;
}
const o = options[optionId];
if (o.alias) {
alias[optionId] = o.alias;

View File

@@ -13,35 +13,35 @@ suite('EnvironmentService', () => {
test('parseExtensionHostPort when built', () => {
const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), true);
assert.deepEqual(parse([]), { port: null, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost']), { port: null, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
assert.deepStrictEqual(parse([]), { port: null, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--debugPluginHost']), { port: null, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: null, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
assert.deepEqual(parse(['--inspect-extensions']), { port: null, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
assert.deepStrictEqual(parse(['--inspect-extensions']), { port: null, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: null, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
});
test('parseExtensionHostPort when unbuilt', () => {
const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), false);
assert.deepEqual(parse([]), { port: 5870, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined });
assert.deepEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
assert.deepStrictEqual(parse([]), { port: 5870, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--debugPluginHost']), { port: 5870, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--debugPluginHost=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--debugBrkPluginHost']), { port: 5870, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--debugBrkPluginHost=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepStrictEqual(parse(['--debugPluginHost=1234', '--debugBrkPluginHost=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
assert.deepEqual(parse(['--inspect-extensions']), { port: 5870, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, debugId: undefined });
assert.deepEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
assert.deepStrictEqual(parse(['--inspect-extensions']), { port: 5870, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--inspect-extensions=1234']), { port: 1234, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--inspect-brk-extensions']), { port: 5870, break: false, debugId: undefined });
assert.deepStrictEqual(parse(['--inspect-brk-extensions=5678']), { port: 5678, break: true, debugId: undefined });
assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
});
test('userDataPath', () => {
@@ -57,10 +57,10 @@ suite('EnvironmentService', () => {
test('careful with boolean file names', function () {
let actual = parseArgs(['-r', 'arg.txt'], OPTIONS);
assert(actual['reuse-window']);
assert.deepEqual(actual._, ['arg.txt']);
assert.deepStrictEqual(actual._, ['arg.txt']);
actual = parseArgs(['-r', 'true.txt'], OPTIONS);
assert(actual['reuse-window']);
assert.deepEqual(actual._, ['true.txt']);
assert.deepStrictEqual(actual._, ['true.txt']);
});
});

View File

@@ -37,11 +37,6 @@ suite('Native Modules (all platforms)', () => {
assert.ok(typeof spdlog.createRotatingLogger === 'function', testErrorMessage('spdlog'));
});
test('v8-inspect-profiler', async () => {
const profiler = await import('v8-inspect-profiler');
assert.ok(typeof profiler.startProfiling === 'function', testErrorMessage('v8-inspect-profiler'));
});
test('vscode-nsfw', async () => {
const nsfWatcher = await import('vscode-nsfw');
assert.ok(typeof nsfWatcher === 'function', testErrorMessage('vscode-nsfw'));
@@ -90,7 +85,7 @@ suite('Native Modules (all platforms)', () => {
test('vscode-windows-ca-certs', async () => {
// @ts-ignore Windows only
const windowsCerts = await import('vscode-windows-ca-certs');
const store = windowsCerts();
const store = new windowsCerts.Crypt32();
assert.ok(windowsCerts, testErrorMessage('vscode-windows-ca-certs'));
let certCount = 0;
try {

View File

@@ -9,7 +9,7 @@ import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGallery
import { getOrDefault } from 'vs/base/common/objects';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IPager } from 'vs/base/common/paging';
import { IRequestService, asJson, asText } from 'vs/platform/request/common/request';
import { IRequestService, asJson, asText, isSuccess } from 'vs/platform/request/common/request';
import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -147,6 +147,35 @@ const DefaultQueryState: IQueryState = {
assetTypes: []
};
type GalleryServiceQueryClassification = {
filterTypes: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
sortBy: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
sortOrder: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', 'isMeasurement': true };
success: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
requestBodySize: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
responseBodySize?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
statusCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
errorCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
count?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
type QueryTelemetryData = {
filterTypes: string[];
sortBy: string;
sortOrder: string;
};
type GalleryServiceQueryEvent = QueryTelemetryData & {
duration: number;
success: boolean;
requestBodySize: string;
responseBodySize?: string;
statusCode?: string;
errorCode?: string;
count?: string;
};
class Query {
constructor(private state = DefaultQueryState) { }
@@ -196,6 +225,14 @@ class Query {
const criterium = this.state.criteria.filter(criterium => criterium.filterType === FilterType.SearchText)[0];
return criterium && criterium.value ? criterium.value : '';
}
get telemetryData(): QueryTelemetryData {
return {
filterTypes: this.state.criteria.map(criterium => String(criterium.filterType)),
sortBy: String(this.sortBy),
sortOrder: String(this.sortOrder)
};
}
}
function getStatistic(statistics: IRawGalleryExtensionStatistics[], name: string): number {
@@ -447,20 +484,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
throw new Error('No extension gallery service configured.');
}
const type = options.names ? 'ids' : (options.text ? 'text' : 'all');
let text = options.text || '';
const pageSize = getOrDefault(options, o => o.pageSize, 50);
type GalleryServiceQueryClassification = {
type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
text: { classification: 'CustomerContent', purpose: 'FeatureInsight' };
};
type GalleryServiceQueryEvent = {
type: string;
text: string;
};
this.telemetryService.publicLog2<GalleryServiceQueryEvent, GalleryServiceQueryClassification>('galleryService:query', { type, text });
let query = new Query()
.withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, pageSize)
@@ -543,27 +569,49 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
'Content-Length': String(data.length)
};
const context = await this.requestService.request({
type: 'POST',
url: this.api('/extensionquery'),
data,
headers
}, token);
const startTime = new Date().getTime();
let context: IRequestContext | undefined, error: any, total: number = 0;
if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) {
return { galleryExtensions: [], total: 0 };
try {
context = await this.requestService.request({
type: 'POST',
url: this.api('/extensionquery'),
data,
headers
}, token);
if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) {
return { galleryExtensions: [], total };
}
const result = await asJson<IRawGalleryQueryResult>(context);
if (result) {
const r = result.results[0];
const galleryExtensions = r.extensions;
const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0];
total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0;
return { galleryExtensions, total };
}
return { galleryExtensions: [], total };
} catch (e) {
error = e;
throw e;
} finally {
this.telemetryService.publicLog2<GalleryServiceQueryEvent, GalleryServiceQueryClassification>('galleryService:query', {
...query.telemetryData,
requestBodySize: String(data.length),
duration: new Date().getTime() - startTime,
success: !!context && isSuccess(context),
responseBodySize: context?.res.headers['Content-Length'],
statusCode: context ? String(context.res.statusCode) : undefined,
errorCode: error
? isPromiseCanceledError(error) ? 'canceled' : getErrorMessage(error).startsWith('XHR timeout') ? 'timeout' : 'failed'
: undefined,
count: String(total)
});
}
const result = await asJson<IRawGalleryQueryResult>(context);
if (result) {
const r = result.results[0];
const galleryExtensions = r.extensions;
const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0];
const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0;
return { galleryExtensions, total };
}
return { galleryExtensions: [], total: 0 };
}
async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void> {

View File

@@ -278,3 +278,19 @@ export const ExtensionsLocalizedLabel = { value: ExtensionsLabel, original: 'Ext
export const ExtensionsChannelId = 'extensions';
export const PreferencesLabel = localize('preferences', "Preferences");
export const PreferencesLocalizedLabel = { value: PreferencesLabel, original: 'Preferences' };
export interface CLIOutput {
log(s: string): void;
error(s: string): void;
}
export const IExtensionManagementCLIService = createDecorator<IExtensionManagementCLIService>('IExtensionManagementCLIService');
export interface IExtensionManagementCLIService {
readonly _serviceBrand: undefined;
listExtensions(showVersions: boolean, category?: string, output?: CLIOutput): Promise<void>;
installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output?: CLIOutput): Promise<void>;
uninstallExtensions(extensions: (string | URI)[], force: boolean, output?: CLIOutput): Promise<void>;
locateExtension(extensions: string[], output?: CLIOutput): Promise<void>;
}

View File

@@ -0,0 +1,347 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { URI } from 'vs/base/common/uri';
import { gt } from 'vs/base/common/semver/semver';
import { CLIOutput, IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions';
import { getBaseLabel } from 'vs/base/common/labels';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Schemas } from 'vs/base/common/network';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id);
const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp');
function getId(manifest: IExtensionManifest, withVersion?: boolean): string {
if (withVersion) {
return `${manifest.publisher}.${manifest.name}@${manifest.version}`;
} else {
return `${manifest.publisher}.${manifest.name}`;
}
}
const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;
export function getIdAndVersion(id: string): [string, string | undefined] {
const matches = EXTENSION_ID_REGEX.exec(id);
if (matches && matches[1]) {
return [adoptToGalleryExtensionId(matches[1]), matches[2]];
}
return [adoptToGalleryExtensionId(id), undefined];
}
type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions };
export class ExtensionManagementCLIService implements IExtensionManagementCLIService {
_serviceBrand: any;
constructor(
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@ILocalizationsService private readonly localizationsService: ILocalizationsService
) { }
protected get location(): string | undefined {
return undefined;
}
public async listExtensions(showVersions: boolean, category?: string, output: CLIOutput = console): Promise<void> {
let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase());
if (category && category !== '') {
if (categories.indexOf(category.toLowerCase()) < 0) {
output.log('Invalid category please enter a valid category. To list valid categories run --category without a category specified');
return;
}
extensions = extensions.filter(e => {
if (e.manifest.categories) {
const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase());
return lowerCaseCategories.indexOf(category.toLowerCase()) > -1;
}
return false;
});
} else if (category === '') {
output.log('Possible Categories: ');
categories.forEach(category => {
output.log(category);
});
return;
}
if (this.location) {
output.log(localize('listFromLocation', "Extensions installed on {0}:", this.location));
}
extensions = extensions.sort((e1, e2) => e1.identifier.id.localeCompare(e2.identifier.id));
let lastId: string | undefined = undefined;
for (let extension of extensions) {
if (lastId !== extension.identifier.id) {
lastId = extension.identifier.id;
output.log(getId(extension.manifest, showVersions));
}
}
}
public async installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output: CLIOutput = console): Promise<void> {
const failed: string[] = [];
const installedExtensionsManifests: IExtensionManifest[] = [];
if (extensions.length) {
output.log(this.location ? localize('installingExtensionsOnLocation', "Installing extensions on {0}...", this.location) : localize('installingExtensions', "Installing extensions..."));
}
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
const checkIfNotInstalled = (id: string, version?: string): boolean => {
const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id }));
if (installedExtension) {
if (!version && !force) {
output.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@<version>' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id));
return false;
}
if (version && installedExtension.manifest.version === version) {
output.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`));
return false;
}
}
return true;
};
const vsixs: URI[] = [];
const installExtensionInfos: InstallExtensionInfo[] = [];
for (const extension of extensions) {
if (extension instanceof URI) {
vsixs.push(extension);
} else {
const [id, version] = getIdAndVersion(extension);
if (checkIfNotInstalled(id, version)) {
installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } });
}
}
}
for (const extension of builtinExtensionIds) {
const [id, version] = getIdAndVersion(extension);
if (checkIfNotInstalled(id, version)) {
installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } });
}
}
if (vsixs.length) {
await Promise.all(vsixs.map(async vsix => {
try {
const manifest = await this.installVSIX(vsix, force, output);
if (manifest) {
installedExtensionsManifests.push(manifest);
}
} catch (err) {
output.error(err.message || err.stack || err);
failed.push(vsix.toString());
}
}));
}
if (installExtensionInfos.length) {
const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos);
await Promise.all(installExtensionInfos.map(async extensionInfo => {
const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase());
if (gallery) {
try {
const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force, output);
if (manifest) {
installedExtensionsManifests.push(manifest);
}
} catch (err) {
output.error(err.message || err.stack || err);
failed.push(extensionInfo.id);
}
} else {
output.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`);
failed.push(extensionInfo.id);
}
}));
}
if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) {
await this.updateLocalizationsCache();
}
if (failed.length) {
throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', ')));
}
}
private async installVSIX(vsix: URI, force: boolean, output: CLIOutput): Promise<IExtensionManifest | null> {
const manifest = await this.extensionManagementService.getManifest(vsix);
if (!manifest) {
throw new Error('Invalid vsix');
}
const valid = await this.validateVSIX(manifest, force, output);
if (valid) {
try {
await this.extensionManagementService.install(vsix);
output.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix)));
return manifest;
} catch (error) {
if (isPromiseCanceledError(error)) {
output.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix)));
return null;
} else {
throw error;
}
}
}
return null;
}
private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise<Map<string, IGalleryExtension>> {
const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id);
const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined);
const galleryExtensions = new Map<string, IGalleryExtension>();
await Promise.all([
(async () => {
const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None);
result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension));
})(),
Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => {
const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version);
if (extension) {
galleryExtensions.set(extension.identifier.id.toLowerCase(), extension);
}
}))
]);
return galleryExtensions;
}
private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean, output: CLIOutput): Promise<IExtensionManifest | null> {
const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None);
if (manifest && !this.validateExtensionKind(manifest, output)) {
return null;
}
const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier));
if (installedExtension) {
if (galleryExtension.version === installedExtension.manifest.version) {
output.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id));
return null;
}
output.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version));
}
try {
if (installOptions.isBuiltin) {
output.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version));
} else {
output.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version));
}
await this.extensionManagementService.installFromGallery(galleryExtension, installOptions);
output.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version));
return manifest;
} catch (error) {
if (isPromiseCanceledError(error)) {
output.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id));
return null;
} else {
throw error;
}
}
}
protected validateExtensionKind(_manifest: IExtensionManifest, output: CLIOutput): boolean {
return true;
}
private async validateVSIX(manifest: IExtensionManifest, force: boolean, output: CLIOutput): Promise<boolean> {
const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && gt(local.manifest.version, manifest.version));
if (newer && !force) {
output.log(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version));
return false;
}
return this.validateExtensionKind(manifest, output);
}
public async uninstallExtensions(extensions: (string | URI)[], force: boolean, output: CLIOutput = console): Promise<void> {
const getExtensionId = async (extensionDescription: string | URI): Promise<string> => {
if (extensionDescription instanceof URI) {
const manifest = await this.extensionManagementService.getManifest(extensionDescription);
return getId(manifest);
}
return extensionDescription;
};
const uninstalledExtensions: ILocalExtension[] = [];
for (const extension of extensions) {
const id = await getExtensionId(extension);
const installed = await this.extensionManagementService.getInstalled();
const extensionsToUninstall = installed.filter(e => areSameExtensions(e.identifier, { id }));
if (!extensionsToUninstall.length) {
throw new Error(`${this.notInstalled(id)}\n${useId}`);
}
if (extensionsToUninstall.some(e => e.type === ExtensionType.System)) {
output.log(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be uninstalled", id));
return;
}
if (!force && extensionsToUninstall.some(e => e.isBuiltin)) {
output.log(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id));
return;
}
output.log(localize('uninstalling', "Uninstalling {0}...", id));
for (const extensionToUninstall of extensionsToUninstall) {
await this.extensionManagementService.uninstall(extensionToUninstall);
uninstalledExtensions.push(extensionToUninstall);
}
if (this.location) {
output.log(localize('successUninstallFromLocation', "Extension '{0}' was successfully uninstalled from {1}!", id, this.location));
} else {
output.log(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id));
}
}
if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) {
await this.updateLocalizationsCache();
}
}
public async locateExtension(extensions: string[], output: CLIOutput = console): Promise<void> {
const installed = await this.extensionManagementService.getInstalled();
extensions.forEach(e => {
installed.forEach(i => {
if (i.identifier.id === e) {
if (i.location.scheme === Schemas.file) {
output.log(i.location.fsPath);
return;
}
}
});
});
}
private updateLocalizationsCache(): Promise<boolean> {
return this.localizationsService.update();
}
private notInstalled(id: string) {
return this.location ? localize('notInstalleddOnLocation', "Extension '{0}' is not installed on {1}.", id, this.location) : localize('notInstalled', "Extension '{0}' is not installed.", id);
}
}

View File

@@ -42,12 +42,16 @@ export class ExtensionIdentifierWithVersion implements IExtensionIdentifierWithV
}
}
export function getExtensionId(publisher: string, name: string): string {
return `${publisher}.${name}`;
}
export function adoptToGalleryExtensionId(id: string): string {
return id.toLocaleLowerCase();
}
export function getGalleryExtensionId(publisher: string, name: string): string {
return `${publisher.toLocaleLowerCase()}.${name.toLocaleLowerCase()}`;
return adoptToGalleryExtensionId(getExtensionId(publisher, name));
}
export function groupByExtension<T>(extensions: T[], getExtensionIdentifier: (t: T) => IExtensionIdentifier): T[][] {

View File

@@ -21,6 +21,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { localize } from 'vs/nls';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { Event } from 'vs/base/common/event';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
type ExeExtensionRecommendationsClassification = {
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
@@ -52,6 +54,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IStorageService private readonly storageService: IStorageService,
@INativeHostService private readonly nativeHostService: INativeHostService,
@IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService,
@IFileService fileService: IFileService,
@IProductService productService: IProductService,
@@ -172,6 +175,11 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
case RecommendationsNotificationResult.Ignored:
this.highImportanceTipsByExe.delete(exeName);
break;
case RecommendationsNotificationResult.IncompatibleWindow:
// Recommended in incompatible window. Schedule the prompt after active window change
const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow)));
this._register(onActiveWindowChange(() => this.promptHighImportanceExeBasedTip()));
break;
case RecommendationsNotificationResult.TooMany:
// Too many notifications. Schedule the prompt after one hour
const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptHighImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */));
@@ -217,6 +225,12 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
this.promptMediumImportanceExeBasedTip();
break;
case RecommendationsNotificationResult.IncompatibleWindow:
// Recommended in incompatible window. Schedule the prompt after active window change
const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow)));
this._register(onActiveWindowChange(() => this.promptMediumImportanceExeBasedTip()));
break;
case RecommendationsNotificationResult.TooMany:
// Too many notifications. Schedule the prompt after one hour
const disposable2 = this._register(disposableTimeout(() => { disposable2.dispose(); this.promptMediumImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */));

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { isUUID } from 'vs/base/common/uuid';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { NullLogService } from 'vs/platform/log/common/log';
import product from 'vs/platform/product/common/product';
import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { mock } from 'vs/base/test/common/mock';
class EnvironmentServiceMock extends mock<IEnvironmentService>() {
constructor(readonly serviceMachineIdResource: URI) {
super();
}
}
suite('Extension Gallery Service', () => {
const disposables: DisposableStore = new DisposableStore();
let fileService: IFileService, environmentService: IEnvironmentService, storageService: IStorageService;
setup(() => {
const serviceMachineIdResource = joinPath(URI.file('tests').with({ scheme: 'vscode-tests' }), 'machineid');
environmentService = new EnvironmentServiceMock(serviceMachineIdResource);
fileService = disposables.add(new FileService(new NullLogService()));
const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider());
fileService.registerProvider(serviceMachineIdResource.scheme, fileSystemProvider);
storageService = new InMemoryStorageService();
});
teardown(() => disposables.clear());
test('marketplace machine id', async () => {
const headers = await resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService);
assert.ok(isUUID(headers['X-Market-User-Id']));
const headers2 = await resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService);
assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']);
});
});

View File

@@ -1,67 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as os from 'os';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { join } from 'vs/base/common/path';
import { mkdirp, RimRafMode, rimraf } from 'vs/base/node/pfs';
import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { isUUID } from 'vs/base/common/uuid';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { NullLogService } from 'vs/platform/log/common/log';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { Schemas } from 'vs/base/common/network';
import product from 'vs/platform/product/common/product';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import { IStorageService } from 'vs/platform/storage/common/storage';
suite('Extension Gallery Service', () => {
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'extensiongalleryservice');
const marketplaceHome = join(parentDir, 'Marketplace');
let fileService: IFileService;
let disposables: DisposableStore;
setup(done => {
disposables = new DisposableStore();
fileService = new FileService(new NullLogService());
disposables.add(fileService);
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
disposables.add(diskFileSystemProvider);
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
// Delete any existing backups completely and then re-create it.
rimraf(marketplaceHome, RimRafMode.MOVE).then(() => {
mkdirp(marketplaceHome).then(() => {
done();
}, error => done(error));
}, error => done(error));
});
teardown(done => {
disposables.clear();
rimraf(marketplaceHome, RimRafMode.MOVE).then(done, done);
});
test('marketplace machine id', () => {
const args = ['--user-data-dir', marketplaceHome];
const environmentService = new NativeEnvironmentService(parseArgs(args, OPTIONS));
const storageService: IStorageService = new TestStorageService();
return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers => {
assert.ok(isUUID(headers['X-Market-User-Id']));
return resolveMarketplaceHeaders(product.version, environmentService, fileService, storageService).then(headers2 => {
assert.equal(headers['X-Market-User-Id'], headers2['X-Market-User-Id']);
});
});
});
});

View File

@@ -11,10 +11,19 @@ export const enum RecommendationSource {
EXE = 3
}
export function RecommendationSourceToString(source: RecommendationSource) {
switch (source) {
case RecommendationSource.FILE: return 'file';
case RecommendationSource.WORKSPACE: return 'workspace';
case RecommendationSource.EXE: return 'exe';
}
}
export const enum RecommendationsNotificationResult {
Ignored = 'ignored',
Cancelled = 'cancelled',
TooMany = 'toomany',
IncompatibleWindow = 'incompatibleWindow',
Accepted = 'reacted',
}

View File

@@ -8,14 +8,24 @@ import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapab
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { VSBuffer } from 'vs/base/common/buffer';
import { joinPath, extUri, dirname } from 'vs/base/common/resources';
import { Throttler } from 'vs/base/common/async';
import { localize } from 'vs/nls';
import * as browser from 'vs/base/browser/browser';
import { joinPath } from 'vs/base/common/resources';
const INDEXEDDB_VSCODE_DB = 'vscode-web-db';
export const INDEXEDDB_USERDATA_OBJECT_STORE = 'vscode-userdata-store';
export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store';
// Standard FS Errors (expected to be thrown in production when invalid FS operations are requested)
const ERR_FILE_NOT_FOUND = createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
const ERR_FILE_IS_DIR = createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory);
const ERR_FILE_NOT_DIR = createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
const ERR_DIR_NOT_EMPTY = createFileSystemProviderError(localize('dirIsNotEmpty', "Directory is not empty"), FileSystemProviderErrorCode.Unknown);
// Arbitrary Internal Errors (should never be thrown in production)
const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occured in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown);
export class IndexedDB {
private indexedDBPromise: Promise<IDBDatabase | null>;
@@ -38,7 +48,7 @@ export class IndexedDB {
}
private openIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase | null> {
if (browser.isEdge) {
if (browser.isEdgeLegacy) {
return Promise.resolve(null);
}
return new Promise((c, e) => {
@@ -65,13 +75,140 @@ export class IndexedDB {
};
});
}
}
export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability {
reset(): Promise<void>;
}
type DirEntry = [string, FileType];
type IndexedDBFileSystemEntry =
| {
path: string,
type: FileType.Directory,
children: Map<string, IndexedDBFileSystemNode>,
}
| {
path: string,
type: FileType.File,
size: number | undefined,
};
class IndexedDBFileSystemNode {
public type: FileType;
constructor(private entry: IndexedDBFileSystemEntry) {
this.type = entry.type;
}
read(path: string) {
return this.doRead(path.split('/').filter(p => p.length));
}
private doRead(pathParts: string[]): IndexedDBFileSystemEntry | undefined {
if (pathParts.length === 0) { return this.entry; }
if (this.entry.type !== FileType.Directory) {
throw ERR_UNKNOWN_INTERNAL('Internal error reading from IndexedDBFSNode -- expected directory at ' + this.entry.path);
}
const next = this.entry.children.get(pathParts[0]);
if (!next) { return undefined; }
return next.doRead(pathParts.slice(1));
}
delete(path: string) {
const toDelete = path.split('/').filter(p => p.length);
if (toDelete.length === 0) {
if (this.entry.type !== FileType.Directory) {
throw ERR_UNKNOWN_INTERNAL(`Internal error deleting from IndexedDBFSNode. Expected root entry to be directory`);
}
this.entry.children.clear();
} else {
return this.doDelete(toDelete, path);
}
}
private doDelete = (pathParts: string[], originalPath: string) => {
if (pathParts.length === 0) {
throw ERR_UNKNOWN_INTERNAL(`Internal error deleting from IndexedDBFSNode -- got no deletion path parts (encountered while deleting ${originalPath})`);
}
else if (this.entry.type !== FileType.Directory) {
throw ERR_UNKNOWN_INTERNAL('Internal error deleting from IndexedDBFSNode -- expected directory at ' + this.entry.path);
}
else if (pathParts.length === 1) {
this.entry.children.delete(pathParts[0]);
}
else {
const next = this.entry.children.get(pathParts[0]);
if (!next) {
throw ERR_UNKNOWN_INTERNAL('Internal error deleting from IndexedDBFSNode -- expected entry at ' + this.entry.path + '/' + next);
}
next.doDelete(pathParts.slice(1), originalPath);
}
};
add(path: string, entry: { type: 'file', size?: number } | { type: 'dir' }) {
this.doAdd(path.split('/').filter(p => p.length), entry, path);
}
private doAdd(pathParts: string[], entry: { type: 'file', size?: number } | { type: 'dir' }, originalPath: string) {
if (pathParts.length === 0) {
throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- adding empty path (encountered while adding ${originalPath})`);
}
else if (this.entry.type !== FileType.Directory) {
throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- parent is not a directory (encountered while adding ${originalPath})`);
}
else if (pathParts.length === 1) {
const next = pathParts[0];
const existing = this.entry.children.get(next);
if (entry.type === 'dir') {
if (existing?.entry.type === FileType.File) {
throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting file with directory: ${this.entry.path}/${next} (encountered while adding ${originalPath})`);
}
this.entry.children.set(next, existing ?? new IndexedDBFileSystemNode({
type: FileType.Directory,
path: this.entry.path + '/' + next,
children: new Map(),
}));
} else {
if (existing?.entry.type === FileType.Directory) {
throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting directory with file: ${this.entry.path}/${next} (encountered while adding ${originalPath})`);
}
this.entry.children.set(next, new IndexedDBFileSystemNode({
type: FileType.File,
path: this.entry.path + '/' + next,
size: entry.size,
}));
}
}
else if (pathParts.length > 1) {
const next = pathParts[0];
let childNode = this.entry.children.get(next);
if (!childNode) {
childNode = new IndexedDBFileSystemNode({
children: new Map(),
path: this.entry.path + '/' + next,
type: FileType.Directory
});
this.entry.children.set(next, childNode);
}
else if (childNode.type === FileType.File) {
throw ERR_UNKNOWN_INTERNAL(`Internal error creating IndexedDBFSNode -- overwriting file entry with directory: ${this.entry.path}/${next} (encountered while adding ${originalPath})`);
}
childNode.doAdd(pathParts.slice(1), entry, originalPath);
}
}
print(indentation = '') {
console.log(indentation + this.entry.path);
if (this.entry.type === FileType.Directory) {
this.entry.children.forEach(child => child.print(indentation + ' '));
}
}
}
class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider {
readonly capabilities: FileSystemProviderCapabilities =
@@ -83,11 +220,14 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
private readonly versions: Map<string, number> = new Map<string, number>();
private readonly dirs: Set<string> = new Set<string>();
constructor(private readonly scheme: string, private readonly database: IDBDatabase, private readonly store: string) {
private cachedFiletree: Promise<IndexedDBFileSystemNode> | undefined;
private writeManyThrottler: Throttler;
constructor(scheme: string, private readonly database: IDBDatabase, private readonly store: string) {
super();
this.dirs.add('/');
this.writeManyThrottler = new Throttler();
}
watch(resource: URI, opts: IWatchOptions): IDisposable {
@@ -98,29 +238,22 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
try {
const resourceStat = await this.stat(resource);
if (resourceStat.type === FileType.File) {
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
throw ERR_FILE_NOT_DIR;
}
} catch (error) { /* Ignore */ }
// Make sure parent dir exists
await this.stat(dirname(resource));
this.dirs.add(resource.path);
(await this.getFiletree()).add(resource.path, { type: 'dir' });
}
async stat(resource: URI): Promise<IStat> {
try {
const content = await this.readFile(resource);
const content = (await this.getFiletree()).read(resource.path);
if (content?.type === FileType.File) {
return {
type: FileType.File,
ctime: 0,
mtime: this.versions.get(resource.toString()) || 0,
size: content.byteLength
size: content.size ?? (await this.readFile(resource)).byteLength
};
} catch (e) {
}
const files = await this.readdir(resource);
if (files.length) {
} else if (content?.type === FileType.Directory) {
return {
type: FileType.Directory,
ctime: 0,
@@ -128,75 +261,112 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
size: 0
};
}
if (this.dirs.has(resource.path)) {
return {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0
};
else {
throw ERR_FILE_NOT_FOUND;
}
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
}
async readdir(resource: URI): Promise<[string, FileType][]> {
const hasKey = await this.hasKey(resource.path);
if (hasKey) {
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
async readdir(resource: URI): Promise<DirEntry[]> {
const entry = (await this.getFiletree()).read(resource.path);
if (!entry) {
// Dirs aren't saved to disk, so empty dirs will be lost on reload.
// Thus we have two options for what happens when you try to read a dir and nothing is found:
// - Throw FileSystemProviderErrorCode.FileNotFound
// - Return []
// We choose to return [] as creating a dir then reading it (even after reload) should not throw an error.
return [];
}
const keys = await this.getAllKeys();
const files: Map<string, [string, FileType]> = new Map<string, [string, FileType]>();
for (const key of keys) {
const keyResource = this.toResource(key);
if (extUri.isEqualOrParent(keyResource, resource)) {
const path = extUri.relativePath(resource, keyResource);
if (path) {
const keySegments = path.split('/');
files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]);
}
}
if (entry.type !== FileType.Directory) {
throw ERR_FILE_NOT_DIR;
}
else {
return [...entry.children.entries()].map(([name, node]) => [name, node.type]);
}
return [...files.values()];
}
async readFile(resource: URI): Promise<Uint8Array> {
const hasKey = await this.hasKey(resource.path);
if (!hasKey) {
throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound);
}
const value = await this.getValue(resource.path);
if (typeof value === 'string') {
return VSBuffer.fromString(value).buffer;
} else {
return value;
}
const buffer = await new Promise<Uint8Array>((c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.get(resource.path);
request.onerror = () => e(request.error);
request.onsuccess = () => {
if (request.result instanceof Uint8Array) {
c(request.result);
} else if (typeof request.result === 'string') {
c(VSBuffer.fromString(request.result).buffer);
}
else {
if (request.result === undefined) {
e(ERR_FILE_NOT_FOUND);
} else {
e(ERR_UNKNOWN_INTERNAL(`IndexedDB entry at "${resource.path}" in unexpected format`));
}
}
};
});
(await this.getFiletree()).add(resource.path, { type: 'file', size: buffer.byteLength });
return buffer;
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
const hasKey = await this.hasKey(resource.path);
if (!hasKey) {
const files = await this.readdir(resource);
if (files.length) {
throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory);
}
const existing = await this.stat(resource).catch(() => undefined);
if (existing?.type === FileType.Directory) {
throw ERR_FILE_IS_DIR;
}
await this.setValue(resource.path, content);
this.fileWriteBatch.push({ content, resource });
await this.writeManyThrottler.queue(() => this.writeMany());
(await this.getFiletree()).add(resource.path, { type: 'file', size: content.byteLength });
this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1);
this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]);
}
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
const hasKey = await this.hasKey(resource.path);
if (hasKey) {
await this.deleteKey(resource.path);
this.versions.delete(resource.path);
this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]);
return;
let stat: IStat;
try {
stat = await this.stat(resource);
} catch (e) {
if (e.code === FileSystemProviderErrorCode.FileNotFound) {
return;
}
throw e;
}
let toDelete: string[];
if (opts.recursive) {
const files = await this.readdir(resource);
await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts)));
const tree = (await this.tree(resource));
toDelete = tree.map(([path]) => path);
} else {
if (stat.type === FileType.Directory && (await this.readdir(resource)).length) {
throw ERR_DIR_NOT_EMPTY;
}
toDelete = [resource.path];
}
await this.deleteKeys(toDelete);
(await this.getFiletree()).delete(resource.path);
toDelete.forEach(key => this.versions.delete(key));
this._onDidChangeFile.fire(toDelete.map(path => ({ resource: resource.with({ path }), type: FileChangeType.DELETED })));
}
private async tree(resource: URI): Promise<DirEntry[]> {
if ((await this.stat(resource)).type === FileType.Directory) {
const topLevelEntries = (await this.readdir(resource)).map(([key, type]) => {
return [joinPath(resource, key).path, type] as [string, FileType];
});
let allEntries = topLevelEntries;
await Promise.all(topLevelEntries.map(
async ([key, type]) => {
if (type === FileType.Directory) {
const childEntries = (await this.tree(resource.with({ path: key })));
allEntries = allEntries.concat(childEntries);
}
}));
return allEntries;
} else {
const entries: DirEntry[] = [[resource.path, FileType.File]];
return entries;
}
}
@@ -204,58 +374,57 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
return Promise.reject(new Error('Not Supported'));
}
private toResource(key: string): URI {
return URI.file(key).with({ scheme: this.scheme });
private getFiletree(): Promise<IndexedDBFileSystemNode> {
if (!this.cachedFiletree) {
this.cachedFiletree = new Promise((c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.getAllKeys();
request.onerror = () => e(request.error);
request.onsuccess = () => {
const rootNode = new IndexedDBFileSystemNode({
children: new Map(),
path: '',
type: FileType.Directory
});
const keys = request.result.map(key => key.toString());
keys.forEach(key => rootNode.add(key, { type: 'file' }));
c(rootNode);
};
});
}
return this.cachedFiletree;
}
async getAllKeys(): Promise<string[]> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.getAllKeys();
request.onerror = () => e(request.error);
request.onsuccess = () => c(<string[]>request.result);
});
}
private fileWriteBatch: { resource: URI, content: Uint8Array }[] = [];
private async writeMany() {
return new Promise<void>((c, e) => {
const fileBatch = this.fileWriteBatch;
this.fileWriteBatch = [];
if (fileBatch.length === 0) { return c(); }
hasKey(key: string): Promise<boolean> {
return new Promise<boolean>(async (c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.getKey(key);
request.onerror = () => e(request.error);
request.onsuccess = () => {
c(!!request.result);
};
});
}
getValue(key: string): Promise<Uint8Array | string> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.get(key);
request.onerror = () => e(request.error);
request.onsuccess = () => c(request.result || '');
});
}
setValue(key: string, value: Uint8Array): Promise<void> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store], 'readwrite');
transaction.onerror = () => e(transaction.error);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.put(value, key);
request.onerror = () => e(request.error);
let request: IDBRequest = undefined!;
for (const entry of fileBatch) {
request = objectStore.put(entry.content, entry.resource.path);
}
request.onsuccess = () => c();
});
}
deleteKey(key: string): Promise<void> {
private deleteKeys(keys: string[]): Promise<void> {
return new Promise(async (c, e) => {
if (keys.length === 0) { return c(); }
const transaction = this.database.transaction([this.store], 'readwrite');
transaction.onerror = () => e(transaction.error);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.delete(key);
request.onerror = () => e(request.error);
let request: IDBRequest = undefined!;
for (const key of keys) {
request = objectStore.delete(key);
}
request.onsuccess = () => c();
});
}

View File

@@ -3,18 +3,18 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { mark } from 'vs/base/common/performance';
import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isAbsolutePath, dirname, basename, joinPath, IExtUri, extUri, extUriIgnorePathCase } from 'vs/base/common/resources';
import { localize } from 'vs/nls';
import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources';
import { TernarySearchTree } from 'vs/base/common/map';
import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays';
import { getBaseLabel } from 'vs/base/common/labels';
import { ILogService } from 'vs/platform/log/common/log';
import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, bufferToStream, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer';
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream } from 'vs/base/common/stream';
import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer';
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, IReadableStreamObservable, observe } from 'vs/base/common/stream';
import { Queue } from 'vs/base/common/async';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { Schemas } from 'vs/base/common/network';
@@ -49,6 +49,8 @@ export class FileService extends Disposable implements IFileService {
throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`);
}
mark(`code/registerFilesystem/${scheme}`);
// Add provider with event
this.provider.set(scheme, provider);
this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider });
@@ -102,7 +104,7 @@ export class FileService extends Disposable implements IFileService {
return !!(provider && (provider.capabilities & capability));
}
listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities }> {
listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities; }> {
return Iterable.map(this.provider, ([scheme, provider]) => ({ scheme, capabilities: provider.capabilities }));
}
@@ -215,14 +217,15 @@ export class FileService extends Disposable implements IFileService {
});
}
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat>;
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType; } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat>;
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, siblings: number | undefined, resolveMetadata: true, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStatWithMetadata>;
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat> {
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat | { type: FileType; } & Partial<IStat>, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: IFileStat, siblings?: number) => boolean): Promise<IFileStat> {
const { providerExtUri } = this.getExtUri(provider);
// convert to file stat
const fileStat: IFileStat = {
resource,
name: getBaseLabel(resource),
name: providerExtUri.basename(resource),
isFile: (stat.type & FileType.File) !== 0,
isDirectory: (stat.type & FileType.Directory) !== 0,
isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
@@ -238,7 +241,7 @@ export class FileService extends Disposable implements IFileService {
const entries = await provider.readdir(resource);
const resolvedEntries = await Promise.all(entries.map(async ([name, type]) => {
try {
const childResource = joinPath(resource, name);
const childResource = providerExtUri.joinPath(resource, name);
const childStat = resolveMetadata ? await provider.stat(childResource) : { type };
return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse);
@@ -263,8 +266,8 @@ export class FileService extends Disposable implements IFileService {
return fileStat;
}
async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise<IResolveFileResult[]>;
async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions }[]): Promise<IResolveFileResultWithMetadata[]>;
async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]>;
async resolveAll(toResolve: { resource: URI, options: IResolveMetadataFileOptions; }[]): Promise<IResolveFileResultWithMetadata[]>;
async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]> {
return Promise.all(toResolve.map(async entry => {
try {
@@ -327,6 +330,7 @@ export class FileService extends Disposable implements IFileService {
async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource);
const { providerExtUri } = this.getExtUri(provider);
try {
@@ -335,7 +339,7 @@ export class FileService extends Disposable implements IFileService {
// mkdir recursively as needed
if (!stat) {
await this.mkdirp(provider, dirname(resource));
await this.mkdirp(provider, providerExtUri.dirname(resource));
}
// optimization: if the provider has unbuffered write capability and the data
@@ -435,7 +439,7 @@ export class FileService extends Disposable implements IFileService {
return this.doReadAsFileStream(provider, resource, options);
}
private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean }): Promise<IFileStreamContent> {
private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean; }): Promise<IFileStreamContent> {
// install a cancellation token that gets cancelled
// when any error occurs. this allows us to resolve
@@ -450,6 +454,8 @@ export class FileService extends Disposable implements IFileService {
throw error;
});
let fileStreamObserver: IReadableStreamObservable | undefined = undefined;
try {
// if the etag is provided, we await the result of the validation
@@ -460,30 +466,41 @@ export class FileService extends Disposable implements IFileService {
await statPromise;
}
let fileStreamPromise: Promise<VSBufferReadableStream>;
let fileStream: VSBufferReadableStream | undefined = undefined;
// read unbuffered (only if either preferred, or the provider has no buffered read capability)
if (!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || (hasReadWriteCapability(provider) && options?.preferUnbuffered)) {
fileStreamPromise = this.readFileUnbuffered(provider, resource, options);
fileStream = this.readFileUnbuffered(provider, resource, options);
}
// read streamed (always prefer over primitive buffered read)
else if (hasFileReadStreamCapability(provider)) {
fileStreamPromise = Promise.resolve(this.readFileStreamed(provider, resource, cancellableSource.token, options));
fileStream = this.readFileStreamed(provider, resource, cancellableSource.token, options);
}
// read buffered
else {
fileStreamPromise = Promise.resolve(this.readFileBuffered(provider, resource, cancellableSource.token, options));
fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options);
}
const [fileStat, fileStream] = await Promise.all([statPromise, fileStreamPromise]);
// observe the stream for the error case below
fileStreamObserver = observe(fileStream);
const fileStat = await statPromise;
return {
...fileStat,
value: fileStream
};
} catch (error) {
// Await the stream to finish so that we exit this method
// in a consistent state with file handles closed
// (https://github.com/microsoft/vscode/issues/114024)
if (fileStreamObserver) {
await fileStreamObserver.errorOrEnd();
}
throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
}
}
@@ -509,23 +526,36 @@ export class FileService extends Disposable implements IFileService {
return stream;
}
private async readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): Promise<VSBufferReadableStream> {
let buffer = await provider.readFile(resource);
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): VSBufferReadableStream {
const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));
// respect position option
if (options && typeof options.position === 'number') {
buffer = buffer.slice(options.position);
}
// Read the file into the stream async but do not wait for
// this to complete because streams work via events
(async () => {
try {
let buffer = await provider.readFile(resource);
// respect length option
if (options && typeof options.length === 'number') {
buffer = buffer.slice(0, options.length);
}
// respect position option
if (options && typeof options.position === 'number') {
buffer = buffer.slice(options.position);
}
// Throw if file is too large to load
this.validateReadFileLimits(resource, buffer.byteLength, options);
// respect length option
if (options && typeof options.length === 'number') {
buffer = buffer.slice(0, options.length);
}
return bufferToStream(VSBuffer.wrap(buffer));
// Throw if file is too large to load
this.validateReadFileLimits(resource, buffer.byteLength, options);
// End stream with data
stream.end(VSBuffer.wrap(buffer));
} catch (err) {
stream.error(err);
}
})();
return stream;
}
private async validateReadFile(resource: URI, options?: IReadFileOptions): Promise<IFileStatWithMetadata> {
@@ -634,7 +664,7 @@ export class FileService extends Disposable implements IFileService {
}
// create parent folders
await this.mkdirp(targetProvider, dirname(target));
await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target));
// copy source => target
if (mode === 'copy') {
@@ -671,7 +701,6 @@ export class FileService extends Disposable implements IFileService {
// across providers: copy to target & delete at source
else {
await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite);
await this.del(source, { recursive: true });
return 'copy';
@@ -710,7 +739,7 @@ export class FileService extends Disposable implements IFileService {
// create children in target
if (Array.isArray(sourceFolder.children)) {
await Promise.all(sourceFolder.children.map(async sourceChild => {
const targetChild = joinPath(targetFolder, sourceChild.name);
const targetChild = this.getExtUri(targetProvider).providerExtUri.joinPath(targetFolder, sourceChild.name);
if (sourceChild.isDirectory) {
return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild);
} else {
@@ -720,21 +749,21 @@ export class FileService extends Disposable implements IFileService {
}
}
private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean }> {
private async doValidateMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean; }> {
let isSameResourceWithDifferentPathCase = false;
// Check if source is equal or parent to target (requires providers to be the same)
if (sourceProvider === targetProvider) {
const { extUri, isPathCaseSensitive } = this.getExtUri(sourceProvider);
const { providerExtUri, isPathCaseSensitive } = this.getExtUri(sourceProvider);
if (!isPathCaseSensitive) {
isSameResourceWithDifferentPathCase = extUri.isEqual(source, target);
isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target);
}
if (isSameResourceWithDifferentPathCase && mode === 'copy') {
throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system", this.resourceForError(source), this.resourceForError(target)));
}
if (!isSameResourceWithDifferentPathCase && extUri.isEqualOrParent(target, source)) {
if (!isSameResourceWithDifferentPathCase && providerExtUri.isEqualOrParent(target, source)) {
throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target)));
}
}
@@ -751,8 +780,8 @@ export class FileService extends Disposable implements IFileService {
// Special case: if the target is a parent of the source, we cannot delete
// it as it would delete the source as well. In this case we have to throw
if (sourceProvider === targetProvider) {
const { extUri } = this.getExtUri(sourceProvider);
if (extUri.isEqualOrParent(source, target)) {
const { providerExtUri } = this.getExtUri(sourceProvider);
if (providerExtUri.isEqualOrParent(source, target)) {
throw new Error(localize('unableToMoveCopyError4', "Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target)));
}
}
@@ -761,11 +790,11 @@ export class FileService extends Disposable implements IFileService {
return { exists, isSameResourceWithDifferentPathCase };
}
private getExtUri(provider: IFileSystemProvider): { extUri: IExtUri, isPathCaseSensitive: boolean } {
private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri, isPathCaseSensitive: boolean; } {
const isPathCaseSensitive = this.isPathCaseSensitive(provider);
return {
extUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
isPathCaseSensitive
};
}
@@ -791,8 +820,8 @@ export class FileService extends Disposable implements IFileService {
const directoriesToCreate: string[] = [];
// mkdir until we reach root
const { extUri } = this.getExtUri(provider);
while (!extUri.isEqual(directory, dirname(directory))) {
const { providerExtUri } = this.getExtUri(provider);
while (!providerExtUri.isEqual(directory, providerExtUri.dirname(directory))) {
try {
const stat = await provider.stat(directory);
if ((stat.type & FileType.Directory) === 0) {
@@ -808,16 +837,16 @@ export class FileService extends Disposable implements IFileService {
}
// Upon error, remember directories that need to be created
directoriesToCreate.push(basename(directory));
directoriesToCreate.push(providerExtUri.basename(directory));
// Continue up
directory = dirname(directory);
directory = providerExtUri.dirname(directory);
}
}
// Create directories as needed
for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
directory = joinPath(directory, directoriesToCreate[i]);
directory = providerExtUri.joinPath(directory, directoriesToCreate[i]);
try {
await provider.mkdir(directory);
@@ -894,11 +923,11 @@ export class FileService extends Disposable implements IFileService {
private readonly _onDidFilesChange = this._register(new Emitter<FileChangesEvent>());
readonly onDidFilesChange = this._onDidFilesChange.event;
private readonly activeWatchers = new Map<string, { disposable: IDisposable, count: number }>();
private readonly activeWatchers = new Map<string, { disposable: IDisposable, count: number; }>();
watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IDisposable {
let watchDisposed = false;
let watchDisposable = toDisposable(() => watchDisposed = true);
let disposeWatch = () => { watchDisposed = true; };
// Watch and wire in disposable which is async but
// check if we got disposed meanwhile and forward
@@ -906,11 +935,11 @@ export class FileService extends Disposable implements IFileService {
if (watchDisposed) {
dispose(disposable);
} else {
watchDisposable = disposable;
disposeWatch = () => dispose(disposable);
}
}, error => this.logService.error(error));
return toDisposable(() => dispose(watchDisposable));
return toDisposable(() => disposeWatch());
}
async doWatch(resource: URI, options: IWatchOptions): Promise<IDisposable> {
@@ -940,12 +969,12 @@ export class FileService extends Disposable implements IFileService {
}
private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string {
const { extUri } = this.getExtUri(provider);
const { providerExtUri } = this.getExtUri(provider);
return [
extUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive
String(options.recursive), // use recursive: true | false as part of the key
options.excludes.join() // use excludes as part of the key
providerExtUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive
String(options.recursive), // use recursive: true | false as part of the key
options.excludes.join() // use excludes as part of the key
].join();
}
@@ -963,8 +992,8 @@ export class FileService extends Disposable implements IFileService {
private readonly writeQueues: Map<string, Queue<void>> = new Map();
private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue<void> {
const { extUri } = this.getExtUri(provider);
const queueKey = extUri.getComparisonKey(resource);
const { providerExtUri } = this.getExtUri(provider);
const queueKey = providerExtUri.getComparisonKey(resource);
// ensure to never write to the same resource without finishing
// the one write. this ensures a write finishes consistently

View File

@@ -278,7 +278,7 @@ export interface IFileSystemProvider {
readonly capabilities: FileSystemProviderCapabilities;
readonly onDidChangeCapabilities: Event<void>;
readonly onDidErrorOccur?: Event<string>; // TODO@ben remove once file watchers are solid
readonly onDidErrorOccur?: Event<string>; // TODO@bpasero remove once file watchers are solid
readonly onDidChangeFile: Event<readonly IFileChange[]>;
watch(resource: URI, opts: IWatchOptions): IDisposable;
@@ -947,9 +947,9 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined
return stat.mtime.toString(29) + stat.size.toString(31);
}
export function whenProviderRegistered(file: URI, fileService: IFileService): Promise<void> {
export async function whenProviderRegistered(file: URI, fileService: IFileService): Promise<void> {
if (fileService.canHandleResource(URI.from({ scheme: file.scheme }))) {
return Promise.resolve();
return;
}
return new Promise(resolve => {

View File

@@ -58,10 +58,11 @@ async function doReadFileIntoStream<T>(provider: IFileSystemProviderWithOpenRead
// open handle through provider
const handle = await provider.open(resource, { create: false });
// Check for cancellation
throwIfCancelled(token);
try {
// Check for cancellation
throwIfCancelled(token);
let totalBytesRead = 0;
let bytesRead = 0;
let allowedRemainingBytes = (options && typeof options.length === 'number') ? options.length : undefined;

View File

@@ -522,7 +522,7 @@ export class DiskFileSystemProvider extends Disposable implements
return this.watchRecursive(resource, opts.excludes);
}
return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases
return this.watchNonRecursive(resource); // TODO@bpasero ideally the same watcher can be used in both cases
}
private watchRecursive(resource: URI, excludes: string[]): IDisposable {

View File

@@ -3,12 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as glob from 'vs/base/common/glob';
import * as extpath from 'vs/base/common/extpath';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import * as nsfw from 'vscode-nsfw';
import * as glob from 'vs/base/common/glob';
import { join } from 'vs/base/common/path';
import { isMacintosh } from 'vs/base/common/platform';
import { isEqualOrParent } from 'vs/base/common/extpath';
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { IWatcherService, IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
import { ThrottledDelayer } from 'vs/base/common/async';
import { FileChangeType } from 'vs/platform/files/common/files';
@@ -111,7 +111,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
// We have to detect this case and massage the events to correct this.
let realBasePathDiffers = false;
let realBasePathLength = request.path.length;
if (platform.isMacintosh) {
if (isMacintosh) {
try {
// First check for symbolic link
@@ -141,7 +141,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
for (const e of events) {
// Logging
if (this.verboseLogging) {
const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || '');
const logPath = e.action === nsfw.actions.RENAMED ? join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : join(e.directory, e.file || '');
this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
}
@@ -149,20 +149,20 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
let absolutePath: string;
if (e.action === nsfw.actions.RENAMED) {
// Rename fires when a file's name changes within a single directory
absolutePath = path.join(e.directory, e.oldFile || '');
absolutePath = join(e.directory, e.oldFile || '');
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
} else if (this.verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
absolutePath = path.join(e.newDirectory || e.directory, e.newFile || '');
absolutePath = join(e.newDirectory || e.directory, e.newFile || '');
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
} else if (this.verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
} else {
absolutePath = path.join(e.directory, e.file || '');
absolutePath = join(e.directory, e.file || '');
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({
type: nsfwActionToRawChangeType[e.action],
@@ -179,7 +179,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
if (platform.isMacintosh) {
if (isMacintosh) {
events.forEach(e => {
// Mac uses NFD unicode form on disk, but we want NFC
@@ -230,7 +230,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
// Normalizes a set of root paths by removing any root paths that are
// sub-paths of other roots.
return roots.filter(r => roots.every(other => {
return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path));
return !(r.path.length > other.path.length && isEqualOrParent(r.path, other.path));
}));
}

View File

@@ -30,28 +30,28 @@ suite('NSFW Watcher Service', async () => {
test('should not impacts roots that don\'t overlap', () => {
const service = new TestNsfwWatcherService();
if (platform.isWindows) {
assert.deepEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
assert.deepStrictEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']);
assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
} else {
assert.deepEqual(service.normalizeRoots(['/a']), ['/a']);
assert.deepEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
assert.deepStrictEqual(service.normalizeRoots(['/a']), ['/a']);
assert.deepStrictEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']);
assert.deepStrictEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
}
});
test('should remove sub-folders of other roots', () => {
const service = new TestNsfwWatcherService();
if (platform.isWindows) {
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepStrictEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepStrictEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
} else {
assert.deepEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']);
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']);
assert.deepStrictEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']);
assert.deepStrictEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepStrictEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepStrictEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']);
}
});
});

View File

@@ -39,9 +39,9 @@ export class FileWatcher extends Disposable {
serverName: 'File Watcher (nsfw)',
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: 'true' // transmit console logs from server to client
VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp',
VSCODE_PIPE_LOGGING: 'true',
VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client
}
}
));

View File

@@ -6,9 +6,8 @@
import * as chokidar from 'chokidar';
import * as fs from 'fs';
import * as gracefulFs from 'graceful-fs';
gracefulFs.gracefulify(fs);
import * as extpath from 'vs/base/common/extpath';
import * as glob from 'vs/base/common/glob';
import { isEqualOrParent } from 'vs/base/common/extpath';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import { normalizeNFC } from 'vs/base/common/normalization';
@@ -20,6 +19,8 @@ import { Emitter, Event } from 'vs/base/common/event';
import { equals } from 'vs/base/common/arrays';
import { Disposable } from 'vs/base/common/lifecycle';
gracefulFs.gracefulify(fs); // enable gracefulFs
process.noAsar = true; // disable ASAR support in watcher process
interface IWatcher {
@@ -311,7 +312,7 @@ function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean {
return false;
}
if (extpath.isEqualOrParent(path, request.path)) {
if (isEqualOrParent(path, request.path)) {
if (!request.parsedPattern) {
if (request.excludes && request.excludes.length > 0) {
const pattern = `{${request.excludes.join(',')}}`;
@@ -343,7 +344,7 @@ export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string
for (const request of requests) {
const basePath = request.path;
const ignored = (request.excludes || []).sort();
if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) {
if (prevRequest && (isEqualOrParent(basePath, prevRequest.path))) {
if (!isEqualIgnore(ignored, prevRequest.excludes)) {
result[prevRequest.path].push({ path: basePath, excludes: ignored });
}

View File

@@ -20,18 +20,18 @@ suite('Chokidar normalizeRoots', async () => {
function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) {
const requests = inputPaths.map(path => newRequest(path));
const actual = normalizeRoots(requests);
assert.deepEqual(Object.keys(actual).sort(), expectedPaths);
assert.deepStrictEqual(Object.keys(actual).sort(), expectedPaths);
}
function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) {
const actual = normalizeRoots(inputRequests);
const actualPath = Object.keys(actual).sort();
const expectedPaths = Object.keys(expectedRequests).sort();
assert.deepEqual(actualPath, expectedPaths);
assert.deepStrictEqual(actualPath, expectedPaths);
for (let path of actualPath) {
let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
assert.deepEqual(a, e);
assert.deepStrictEqual(a, e);
}
}

View File

@@ -40,9 +40,9 @@ export class FileWatcher extends Disposable {
serverName: 'File Watcher (chokidar)',
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: 'true' // transmit console logs from server to client
VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp',
VSCODE_PIPE_LOGGING: 'true',
VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client
}
}
));

View File

@@ -19,7 +19,7 @@ suite('File Service', () => {
const resource = URI.parse('test://foo/bar');
const provider = new NullFileSystemProvider();
assert.equal(service.canHandleResource(resource), false);
assert.strictEqual(service.canHandleResource(resource), false);
const registrations: IFileSystemProviderRegistrationEvent[] = [];
service.onDidChangeFileSystemProviderRegistrations(e => {
@@ -47,33 +47,33 @@ suite('File Service', () => {
await service.activateProvider('test');
assert.equal(service.canHandleResource(resource), true);
assert.strictEqual(service.canHandleResource(resource), true);
assert.equal(registrations.length, 1);
assert.equal(registrations[0].scheme, 'test');
assert.equal(registrations[0].added, true);
assert.strictEqual(registrations.length, 1);
assert.strictEqual(registrations[0].scheme, 'test');
assert.strictEqual(registrations[0].added, true);
assert.ok(registrationDisposable);
assert.equal(capabilityChanges.length, 0);
assert.strictEqual(capabilityChanges.length, 0);
provider.setCapabilities(FileSystemProviderCapabilities.FileFolderCopy);
assert.equal(capabilityChanges.length, 1);
assert.strictEqual(capabilityChanges.length, 1);
provider.setCapabilities(FileSystemProviderCapabilities.Readonly);
assert.equal(capabilityChanges.length, 2);
assert.strictEqual(capabilityChanges.length, 2);
await service.activateProvider('test');
assert.equal(callCount, 2); // activation is called again
assert.strictEqual(callCount, 2); // activation is called again
assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true);
assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false);
assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true);
assert.strictEqual(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false);
registrationDisposable!.dispose();
assert.equal(service.canHandleResource(resource), false);
assert.strictEqual(service.canHandleResource(resource), false);
assert.equal(registrations.length, 2);
assert.equal(registrations[1].scheme, 'test');
assert.equal(registrations[1].added, false);
assert.strictEqual(registrations.length, 2);
assert.strictEqual(registrations[1].scheme, 'test');
assert.strictEqual(registrations[1].added, false);
});
test('watch', async () => {
@@ -91,9 +91,9 @@ suite('File Service', () => {
const watcher1Disposable = service.watch(resource1);
await timeout(0); // service.watch() is async
assert.equal(disposeCounter, 0);
assert.strictEqual(disposeCounter, 0);
watcher1Disposable.dispose();
assert.equal(disposeCounter, 1);
assert.strictEqual(disposeCounter, 1);
disposeCounter = 0;
const resource2 = URI.parse('test://foo/bar2');
@@ -102,13 +102,13 @@ suite('File Service', () => {
const watcher2Disposable3 = service.watch(resource2);
await timeout(0); // service.watch() is async
assert.equal(disposeCounter, 0);
assert.strictEqual(disposeCounter, 0);
watcher2Disposable1.dispose();
assert.equal(disposeCounter, 0);
assert.strictEqual(disposeCounter, 0);
watcher2Disposable2.dispose();
assert.equal(disposeCounter, 0);
assert.strictEqual(disposeCounter, 0);
watcher2Disposable3.dispose();
assert.equal(disposeCounter, 1);
assert.strictEqual(disposeCounter, 1);
disposeCounter = 0;
const resource3 = URI.parse('test://foo/bar3');
@@ -116,10 +116,10 @@ suite('File Service', () => {
const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] });
await timeout(0); // service.watch() is async
assert.equal(disposeCounter, 0);
assert.strictEqual(disposeCounter, 0);
watcher3Disposable1.dispose();
assert.equal(disposeCounter, 1);
assert.strictEqual(disposeCounter, 1);
watcher3Disposable2.dispose();
assert.equal(disposeCounter, 2);
assert.strictEqual(disposeCounter, 2);
});
});

View File

@@ -6,20 +6,20 @@
import * as assert from 'assert';
import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas } from 'vs/base/common/network';
import { posix } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { FileOperation, FileOperationEvent } from 'vs/platform/files/common/files';
import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileSystemProviderErrorCode, FileType, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { NullLogService } from 'vs/platform/log/common/log';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider';
import { assertIsDefined } from 'vs/base/common/types';
// FileService doesn't work with \ leading a path. Windows join swaps /'s for \'s,
// making /-style absolute paths fail isAbsolute checks.
const join = posix.join;
import { basename, joinPath } from 'vs/base/common/resources';
import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
suite('IndexedDB File Service', function () {
// IDB sometimes under pressure in build machines.
this.retries(3);
const logSchema = 'logs';
let service: FileService;
@@ -27,12 +27,43 @@ suite('IndexedDB File Service', function () {
let userdataFileProvider: IIndexedDBFileSystemProvider;
const testDir = '/';
const makeLogfileURI = (path: string) => URI.from({ scheme: logSchema, path });
const makeUserdataURI = (path: string) => URI.from({ scheme: Schemas.userData, path });
const logfileURIFromPaths = (paths: string[]) => joinPath(URI.from({ scheme: logSchema, path: testDir }), ...paths);
const userdataURIFromPaths = (paths: readonly string[]) => joinPath(URI.from({ scheme: Schemas.userData, path: testDir }), ...paths);
const disposables = new DisposableStore();
setup(async () => {
const initFixtures = async () => {
await Promise.all(
[['fixtures', 'resolver', 'examples'],
['fixtures', 'resolver', 'other', 'deep'],
['fixtures', 'service', 'deep'],
['batched']]
.map(path => userdataURIFromPaths(path))
.map(uri => service.createFolder(uri)));
await Promise.all(
([
[['fixtures', 'resolver', 'examples', 'company.js'], 'class company {}'],
[['fixtures', 'resolver', 'examples', 'conway.js'], 'export function conway() {}'],
[['fixtures', 'resolver', 'examples', 'employee.js'], 'export const employee = "jax"'],
[['fixtures', 'resolver', 'examples', 'small.js'], ''],
[['fixtures', 'resolver', 'other', 'deep', 'company.js'], 'class company {}'],
[['fixtures', 'resolver', 'other', 'deep', 'conway.js'], 'export function conway() {}'],
[['fixtures', 'resolver', 'other', 'deep', 'employee.js'], 'export const employee = "jax"'],
[['fixtures', 'resolver', 'other', 'deep', 'small.js'], ''],
[['fixtures', 'resolver', 'index.html'], '<p>p</p>'],
[['fixtures', 'resolver', 'site.css'], '.p {color: red;}'],
[['fixtures', 'service', 'deep', 'company.js'], 'class company {}'],
[['fixtures', 'service', 'deep', 'conway.js'], 'export function conway() {}'],
[['fixtures', 'service', 'deep', 'employee.js'], 'export const employee = "jax"'],
[['fixtures', 'service', 'deep', 'small.js'], ''],
[['fixtures', 'service', 'binary.txt'], '<p>p</p>'],
] as const)
.map(([path, contents]) => [userdataURIFromPaths(path), contents] as const)
.map(([uri, contents]) => service.createFile(uri, VSBuffer.fromString(contents)))
);
};
const reload = async () => {
const logService = new NullLogService();
service = new FileService(logService);
@@ -45,33 +76,302 @@ suite('IndexedDB File Service', function () {
userdataFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(logSchema, INDEXEDDB_USERDATA_OBJECT_STORE));
disposables.add(service.registerProvider(Schemas.userData, userdataFileProvider));
disposables.add(userdataFileProvider);
};
setup(async () => {
await reload();
});
teardown(async () => {
disposables.clear();
await logFileProvider.delete(logfileURIFromPaths([]), { recursive: true, useTrash: false });
await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false });
});
await logFileProvider.delete(makeLogfileURI(testDir), { recursive: true, useTrash: false });
await userdataFileProvider.delete(makeUserdataURI(testDir), { recursive: true, useTrash: false });
test('root is always present', async () => {
assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory);
await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false });
assert.strictEqual((await userdataFileProvider.stat(userdataURIFromPaths([]))).type, FileType.Directory);
});
test('createFolder', async () => {
let event: FileOperationEvent | undefined;
disposables.add(service.onDidRunOperation(e => event = e));
const parent = await service.resolve(makeUserdataURI(testDir));
const parent = await service.resolve(userdataURIFromPaths([]));
const newFolderResource = joinPath(parent.resource, 'newFolder');
const newFolderResource = makeUserdataURI(join(parent.resource.path, 'newFolder'));
assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 0);
assert.strictEqual((await userdataFileProvider.readdir(parent.resource)).length, 0);
const newFolder = await service.createFolder(newFolderResource);
assert.equal(newFolder.name, 'newFolder');
// Invalid.. dirs dont exist in our IDBFSB.
// assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 1);
assert.strictEqual(newFolder.name, 'newFolder');
assert.strictEqual((await userdataFileProvider.readdir(parent.resource)).length, 1);
assert.strictEqual((await userdataFileProvider.stat(newFolderResource)).type, FileType.Directory);
assert.ok(event);
assert.equal(event!.resource.path, newFolderResource.path);
assert.equal(event!.operation, FileOperation.CREATE);
assert.equal(event!.target!.resource.path, newFolderResource.path);
assert.equal(event!.target!.isDirectory, true);
assert.strictEqual(event!.resource.path, newFolderResource.path);
assert.strictEqual(event!.operation, FileOperation.CREATE);
assert.strictEqual(event!.target!.resource.path, newFolderResource.path);
assert.strictEqual(event!.target!.isDirectory, true);
});
test('createFolder: creating multiple folders at once', async () => {
let event: FileOperationEvent;
disposables.add(service.onDidRunOperation(e => event = e));
const multiFolderPaths = ['a', 'couple', 'of', 'folders'];
const parent = await service.resolve(userdataURIFromPaths([]));
const newFolderResource = joinPath(parent.resource, ...multiFolderPaths);
const newFolder = await service.createFolder(newFolderResource);
const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1];
assert.strictEqual(newFolder.name, lastFolderName);
assert.strictEqual((await userdataFileProvider.stat(newFolderResource)).type, FileType.Directory);
assert.ok(event!);
assert.strictEqual(event!.resource.path, newFolderResource.path);
assert.strictEqual(event!.operation, FileOperation.CREATE);
assert.strictEqual(event!.target!.resource.path, newFolderResource.path);
assert.strictEqual(event!.target!.isDirectory, true);
});
test('exists', async () => {
let exists = await service.exists(userdataURIFromPaths([]));
assert.strictEqual(exists, true);
exists = await service.exists(userdataURIFromPaths(['hello']));
assert.strictEqual(exists, false);
});
test('resolve - file', async () => {
await initFixtures();
const resource = userdataURIFromPaths(['fixtures', 'resolver', 'index.html']);
const resolved = await service.resolve(resource);
assert.strictEqual(resolved.name, 'index.html');
assert.strictEqual(resolved.isFile, true);
assert.strictEqual(resolved.isDirectory, false);
assert.strictEqual(resolved.isSymbolicLink, false);
assert.strictEqual(resolved.resource.toString(), resource.toString());
assert.strictEqual(resolved.children, undefined);
assert.ok(resolved.size! > 0);
});
test('resolve - directory', async () => {
await initFixtures();
const testsElements = ['examples', 'other', 'index.html', 'site.css'];
const resource = userdataURIFromPaths(['fixtures', 'resolver']);
const result = await service.resolve(resource);
assert.ok(result);
assert.strictEqual(result.resource.toString(), resource.toString());
assert.strictEqual(result.name, 'resolver');
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result!.isDirectory);
assert.strictEqual(result.children!.length, testsElements.length);
assert.ok(result.children!.every(entry => {
return testsElements.some(name => {
return basename(entry.resource) === name;
});
}));
result.children!.forEach(value => {
assert.ok(basename(value.resource));
if (['examples', 'other'].indexOf(basename(value.resource)) >= 0) {
assert.ok(value.isDirectory);
assert.strictEqual(value.mtime, undefined);
assert.strictEqual(value.ctime, undefined);
} else if (basename(value.resource) === 'index.html') {
assert.ok(!value.isDirectory);
assert.ok(!value.children);
assert.strictEqual(value.mtime, undefined);
assert.strictEqual(value.ctime, undefined);
} else if (basename(value.resource) === 'site.css') {
assert.ok(!value.isDirectory);
assert.ok(!value.children);
assert.strictEqual(value.mtime, undefined);
assert.strictEqual(value.ctime, undefined);
} else {
assert.ok(!'Unexpected value ' + basename(value.resource));
}
});
});
test('createFile', async () => {
return assertCreateFile(contents => VSBuffer.fromString(contents));
});
test('createFile (readable)', async () => {
return assertCreateFile(contents => bufferToReadable(VSBuffer.fromString(contents)));
});
test('createFile (stream)', async () => {
return assertCreateFile(contents => bufferToStream(VSBuffer.fromString(contents)));
});
async function assertCreateFile(converter: (content: string) => VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<void> {
let event: FileOperationEvent;
disposables.add(service.onDidRunOperation(e => event = e));
const contents = 'Hello World';
const resource = userdataURIFromPaths(['test.txt']);
assert.strictEqual(await service.canCreateFile(resource), true);
const fileStat = await service.createFile(resource, converter(contents));
assert.strictEqual(fileStat.name, 'test.txt');
assert.strictEqual((await userdataFileProvider.stat(fileStat.resource)).type, FileType.File);
assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(fileStat.resource)), contents);
assert.ok(event!);
assert.strictEqual(event!.resource.path, resource.path);
assert.strictEqual(event!.operation, FileOperation.CREATE);
assert.strictEqual(event!.target!.resource.path, resource.path);
}
const makeBatchTester = (size: number, name: string) => {
const batch = Array.from({ length: 50 }).map((_, i) => ({ contents: `Hello${i}`, resource: userdataURIFromPaths(['batched', name, `Hello${i}.txt`]) }));
let stats: Promise<IFileStatWithMetadata[]> | undefined = undefined;
return {
async create() {
return stats = Promise.all(batch.map(entry => service.createFile(entry.resource, VSBuffer.fromString(entry.contents))));
},
async assertContentsCorrect() {
await Promise.all(batch.map(async (entry, i) => {
if (!stats) { throw Error('read called before create'); }
const stat = (await stats!)[i];
assert.strictEqual(stat.name, `Hello${i}.txt`);
assert.strictEqual((await userdataFileProvider.stat(stat.resource)).type, FileType.File);
assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(stat.resource)), entry.contents);
}));
},
async delete() {
await service.del(userdataURIFromPaths(['batched', name]), { recursive: true, useTrash: false });
},
async assertContentsEmpty() {
if (!stats) { throw Error('assertContentsEmpty called before create'); }
await Promise.all((await stats).map(async stat => {
const newStat = await userdataFileProvider.stat(stat.resource).catch(e => e.code);
assert.strictEqual(newStat, FileSystemProviderErrorCode.FileNotFound);
}));
}
};
};
test('createFile (small batch)', async () => {
const tester = makeBatchTester(50, 'smallBatch');
await tester.create();
await tester.assertContentsCorrect();
await tester.delete();
await tester.assertContentsEmpty();
});
test('createFile (mixed parallel/sequential)', async () => {
const single1 = makeBatchTester(1, 'single1');
const single2 = makeBatchTester(1, 'single2');
const batch1 = makeBatchTester(20, 'batch1');
const batch2 = makeBatchTester(20, 'batch2');
single1.create();
batch1.create();
await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]);
single2.create();
batch2.create();
await Promise.all([single2.assertContentsCorrect(), batch2.assertContentsCorrect()]);
await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]);
await (Promise.all([single1.delete(), single2.delete(), batch1.delete(), batch2.delete()]));
await (Promise.all([single1.assertContentsEmpty(), single2.assertContentsEmpty(), batch1.assertContentsEmpty(), batch2.assertContentsEmpty()]));
});
test('deleteFile', async () => {
await initFixtures();
let event: FileOperationEvent;
disposables.add(service.onDidRunOperation(e => event = e));
const anotherResource = userdataURIFromPaths(['fixtures', 'service', 'deep', 'company.js']);
const resource = userdataURIFromPaths(['fixtures', 'service', 'deep', 'conway.js']);
const source = await service.resolve(resource);
assert.strictEqual(await service.canDelete(source.resource, { useTrash: false }), true);
await service.del(source.resource, { useTrash: false });
assert.strictEqual(await service.exists(source.resource), false);
assert.strictEqual(await service.exists(anotherResource), true);
assert.ok(event!);
assert.strictEqual(event!.resource.path, resource.path);
assert.strictEqual(event!.operation, FileOperation.DELETE);
{
let error: Error | undefined = undefined;
try {
await service.del(source.resource, { useTrash: false });
} catch (e) {
error = e;
}
assert.ok(error);
assert.strictEqual((<FileOperationError>error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND);
}
await reload();
{
let error: Error | undefined = undefined;
try {
await service.del(source.resource, { useTrash: false });
} catch (e) {
error = e;
}
assert.ok(error);
assert.strictEqual((<FileOperationError>error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND);
}
});
test('deleteFolder (recursive)', async () => {
await initFixtures();
let event: FileOperationEvent;
disposables.add(service.onDidRunOperation(e => event = e));
const resource = userdataURIFromPaths(['fixtures', 'service', 'deep']);
const subResource1 = userdataURIFromPaths(['fixtures', 'service', 'deep', 'company.js']);
const subResource2 = userdataURIFromPaths(['fixtures', 'service', 'deep', 'conway.js']);
assert.strictEqual(await service.exists(subResource1), true);
assert.strictEqual(await service.exists(subResource2), true);
const source = await service.resolve(resource);
assert.strictEqual(await service.canDelete(source.resource, { recursive: true, useTrash: false }), true);
await service.del(source.resource, { recursive: true, useTrash: false });
assert.strictEqual(await service.exists(source.resource), false);
assert.strictEqual(await service.exists(subResource1), false);
assert.strictEqual(await service.exists(subResource2), false);
assert.ok(event!);
assert.strictEqual(event!.resource.fsPath, resource.fsPath);
assert.strictEqual(event!.operation, FileOperation.DELETE);
});
test('deleteFolder (non recursive)', async () => {
await initFixtures();
const resource = userdataURIFromPaths(['fixtures', 'service', 'deep']);
const source = await service.resolve(resource);
assert.ok((await service.canDelete(source.resource)) instanceof Error);
let error;
try {
await service.del(source.resource);
} catch (e) {
error = e;
}
assert.ok(error);
});
});

View File

@@ -64,7 +64,7 @@ suite('Normalizer', () => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 3);
assert.strictEqual(e.changes.length, 3);
assert.ok(e.contains(added, FileChangeType.ADDED));
assert.ok(e.contains(updated, FileChangeType.UPDATED));
assert.ok(e.contains(deleted, FileChangeType.DELETED));
@@ -103,7 +103,7 @@ suite('Normalizer', () => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 5);
assert.strictEqual(e.changes.length, 5);
assert.ok(e.contains(deletedFolderA, FileChangeType.DELETED));
assert.ok(e.contains(deletedFolderB, FileChangeType.DELETED));
@@ -133,7 +133,7 @@ suite('Normalizer', () => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 1);
assert.strictEqual(e.changes.length, 1);
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
@@ -158,7 +158,7 @@ suite('Normalizer', () => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
assert.strictEqual(e.changes.length, 2);
assert.ok(e.contains(deleted, FileChangeType.UPDATED));
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
@@ -184,7 +184,7 @@ suite('Normalizer', () => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
assert.strictEqual(e.changes.length, 2);
assert.ok(e.contains(created, FileChangeType.ADDED));
assert.ok(!e.contains(created, FileChangeType.UPDATED));
@@ -213,7 +213,7 @@ suite('Normalizer', () => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
assert.strictEqual(e.changes.length, 2);
assert.ok(e.contains(deleted, FileChangeType.DELETED));
assert.ok(!e.contains(updated, FileChangeType.UPDATED));

View File

@@ -132,15 +132,31 @@ export class InstantiationService implements IInstantiationService {
private _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>, _trace: Trace): T {
let thing = this._getServiceInstanceOrDescriptor(id);
if (thing instanceof SyncDescriptor) {
return this._createAndCacheServiceInstance(id, thing, _trace.branch(id, true));
return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true));
} else {
_trace.branch(id, false);
return thing;
}
}
private readonly _activeInstantiations = new Set<ServiceIdentifier<any>>();
private _safeCreateAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {
if (this._activeInstantiations.has(id)) {
throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`);
}
this._activeInstantiations.add(id);
try {
return this._createAndCacheServiceInstance(id, desc, _trace);
} finally {
this._activeInstantiations.delete(id);
}
}
private _createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {
type Triple = { id: ServiceIdentifier<any>, desc: SyncDescriptor<any>, _trace: Trace };
type Triple = { id: ServiceIdentifier<any>, desc: SyncDescriptor<any>, _trace: Trace; };
const graph = new Graph<Triple>(data => data.id.toString());
let cycleCount = 0;
@@ -195,7 +211,6 @@ export class InstantiationService implements IInstantiationService {
graph.removeNode(data);
}
}
return <T>this._getServiceInstanceOrDescriptor(id);
}

View File

@@ -4,7 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IpcRendererEvent } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp';
import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { generateUuid } from 'vs/base/common/uuid';
import { ILogService } from 'vs/platform/log/common/log';
import { Disposable } from 'vs/base/common/lifecycle';
export const ISharedProcessService = createDecorator<ISharedProcessService>('sharedProcessService');
@@ -14,7 +22,47 @@ export interface ISharedProcessService {
getChannel(channelName: string): IChannel;
registerChannel(channelName: string, channel: IServerChannel<string>): void;
whenSharedProcessReady(): Promise<void>;
toggleSharedProcessWindow(): Promise<void>;
}
export class SharedProcessService extends Disposable implements ISharedProcessService {
declare readonly _serviceBrand: undefined;
private readonly withSharedProcessConnection: Promise<MessagePortClient>;
constructor(
@INativeHostService private readonly nativeHostService: INativeHostService,
@ILogService private readonly logService: ILogService
) {
super();
this.withSharedProcessConnection = this.connect();
}
private async connect(): Promise<MessagePortClient> {
this.logService.trace('Renderer->SharedProcess#connect');
// Ask to create message channel inside the window
// and send over a UUID to correlate the response
const nonce = generateUuid();
ipcRenderer.send('vscode:createSharedProcessMessageChannel', nonce);
// Wait until the main side has returned the `MessagePort`
// We need to filter by the `nonce` to ensure we listen
// to the right response.
const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePort }>(ipcRenderer, 'vscode:createSharedProcessMessageChannelResult', (e: IpcRendererEvent, nonce: string) => ({ nonce, port: e.ports[0] }));
const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce)));
this.logService.trace('Renderer->SharedProcess#connect: connection established');
return this._register(new MessagePortClient(port, `window:${this.nativeHostService.windowId}`));
}
getChannel(channelName: string): IChannel {
return getDelayedChannel(this.withSharedProcessConnection.then(connection => connection.getChannel(channelName)));
}
registerChannel(channelName: string, channel: IServerChannel<string>): void {
this.withSharedProcessConnection.then(connection => connection.registerChannel(channelName, channel));
}
}

View File

@@ -1,36 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const ISharedProcessMainService = createDecorator<ISharedProcessMainService>('sharedProcessMainService');
export interface ISharedProcessMainService {
readonly _serviceBrand: undefined;
whenSharedProcessReady(): Promise<void>;
toggleSharedProcessWindow(): Promise<void>;
}
export interface ISharedProcess {
whenReady(): Promise<void>;
toggle(): void;
}
export class SharedProcessMainService implements ISharedProcessMainService {
declare readonly _serviceBrand: undefined;
constructor(private sharedProcess: ISharedProcess) { }
whenSharedProcessReady(): Promise<void> {
return this.sharedProcess.whenReady();
}
async toggleSharedProcessWindow(): Promise<void> {
return this.sharedProcess.toggle();
}
}

View File

@@ -3,10 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox';
import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
import { Client as IPCElectronClient } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron';
import { Disposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp';
export const IMainProcessService = createDecorator<IMainProcessService>('mainProcessService');
@@ -19,18 +20,21 @@ export interface IMainProcessService {
registerChannel(channelName: string, channel: IServerChannel<string>): void;
}
export class MainProcessService extends Disposable implements IMainProcessService {
/**
* An implementation of `IMainProcessService` that leverages Electron's IPC.
*/
export class ElectronIPCMainProcessService extends Disposable implements IMainProcessService {
declare readonly _serviceBrand: undefined;
private mainProcessConnection: Client;
private mainProcessConnection: IPCElectronClient;
constructor(
windowId: number
) {
super();
this.mainProcessConnection = this._register(new Client(`window:${windowId}`));
this.mainProcessConnection = this._register(new IPCElectronClient(`window:${windowId}`));
}
getChannel(channelName: string): IChannel {
@@ -41,3 +45,24 @@ export class MainProcessService extends Disposable implements IMainProcessServic
this.mainProcessConnection.registerChannel(channelName, channel);
}
}
/**
* An implementation of `IMainProcessService` that leverages MessagePorts.
*/
export class MessagePortMainProcessService implements IMainProcessService {
declare readonly _serviceBrand: undefined;
constructor(
private server: MessagePortServer,
private router: StaticRouter
) { }
getChannel(channelName: string): IChannel {
return this.server.getChannel(channelName, this.router);
}
registerChannel(channelName: string, channel: IServerChannel<string>): void {
this.server.registerChannel(channelName, channel);
}
}

View File

@@ -56,6 +56,7 @@ export interface IssueReporterData extends WindowData {
issueType?: IssueType;
extensionId?: string;
experiments?: string;
githubAccessToken: string;
readonly issueTitle?: string;
readonly issueBody?: string;
}
@@ -72,7 +73,6 @@ export interface IssueReporterFeatures {
export interface ProcessExplorerStyles extends WindowStyles {
hoverBackground?: string;
hoverForeground?: string;
highlightForeground?: string;
}
export interface ProcessExplorerData extends WindowData {

View File

@@ -17,7 +17,7 @@ import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform';
import { ILogService } from 'vs/platform/log/common/log';
import { IWindowState } from 'vs/platform/windows/electron-main/windows';
import { listProcesses } from 'vs/base/node/ps';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows';
import { FileAccess } from 'vs/base/common/network';
@@ -185,120 +185,118 @@ export class IssueMainService implements ICommonIssueService {
}
}
openReporter(data: IssueReporterData): Promise<void> {
return new Promise(_ => {
if (!this._issueWindow) {
this._issueParentWindow = BrowserWindow.getFocusedWindow();
if (this._issueParentWindow) {
const position = this.getWindowPosition(this._issueParentWindow, 700, 800);
async openReporter(data: IssueReporterData): Promise<void> {
if (!this._issueWindow) {
this._issueParentWindow = BrowserWindow.getFocusedWindow();
if (this._issueParentWindow) {
const position = this.getWindowPosition(this._issueParentWindow, 700, 800);
this._issueWindow = new BrowserWindow({
fullscreen: false,
width: position.width,
height: position.height,
minWidth: 300,
minHeight: 200,
x: position.x,
y: position.y,
title: localize('issueReporter', "Issue Reporter"),
backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR,
webPreferences: {
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
enableWebSQL: false,
enableRemoteModule: false,
spellcheck: false,
nativeWindowOpen: true,
zoomFactor: zoomLevelToZoomFactor(data.zoomLevel),
sandbox: true,
contextIsolation: true
}
});
this._issueWindow = new BrowserWindow({
fullscreen: false,
width: position.width,
height: position.height,
minWidth: 300,
minHeight: 200,
x: position.x,
y: position.y,
title: localize('issueReporter', "Issue Reporter"),
backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR,
webPreferences: {
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
v8CacheOptions: 'bypassHeatCheck',
enableWebSQL: false,
enableRemoteModule: false,
spellcheck: false,
nativeWindowOpen: true,
zoomFactor: zoomLevelToZoomFactor(data.zoomLevel),
sandbox: true,
contextIsolation: true
}
});
this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented
this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented
// Modified when testing UI
const features: IssueReporterFeatures = {};
// Modified when testing UI
const features: IssueReporterFeatures = {};
this.logService.trace('issueService#openReporter: opening issue reporter');
this._issueWindow.loadURL(this.getIssueReporterPath(data, features));
this.logService.trace('issueService#openReporter: opening issue reporter');
this._issueWindow.loadURL(this.getIssueReporterPath(data, features));
this._issueWindow.on('close', () => this._issueWindow = null);
this._issueWindow.on('close', () => this._issueWindow = null);
this._issueParentWindow.on('closed', () => {
if (this._issueWindow) {
this._issueWindow.close();
this._issueWindow = null;
}
});
}
this._issueParentWindow.on('closed', () => {
if (this._issueWindow) {
this._issueWindow.close();
this._issueWindow = null;
}
});
}
}
if (this._issueWindow) {
this._issueWindow.focus();
}
});
if (this._issueWindow) {
this._issueWindow.focus();
}
}
openProcessExplorer(data: ProcessExplorerData): Promise<void> {
return new Promise(_ => {
// Create as singleton
if (!this._processExplorerWindow) {
this._processExplorerParentWindow = BrowserWindow.getFocusedWindow();
if (this._processExplorerParentWindow) {
const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500);
this._processExplorerWindow = new BrowserWindow({
skipTaskbar: true,
resizable: true,
fullscreen: false,
width: position.width,
height: position.height,
minWidth: 300,
minHeight: 200,
x: position.x,
y: position.y,
backgroundColor: data.styles.backgroundColor,
title: localize('processExplorer', "Process Explorer"),
webPreferences: {
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
enableWebSQL: false,
enableRemoteModule: false,
spellcheck: false,
nativeWindowOpen: true,
zoomFactor: zoomLevelToZoomFactor(data.zoomLevel),
sandbox: true,
contextIsolation: true
}
});
async openProcessExplorer(data: ProcessExplorerData): Promise<void> {
// Create as singleton
if (!this._processExplorerWindow) {
this._processExplorerParentWindow = BrowserWindow.getFocusedWindow();
if (this._processExplorerParentWindow) {
const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500);
this._processExplorerWindow = new BrowserWindow({
skipTaskbar: true,
resizable: true,
fullscreen: false,
width: position.width,
height: position.height,
minWidth: 300,
minHeight: 200,
x: position.x,
y: position.y,
backgroundColor: data.styles.backgroundColor,
title: localize('processExplorer', "Process Explorer"),
webPreferences: {
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
v8CacheOptions: 'bypassHeatCheck',
enableWebSQL: false,
enableRemoteModule: false,
spellcheck: false,
nativeWindowOpen: true,
zoomFactor: zoomLevelToZoomFactor(data.zoomLevel),
sandbox: true,
contextIsolation: true
}
});
this._processExplorerWindow.setMenuBarVisibility(false);
this._processExplorerWindow.setMenuBarVisibility(false);
const windowConfiguration = {
appRoot: this.environmentService.appRoot,
windowId: this._processExplorerWindow.id,
userEnv: this.userEnv,
machineId: this.machineId,
data
};
const windowConfiguration = {
appRoot: this.environmentService.appRoot,
windowId: this._processExplorerWindow.id,
userEnv: this.userEnv,
machineId: this.machineId,
data
};
this._processExplorerWindow.loadURL(
toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration));
this._processExplorerWindow.loadURL(
toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration));
this._processExplorerWindow.on('close', () => this._processExplorerWindow = null);
this._processExplorerWindow.on('close', () => this._processExplorerWindow = null);
this._processExplorerParentWindow.on('close', () => {
if (this._processExplorerWindow) {
this._processExplorerWindow.close();
this._processExplorerWindow = null;
}
});
}
this._processExplorerParentWindow.on('close', () => {
if (this._processExplorerWindow) {
this._processExplorerWindow.close();
this._processExplorerWindow = null;
}
});
}
}
// Focus
if (this._processExplorerWindow) {
this._processExplorerWindow.focus();
}
});
// Focus
if (this._processExplorerWindow) {
this._processExplorerWindow.focus();
}
}
public async getSystemStatus(): Promise<string> {
@@ -414,7 +412,7 @@ export class IssueMainService implements ICommonIssueService {
},
product: {
nameShort: product.nameShort,
version: product.version,
version: !!product.darwinUniversalAssetId ? `${product.version} (Universal)` : product.version,
commit: product.commit,
date: product.date,
reportIssueUrl: product.reportIssueUrl
@@ -436,7 +434,7 @@ function toWindowUrl<T>(modulePathToHtml: string, windowConfiguration: T): strin
}
return FileAccess
._asCodeFileUri(modulePathToHtml, require)
.asBrowserUri(modulePathToHtml, require, true)
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` })
.toString(true);
}

View File

@@ -3,9 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { MenuRegistry } from 'vs/platform/actions/common/actions';
import { CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands';
import { IContext, ContextKeyExpression, ContextKeyExprType } from 'vs/platform/contextkey/common/contextkey';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
@@ -340,39 +337,6 @@ export class KeybindingResolver {
}
return rules.evaluate(context);
}
public static getAllUnboundCommands(boundCommands: Map<string, boolean>): string[] {
const unboundCommands: string[] = [];
const seenMap: Map<string, boolean> = new Map<string, boolean>();
const addCommand = (id: string, includeCommandWithArgs: boolean) => {
if (seenMap.has(id)) {
return;
}
seenMap.set(id, true);
if (id[0] === '_' || id.indexOf('vscode.') === 0) { // private command
return;
}
if (boundCommands.get(id) === true) {
return;
}
if (!includeCommandWithArgs) {
const command = CommandsRegistry.getCommand(id);
if (command && typeof command.description === 'object'
&& isNonEmptyArray((<ICommandHandlerDescription>command.description).args)) { // command with args
return;
}
}
unboundCommands.push(id);
};
for (const id of MenuRegistry.getCommands().keys()) {
addCommand(id, true);
}
for (const id of CommandsRegistry.getCommands().keys()) {
addCommand(id, false);
}
return unboundCommands;
}
}
function printWhenExplanation(when: ContextKeyExpression | undefined): string {

View File

@@ -211,13 +211,13 @@ suite('AbstractKeybindingService', () => {
// send Ctrl/Cmd + K
let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, []);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, [
assert.strictEqual(shouldPreventDefault, true);
assert.deepStrictEqual(executeCommandCalls, []);
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, [
`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...`
]);
assert.deepEqual(statusMessageCallsDisposed, []);
assert.deepStrictEqual(statusMessageCallsDisposed, []);
executeCommandCalls = [];
showMessageCalls = [];
statusMessageCalls = [];
@@ -225,13 +225,13 @@ suite('AbstractKeybindingService', () => {
// send backspace
shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace);
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, []);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, [
assert.strictEqual(shouldPreventDefault, true);
assert.deepStrictEqual(executeCommandCalls, []);
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, [
`The key combination (${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}, ${toUsLabel(KeyCode.Backspace)}) is not a command.`
]);
assert.deepEqual(statusMessageCallsDisposed, [
assert.deepStrictEqual(statusMessageCallsDisposed, [
`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...`
]);
executeCommandCalls = [];
@@ -241,14 +241,14 @@ suite('AbstractKeybindingService', () => {
// send backspace
shouldPreventDefault = kbService.testDispatch(KeyCode.Backspace);
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, [{
assert.strictEqual(shouldPreventDefault, true);
assert.deepStrictEqual(executeCommandCalls, [{
commandId: 'simpleCommand',
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
assert.deepEqual(statusMessageCallsDisposed, []);
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, []);
assert.deepStrictEqual(statusMessageCallsDisposed, []);
executeCommandCalls = [];
showMessageCalls = [];
statusMessageCalls = [];
@@ -273,11 +273,11 @@ suite('AbstractKeybindingService', () => {
function assertIsIgnored(keybinding: number): void {
let shouldPreventDefault = kbService.testDispatch(keybinding);
assert.equal(shouldPreventDefault, false);
assert.deepEqual(executeCommandCalls, []);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
assert.deepEqual(statusMessageCallsDisposed, []);
assert.strictEqual(shouldPreventDefault, false);
assert.deepStrictEqual(executeCommandCalls, []);
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, []);
assert.deepStrictEqual(statusMessageCallsDisposed, []);
executeCommandCalls = [];
showMessageCalls = [];
statusMessageCalls = [];
@@ -310,14 +310,14 @@ suite('AbstractKeybindingService', () => {
key1: true
});
let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, [{
assert.strictEqual(shouldPreventDefault, true);
assert.deepStrictEqual(executeCommandCalls, [{
commandId: 'simpleCommand',
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
assert.deepEqual(statusMessageCallsDisposed, []);
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, []);
assert.deepStrictEqual(statusMessageCallsDisposed, []);
executeCommandCalls = [];
showMessageCalls = [];
statusMessageCalls = [];
@@ -326,13 +326,13 @@ suite('AbstractKeybindingService', () => {
// send Ctrl/Cmd + K
currentContextValue = createContext({});
shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, []);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, [
assert.strictEqual(shouldPreventDefault, true);
assert.deepStrictEqual(executeCommandCalls, []);
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, [
`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...`
]);
assert.deepEqual(statusMessageCallsDisposed, []);
assert.deepStrictEqual(statusMessageCallsDisposed, []);
executeCommandCalls = [];
showMessageCalls = [];
statusMessageCalls = [];
@@ -341,14 +341,14 @@ suite('AbstractKeybindingService', () => {
// send Ctrl/Cmd + X
currentContextValue = createContext({});
shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X);
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, [{
assert.strictEqual(shouldPreventDefault, true);
assert.deepStrictEqual(executeCommandCalls, [{
commandId: 'chordCommand',
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
assert.deepEqual(statusMessageCallsDisposed, [
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, []);
assert.deepStrictEqual(statusMessageCallsDisposed, [
`(${toUsLabel(KeyMod.CtrlCmd | KeyCode.KEY_K)}) was pressed. Waiting for second key of chord...`
]);
executeCommandCalls = [];
@@ -370,14 +370,14 @@ suite('AbstractKeybindingService', () => {
// send Ctrl/Cmd + K
currentContextValue = createContext({});
let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, [{
assert.strictEqual(shouldPreventDefault, true);
assert.deepStrictEqual(executeCommandCalls, [{
commandId: 'simpleCommand',
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
assert.deepEqual(statusMessageCallsDisposed, []);
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, []);
assert.deepStrictEqual(statusMessageCallsDisposed, []);
executeCommandCalls = [];
showMessageCalls = [];
statusMessageCalls = [];
@@ -388,14 +388,14 @@ suite('AbstractKeybindingService', () => {
key1: true
});
shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, [{
assert.strictEqual(shouldPreventDefault, true);
assert.deepStrictEqual(executeCommandCalls, [{
commandId: 'simpleCommand',
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
assert.deepEqual(statusMessageCallsDisposed, []);
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, []);
assert.deepStrictEqual(statusMessageCallsDisposed, []);
executeCommandCalls = [];
showMessageCalls = [];
statusMessageCalls = [];
@@ -406,11 +406,11 @@ suite('AbstractKeybindingService', () => {
key1: true
});
shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_X);
assert.equal(shouldPreventDefault, false);
assert.deepEqual(executeCommandCalls, []);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
assert.deepEqual(statusMessageCallsDisposed, []);
assert.strictEqual(shouldPreventDefault, false);
assert.deepStrictEqual(executeCommandCalls, []);
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, []);
assert.deepStrictEqual(statusMessageCallsDisposed, []);
executeCommandCalls = [];
showMessageCalls = [];
statusMessageCalls = [];
@@ -428,14 +428,14 @@ suite('AbstractKeybindingService', () => {
// send Ctrl/Cmd + K
currentContextValue = createContext({});
let shouldPreventDefault = kbService.testDispatch(KeyMod.CtrlCmd | KeyCode.KEY_K);
assert.equal(shouldPreventDefault, false);
assert.deepEqual(executeCommandCalls, [{
assert.strictEqual(shouldPreventDefault, false);
assert.deepStrictEqual(executeCommandCalls, [{
commandId: 'simpleCommand',
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
assert.deepEqual(statusMessageCallsDisposed, []);
assert.deepStrictEqual(showMessageCalls, []);
assert.deepStrictEqual(statusMessageCalls, []);
assert.deepStrictEqual(statusMessageCallsDisposed, []);
executeCommandCalls = [];
showMessageCalls = [];
statusMessageCalls = [];

View File

@@ -11,7 +11,7 @@ suite('KeybindingLabels', () => {
function assertUSLabel(OS: OperatingSystem, keybinding: number, expected: string): void {
const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS);
assert.equal(usResolvedKeybinding.getLabel(), expected);
assert.strictEqual(usResolvedKeybinding.getLabel(), expected);
}
test('Windows US label', () => {
@@ -116,7 +116,7 @@ suite('KeybindingLabels', () => {
test('Aria label', () => {
function assertAriaLabel(OS: OperatingSystem, keybinding: number, expected: string): void {
const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS);
assert.equal(usResolvedKeybinding.getAriaLabel(), expected);
assert.strictEqual(usResolvedKeybinding.getAriaLabel(), expected);
}
assertAriaLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Windows+A');
@@ -127,7 +127,7 @@ suite('KeybindingLabels', () => {
test('Electron Accelerator label', () => {
function assertElectronAcceleratorLabel(OS: OperatingSystem, keybinding: number, expected: string | null): void {
const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS);
assert.equal(usResolvedKeybinding.getElectronAccelerator(), expected);
assert.strictEqual(usResolvedKeybinding.getElectronAccelerator(), expected);
}
assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A');
@@ -154,7 +154,7 @@ suite('KeybindingLabels', () => {
test('User Settings label', () => {
function assertElectronAcceleratorLabel(OS: OperatingSystem, keybinding: number, expected: string): void {
const usResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS);
assert.equal(usResolvedKeybinding.getUserSettingsLabel(), expected);
assert.strictEqual(usResolvedKeybinding.getUserSettingsLabel(), expected);
}
assertElectronAcceleratorLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'ctrl+shift+alt+win+a');

View File

@@ -43,12 +43,12 @@ suite('KeybindingResolver', () => {
let contextRules = ContextKeyExpr.equals('bar', 'baz');
let keybindingItem = kbItem(keybinding, 'yes', null, contextRules, true);
assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'baz' }), contextRules), true);
assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'bz' }), contextRules), false);
assert.strictEqual(KeybindingResolver.contextMatchesRules(createContext({ bar: 'baz' }), contextRules), true);
assert.strictEqual(KeybindingResolver.contextMatchesRules(createContext({ bar: 'bz' }), contextRules), false);
let resolver = new KeybindingResolver([keybindingItem], [], () => { });
assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandId, 'yes');
assert.equal(resolver.resolve(createContext({ bar: 'bz' }), null, getDispatchStr(runtimeKeybinding)), null);
assert.strictEqual(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandId, 'yes');
assert.strictEqual(resolver.resolve(createContext({ bar: 'bz' }), null, getDispatchStr(runtimeKeybinding)), null);
});
test('resolve key with arguments', function () {
@@ -59,7 +59,7 @@ suite('KeybindingResolver', () => {
let keybindingItem = kbItem(keybinding, 'yes', commandArgs, contextRules, true);
let resolver = new KeybindingResolver([keybindingItem], [], () => { });
assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandArgs, commandArgs);
assert.strictEqual(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandArgs, commandArgs);
});
test('KeybindingResolver.combine simple 1', function () {
@@ -70,7 +70,7 @@ suite('KeybindingResolver', () => {
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false)
];
let actual = KeybindingResolver.combine(defaults, overrides);
assert.deepEqual(actual, [
assert.deepStrictEqual(actual, [
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), false),
]);
@@ -85,7 +85,7 @@ suite('KeybindingResolver', () => {
kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false)
];
let actual = KeybindingResolver.combine(defaults, overrides);
assert.deepEqual(actual, [
assert.deepStrictEqual(actual, [
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true),
kbItem(KeyCode.KEY_C, 'yes3', null, ContextKeyExpr.equals('3', 'c'), false),
@@ -101,7 +101,7 @@ suite('KeybindingResolver', () => {
kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'b'), false)
];
let actual = KeybindingResolver.combine(defaults, overrides);
assert.deepEqual(actual, [
assert.deepStrictEqual(actual, [
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
]);
@@ -116,7 +116,7 @@ suite('KeybindingResolver', () => {
kbItem(KeyCode.KEY_B, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false)
];
let actual = KeybindingResolver.combine(defaults, overrides);
assert.deepEqual(actual, [
assert.deepStrictEqual(actual, [
kbItem(KeyCode.KEY_A, 'yes1', null, ContextKeyExpr.equals('1', 'a'), true),
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
]);
@@ -131,7 +131,7 @@ suite('KeybindingResolver', () => {
kbItem(KeyCode.KEY_A, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false)
];
let actual = KeybindingResolver.combine(defaults, overrides);
assert.deepEqual(actual, [
assert.deepStrictEqual(actual, [
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
]);
});
@@ -145,7 +145,7 @@ suite('KeybindingResolver', () => {
kbItem(0, '-yes1', null, ContextKeyExpr.equals('1', 'a'), false)
];
let actual = KeybindingResolver.combine(defaults, overrides);
assert.deepEqual(actual, [
assert.deepStrictEqual(actual, [
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
]);
});
@@ -159,7 +159,7 @@ suite('KeybindingResolver', () => {
kbItem(KeyCode.KEY_A, '-yes1', null, null!, false)
];
let actual = KeybindingResolver.combine(defaults, overrides);
assert.deepEqual(actual, [
assert.deepStrictEqual(actual, [
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
]);
});
@@ -173,7 +173,7 @@ suite('KeybindingResolver', () => {
kbItem(0, '-yes1', null, null!, false)
];
let actual = KeybindingResolver.combine(defaults, overrides);
assert.deepEqual(actual, [
assert.deepStrictEqual(actual, [
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
]);
});
@@ -187,17 +187,17 @@ suite('KeybindingResolver', () => {
kbItem(KeyCode.KEY_A, '-yes1', null, null!, false)
];
let actual = KeybindingResolver.combine(defaults, overrides);
assert.deepEqual(actual, [
assert.deepStrictEqual(actual, [
kbItem(KeyCode.KEY_B, 'yes2', null, ContextKeyExpr.equals('2', 'b'), true)
]);
});
test('contextIsEntirelyIncluded', () => {
const assertIsIncluded = (a: string | null, b: string | null) => {
assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true);
assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true);
};
const assertIsNotIncluded = (a: string | null, b: string | null) => {
assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false);
assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false);
};
assertIsIncluded('key1', null);
@@ -314,11 +314,11 @@ suite('KeybindingResolver', () => {
let testKey = (commandId: string, expectedKeys: number[]) => {
// Test lookup
let lookupResult = resolver.lookupKeybindings(commandId);
assert.equal(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId + '; GOT: ' + JSON.stringify(lookupResult, null, '\t'));
assert.strictEqual(lookupResult.length, expectedKeys.length, 'Length mismatch @ commandId ' + commandId + '; GOT: ' + JSON.stringify(lookupResult, null, '\t'));
for (let i = 0, len = lookupResult.length; i < len; i++) {
const expected = new USLayoutResolvedKeybinding(createKeybinding(expectedKeys[i], OS)!, OS);
assert.equal(lookupResult[i].resolvedKeybinding!.getUserSettingsLabel(), expected.getUserSettingsLabel(), 'value mismatch @ commandId ' + commandId);
assert.strictEqual(lookupResult[i].resolvedKeybinding!.getUserSettingsLabel(), expected.getUserSettingsLabel(), 'value mismatch @ commandId ' + commandId);
}
};
@@ -333,14 +333,14 @@ suite('KeybindingResolver', () => {
// if it's the final part, then we should find a valid command,
// and there should not be a chord.
assert.ok(result !== null, `Enters chord for ${commandId} at part ${i}`);
assert.equal(result!.commandId, commandId, `Enters chord for ${commandId} at part ${i}`);
assert.equal(result!.enterChord, false, `Enters chord for ${commandId} at part ${i}`);
assert.strictEqual(result!.commandId, commandId, `Enters chord for ${commandId} at part ${i}`);
assert.strictEqual(result!.enterChord, false, `Enters chord for ${commandId} at part ${i}`);
} else {
// if it's not the final part, then we should not find a valid command,
// and there should be a chord.
assert.ok(result !== null, `Enters chord for ${commandId} at part ${i}`);
assert.equal(result!.commandId, null, `Enters chord for ${commandId} at part ${i}`);
assert.equal(result!.enterChord, true, `Enters chord for ${commandId} at part ${i}`);
assert.strictEqual(result!.commandId, null, `Enters chord for ${commandId} at part ${i}`);
assert.strictEqual(result!.enterChord, true, `Enters chord for ${commandId} at part ${i}`);
}
previousPart = part;
}

View File

@@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { IWorkspace } from 'vs/platform/workspace/common/workspace';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
export const ILabelService = createDecorator<ILabelService>('labelService');
@@ -23,7 +23,7 @@ export interface ILabelService {
*/
getUriLabel(resource: URI, options?: { relative?: boolean, noPrefix?: boolean, endWithSeparator?: boolean }): string;
getUriBasenameLabel(resource: URI): string;
getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string;
getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | IWorkspace), options?: { verbose: boolean }): string;
getHostLabel(scheme: string, authority?: string): string;
getSeparator(scheme: string, authority?: string): '/' | '\\';

View File

@@ -9,10 +9,9 @@ import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWindowSettings } from 'vs/platform/windows/common/windows';
import { OpenContext } from 'vs/platform/windows/node/window';
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows';
import { whenDeleted } from 'vs/base/node/pfs';
import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { URI } from 'vs/base/common/uri';
import { BrowserWindow, ipcMain, Event as IpcEvent, app } from 'electron';
@@ -21,6 +20,7 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo, IRemoteDiagnosticInfo, IRemote
import { IMainProcessInfo, IWindowInfo } from 'vs/platform/launch/node/launch';
import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper';
import { CancellationToken } from 'vs/base/common/cancellation';
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
export const ID = 'launchMainService';
export const ILaunchMainService = createDecorator<ILaunchMainService>(ID);
@@ -35,23 +35,6 @@ export interface IRemoteDiagnosticOptions {
includeWorkspaceMetadata?: boolean;
}
function parseOpenUrl(args: NativeParsedArgs): { uri: URI, url: string }[] {
if (args['open-url'] && args._urls && args._urls.length > 0) {
// --open-url must contain -- followed by the url(s)
// process.argv is used over args._ as args._ are resolved to file paths at this point
return coalesce(args._urls
.map(url => {
try {
return { uri: URI.parse(url), url };
} catch (err) {
return null;
}
}));
}
return [];
}
export interface ILaunchMainService {
readonly _serviceBrand: undefined;
start(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise<void>;
@@ -68,7 +51,7 @@ export class LaunchMainService implements ILaunchMainService {
@ILogService private readonly logService: ILogService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@IURLService private readonly urlService: IURLService,
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
@IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService,
@IConfigurationService private readonly configurationService: IConfigurationService
) { }
@@ -89,7 +72,7 @@ export class LaunchMainService implements ILaunchMainService {
}
// Check early for open-url which is handled in URL service
const urlsToOpen = parseOpenUrl(args);
const urlsToOpen = this.parseOpenUrl(args);
if (urlsToOpen.length) {
let whenWindowReady: Promise<unknown> = Promise.resolve();
@@ -113,7 +96,24 @@ export class LaunchMainService implements ILaunchMainService {
}
}
private startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise<void> {
private parseOpenUrl(args: NativeParsedArgs): { uri: URI, url: string }[] {
if (args['open-url'] && args._urls && args._urls.length > 0) {
// --open-url must contain -- followed by the url(s)
// process.argv is used over args._ as args._ are resolved to file paths at this point
return coalesce(args._urls
.map(url => {
try {
return { uri: URI.parse(url), url };
} catch (err) {
return null;
}
}));
}
return [];
}
private async startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise<void> {
const context = isLaunchedFromCli(userEnv) ? OpenContext.CLI : OpenContext.DESKTOP;
let usedWindows: ICodeWindow[] = [];
@@ -205,17 +205,15 @@ export class LaunchMainService implements ILaunchMainService {
whenDeleted(waitMarkerFileURI.fsPath)
]).then(() => undefined, () => undefined);
}
return Promise.resolve(undefined);
}
getMainProcessId(): Promise<number> {
async getMainProcessId(): Promise<number> {
this.logService.trace('Received request for process ID from other instance.');
return Promise.resolve(process.pid);
return process.pid;
}
getMainProcessInfo(): Promise<IMainProcessInfo> {
async getMainProcessInfo(): Promise<IMainProcessInfo> {
this.logService.trace('Received request for main process info from other instance.');
const windows: IWindowInfo[] = [];
@@ -228,18 +226,18 @@ export class LaunchMainService implements ILaunchMainService {
}
});
return Promise.resolve({
return {
mainPID: process.pid,
mainArguments: process.argv.slice(1),
windows,
screenReader: !!app.accessibilitySupportEnabled,
gpuFeatureStatus: app.getGPUFeatureStatus()
});
};
}
getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> {
async getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> {
const windows = this.windowsMainService.getWindows();
const promises: Promise<IDiagnosticInfo | IRemoteDiagnosticError | undefined>[] = windows.map(window => {
const diagnostics: Array<IDiagnosticInfo | IRemoteDiagnosticError | undefined> = await Promise.all(windows.map(window => {
return new Promise<IDiagnosticInfo | IRemoteDiagnosticError | undefined>((resolve) => {
const remoteAuthority = window.remoteAuthority;
if (remoteAuthority) {
@@ -267,27 +265,26 @@ export class LaunchMainService implements ILaunchMainService {
resolve(undefined);
}
});
});
}));
return Promise.all(promises).then(diagnostics => diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x));
return diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x);
}
private getFolderURIs(window: ICodeWindow): URI[] {
const folderURIs: URI[] = [];
if (window.openedFolderUri) {
folderURIs.push(window.openedFolderUri);
} else if (window.openedWorkspace) {
// workspace folders can only be shown for local workspaces
const workspaceConfigPath = window.openedWorkspace.configPath;
const resolvedWorkspace = this.workspacesMainService.resolveLocalWorkspaceSync(workspaceConfigPath);
const workspace = window.openedWorkspace;
if (isSingleFolderWorkspaceIdentifier(workspace)) {
folderURIs.push(workspace.uri);
} else if (isWorkspaceIdentifier(workspace)) {
const resolvedWorkspace = this.workspacesManagementMainService.resolveLocalWorkspaceSync(workspace.configPath); // workspace folders can only be shown for local (resolved) workspaces
if (resolvedWorkspace) {
const rootFolders = resolvedWorkspace.folders;
rootFolders.forEach(root => {
folderURIs.push(root.uri);
});
} else {
//TODO: can we add the workspace file here?
//TODO@RMacfarlane: can we add the workspace file here?
}
}
@@ -296,13 +293,14 @@ export class LaunchMainService implements ILaunchMainService {
private codeWindowToInfo(window: ICodeWindow): IWindowInfo {
const folderURIs = this.getFolderURIs(window);
return this.browserWindowToInfo(window.win, folderURIs, window.remoteAuthority);
}
private browserWindowToInfo(win: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo {
private browserWindowToInfo(window: BrowserWindow, folderURIs: URI[] = [], remoteAuthority?: string): IWindowInfo {
return {
pid: win.webContents.getOSProcessId(),
title: win.getTitle(),
pid: window.webContents.getOSProcessId(),
title: window.getTitle(),
folderURIs,
remoteAuthority
};

View File

@@ -576,7 +576,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
if (window && !window.isDestroyed()) {
let whenWindowClosed: Promise<void>;
if (window.webContents && !window.webContents.isDestroyed()) {
whenWindowClosed = new Promise(c => window.once('closed', c));
whenWindowClosed = new Promise(resolve => window.once('closed', resolve));
} else {
whenWindowClosed = Promise.resolve();
}

View File

@@ -429,12 +429,14 @@ export interface IResourceNavigatorOptions {
export interface SelectionKeyboardEvent extends KeyboardEvent {
preserveFocus?: boolean;
pinned?: boolean;
__forceEvent?: boolean;
}
export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: boolean): SelectionKeyboardEvent {
export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: boolean, pinned?: boolean): SelectionKeyboardEvent {
const e = new KeyboardEvent(typeArg);
(<SelectionKeyboardEvent>e).preserveFocus = preserveFocus;
(<SelectionKeyboardEvent>e).pinned = pinned;
(<SelectionKeyboardEvent>e).__forceEvent = true;
return e;
@@ -478,8 +480,9 @@ abstract class ResourceNavigator<T> extends Disposable {
const focus = this.widget.getFocus();
this.widget.setSelection(focus, event.browserEvent);
const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true;
const pinned = !preserveFocus;
const selectionKeyboardEvent = event.browserEvent as SelectionKeyboardEvent;
const preserveFocus = typeof selectionKeyboardEvent.preserveFocus === 'boolean' ? selectionKeyboardEvent.preserveFocus! : true;
const pinned = typeof selectionKeyboardEvent.pinned === 'boolean' ? selectionKeyboardEvent.pinned! : !preserveFocus;
const sideBySide = false;
this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent);
@@ -490,8 +493,9 @@ abstract class ResourceNavigator<T> extends Disposable {
return;
}
const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true;
const pinned = !preserveFocus;
const selectionKeyboardEvent = event.browserEvent as SelectionKeyboardEvent;
const preserveFocus = typeof selectionKeyboardEvent.preserveFocus === 'boolean' ? selectionKeyboardEvent.preserveFocus! : true;
const pinned = typeof selectionKeyboardEvent.pinned === 'boolean' ? selectionKeyboardEvent.pinned! : !preserveFocus;
const sideBySide = false;
this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent);
@@ -826,7 +830,7 @@ function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTre
};
const accessibilityOn = accessibilityService.isScreenReaderOptimized();
const keyboardNavigation = accessibilityOn ? 'simple' : configurationService.getValue<string>(keyboardNavigationSettingKey);
const keyboardNavigation = options.simpleKeyboardNavigation || accessibilityOn ? 'simple' : configurationService.getValue<string>(keyboardNavigationSettingKey);
const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : configurationService.getValue<boolean>(horizontalScrollingKey);
const [workbenchListOptions, disposable] = toWorkbenchListOptions(options, configurationService, keybindingService);
const additionalScrollHeight = options.additionalScrollHeight;

View File

@@ -25,6 +25,8 @@ export interface ILocalizationsService {
readonly onDidLanguagesChange: Event<void>;
getLanguageIds(): Promise<string[]>;
update(): Promise<boolean>;
}
export function isValidLocalization(localization: ILocalization): boolean {

View File

@@ -9,8 +9,8 @@ import { URI } from 'vs/base/common/uri';
import { basename, extname, dirname } from 'vs/base/common/resources';
import { Schemas } from 'vs/base/common/network';
import { FileLogService } from 'vs/platform/log/common/fileLogService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
import { IFileService } from 'vs/platform/files/common/files';
export class LoggerService extends Disposable implements ILoggerService {
@@ -20,7 +20,7 @@ export class LoggerService extends Disposable implements ILoggerService {
constructor(
@ILogService private logService: ILogService,
@IInstantiationService private instantiationService: IInstantiationService,
@IFileService private fileService: IFileService
) {
super();
this._register(logService.onDidChangeLogLevel(level => this.loggers.forEach(logger => logger.setLevel(level))));
@@ -34,7 +34,7 @@ export class LoggerService extends Disposable implements ILoggerService {
const ext = extname(resource);
logger = new SpdLogService(baseName.substring(0, baseName.length - ext.length), dirname(resource).fsPath, this.logService.getLevel());
} else {
logger = this.instantiationService.createInstance(FileLogService, basename(resource), resource, this.logService.getLevel());
logger = new FileLogService(basename(resource), resource, this.logService.getLevel(), this.fileService);
}
this.loggers.set(resource.toString(), logger);
}

View File

@@ -145,14 +145,11 @@ export class MarkerService implements IMarkerService {
readonly onMarkerChanged: Event<readonly URI[]> = Event.debounce(this._onMarkerChanged.event, MarkerService._debouncer, 0);
private readonly _data = new DoubleResourceMap<IMarker[]>();
private readonly _stats: MarkerStats;
constructor() {
this._stats = new MarkerStats(this);
}
private readonly _stats = new MarkerStats(this);
dispose(): void {
this._stats.dispose();
this._onMarkerChanged.dispose();
}
getStatistics(): MarkerStatistics {

View File

@@ -6,9 +6,8 @@
import * as nls from 'vs/nls';
import { isMacintosh, language } from 'vs/base/common/platform';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, Event, KeyboardEvent } from 'electron';
import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, KeyboardEvent } from 'electron';
import { getTitleBarStyle, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable } from 'vs/platform/windows/common/windows';
import { OpenContext } from 'vs/platform/windows/node/window';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUpdateService, StateType } from 'vs/platform/update/common/update';
@@ -16,7 +15,7 @@ import product from 'vs/platform/product/common/product';
import { RunOnceScheduler } from 'vs/base/common/async';
import { ILogService } from 'vs/platform/log/common/log';
import { mnemonicMenuLabel } from 'vs/base/common/labels';
import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows';
import { IWindowsMainService, IWindowsCountChangedEvent, OpenContext } from 'vs/platform/windows/electron-main/windows';
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar';
import { URI } from 'vs/base/common/uri';
@@ -62,7 +61,7 @@ export class Menubar {
private keybindings: { [commandId: string]: IMenubarKeybinding };
private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow | undefined, event: Event) => void } = Object.create(null);
private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow | undefined, event: KeyboardEvent) => void } = Object.create(null);
constructor(
@IUpdateService private readonly updateService: IUpdateService,
@@ -608,45 +607,18 @@ export class Menubar {
}
}
private static _menuItemIsTriggeredViaKeybinding(event: KeyboardEvent, userSettingsLabel: string): boolean {
// The event coming in from Electron will inform us only about the modifier keys pressed.
// The strategy here is to check if the modifier keys match those of the keybinding,
// since it is highly unlikely to use modifier keys when clicking with the mouse
if (!userSettingsLabel) {
// There is no keybinding
return false;
}
let ctrlRequired = /ctrl/.test(userSettingsLabel);
let shiftRequired = /shift/.test(userSettingsLabel);
let altRequired = /alt/.test(userSettingsLabel);
let metaRequired = /cmd/.test(userSettingsLabel) || /super/.test(userSettingsLabel);
if (!ctrlRequired && !shiftRequired && !altRequired && !metaRequired) {
// This keybinding does not use any modifier keys, so we cannot use this heuristic
return false;
}
return (
ctrlRequired === event.ctrlKey
&& shiftRequired === event.shiftKey
&& altRequired === event.altKey
&& metaRequired === event.metaKey
);
}
private createMenuItem(label: string, commandId: string | string[], enabled?: boolean, checked?: boolean): MenuItem;
private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): MenuItem;
private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): MenuItem {
const label = this.mnemonicLabel(arg1);
const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: Event) => {
const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: KeyboardEvent) => {
const userSettingsLabel = menuItem ? menuItem.userSettingsLabel : null;
let commandId = arg2;
if (Array.isArray(arg2)) {
commandId = this.isOptionClick(event) ? arg2[1] : arg2[0]; // support alternative action if we got multiple action Ids and the option key was pressed while invoking
}
if (userSettingsLabel && Menubar._menuItemIsTriggeredViaKeybinding(event, userSettingsLabel)) {
if (userSettingsLabel && event.triggeredByAccelerator) {
this.runActionInRenderer({ type: 'keybinding', userSettingsLabel });
} else {
this.runActionInRenderer({ type: 'commandId', commandId });
@@ -706,8 +678,8 @@ export class Menubar {
return new MenuItem(this.withKeybinding(commandId, options));
}
private makeContextAwareClickHandler(click: () => void, contextSpecificHandlers: IMenuItemClickHandler): () => void {
return () => {
private makeContextAwareClickHandler(click: (menuItem: MenuItem, win: BrowserWindow, event: KeyboardEvent) => void, contextSpecificHandlers: IMenuItemClickHandler): (menuItem: MenuItem, win: BrowserWindow | undefined, event: KeyboardEvent) => void {
return (menuItem: MenuItem, win: BrowserWindow | undefined, event: KeyboardEvent) => {
// No Active Window
const activeWindow = BrowserWindow.getFocusedWindow();
@@ -722,7 +694,7 @@ export class Menubar {
}
// Finally execute command in Window
click();
click(menuItem, win || activeWindow, event);
};
}

View File

@@ -49,7 +49,7 @@ export interface ICommonNativeHostService {
readonly onDidChangeColorScheme: Event<IColorScheme>;
readonly onDidChangePassword: Event<void>;
readonly onDidChangePassword: Event<{ service: string, account: string }>;
// Window
getWindows(): Promise<IOpenedWindow[]>;
@@ -137,6 +137,7 @@ export interface ICommonNativeHostService {
// Development
openDevTools(options?: OpenDevToolsOptions): Promise<void>;
toggleDevTools(): Promise<void>;
toggleSharedProcessWindow(): Promise<void>;
sendInputEvent(event: MouseInputEvent): Promise<void>;
// Connectivity

View File

@@ -4,18 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows';
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme } from 'electron';
import { OpenContext } from 'vs/platform/windows/node/window';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform';
import { isMacintosh, isWindows, isLinux, isLinuxSnap } from 'vs/base/common/platform';
import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { AddFirstParameterToFunctions } from 'vs/base/common/types';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService';
import { dirExists } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -28,6 +27,7 @@ import { dirname, join } from 'vs/base/common/path';
import product from 'vs/platform/product/common/product';
import { memoize } from 'vs/base/common/decorators';
import { Disposable } from 'vs/base/common/lifecycle';
import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess';
export interface INativeHostMainService extends AddFirstParameterToFunctions<ICommonNativeHostService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
@@ -43,6 +43,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
declare readonly _serviceBrand: undefined;
constructor(
private sharedProcess: ISharedProcess,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@IDialogMainService private readonly dialogMainService: IDialogMainService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@@ -91,7 +92,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
private readonly _onDidChangeColorScheme = this._register(new Emitter<IColorScheme>());
readonly onDidChangeColorScheme = this._onDidChangeColorScheme.event;
private readonly _onDidChangePassword = this._register(new Emitter<void>());
private readonly _onDidChangePassword = this._register(new Emitter<{ account: string, service: string }>());
readonly onDidChangePassword = this._onDidChangePassword.event;
//#endregion
@@ -104,7 +105,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
return windows.map(window => ({
id: window.id,
workspace: window.openedWorkspace,
folderUri: window.openedFolderUri,
title: window.win.getTitle(),
filename: window.getRepresentedFilename(),
dirty: window.isDocumentEdited()
@@ -334,8 +334,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
async openExternal(windowId: number | undefined, url: string): Promise<boolean> {
if (isLinux && process.env.SNAP && process.env.SNAP_REVISION) {
NativeHostMainService._safeSnapOpenExternal(url);
if (isLinuxSnap) {
this.safeSnapOpenExternal(url);
} else {
shell.openExternal(url);
}
@@ -343,7 +343,9 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
return true;
}
private static _safeSnapOpenExternal(url: string): void {
private safeSnapOpenExternal(url: string): void {
// Remove some environment variables before opening to avoid issues...
const gdkPixbufModuleFile = process.env['GDK_PIXBUF_MODULE_FILE'];
const gdkPixbufModuleDir = process.env['GDK_PIXBUF_MODULEDIR'];
delete process.env['GDK_PIXBUF_MODULE_FILE'];
@@ -351,6 +353,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
shell.openExternal(url);
// ...but restore them after
process.env['GDK_PIXBUF_MODULE_FILE'] = gdkPixbufModuleFile;
process.env['GDK_PIXBUF_MODULEDIR'] = gdkPixbufModuleDir;
}
@@ -626,6 +629,10 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
}
async toggleSharedProcessWindow(): Promise<void> {
return this.sharedProcess.toggle();
}
//#endregion
//#region Registry (windows)
@@ -703,7 +710,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
await keytar.setPassword(service, account, password);
}
this._onDidChangePassword.fire();
this._onDidChangePassword.fire({ service, account });
}
async deletePassword(windowId: number | undefined, service: string, account: string): Promise<boolean> {
@@ -711,7 +718,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
const didDelete = await keytar.deletePassword(service, account);
if (didDelete) {
this._onDidChangePassword.fire();
this._onDidChangePassword.fire({ service, account });
}
return didDelete;

View File

@@ -20,11 +20,13 @@ export interface ILinkDescriptor {
export interface ILinkStyles {
readonly textLinkForeground?: Color;
readonly disabled?: boolean;
}
export class Link extends Disposable {
readonly el: HTMLAnchorElement;
private disabled: boolean;
private styles: ILinkStyles = {
textLinkForeground: Color.fromHex('#006AB1')
};
@@ -50,9 +52,12 @@ export class Link extends Disposable {
this._register(onOpen(e => {
EventHelper.stop(e, true);
openerService.open(link.href);
if (!this.disabled) {
openerService.open(link.href);
}
}));
this.disabled = false;
this.applyStyles();
}
@@ -62,6 +67,26 @@ export class Link extends Disposable {
}
private applyStyles(): void {
this.el.style.color = this.styles.textLinkForeground?.toString() || '';
const color = this.styles.textLinkForeground?.toString();
if (color) {
this.el.style.color = color;
}
if (typeof this.styles.disabled === 'boolean' && this.styles.disabled !== this.disabled) {
if (this.styles.disabled) {
this.el.setAttribute('aria-disabled', 'true');
this.el.tabIndex = -1;
this.el.style.pointerEvents = 'none';
this.el.style.opacity = '0.4';
this.el.style.cursor = 'default';
this.disabled = true;
} else {
this.el.setAttribute('aria-disabled', 'false');
this.el.tabIndex = 0;
this.el.style.pointerEvents = 'auto';
this.el.style.opacity = '1';
this.el.style.cursor = 'pointer';
this.disabled = false;
}
}
}
}

View File

@@ -3,15 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IOpenerService = createDecorator<IOpenerService>('openerService');
type OpenInternalOptions = {
export type OpenInternalOptions = {
/**
* Signals that the intent is to open an editor to the side
@@ -31,7 +32,11 @@ type OpenInternalOptions = {
readonly fromUserGesture?: boolean;
};
type OpenExternalOptions = { readonly openExternal?: boolean; readonly allowTunneling?: boolean };
export type OpenExternalOptions = {
readonly openExternal?: boolean;
readonly allowTunneling?: boolean;
readonly allowContributedOpeners?: boolean | string;
};
export type OpenOptions = OpenInternalOptions & OpenExternalOptions;
@@ -46,7 +51,8 @@ export interface IOpener {
}
export interface IExternalOpener {
openExternal(href: string): Promise<boolean>;
openExternal(href: string, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise<boolean>;
dispose?(): void;
}
export interface IValidator {
@@ -81,7 +87,12 @@ export interface IOpenerService {
* Sets the handler for opening externally. If not provided,
* a default handler will be used.
*/
setExternalOpener(opener: IExternalOpener): void;
setDefaultExternalOpener(opener: IExternalOpener): void;
/**
* Registers a new opener external resources openers.
*/
registerExternalOpener(opener: IExternalOpener): IDisposable;
/**
* Opens a resource, like a webaddress, a document uri, or executes command.
@@ -97,15 +108,16 @@ export interface IOpenerService {
resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise<IResolvedExternalUri>;
}
export const NullOpenerService: IOpenerService = Object.freeze({
export const NullOpenerService = Object.freeze({
_serviceBrand: undefined,
registerOpener() { return Disposable.None; },
registerValidator() { return Disposable.None; },
registerExternalUriResolver() { return Disposable.None; },
setExternalOpener() { },
setDefaultExternalOpener() { },
registerExternalOpener() { return Disposable.None; },
async open() { return false; },
async resolveExternalUri(uri: URI) { return { resolved: uri, dispose() { } }; },
});
} as IOpenerService);
export function matchesScheme(target: URI | string, scheme: string) {
if (URI.isUri(target)) {

View File

@@ -20,7 +20,7 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire !
// Running out of sources
if (Object.keys(product).length === 0) {
Object.assign(product, {
version: '1.52.0-dev',
version: '1.53.0-dev',
nameShort: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev',
nameLong: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev',
applicationName: 'code-oss',

View File

@@ -131,6 +131,8 @@ export interface IProductConfiguration {
readonly linkProtectionTrustedDomains?: readonly string[];
readonly 'configurationSync.store'?: ConfigurationSyncStore;
readonly darwinUniversalAssetId?: string;
}
export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean };

View File

@@ -208,8 +208,12 @@ export class BrowserSocketFactory implements ISocketFactory {
}
connect(host: string, port: number, query: string, callback: IConnectCallback): void {
<<<<<<< HEAD
// NOTE@coder: Modified to work against the current path.
const socket = this._webSocketFactory.create(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}${window.location.pathname}?${query}&skipWebSocketFrames=false`);
=======
const socket = this._webSocketFactory.create(`ws://${/:/.test(host) ? `[${host}]` : host}:${port}/?${query}&skipWebSocketFrames=false`);
>>>>>>> 89b6e0164fa770333755b11504e19a4232b1a2d4
const errorListener = socket.onError((err) => callback(err, undefined));
socket.onOpen(() => {
errorListener.dispose();

View File

@@ -8,7 +8,7 @@ import { generateUuid } from 'vs/base/common/uuid';
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { Disposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter } from 'vs/base/common/event';
import { Emitter, Event } from 'vs/base/common/event';
import { RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
import { ISignService } from 'vs/platform/sign/common/sign';
@@ -86,6 +86,7 @@ export interface ISocketFactory {
connect(host: string, port: number, query: string, callback: IConnectCallback): void;
}
<<<<<<< HEAD
async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> {
const logPrefix = connectLogPrefix(options, connectionType);
const { protocol, ownsProtocol } = await new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => {
@@ -101,81 +102,126 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio
e(err);
return;
}
=======
async function readOneControlMessage<T>(protocol: PersistentProtocol): Promise<T> {
const raw = await Event.toPromise(protocol.onControlMessage);
const msg = JSON.parse(raw.toString());
const error = getErrorFromMessage(msg);
if (error) {
throw error;
}
return msg;
}
>>>>>>> 89b6e0164fa770333755b11504e19a4232b1a2d4
options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`);
if (options.reconnectionProtocol) {
options.reconnectionProtocol.beginAcceptReconnection(socket, null);
c({ protocol: options.reconnectionProtocol, ownsProtocol: false });
} else {
c({ protocol: new PersistentProtocol(socket, null), ownsProtocol: true });
}
function waitWithTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timeoutToken = setTimeout(() => {
const error: any = new Error('Timeout');
error.code = 'ETIMEDOUT';
error.syscall = 'connect';
reject(error);
}, timeout);
promise.then(
(result) => {
clearTimeout(timeoutToken);
resolve(result);
},
(error) => {
clearTimeout(timeoutToken);
reject(error);
}
);
});
}
return new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => {
function createSocket(socketFactory: ISocketFactory, host: string, port: number, query: string): Promise<ISocket> {
return new Promise<ISocket>((resolve, reject) => {
socketFactory.connect(host, port, query, (err: any, socket: ISocket | undefined) => {
if (err || !socket) {
return reject(err);
}
resolve(socket);
});
});
}
const errorTimeoutToken = setTimeout(() => {
const error: any = new Error('handshake timeout');
error.code = 'ETIMEDOUT';
error.syscall = 'connect';
async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> {
const logPrefix = connectLogPrefix(options, connectionType);
options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`);
let socket: ISocket;
try {
socket = await createSocket(options.socketFactory, options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`);
} catch (error) {
options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`);
options.logService.error(error);
throw error;
}
options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`);
let protocol: PersistentProtocol;
let ownsProtocol: boolean;
if (options.reconnectionProtocol) {
options.reconnectionProtocol.beginAcceptReconnection(socket, null);
protocol = options.reconnectionProtocol;
ownsProtocol = false;
} else {
protocol = new PersistentProtocol(socket, null);
ownsProtocol = true;
}
options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`);
const authRequest: AuthRequest = {
type: 'auth',
auth: options.connectionToken || '00000000000000000000'
};
protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest)));
try {
const msg = await waitWithTimeout(readOneControlMessage<HandshakeMessage>(protocol), 10000);
if (msg.type !== 'sign' || typeof msg.data !== 'string') {
const error: any = new Error('Unexpected handshake message');
error.code = 'VSCODE_CONNECTION_ERROR';
throw error;
}
options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`);
const signed = await options.signService.sign(msg.data);
const connTypeRequest: ConnectionTypeRequest = {
type: 'connectionType',
commit: options.commit,
signedData: signed,
desiredConnectionType: connectionType
};
if (args) {
connTypeRequest.args = args;
}
options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`);
protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest)));
return { protocol, ownsProtocol };
} catch (error) {
if (error && error.code === 'ETIMEDOUT') {
options.logService.error(`${logPrefix} the handshake took longer than 10 seconds. Error:`);
options.logService.error(error);
if (ownsProtocol) {
safeDisposeProtocolAndSocket(protocol);
}
e(error);
}, 10000);
const messageRegistration = protocol.onControlMessage(async raw => {
const msg = <HandshakeMessage>JSON.parse(raw.toString());
// Stop listening for further events
messageRegistration.dispose();
const error = getErrorFromMessage(msg);
if (error) {
options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`);
options.logService.error(error);
if (ownsProtocol) {
safeDisposeProtocolAndSocket(protocol);
}
return e(error);
}
if (msg.type === 'sign') {
options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`);
const signed = await options.signService.sign(msg.data);
const connTypeRequest: ConnectionTypeRequest = {
type: 'connectionType',
commit: options.commit,
signedData: signed,
desiredConnectionType: connectionType
};
if (args) {
connTypeRequest.args = args;
}
options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`);
protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest)));
clearTimeout(errorTimeoutToken);
c({ protocol, ownsProtocol });
} else {
const error = new Error('handshake error');
options.logService.error(`${logPrefix} received unexpected control message. Error:`);
options.logService.error(error);
if (ownsProtocol) {
safeDisposeProtocolAndSocket(protocol);
}
e(error);
}
});
options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`);
const authRequest: AuthRequest = {
type: 'auth',
auth: options.connectionToken || '00000000000000000000'
};
protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest)));
});
}
if (error && error.code === 'VSCODE_CONNECTION_ERROR') {
options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`);
options.logService.error(error);
}
if (ownsProtocol) {
safeDisposeProtocolAndSocket(protocol);
}
throw error;
}
}
interface IManagementConnectionResult {
@@ -287,7 +333,7 @@ export async function connectRemoteAgentManagement(options: IConnectionOptions,
} catch (err) {
options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`);
options.logService.error(err);
PersistentConnection.triggerPermanentFailure();
PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err));
throw err;
}
}
@@ -301,7 +347,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption
} catch (err) {
options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`);
options.logService.error(err);
PersistentConnection.triggerPermanentFailure();
PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err));
throw err;
}
}
@@ -333,10 +379,16 @@ export const enum PersistentConnectionEventType {
}
export class ConnectionLostEvent {
public readonly type = PersistentConnectionEventType.ConnectionLost;
constructor(
public readonly reconnectionToken: string,
public readonly millisSinceLastIncomingData: number
) { }
}
export class ReconnectionWaitEvent {
public readonly type = PersistentConnectionEventType.ReconnectionWait;
constructor(
public readonly reconnectionToken: string,
public readonly millisSinceLastIncomingData: number,
public readonly durationSeconds: number,
private readonly cancellableTimer: CancelablePromise<void>,
public readonly connectionAttempt: number
@@ -348,22 +400,44 @@ export class ReconnectionWaitEvent {
}
export class ReconnectionRunningEvent {
public readonly type = PersistentConnectionEventType.ReconnectionRunning;
constructor(
public readonly reconnectionToken: string,
public readonly millisSinceLastIncomingData: number,
public readonly attempt: number
) { }
}
export class ConnectionGainEvent {
public readonly type = PersistentConnectionEventType.ConnectionGain;
constructor(
public readonly reconnectionToken: string,
public readonly millisSinceLastIncomingData: number,
public readonly attempt: number
) { }
}
export class ReconnectionPermanentFailureEvent {
public readonly type = PersistentConnectionEventType.ReconnectionPermanentFailure;
constructor(
public readonly reconnectionToken: string,
public readonly millisSinceLastIncomingData: number,
public readonly attempt: number,
public readonly handled: boolean
) { }
}
export type PersistentConnectionEvent = ConnectionGainEvent | ConnectionLostEvent | ReconnectionWaitEvent | ReconnectionRunningEvent | ReconnectionPermanentFailureEvent;
abstract class PersistentConnection extends Disposable {
public static triggerPermanentFailure(): void {
public static triggerPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void {
this._permanentFailure = true;
this._instances.forEach(instance => instance._gotoPermanentFailure());
this._permanentFailureMillisSinceLastIncomingData = millisSinceLastIncomingData;
this._permanentFailureAttempt = attempt;
this._permanentFailureHandled = handled;
this._instances.forEach(instance => instance._gotoPermanentFailure(this._permanentFailureMillisSinceLastIncomingData, this._permanentFailureAttempt, this._permanentFailureHandled));
}
private static _permanentFailure: boolean = false;
private static _permanentFailureMillisSinceLastIncomingData: number = 0;
private static _permanentFailureAttempt: number = 0;
private static _permanentFailureHandled: boolean = false;
private static _instances: PersistentConnection[] = [];
private readonly _onDidStateChange = this._register(new Emitter<PersistentConnectionEvent>());
@@ -382,7 +456,7 @@ abstract class PersistentConnection extends Disposable {
this.protocol = protocol;
this._isReconnecting = false;
this._onDidStateChange.fire(new ConnectionGainEvent());
this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, 0, 0));
this._register(protocol.onSocketClose(() => this._beginReconnecting()));
this._register(protocol.onSocketTimeout(() => this._beginReconnecting()));
@@ -390,7 +464,7 @@ abstract class PersistentConnection extends Disposable {
PersistentConnection._instances.push(this);
if (PersistentConnection._permanentFailure) {
this._gotoPermanentFailure();
this._gotoPermanentFailure(PersistentConnection._permanentFailureMillisSinceLastIncomingData, PersistentConnection._permanentFailureAttempt, PersistentConnection._permanentFailureHandled);
}
}
@@ -414,14 +488,15 @@ abstract class PersistentConnection extends Disposable {
}
const logPrefix = commonLogPrefix(this._connectionType, this.reconnectionToken, true);
this._options.logService.info(`${logPrefix} starting reconnecting loop. You can get more information with the trace log level.`);
this._onDidStateChange.fire(new ConnectionLostEvent());
const TIMES = [5, 5, 10, 10, 10, 10, 10, 30];
this._onDidStateChange.fire(new ConnectionLostEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData()));
const TIMES = [0, 5, 5, 10, 10, 10, 10, 10, 30];
const disconnectStartTime = Date.now();
let attempt = -1;
do {
attempt++;
const waitTime = (attempt < TIMES.length ? TIMES[attempt] : TIMES[TIMES.length - 1]);
try {
<<<<<<< HEAD
const sleepPromise = sleep(waitTime);
this._onDidStateChange.fire(new ReconnectionWaitEvent(waitTime, sleepPromise, attempt+1));
@@ -429,6 +504,17 @@ abstract class PersistentConnection extends Disposable {
try {
await sleepPromise;
} catch { } // User canceled timer
=======
if (waitTime > 0) {
const sleepPromise = sleep(waitTime);
this._onDidStateChange.fire(new ReconnectionWaitEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), waitTime, sleepPromise));
this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`);
try {
await sleepPromise;
} catch { } // User canceled timer
}
>>>>>>> 89b6e0164fa770333755b11504e19a4232b1a2d4
if (PersistentConnection._permanentFailure) {
this._options.logService.error(`${logPrefix} permanent failure occurred while running the reconnecting loop.`);
@@ -436,26 +522,26 @@ abstract class PersistentConnection extends Disposable {
}
// connection was lost, let's try to re-establish it
this._onDidStateChange.fire(new ReconnectionRunningEvent());
this._onDidStateChange.fire(new ReconnectionRunningEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1));
this._options.logService.info(`${logPrefix} resolving connection...`);
const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol);
this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.host}:${simpleOptions.port}...`);
await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), RECONNECT_TIMEOUT);
this._options.logService.info(`${logPrefix} reconnected!`);
this._onDidStateChange.fire(new ConnectionGainEvent());
this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1));
break;
} catch (err) {
if (err.code === 'VSCODE_CONNECTION_ERROR') {
this._options.logService.error(`${logPrefix} A permanent error occurred in the reconnecting loop! Will give up now! Error:`);
this._options.logService.error(err);
PersistentConnection.triggerPermanentFailure();
PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false);
break;
}
if (Date.now() - disconnectStartTime > ProtocolConstants.ReconnectionGraceTime) {
this._options.logService.error(`${logPrefix} An error occurred while reconnecting, but it will be treated as a permanent error because the reconnection grace time has expired! Will give up now! Error:`);
this._options.logService.error(err);
PersistentConnection.triggerPermanentFailure();
PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false);
break;
}
if (RemoteAuthorityResolverError.isTemporarilyNotAvailable(err)) {
@@ -476,16 +562,22 @@ abstract class PersistentConnection extends Disposable {
// try again!
continue;
}
if (err instanceof RemoteAuthorityResolverError) {
this._options.logService.error(`${logPrefix} A RemoteAuthorityResolverError occurred while trying to reconnect. Will give up now! Error:`);
this._options.logService.error(err);
PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, RemoteAuthorityResolverError.isHandled(err));
break;
}
this._options.logService.error(`${logPrefix} An unknown error occurred while trying to reconnect, since this is an unknown case, it will be treated as a permanent error! Will give up now! Error:`);
this._options.logService.error(err);
PersistentConnection.triggerPermanentFailure();
PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false);
break;
}
} while (!PersistentConnection._permanentFailure);
}
private _gotoPermanentFailure(): void {
this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent());
private _gotoPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void {
this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent(this.reconnectionToken, millisSinceLastIncomingData, attempt, handled));
safeDisposeProtocolAndSocket(this.protocol);
}

View File

@@ -5,6 +5,7 @@
import { URI } from 'vs/base/common/uri';
import { OperatingSystem } from 'vs/base/common/platform';
import * as performance from 'vs/base/common/performance';
export interface IRemoteAgentEnvironment {
pid: number;
@@ -18,6 +19,7 @@ export interface IRemoteAgentEnvironment {
workspaceStorageHome: URI;
userHome: URI;
os: OperatingSystem;
marks: performance.PerformanceMark[];
}
export interface RemoteAgentConnectionContext {

View File

@@ -19,7 +19,7 @@ export function getRemoteName(authority: string | undefined): string | undefined
}
const pos = authority.indexOf('+');
if (pos < 0) {
// funky? bad authority?
// e.g. localhost:8000
return authority;
}
return authority.substr(0, pos);

View File

@@ -5,6 +5,7 @@
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { isWindows, OperatingSystem } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
@@ -17,36 +18,64 @@ export interface RemoteTunnel {
readonly tunnelRemoteHost: string;
readonly tunnelLocalPort?: number;
readonly localAddress: string;
dispose(silent?: boolean): void;
readonly public: boolean;
dispose(silent?: boolean): Promise<void>;
}
export interface TunnelOptions {
remoteAddress: { port: number, host: string };
remoteAddress: { port: number, host: string; };
localAddressPort?: number;
label?: string;
public?: boolean;
}
export interface TunnelCreationOptions {
elevationRequired?: boolean;
}
export interface TunnelProviderFeatures {
elevation: boolean;
public: boolean;
}
export interface ITunnelProvider {
forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel> | undefined;
forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel | undefined> | undefined;
}
export interface ITunnel {
remoteAddress: { port: number, host: string };
/**
* The complete local address(ex. localhost:1234)
*/
localAddress: string;
public?: boolean;
/**
* Implementers of Tunnel should fire onDidDispose when dispose is called.
*/
onDidDispose: Event<void>;
dispose(): Promise<void> | void;
}
export interface ITunnelService {
readonly _serviceBrand: undefined;
readonly tunnels: Promise<readonly RemoteTunnel[]>;
readonly canMakePublic: boolean;
readonly onTunnelOpened: Event<RemoteTunnel>;
readonly onTunnelClosed: Event<{ host: string, port: number }>;
readonly onTunnelClosed: Event<{ host: string, port: number; }>;
readonly canElevate: boolean;
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
canTunnel(uri: URI): boolean;
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, isPublic?: boolean): Promise<RemoteTunnel | undefined> | undefined;
closeTunnel(remoteHost: string, remotePort: number): Promise<void>;
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable;
setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable;
}
export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number } | undefined {
export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number; } | undefined {
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
return undefined;
}
@@ -74,51 +103,88 @@ function getOtherLocalhost(host: string): string | undefined {
return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined);
}
export function isPortPrivileged(port: number, os?: OperatingSystem): boolean {
if (os) {
return os !== OperatingSystem.Windows && (port < 1024);
} else {
return !isWindows && (port < 1024);
}
}
export abstract class AbstractTunnelService implements ITunnelService {
declare readonly _serviceBrand: undefined;
private _onTunnelOpened: Emitter<RemoteTunnel> = new Emitter();
public onTunnelOpened: Event<RemoteTunnel> = this._onTunnelOpened.event;
private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter();
public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event;
protected readonly _tunnels = new Map</*host*/ string, Map</* port */ number, { refcount: number, readonly value: Promise<RemoteTunnel> }>>();
private _onTunnelClosed: Emitter<{ host: string, port: number; }> = new Emitter();
public onTunnelClosed: Event<{ host: string, port: number; }> = this._onTunnelClosed.event;
protected readonly _tunnels = new Map</*host*/ string, Map</* port */ number, { refcount: number, readonly value: Promise<RemoteTunnel | undefined>; }>>();
protected _tunnelProvider: ITunnelProvider | undefined;
protected _canElevate: boolean = false;
private _canMakePublic: boolean = false;
public constructor(
@ILogService protected readonly logService: ILogService
) { }
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable {
setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable {
this._tunnelProvider = provider;
if (!provider) {
// clear features
this._canElevate = false;
this._canMakePublic = false;
return {
dispose: () => { }
};
}
this._tunnelProvider = provider;
this._canElevate = features.elevation;
this._canMakePublic = features.public;
return {
dispose: () => {
this._tunnelProvider = undefined;
this._canElevate = false;
this._canMakePublic = false;
}
};
}
public get tunnels(): Promise<readonly RemoteTunnel[]> {
const promises: Promise<RemoteTunnel>[] = [];
Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value)));
return Promise.all(promises);
public get canElevate(): boolean {
return this._canElevate;
}
dispose(): void {
public get canMakePublic() {
return this._canMakePublic;
}
public get tunnels(): Promise<readonly RemoteTunnel[]> {
return new Promise(async (resolve) => {
const tunnels: RemoteTunnel[] = [];
const tunnelArray = Array.from(this._tunnels.values());
for (let portMap of tunnelArray) {
const portArray = Array.from(portMap.values());
for (let x of portArray) {
const tunnelValue = await x.value;
if (tunnelValue) {
tunnels.push(tunnelValue);
}
}
}
resolve(tunnels);
});
}
async dispose(): Promise<void> {
for (const portMap of this._tunnels.values()) {
for (const { value } of portMap.values()) {
value.then(tunnel => tunnel.dispose());
await value.then(tunnel => tunnel?.dispose());
}
portMap.clear();
}
this._tunnels.clear();
}
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise<RemoteTunnel> | undefined {
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, isPublic: boolean = false): Promise<RemoteTunnel | undefined> | undefined {
this.logService.trace(`openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`);
if (!addressProvider) {
return undefined;
}
@@ -127,12 +193,19 @@ export abstract class AbstractTunnelService implements ITunnelService {
remoteHost = 'localhost';
}
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort);
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic);
if (!resolvedTunnel) {
this.logService.trace(`Tunnel was not created.`);
return resolvedTunnel;
}
return resolvedTunnel.then(tunnel => {
if (!tunnel) {
this.logService.trace('New tunnel is undefined.');
this.removeEmptyTunnelFromMap(remoteHost!, remotePort);
return undefined;
}
this.logService.trace('New tunnel established.');
const newTunnel = this.makeTunnel(tunnel);
if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) {
this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.');
@@ -148,24 +221,28 @@ export abstract class AbstractTunnelService implements ITunnelService {
tunnelRemoteHost: tunnel.tunnelRemoteHost,
tunnelLocalPort: tunnel.tunnelLocalPort,
localAddress: tunnel.localAddress,
dispose: () => {
public: tunnel.public,
dispose: async () => {
const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost);
if (existingHost) {
const existing = existingHost.get(tunnel.tunnelRemotePort);
if (existing) {
existing.refcount--;
this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing);
await this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing);
}
}
}
};
}
private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise<RemoteTunnel> }): Promise<void> {
private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise<RemoteTunnel | undefined> }): Promise<void> {
if (tunnel.refcount <= 0) {
const disposePromise: Promise<void> = tunnel.value.then(tunnel => {
tunnel.dispose(true);
this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
this.logService.trace(`Tunnel is being disposed ${remoteHost}:${remotePort}.`);
const disposePromise: Promise<void> = tunnel.value.then(async (tunnel) => {
if (tunnel) {
await tunnel.dispose(true);
this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
}
});
if (this._tunnels.has(remoteHost)) {
this._tunnels.get(remoteHost)!.delete(remotePort);
@@ -183,16 +260,30 @@ export abstract class AbstractTunnelService implements ITunnelService {
}
}
protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise<RemoteTunnel>) {
protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise<RemoteTunnel | undefined>) {
if (!this._tunnels.has(remoteHost)) {
this._tunnels.set(remoteHost, new Map());
}
this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel });
}
protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise<RemoteTunnel> } | undefined {
private async removeEmptyTunnelFromMap(remoteHost: string, remotePort: number) {
const hostMap = this._tunnels.get(remoteHost);
if (hostMap) {
const tunnel = hostMap.get(remotePort);
const tunnelResult = await tunnel;
if (!tunnelResult) {
hostMap.delete(remotePort);
}
if (hostMap.size === 0) {
this._tunnels.delete(remoteHost);
}
}
}
protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise<RemoteTunnel | undefined> } | undefined {
const otherLocalhost = getOtherLocalhost(remoteHost);
let portMap: Map<number, { refcount: number, readonly value: Promise<RemoteTunnel> }> | undefined;
let portMap: Map<number, { refcount: number, readonly value: Promise<RemoteTunnel | undefined> }> | undefined;
if (otherLocalhost) {
const firstMap = this._tunnels.get(remoteHost);
const secondMap = this._tunnels.get(otherLocalhost);
@@ -207,31 +298,25 @@ export abstract class AbstractTunnelService implements ITunnelService {
return portMap ? portMap.get(remotePort) : undefined;
}
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
canTunnel(uri: URI): boolean {
return !!extractLocalHostUriMetaDataForPortMapping(uri);
}
protected isPortPrivileged(port: number): boolean {
return port < 1024;
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise<RemoteTunnel | undefined> | undefined;
protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise<RemoteTunnel | undefined> | undefined {
this.logService.trace(`Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false };
const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, public: isPublic };
const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo);
this.logService.trace('Tunnel created by provider.');
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}
return tunnel;
}
}
export class TunnelService extends AbstractTunnelService {
protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise<RemoteTunnel> | undefined {
const existing = this.getTunnelFromMap(remoteHost, remotePort);
if (existing) {
++existing.refcount;
return existing.value;
}
if (this._tunnelProvider) {
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort };
const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) };
const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo);
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}
return tunnel;
}
return undefined;
}
}

View File

@@ -22,7 +22,7 @@ export const nodeSocketFactory = new class implements ISocketFactory {
const nonce = buffer.toString('base64');
let headers = [
`GET ws://${host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`,
`GET ws://${/:/.test(host) ? `[${host}]` : host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`,
`Connection: Upgrade`,
`Upgrade: websocket`,
`Sec-WebSocket-Key: ${nonce}`

View File

@@ -10,7 +10,7 @@ import { findFreePortFaster } from 'vs/base/node/ports';
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
import { ISignService } from 'vs/platform/sign/common/sign';
@@ -26,6 +26,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
public tunnelLocalPort!: number;
public tunnelRemoteHost: string;
public localAddress!: string;
public readonly public = false;
private readonly _options: IConnectionOptions;
private readonly _server: net.Server;
@@ -57,7 +58,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
this.tunnelRemoteHost = tunnelRemoteHost;
}
public dispose(): void {
public async dispose(): Promise<void> {
super.dispose();
this._server.removeListener('listening', this._listeningListener);
this._server.removeListener('connection', this._connectionListener);
@@ -129,8 +130,9 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
}
}
export class TunnelService extends AbstractTunnelService {
export class BaseTunnelService extends AbstractTunnelService {
public constructor(
private readonly socketFactory: ISocketFactory,
@ILogService logService: ILogService,
@ISignService private readonly signService: ISignService,
@IProductService private readonly productService: IProductService
@@ -138,7 +140,7 @@ export class TunnelService extends AbstractTunnelService {
super(logService);
}
protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise<RemoteTunnel | undefined> | undefined {
const existing = this.getTunnelFromMap(remoteHost, remotePort);
if (existing) {
++existing.refcount;
@@ -146,18 +148,12 @@ export class TunnelService extends AbstractTunnelService {
}
if (this._tunnelProvider) {
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) };
const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort };
const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo);
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}
return tunnel;
return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic);
} else {
this.logService.trace(`Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
const options: IConnectionOptions = {
commit: this.productService.commit,
socketFactory: nodeSocketFactory,
socketFactory: this.socketFactory,
addressProvider,
signService: this.signService,
logService: this.logService,
@@ -165,8 +161,19 @@ export class TunnelService extends AbstractTunnelService {
};
const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort);
this.logService.trace('Tunnel created without provider.');
this.addTunnelToMap(remoteHost, remotePort, tunnel);
return tunnel;
}
}
}
export class TunnelService extends BaseTunnelService {
public constructor(
@ILogService logService: ILogService,
@ISignService signService: ISignService,
@IProductService productService: IProductService
) {
super(nodeSocketFactory, logService, signService, productService);
}
}

View File

@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { LogLevel } from 'vs/platform/log/common/log';
export interface ISharedProcess {
/**
* Toggles the visibility of the otherwise hidden
* shared process window.
*/
toggle(): Promise<void>;
}
export interface ISharedProcessConfiguration {
readonly machineId: string;
readonly windowId: number;
readonly appRoot: string;
readonly userEnv: NodeJS.ProcessEnv;
readonly sharedIPCHandle: string;
readonly args: NativeParsedArgs;
readonly logLevel: LogLevel;
readonly nodeCachedDataDir?: string;
readonly backupWorkspacesPath: string;
}

View File

@@ -12,6 +12,8 @@ export interface IStateService {
getItem<T>(key: string, defaultValue: T): T;
getItem<T>(key: string, defaultValue?: T): T | undefined;
setItem(key: string, data?: object | string | number | boolean | undefined | null): void;
removeItem(key: string): void;
}

View File

@@ -143,7 +143,7 @@ export class StateService implements IStateService {
}
getItem<T>(key: string, defaultValue: T): T;
getItem<T>(key: string, defaultValue: T | undefined): T | undefined;
getItem<T>(key: string, defaultValue?: T): T | undefined;
getItem<T>(key: string, defaultValue?: T): T | undefined {
return this.fileStorage.getItem(key, defaultValue);
}

View File

@@ -4,31 +4,37 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as os from 'os';
import * as path from 'vs/base/common/path';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { tmpdir } from 'os';
import { join } from 'vs/base/common/path';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { FileStorage } from 'vs/platform/state/node/stateService';
import { mkdirp, rimraf, RimRafMode, writeFileSync } from 'vs/base/node/pfs';
import { mkdirp, rimraf, writeFileSync } from 'vs/base/node/pfs';
suite('StateService', () => {
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'stateservice');
const storageFile = path.join(parentDir, 'storage.json');
flakySuite('StateService', () => {
teardown(async () => {
await rimraf(parentDir, RimRafMode.MOVE);
let testDir: string;
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'stateservice');
return mkdirp(testDir);
});
test('Basics', async () => {
await mkdirp(parentDir);
teardown(() => {
return rimraf(testDir);
});
test('Basics', async function () {
const storageFile = join(testDir, 'storage.json');
writeFileSync(storageFile, '');
let service = new FileStorage(storageFile, () => null);
service.setItem('some.key', 'some.value');
assert.equal(service.getItem('some.key'), 'some.value');
assert.strictEqual(service.getItem('some.key'), 'some.value');
service.removeItem('some.key');
assert.equal(service.getItem('some.key', 'some.default'), 'some.default');
assert.strictEqual(service.getItem('some.key', 'some.default'), 'some.default');
assert.ok(!service.getItem('some.unknonw.key'));
@@ -36,15 +42,15 @@ suite('StateService', () => {
service = new FileStorage(storageFile, () => null);
assert.equal(service.getItem('some.other.key'), 'some.other.value');
assert.strictEqual(service.getItem('some.other.key'), 'some.other.value');
service.setItem('some.other.key', 'some.other.value');
assert.equal(service.getItem('some.other.key'), 'some.other.value');
assert.strictEqual(service.getItem('some.other.key'), 'some.other.value');
service.setItem('some.undefined.key', undefined);
assert.equal(service.getItem('some.undefined.key', 'some.default'), 'some.default');
assert.strictEqual(service.getItem('some.undefined.key', 'some.default'), 'some.default');
service.setItem('some.null.key', null);
assert.equal(service.getItem('some.null.key', 'some.default'), 'some.default');
assert.strictEqual(service.getItem('some.null.key', 'some.default'), 'some.default');
});
});
});

View File

@@ -200,12 +200,10 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS
return this.channel.call('updateItems', serializableRequest);
}
close(): Promise<void> {
async close(): Promise<void> {
// when we are about to close, we start to ignore main-side changes since we close anyway
dispose(this.onDidChangeItemsOnMainListener);
return Promise.resolve(); // global storage is closed on the main side
}
dispose(): void {

View File

@@ -12,7 +12,7 @@ import { mark } from 'vs/base/common/performance';
import { join } from 'vs/base/common/path';
import { copy, exists, mkdirp, writeFile } from 'vs/base/node/pfs';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { assertIsDefined } from 'vs/base/common/types';
import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
@@ -83,7 +83,7 @@ export class NativeStorageService extends AbstractStorageService {
const useInMemoryStorage = !!this.environmentService.extensionTestsLocationURI; // no storage during extension tests!
// Create workspace storage and initialize
mark('willInitWorkspaceStorage');
mark('code/willInitWorkspaceStorage');
try {
const workspaceStorage = this.createWorkspaceStorage(
useInMemoryStorage ? SQLiteStorageDatabase.IN_MEMORY_PATH : join(result.path, NativeStorageService.WORKSPACE_STORAGE_NAME),
@@ -99,7 +99,7 @@ export class NativeStorageService extends AbstractStorageService {
workspaceStorage.set(IS_NEW_KEY, false);
}
} finally {
mark('didInitWorkspaceStorage');
mark('code/didInitWorkspaceStorage');
}
} catch (error) {
this.logService.error(`[storage] initializeWorkspaceStorage(): Unable to init workspace storage due to ${error}`);
@@ -148,23 +148,22 @@ export class NativeStorageService extends AbstractStorageService {
private ensureWorkspaceStorageFolderMeta(payload: IWorkspaceInitializationPayload): void {
let meta: object | undefined = undefined;
if (isSingleFolderWorkspaceInitializationPayload(payload)) {
meta = { folder: payload.folder.toString() };
if (isSingleFolderWorkspaceIdentifier(payload)) {
meta = { folder: payload.uri.toString() };
} else if (isWorkspaceIdentifier(payload)) {
meta = { configuration: payload.configPath };
meta = { workspace: payload.configPath.toString() };
}
if (meta) {
const logService = this.logService;
const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME);
(async function () {
(async () => {
try {
const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), NativeStorageService.WORKSPACE_META_NAME);
const storageExists = await exists(workspaceStorageMetaPath);
if (!storageExists) {
await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2));
}
} catch (error) {
logService.error(error);
this.logService.error(error);
}
})();
}

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { strictEqual, ok, equal } from 'assert';
import { strictEqual, ok } from 'assert';
import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent } from 'vs/platform/storage/common/storage';
suite('StorageService', function () {
@@ -32,15 +32,15 @@ suite('StorageService', function () {
storage.store('test.get', 'foobar', scope, StorageTarget.MACHINE);
strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar');
let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get');
equal(storageValueChangeEvent?.scope, scope);
equal(storageValueChangeEvent?.key, 'test.get');
strictEqual(storageValueChangeEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.get');
storageValueChangeEvents = [];
storage.store('test.get', '', scope, StorageTarget.MACHINE);
strictEqual(storage.get('test.get', scope, (undefined)!), '');
storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get');
equal(storageValueChangeEvent!.scope, scope);
equal(storageValueChangeEvent!.key, 'test.get');
strictEqual(storageValueChangeEvent!.scope, scope);
strictEqual(storageValueChangeEvent!.key, 'test.get');
storage.store('test.getNumber', 5, scope, StorageTarget.MACHINE);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5);
@@ -79,8 +79,8 @@ suite('StorageService', function () {
storage.remove('test.remove', scope);
ok(!storage.get('test.remove', scope, (undefined)!));
let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.remove');
equal(storageValueChangeEvent?.scope, scope);
equal(storageValueChangeEvent?.key, 'test.remove');
strictEqual(storageValueChangeEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.remove');
}
test('Keys (in-memory)', () => {
@@ -107,20 +107,20 @@ suite('StorageService', function () {
storage.store('test.target1', 'value1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
equal(storageTargetEvent?.scope, scope);
equal(storageValueChangeEvent?.key, 'test.target1');
equal(storageValueChangeEvent?.scope, scope);
equal(storageValueChangeEvent?.target, target);
strictEqual(storageTargetEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.target1');
strictEqual(storageValueChangeEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.target, target);
storageTargetEvent = undefined;
storageValueChangeEvent = Object.create(null);
storage.store('test.target1', 'otherValue1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
equal(storageTargetEvent, undefined);
equal(storageValueChangeEvent?.key, 'test.target1');
equal(storageValueChangeEvent?.scope, scope);
equal(storageValueChangeEvent?.target, target);
strictEqual(storageTargetEvent, undefined);
strictEqual(storageValueChangeEvent?.key, 'test.target1');
strictEqual(storageValueChangeEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.target, target);
storage.store('test.target2', 'value2', scope, target);
storage.store('test.target3', 'value3', scope, target);
@@ -142,9 +142,9 @@ suite('StorageService', function () {
storage.remove('test.target4', scope);
strictEqual(storage.keys(scope, target).length, keysLength);
equal(storageTargetEvent?.scope, scope);
equal(storageValueChangeEvent?.key, 'test.target4');
equal(storageValueChangeEvent?.scope, scope);
strictEqual(storageTargetEvent?.scope, scope);
strictEqual(storageValueChangeEvent?.key, 'test.target4');
strictEqual(storageValueChangeEvent?.scope, scope);
}
}
@@ -171,7 +171,7 @@ suite('StorageService', function () {
storage.store('test.target1', undefined, scope, target);
strictEqual(storage.keys(scope, target).length, 0);
equal(storageTargetEvent?.scope, scope);
strictEqual(storageTargetEvent?.scope, scope);
storage.store('test.target1', '', scope, target);
strictEqual(storage.keys(scope, target).length, 1);

View File

@@ -3,12 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equal } from 'assert';
import { strictEqual } from 'assert';
import { FileStorageDatabase } from 'vs/platform/storage/browser/storageService';
import { generateUuid } from 'vs/base/common/uuid';
import { join } from 'vs/base/common/path';
import { tmpdir } from 'os';
import { rimraf, RimRafMode } from 'vs/base/node/pfs';
import { rimraf } from 'vs/base/node/pfs';
import { NullLogService } from 'vs/platform/log/common/log';
import { Storage } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
@@ -20,11 +19,10 @@ import { Schemas } from 'vs/base/common/network';
suite('Storage', () => {
const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
let testDir: string;
let fileService: FileService;
let fileProvider: DiskFileSystemProvider;
let testDir: string;
const disposables = new DisposableStore();
@@ -38,14 +36,13 @@ suite('Storage', () => {
disposables.add(fileService.registerProvider(Schemas.file, fileProvider));
disposables.add(fileProvider);
const id = generateUuid();
testDir = join(parentDir, id);
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
});
teardown(async () => {
teardown(() => {
disposables.clear();
await rimraf(parentDir, RimRafMode.MOVE);
return rimraf(testDir);
});
test('File Based Storage', async () => {
@@ -57,9 +54,9 @@ suite('Storage', () => {
storage.set('barNumber', 55);
storage.set('barBoolean', true);
equal(storage.get('bar'), 'foo');
equal(storage.get('barNumber'), '55');
equal(storage.get('barBoolean'), 'true');
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.get('barNumber'), '55');
strictEqual(storage.get('barBoolean'), 'true');
await storage.close();
@@ -67,17 +64,17 @@ suite('Storage', () => {
await storage.init();
equal(storage.get('bar'), 'foo');
equal(storage.get('barNumber'), '55');
equal(storage.get('barBoolean'), 'true');
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.get('barNumber'), '55');
strictEqual(storage.get('barBoolean'), 'true');
storage.delete('bar');
storage.delete('barNumber');
storage.delete('barBoolean');
equal(storage.get('bar', 'undefined'), 'undefined');
equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber');
equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean');
strictEqual(storage.get('bar', 'undefined'), 'undefined');
strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber');
strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean');
await storage.close();
@@ -85,8 +82,8 @@ suite('Storage', () => {
await storage.init();
equal(storage.get('bar', 'undefined'), 'undefined');
equal(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber');
equal(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean');
strictEqual(storage.get('bar', 'undefined'), 'undefined');
strictEqual(storage.get('barNumber', 'undefinedNumber'), 'undefinedNumber');
strictEqual(storage.get('barBoolean', 'undefinedBoolean'), 'undefinedBoolean');
});
});

View File

@@ -3,33 +3,33 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equal } from 'assert';
import { strictEqual } from 'assert';
import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { NativeStorageService } from 'vs/platform/storage/node/storageService';
import { generateUuid } from 'vs/base/common/uuid';
import { join } from 'vs/base/common/path';
import { tmpdir } from 'os';
import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs';
import { mkdirp, rimraf } from 'vs/base/node/pfs';
import { NullLogService } from 'vs/platform/log/common/log';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
suite('NativeStorageService', function () {
flakySuite('NativeStorageService', function () {
function uniqueStorageDir(): string {
const id = generateUuid();
let testDir: string;
return join(tmpdir(), 'vsctests', id, 'storage2', id);
}
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storageservice');
test('Migrate Data', async () => {
return mkdirp(testDir);
});
// Given issues such as https://github.com/microsoft/vscode/issues/108113
// we see random test failures when accessing the native file system.
this.retries(3);
this.timeout(1000 * 20);
teardown(() => {
return rimraf(testDir);
});
test('Migrate Data', async function () {
class StorageTestEnvironmentService extends NativeEnvironmentService {
@@ -46,27 +46,23 @@ suite('NativeStorageService', function () {
}
}
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(storageDir), storageDir));
const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(testDir), testDir));
await storage.initialize({ id: String(Date.now()) });
storage.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE);
storage.store('barNumber', 55, StorageScope.WORKSPACE, StorageTarget.MACHINE);
storage.store('barBoolean', true, StorageScope.GLOBAL, StorageTarget.MACHINE);
equal(storage.get('bar', StorageScope.WORKSPACE), 'foo');
equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55);
equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true);
strictEqual(storage.get('bar', StorageScope.WORKSPACE), 'foo');
strictEqual(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55);
strictEqual(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true);
await storage.migrate({ id: String(Date.now() + 100) });
equal(storage.get('bar', StorageScope.WORKSPACE), 'foo');
equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55);
equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true);
strictEqual(storage.get('bar', StorageScope.WORKSPACE), 'foo');
strictEqual(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55);
strictEqual(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true);
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
});

View File

@@ -3,12 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as Platform from 'vs/base/common/platform';
import * as os from 'os';
import * as uuid from 'vs/base/common/uuid';
import { readFile } from 'vs/base/node/pfs';
import { IFileService } from 'vs/platform/files/common/files';
import { isLinuxSnap, PlatformToString, platform } from 'vs/base/common/platform';
import { platform as nodePlatform, env } from 'vs/base/common/process';
import { generateUuid } from 'vs/base/common/uuid';
import { URI } from 'vs/base/common/uri';
export async function resolveCommonProperties(
fileService: IFileService,
release: string,
arch: string,
commit: string | undefined,
version: string | undefined,
machineId: string | undefined,
@@ -21,19 +25,19 @@ export async function resolveCommonProperties(
// __GDPR__COMMON__ "common.machineId" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" }
result['common.machineId'] = machineId;
// __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['sessionID'] = uuid.generateUuid() + Date.now();
result['sessionID'] = generateUuid() + Date.now();
// __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
result['commitHash'] = commit;
// __GDPR__COMMON__ "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['version'] = version;
// __GDPR__COMMON__ "common.platformVersion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.platformVersion'] = (os.release() || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3');
result['common.platformVersion'] = (release || '').replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, '$1$2$3');
// __GDPR__COMMON__ "common.platform" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.platform'] = Platform.PlatformToString(Platform.platform);
result['common.platform'] = PlatformToString(platform);
// __GDPR__COMMON__ "common.nodePlatform" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
result['common.nodePlatform'] = process.platform;
result['common.nodePlatform'] = nodePlatform;
// __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
result['common.nodeArch'] = process.arch;
result['common.nodeArch'] = arch;
// __GDPR__COMMON__ "common.product" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
result['common.product'] = product || 'desktop';
@@ -64,16 +68,16 @@ export async function resolveCommonProperties(
}
});
if (process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION) {
if (isLinuxSnap) {
// __GDPR__COMMON__ "common.snap" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.snap'] = 'true';
}
try {
const contents = await readFile(installSourcePath, 'utf8');
const contents = await fileService.readFile(URI.file(installSourcePath));
// __GDPR__COMMON__ "common.source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.source'] = contents.slice(0, 30);
result['common.source'] = contents.value.toString().slice(0, 30);
} catch (error) {
// ignore error
}
@@ -82,10 +86,11 @@ export async function resolveCommonProperties(
}
function verifyMicrosoftInternalDomain(domainList: readonly string[]): boolean {
if (!process || !process.env || !process.env['USERDNSDOMAIN']) {
const userDnsDomain = env['USERDNSDOMAIN'];
if (!userDnsDomain) {
return false;
}
const domain = process.env['USERDNSDOMAIN']!.toLowerCase();
const domain = userDnsDomain.toLowerCase();
return domainList.some(msftDomain => domain === msftDomain);
}

View File

@@ -332,6 +332,12 @@ export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.s
*/
export const editorActiveLinkForeground = registerColor('editorLink.activeForeground', { dark: '#4E94CE', light: Color.blue, hc: Color.cyan }, nls.localize('activeLinkForeground', 'Color of active links.'));
/**
* Inline hints
*/
export const editorInlineHintForeground = registerColor('editorInlineHint.foreground', { dark: editorWidgetBackground, light: editorWidgetForeground, hc: editorWidgetBackground }, nls.localize('editorInlineHintForeground', 'Foreground color of inline hints'));
export const editorInlineHintBackground = registerColor('editorInlineHint.background', { dark: editorWidgetForeground, light: editorWidgetBackground, hc: editorWidgetForeground }, nls.localize('editorInlineHintBackground', 'Background color of inline hints'));
/**
* Editor lighbulb icon colors
*/

View File

@@ -12,7 +12,6 @@ import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/plat
import { RunOnceScheduler } from 'vs/base/common/async';
import * as Codicons from 'vs/base/common/codicons';
// ------ API types
@@ -190,11 +189,6 @@ class IconRegistry implements IIconRegistry {
public toString() {
const sorter = (i1: IconContribution, i2: IconContribution) => {
const isThemeIcon1 = ThemeIcon.isThemeIcon(i1.defaults);
const isThemeIcon2 = ThemeIcon.isThemeIcon(i2.defaults);
if (isThemeIcon1 !== isThemeIcon2) {
return isThemeIcon1 ? -1 : 1;
}
return i1.id.localeCompare(i2.id);
};
const classNames = (i: IconContribution) => {
@@ -205,18 +199,24 @@ class IconRegistry implements IIconRegistry {
};
let reference = [];
let docCss = [];
reference.push(`| preview | identifier | default codicon id | description`);
reference.push(`| ----------- | --------------------------------- | --------------------------------- | --------------------------------- |`);
const contributions = Object.keys(this.iconsById).map(key => this.iconsById[key]);
for (const i of contributions.sort(sorter)) {
reference.push(`|<i class="${classNames(i)}"></i>|${i.id}|${ThemeIcon.isThemeIcon(i.defaults) ? i.defaults.id : ''}|${i.description || ''}|`);
if (!ThemeIcon.isThemeIcon((i.defaults))) {
docCss.push(`.codicon-${i.id}:before { content: "${i.defaults.character}" }`);
}
for (const i of contributions.filter(i => !!i.description).sort(sorter)) {
reference.push(`|<i class="${classNames(i)}"></i>|${i.id}|${ThemeIcon.isThemeIcon(i.defaults) ? i.defaults.id : i.id}|${i.description || ''}|`);
}
return reference.join('\n') + '\n\n' + docCss.join('\n');
reference.push(`| preview | identifier `);
reference.push(`| ----------- | --------------------------------- |`);
for (const i of contributions.filter(i => !ThemeIcon.isThemeIcon(i.defaults)).sort(sorter)) {
reference.push(`|<i class="${classNames(i)}"></i>|${i.id}|`);
}
return reference.join('\n');
}
}
@@ -262,3 +262,5 @@ export const widgetClose = registerIcon('widget-close', Codicons.Codicon.close,
export const gotoPreviousLocation = registerIcon('goto-previous-location', Codicons.Codicon.arrowUp, localize('previousChangeIcon', 'Icon for goto previous editor location.'));
export const gotoNextLocation = registerIcon('goto-next-location', Codicons.Codicon.arrowDown, localize('nextChangeIcon', 'Icon for goto next editor location.'));
export const syncing = ThemeIcon.modify(Codicons.Codicon.sync, 'spin');

View File

@@ -11,7 +11,7 @@ import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';
import { Event, Emitter } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ColorScheme } from 'vs/platform/theme/common/theme';
import { CSSIcon } from 'vs/base/common/codicons';
import { Codicon, CSSIcon } from 'vs/base/common/codicons';
export const IThemeService = createDecorator<IThemeService>('themeService');
@@ -70,50 +70,13 @@ export namespace ThemeIcon {
return ti1.id === ti2.id && ti1.color?.id === ti2.color?.id;
}
const _regexAsClassName = /^(codicon\/)?([a-z-]+)(~[a-z]+)?$/i;
export function asClassNameArray(icon: ThemeIcon): string[] {
const match = _regexAsClassName.exec(icon.id);
if (!match) {
return ['codicon', 'codicon-error'];
}
let [, , name, modifier] = match;
let className = `codicon-${name}`;
if (modifier) {
return ['codicon', className, modifier.substr(1)];
}
return ['codicon', className];
}
export function asClassName(icon: ThemeIcon): string {
return asClassNameArray(icon).join(' ');
}
export function asCSSSelector(icon: ThemeIcon): string {
return '.' + asClassNameArray(icon).join('.');
}
export function asCSSIcon(icon: ThemeIcon): CSSIcon {
return {
classNames: asClassName(icon)
};
}
export function asCodiconLabel(icon: ThemeIcon): string {
return '$(' + icon.id + ')';
}
export function revive(icon: any): ThemeIcon | undefined {
if (ThemeIcon.isThemeIcon(icon)) {
return { id: icon.id, color: icon.color ? { id: icon.color.id } : undefined };
}
return undefined;
}
export const asClassNameArray: (icon: ThemeIcon) => string[] = CSSIcon.asClassNameArray;
export const asClassName: (icon: ThemeIcon) => string = CSSIcon.asClassName;
export const asCSSSelector: (icon: ThemeIcon) => string = CSSIcon.asCSSSelector;
}
export const FileThemeIcon = { id: 'file' };
export const FolderThemeIcon = { id: 'folder' };
export const FileThemeIcon = Codicon.file;
export const FolderThemeIcon = Codicon.folder;
export function getThemeTypeSelector(type: ColorScheme): string {
switch (type) {

View File

@@ -12,8 +12,11 @@ export class TestColorTheme implements IColorTheme {
public readonly label = 'test';
constructor(private colors: { [id: string]: string; } = {}, public type = ColorScheme.DARK) {
}
constructor(
private colors: { [id: string]: string; } = {},
public type = ColorScheme.DARK,
public readonly semanticHighlighting = false
) { }
getColor(color: string, useDefault?: boolean): Color | undefined {
let value = this.colors[color];
@@ -31,8 +34,6 @@ export class TestColorTheme implements IColorTheme {
return undefined;
}
readonly semanticHighlighting = false;
get tokenColorMap(): string[] {
return [];
}

View File

@@ -18,6 +18,10 @@ export interface IResourceUndoRedoElement {
readonly type: UndoRedoElementType.Resource;
readonly resource: URI;
readonly label: string;
/**
* Show a message to the user confirming when trying to undo this element
*/
readonly confirmBeforeUndo?: boolean;
undo(): Promise<void> | void;
redo(): Promise<void> | void;
}
@@ -26,6 +30,10 @@ export interface IWorkspaceUndoRedoElement {
readonly type: UndoRedoElementType.Workspace;
readonly resources: readonly URI[];
readonly label: string;
/**
* Show a message to the user confirming when trying to undo this element
*/
readonly confirmBeforeUndo?: boolean;
undo(): Promise<void> | void;
redo(): Promise<void> | void;

View File

@@ -27,6 +27,7 @@ class ResourceStackElement {
public readonly type = UndoRedoElementType.Resource;
public readonly actual: IUndoRedoElement;
public readonly label: string;
public readonly confirmBeforeUndo: boolean;
public readonly resourceLabel: string;
public readonly strResource: string;
@@ -41,6 +42,7 @@ class ResourceStackElement {
constructor(actual: IUndoRedoElement, resourceLabel: string, strResource: string, groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) {
this.actual = actual;
this.label = actual.label;
this.confirmBeforeUndo = actual.confirmBeforeUndo || false;
this.resourceLabel = resourceLabel;
this.strResource = strResource;
this.resourceLabels = [this.resourceLabel];
@@ -129,6 +131,7 @@ class WorkspaceStackElement {
public readonly type = UndoRedoElementType.Workspace;
public readonly actual: IWorkspaceUndoRedoElement;
public readonly label: string;
public readonly confirmBeforeUndo: boolean;
public readonly resourceLabels: string[];
public readonly strResources: string[];
@@ -142,6 +145,7 @@ class WorkspaceStackElement {
constructor(actual: IWorkspaceUndoRedoElement, resourceLabels: string[], strResources: string[], groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) {
this.actual = actual;
this.label = actual.label;
this.confirmBeforeUndo = actual.confirmBeforeUndo || false;
this.resourceLabels = resourceLabels;
this.strResources = strResources;
this.groupId = groupId;
@@ -811,7 +815,7 @@ export class UndoRedoService implements IUndoRedoService {
if (element.canSplit()) {
this._splitPastWorkspaceElement(element, ignoreResources);
this._notificationService.info(message);
return new WorkspaceVerificationError(this._undo(strResource));
return new WorkspaceVerificationError(this._undo(strResource, 0, true));
} else {
// Cannot safely split this workspace element => flush all undo/redo stacks
for (const strResource of element.strResources) {
@@ -899,13 +903,13 @@ export class UndoRedoService implements IUndoRedoService {
return null;
}
private _workspaceUndo(strResource: string, element: WorkspaceStackElement): Promise<void> | void {
private _workspaceUndo(strResource: string, element: WorkspaceStackElement, undoConfirmed: boolean): Promise<void> | void {
const affectedEditStacks = this._getAffectedEditStacks(element);
const verificationError = this._checkWorkspaceUndo(strResource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false);
if (verificationError) {
return verificationError.returnValue;
}
return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks);
return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks, undoConfirmed);
}
private _isPartOfUndoGroup(element: WorkspaceStackElement): boolean {
@@ -933,7 +937,7 @@ export class UndoRedoService implements IUndoRedoService {
return false;
}
private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot): Promise<void> {
private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, undoConfirmed: boolean): Promise<void> {
if (element.canSplit() && !this._isPartOfUndoGroup(element)) {
// this element can be split
@@ -959,7 +963,7 @@ export class UndoRedoService implements IUndoRedoService {
if (result.choice === 1) {
// choice: undo this file
this._splitPastWorkspaceElement(element, null);
return this._undo(strResource);
return this._undo(strResource, 0, true);
}
// choice: undo in all files
@@ -969,6 +973,8 @@ export class UndoRedoService implements IUndoRedoService {
if (verificationError1) {
return verificationError1.returnValue;
}
undoConfirmed = true;
}
// prepare
@@ -989,10 +995,10 @@ export class UndoRedoService implements IUndoRedoService {
for (const editStack of editStackSnapshot.editStacks) {
editStack.moveBackward(element);
}
return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId));
return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed));
}
private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise<void> | void {
private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement, undoConfirmed: boolean): Promise<void> | void {
if (!element.isValid) {
// invalid element => immediately flush edit stack!
editStack.flushAllElements();
@@ -1008,7 +1014,7 @@ export class UndoRedoService implements IUndoRedoService {
}
return this._invokeResourcePrepare(element, (cleanup) => {
editStack.moveBackward(element);
return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId));
return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup, () => this._continueUndoInGroup(element.groupId, undoConfirmed));
});
}
@@ -1037,29 +1043,29 @@ export class UndoRedoService implements IUndoRedoService {
return [matchedElement, matchedStrResource];
}
private _continueUndoInGroup(groupId: number): Promise<void> | void {
private _continueUndoInGroup(groupId: number, undoConfirmed: boolean): Promise<void> | void {
if (!groupId) {
return;
}
const [, matchedStrResource] = this._findClosestUndoElementInGroup(groupId);
if (matchedStrResource) {
return this._undo(matchedStrResource);
return this._undo(matchedStrResource, 0, undoConfirmed);
}
}
public undo(resourceOrSource: URI | UndoRedoSource): Promise<void> | void {
if (resourceOrSource instanceof UndoRedoSource) {
const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id);
return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id) : undefined;
return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id, false) : undefined;
}
if (typeof resourceOrSource === 'string') {
return this._undo(resourceOrSource);
return this._undo(resourceOrSource, 0, false);
}
return this._undo(this.getUriComparisonKey(resourceOrSource));
return this._undo(this.getUriComparisonKey(resourceOrSource), 0, false);
}
private _undo(strResource: string, sourceId: number = 0): Promise<void> | void {
private _undo(strResource: string, sourceId: number = 0, undoConfirmed: boolean): Promise<void> | void {
if (!this._editStacks.has(strResource)) {
return;
}
@@ -1075,20 +1081,21 @@ export class UndoRedoService implements IUndoRedoService {
const [matchedElement, matchedStrResource] = this._findClosestUndoElementInGroup(element.groupId);
if (element !== matchedElement && matchedStrResource) {
// there is an element in the same group that should be undone before this one
return this._undo(matchedStrResource);
return this._undo(matchedStrResource, sourceId, undoConfirmed);
}
}
if (element.sourceId !== sourceId) {
// Hit a different source, prompt for confirmation
return this._confirmDifferentSourceAndContinueUndo(strResource, element);
const shouldPromptForConfirmation = (element.sourceId !== sourceId || element.confirmBeforeUndo);
if (shouldPromptForConfirmation && !undoConfirmed) {
// Hit a different source or the element asks for prompt before undo, prompt for confirmation
return this._confirmAndContinueUndo(strResource, sourceId, element);
}
try {
if (element.type === UndoRedoElementType.Workspace) {
return this._workspaceUndo(strResource, element);
return this._workspaceUndo(strResource, element, undoConfirmed);
} else {
return this._resourceUndo(editStack, element);
return this._resourceUndo(editStack, element, undoConfirmed);
}
} finally {
if (DEBUG) {
@@ -1097,7 +1104,7 @@ export class UndoRedoService implements IUndoRedoService {
}
}
private async _confirmDifferentSourceAndContinueUndo(strResource: string, element: StackElement): Promise<void> {
private async _confirmAndContinueUndo(strResource: string, sourceId: number, element: StackElement): Promise<void> {
const result = await this._dialogService.show(
Severity.Info,
nls.localize('confirmDifferentSource', "Would you like to undo '{0}'?", element.label),
@@ -1116,7 +1123,7 @@ export class UndoRedoService implements IUndoRedoService {
}
// choice: undo
return this._undo(strResource, element.sourceId);
return this._undo(strResource, sourceId, true);
}
private _findClosestRedoElementWithSource(sourceId: number): [StackElement | null, string | null] {

View File

@@ -23,9 +23,9 @@ suite('UndoRedoService', () => {
const resource = URI.file('test.txt');
const service = createUndoRedoService();
assert.equal(service.canUndo(resource), false);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), false);
assert.strictEqual(service.canUndo(resource), false);
assert.strictEqual(service.canRedo(resource), false);
assert.strictEqual(service.hasElements(resource), false);
assert.ok(service.getLastElement(resource) === null);
let undoCall1 = 0;
@@ -39,27 +39,27 @@ suite('UndoRedoService', () => {
};
service.pushElement(element1);
assert.equal(undoCall1, 0);
assert.equal(redoCall1, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.strictEqual(undoCall1, 0);
assert.strictEqual(redoCall1, 0);
assert.strictEqual(service.canUndo(resource), true);
assert.strictEqual(service.canRedo(resource), false);
assert.strictEqual(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element1);
service.undo(resource);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 0);
assert.equal(service.canUndo(resource), false);
assert.equal(service.canRedo(resource), true);
assert.equal(service.hasElements(resource), true);
assert.strictEqual(undoCall1, 1);
assert.strictEqual(redoCall1, 0);
assert.strictEqual(service.canUndo(resource), false);
assert.strictEqual(service.canRedo(resource), true);
assert.strictEqual(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === null);
service.redo(resource);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.strictEqual(undoCall1, 1);
assert.strictEqual(redoCall1, 1);
assert.strictEqual(service.canUndo(resource), true);
assert.strictEqual(service.canRedo(resource), false);
assert.strictEqual(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element1);
let undoCall2 = 0;
@@ -73,24 +73,24 @@ suite('UndoRedoService', () => {
};
service.pushElement(element2);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 0);
assert.equal(redoCall2, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.strictEqual(undoCall1, 1);
assert.strictEqual(redoCall1, 1);
assert.strictEqual(undoCall2, 0);
assert.strictEqual(redoCall2, 0);
assert.strictEqual(service.canUndo(resource), true);
assert.strictEqual(service.canRedo(resource), false);
assert.strictEqual(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element2);
service.undo(resource);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 1);
assert.equal(redoCall2, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), true);
assert.equal(service.hasElements(resource), true);
assert.strictEqual(undoCall1, 1);
assert.strictEqual(redoCall1, 1);
assert.strictEqual(undoCall2, 1);
assert.strictEqual(redoCall2, 0);
assert.strictEqual(service.canUndo(resource), true);
assert.strictEqual(service.canRedo(resource), true);
assert.strictEqual(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === null);
let undoCall3 = 0;
@@ -104,28 +104,28 @@ suite('UndoRedoService', () => {
};
service.pushElement(element3);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 1);
assert.equal(redoCall2, 0);
assert.equal(undoCall3, 0);
assert.equal(redoCall3, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), false);
assert.equal(service.hasElements(resource), true);
assert.strictEqual(undoCall1, 1);
assert.strictEqual(redoCall1, 1);
assert.strictEqual(undoCall2, 1);
assert.strictEqual(redoCall2, 0);
assert.strictEqual(undoCall3, 0);
assert.strictEqual(redoCall3, 0);
assert.strictEqual(service.canUndo(resource), true);
assert.strictEqual(service.canRedo(resource), false);
assert.strictEqual(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === element3);
service.undo(resource);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall2, 1);
assert.equal(redoCall2, 0);
assert.equal(undoCall3, 1);
assert.equal(redoCall3, 0);
assert.equal(service.canUndo(resource), true);
assert.equal(service.canRedo(resource), true);
assert.equal(service.hasElements(resource), true);
assert.strictEqual(undoCall1, 1);
assert.strictEqual(redoCall1, 1);
assert.strictEqual(undoCall2, 1);
assert.strictEqual(redoCall2, 0);
assert.strictEqual(undoCall3, 1);
assert.strictEqual(redoCall3, 0);
assert.strictEqual(service.canUndo(resource), true);
assert.strictEqual(service.canRedo(resource), true);
assert.strictEqual(service.hasElements(resource), true);
assert.ok(service.getLastElement(resource) === null);
});
@@ -169,50 +169,50 @@ suite('UndoRedoService', () => {
};
service.pushElement(element1);
assert.equal(service.canUndo(resource1), true);
assert.equal(service.canRedo(resource1), false);
assert.equal(service.hasElements(resource1), true);
assert.strictEqual(service.canUndo(resource1), true);
assert.strictEqual(service.canRedo(resource1), false);
assert.strictEqual(service.hasElements(resource1), true);
assert.ok(service.getLastElement(resource1) === element1);
assert.equal(service.canUndo(resource2), true);
assert.equal(service.canRedo(resource2), false);
assert.equal(service.hasElements(resource2), true);
assert.strictEqual(service.canUndo(resource2), true);
assert.strictEqual(service.canRedo(resource2), false);
assert.strictEqual(service.hasElements(resource2), true);
assert.ok(service.getLastElement(resource2) === element1);
await service.undo(resource1);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 0);
assert.equal(service.canUndo(resource1), false);
assert.equal(service.canRedo(resource1), true);
assert.equal(service.hasElements(resource1), true);
assert.strictEqual(undoCall1, 1);
assert.strictEqual(redoCall1, 0);
assert.strictEqual(service.canUndo(resource1), false);
assert.strictEqual(service.canRedo(resource1), true);
assert.strictEqual(service.hasElements(resource1), true);
assert.ok(service.getLastElement(resource1) === null);
assert.equal(service.canUndo(resource2), false);
assert.equal(service.canRedo(resource2), true);
assert.equal(service.hasElements(resource2), true);
assert.strictEqual(service.canUndo(resource2), false);
assert.strictEqual(service.canRedo(resource2), true);
assert.strictEqual(service.hasElements(resource2), true);
assert.ok(service.getLastElement(resource2) === null);
await service.redo(resource2);
assert.equal(undoCall1, 1);
assert.equal(redoCall1, 1);
assert.equal(undoCall11, 0);
assert.equal(redoCall11, 0);
assert.equal(undoCall12, 0);
assert.equal(redoCall12, 0);
assert.equal(service.canUndo(resource1), true);
assert.equal(service.canRedo(resource1), false);
assert.equal(service.hasElements(resource1), true);
assert.strictEqual(undoCall1, 1);
assert.strictEqual(redoCall1, 1);
assert.strictEqual(undoCall11, 0);
assert.strictEqual(redoCall11, 0);
assert.strictEqual(undoCall12, 0);
assert.strictEqual(redoCall12, 0);
assert.strictEqual(service.canUndo(resource1), true);
assert.strictEqual(service.canRedo(resource1), false);
assert.strictEqual(service.hasElements(resource1), true);
assert.ok(service.getLastElement(resource1) === element1);
assert.equal(service.canUndo(resource2), true);
assert.equal(service.canRedo(resource2), false);
assert.equal(service.hasElements(resource2), true);
assert.strictEqual(service.canUndo(resource2), true);
assert.strictEqual(service.canRedo(resource2), false);
assert.strictEqual(service.hasElements(resource2), true);
assert.ok(service.getLastElement(resource2) === element1);
});
test('UndoRedoGroup.None uses id 0', () => {
assert.equal(UndoRedoGroup.None.id, 0);
assert.equal(UndoRedoGroup.None.nextOrder(), 0);
assert.equal(UndoRedoGroup.None.nextOrder(), 0);
assert.strictEqual(UndoRedoGroup.None.id, 0);
assert.strictEqual(UndoRedoGroup.None.nextOrder(), 0);
assert.strictEqual(UndoRedoGroup.None.nextOrder(), 0);
});
});

View File

@@ -15,6 +15,7 @@ import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/e
import { ILogService } from 'vs/platform/log/common/log';
import { AbstractUpdateService, createUpdateURL, UpdateNotAvailableClassification } from 'vs/platform/update/electron-main/abstractUpdateService';
import { IRequestService } from 'vs/platform/request/common/request';
import product from 'vs/platform/product/common/product';
export class DarwinUpdateService extends AbstractUpdateService {
@@ -56,7 +57,12 @@ export class DarwinUpdateService extends AbstractUpdateService {
}
protected buildUpdateFeedUrl(quality: string): string | undefined {
const assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
let assetID: string;
if (!product.darwinUniversalAssetId) {
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
} else {
assetID = product.darwinUniversalAssetId;
}
const url = createUpdateURL(assetID, quality);
try {
electron.autoUpdater.setFeedURL({ url });

View File

@@ -56,7 +56,7 @@ export class Win32UpdateService extends AbstractUpdateService {
@memoize
get cachePath(): Promise<string> {
const result = path.join(tmpdir(), `vscode-update-${product.target}-${process.arch}`);
return pfs.mkdirp(result, undefined).then(() => result);
return pfs.mkdirp(result).then(() => result);
}
constructor(

View File

@@ -5,6 +5,7 @@
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
@@ -32,7 +33,7 @@ export class ExtensionsStorageSyncService extends Disposable implements IExtensi
declare readonly _serviceBrand: undefined;
private static toKey(extension: IExtensionIdWithVersion): string {
return `extensionKeys/${extension.id}@${extension.version}`;
return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`;
}
private static fromKey(key: string): IExtensionIdWithVersion | undefined {

View File

@@ -11,7 +11,7 @@ import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { areSameExtensions, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IFileService } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
@@ -25,7 +25,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag
import { CancellationToken } from 'vs/base/common/cancellation';
import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions';
import { getErrorMessage } from 'vs/base/common/errors';
import { forEach, IStringDictionary } from 'vs/base/common/collections';
import { IStringDictionary } from 'vs/base/common/collections';
import { IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync';
interface IExtensionResourceMergeResult extends IAcceptResult {
@@ -73,6 +73,15 @@ async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagemen
return extensions;
}
function getExtensionStorageState(publisher: string, name: string, storageService: IStorageService): IStringDictionary<any> {
const extensionStorageValue = storageService.get(getExtensionId(publisher, name) /* use the same id used in extension host */, StorageScope.GLOBAL) || '{}';
return JSON.parse(extensionStorageValue);
}
function storeExtensionStorageState(publisher: string, name: string, extensionState: IStringDictionary<any>, storageService: IStorageService): void {
storageService.store(getExtensionId(publisher, name) /* use the same id used in extension host */, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` });
@@ -99,7 +108,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
@IIgnoredExtensionsManagementService private readonly extensionSyncManagementService: IIgnoredExtensionsManagementService,
@IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IConfigurationService configurationService: IConfigurationService,
@@ -125,7 +134,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
if (remoteExtensions) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`);
@@ -209,7 +218,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
return {
@@ -225,7 +234,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
if (remoteExtensions !== null) {
const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions);
@@ -285,7 +294,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
async resolveContent(uri: URI): Promise<string | null> {
if (this.extUri.isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) {
const installedExtensions = await this.extensionManagementService.getInstalled();
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
const localExtensions = this.getLocalExtensions(installedExtensions).filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier)));
return this.format(localExtensions);
}
@@ -363,7 +372,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
// Builtin Extension Sync: Enablement & State
if (installedExtension && installedExtension.isBuiltin) {
if (e.state && installedExtension.manifest.version === e.version) {
this.updateExtensionState(e.state, e.identifier.id, installedExtension.manifest.version);
this.updateExtensionState(e.state, installedExtension.manifest.publisher, installedExtension.manifest.name, installedExtension.manifest.version);
}
if (e.disabled) {
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id);
@@ -382,14 +391,16 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier);
/* Update extension state only if
* extension is installed and version is same as synced version or
* extension is not installed and installable
* extension is installed and version is same as synced version or
* extension is not installed and installable
*/
if (e.state &&
(installedExtension ? installedExtension.manifest.version === e.version /* Installed and has same version */
: !!extension /* Installable */)
) {
this.updateExtensionState(e.state, e.identifier.id, installedExtension?.manifest.version);
const publisher = installedExtension ? installedExtension.manifest.publisher : extension!.publisher;
const name = installedExtension ? installedExtension.manifest.name : extension!.name;
this.updateExtensionState(e.state, publisher, name, installedExtension?.manifest.version);
}
if (extension) {
@@ -436,15 +447,15 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return newSkippedExtensions;
}
private updateExtensionState(state: IStringDictionary<any>, id: string, version?: string): void {
const extensionState = JSON.parse(this.storageService.get(id, StorageScope.GLOBAL) || '{}');
const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id, version }) : undefined;
private updateExtensionState(state: IStringDictionary<any>, publisher: string, name: string, version: string | undefined): void {
const extensionState = getExtensionStorageState(publisher, name, this.storageService);
const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id: getGalleryExtensionId(publisher, name), version }) : undefined;
if (keys) {
keys.forEach(key => extensionState[key] = state[key]);
keys.forEach(key => { extensionState[key] = state[key]; });
} else {
forEach(state, ({ key, value }) => extensionState[key] = value);
Object.keys(state).forEach(key => extensionState[key] = state[key]);
}
this.storageService.store(id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE);
storeExtensionStorageState(publisher, name, extensionState, this.storageService);
}
private parseExtensions(syncData: ISyncData): ISyncExtension[] {
@@ -465,8 +476,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
try {
const keys = this.extensionsStorageSyncService.getKeysForSync({ id: identifier.id, version: manifest.version });
if (keys) {
const extensionStorageValue = this.storageService.get(identifier.id, StorageScope.GLOBAL) || '{}';
const extensionStorageState = JSON.parse(extensionStorageValue);
const extensionStorageState = getExtensionStorageState(manifest.publisher, manifest.name, this.storageService);
syncExntesion.state = Object.keys(extensionStorageState).reduce((state: IStringDictionary<any>, key) => {
if (keys.includes(key)) {
state[key] = extensionStorageState[key];
@@ -490,6 +500,7 @@ export class ExtensionsInitializer extends AbstractInitializer {
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
@IStorageService private readonly storageService: IStorageService,
@IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@@ -511,12 +522,18 @@ export class ExtensionsInitializer extends AbstractInitializer {
const newlyEnabledExtensions: ILocalExtension[] = [];
const installedExtensions = await this.extensionManagementService.getInstalled();
const newExtensionsToSync = new Map<string, ISyncExtension>();
const installedExtensionsToSync: ISyncExtension[] = [];
const installedExtensionsToSync: { syncExtension: ISyncExtension, installedExtension: ILocalExtension }[] = [];
const toInstall: { names: string[], uuids: string[] } = { names: [], uuids: [] };
const toDisable: IExtensionIdentifier[] = [];
for (const extension of remoteExtensions) {
if (installedExtensions.some(i => areSameExtensions(i.identifier, extension.identifier))) {
installedExtensionsToSync.push(extension);
if (this.ignoredExtensionsManagementService.hasToNeverSyncExtension(extension.identifier.id)) {
// Skip extension ignored to sync
continue;
}
const installedExtension = installedExtensions.find(i => areSameExtensions(i.identifier, extension.identifier));
if (installedExtension) {
installedExtensionsToSync.push({ syncExtension: extension, installedExtension });
if (extension.disabled) {
toDisable.push(extension.identifier);
}
@@ -528,17 +545,39 @@ export class ExtensionsInitializer extends AbstractInitializer {
} else {
toInstall.names.push(extension.identifier.id);
}
if (extension.disabled) {
toDisable.push(extension.identifier);
}
}
}
}
// 1. Initialise already installed extensions state
for (const { syncExtension, installedExtension } of installedExtensionsToSync) {
if (syncExtension.state) {
const extensionState = getExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, this.storageService);
Object.keys(syncExtension.state).forEach(key => extensionState[key] = syncExtension.state![key]);
storeExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, extensionState, this.storageService);
}
}
// 2. Initialise extensions enablement
if (toDisable.length) {
for (const identifier of toDisable) {
this.logService.trace(`Disabling extension...`, identifier.id);
await this.extensionEnablementService.disableExtension(identifier);
this.logService.info(`Disabling extension`, identifier.id);
}
}
// 3. Install extensions
if (toInstall.names.length || toInstall.uuids.length) {
const galleryExtensions = (await this.galleryService.query({ ids: toInstall.uuids, names: toInstall.names, pageSize: toInstall.uuids.length + toInstall.names.length }, CancellationToken.None)).firstPage;
for (const galleryExtension of galleryExtensions) {
try {
const extensionToSync = newExtensionsToSync.get(galleryExtension.identifier.id.toLowerCase())!;
if (extensionToSync.state) {
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionToSync.state), StorageScope.GLOBAL, StorageTarget.MACHINE);
storeExtensionStorageState(galleryExtension.publisher, galleryExtension.name, extensionToSync.state, this.storageService);
}
this.logService.trace(`Installing extension...`, galleryExtension.identifier.id);
const local = await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false } /* pass options to prevent install and sync dialog in web */);
@@ -552,22 +591,6 @@ export class ExtensionsInitializer extends AbstractInitializer {
}
}
if (toDisable.length) {
for (const identifier of toDisable) {
this.logService.trace(`Enabling extension...`, identifier.id);
await this.extensionEnablementService.disableExtension(identifier);
this.logService.info(`Enabled extension`, identifier.id);
}
}
for (const extensionToSync of installedExtensionsToSync) {
if (extensionToSync.state) {
const extensionState = JSON.parse(this.storageService.get(extensionToSync.identifier.id, StorageScope.GLOBAL) || '{}');
forEach(extensionToSync.state, ({ key, value }) => extensionState[key] = value);
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
}
return newlyEnabledExtensions;
}

View File

@@ -31,9 +31,10 @@ export function getDisallowedIgnoredSettings(): string[] {
export function getDefaultIgnoredSettings(): string[] {
const allSettings = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
const ignoreSyncSettings = Object.keys(allSettings).filter(setting => !!allSettings[setting].ignoreSync);
const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE);
const disallowedSettings = getDisallowedIgnoredSettings();
return distinct([CONFIGURATION_SYNC_STORE_KEY, ...machineSettings, ...disallowedSettings]);
return distinct([CONFIGURATION_SYNC_STORE_KEY, ...ignoreSyncSettings, ...machineSettings, ...disallowedSettings]);
}
export function registerConfiguration(): IDisposable {
@@ -52,10 +53,6 @@ export function registerConfiguration(): IDisposable {
scope: ConfigurationScope.APPLICATION,
tags: ['sync', 'usesOnlineServices']
},
'sync.keybindingsPerPlatform': {
type: 'boolean',
deprecationMessage: localize('sync.keybindingsPerPlatform.deprecated', "Deprecated, use settingsSync.keybindingsPerPlatform instead"),
},
'settingsSync.ignoredExtensions': {
'type': 'array',
markdownDescription: localize('settingsSync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always `${publisher}.${name}`. For example: `vscode.csharp`."),
@@ -70,10 +67,6 @@ export function registerConfiguration(): IDisposable {
disallowSyncIgnore: true,
tags: ['sync', 'usesOnlineServices']
},
'sync.ignoredExtensions': {
'type': 'array',
deprecationMessage: localize('sync.ignoredExtensions.deprecated', "Deprecated, use settingsSync.ignoredExtensions instead"),
},
'settingsSync.ignoredSettings': {
'type': 'array',
description: localize('settingsSync.ignoredSettings', "Configure settings to be ignored while synchronizing."),
@@ -84,10 +77,6 @@ export function registerConfiguration(): IDisposable {
uniqueItems: true,
disallowSyncIgnore: true,
tags: ['sync', 'usesOnlineServices']
},
'sync.ignoredSettings': {
'type': 'array',
deprecationMessage: localize('sync.ignoredSettings.deprecated', "Deprecated, use settingsSync.ignoredSettings instead"),
}
}
});

Some files were not shown because too many files have changed in this diff Show More