mirror of
https://github.com/coder/code-server.git
synced 2026-05-05 20:15:19 +02:00
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:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
|
||||
@@ -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']
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,7 @@ export interface IDialogOptions {
|
||||
cancelId?: number;
|
||||
detail?: string;
|
||||
checkbox?: ICheckbox;
|
||||
useCustom?: boolean;
|
||||
}
|
||||
|
||||
export interface IInput {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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[][] {
|
||||
|
||||
@@ -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 */));
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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): '/' | '\\';
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface ILocalizationsService {
|
||||
|
||||
readonly onDidLanguagesChange: Event<void>;
|
||||
getLanguageIds(): Promise<string[]>;
|
||||
|
||||
update(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export function isValidLocalization(localization: ILocalization): boolean {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user