Merge from vscode a348d103d1256a06a2c9b3f9b406298a9fef6898 (#15681)

* Merge from vscode a348d103d1256a06a2c9b3f9b406298a9fef6898

* Fixes and cleanup

* Distro

* Fix hygiene yarn

* delete no yarn lock changes file

* Fix hygiene

* Fix layer check

* Fix CI

* Skip lib checks

* Remove tests deleted in vs code

* Fix tests

* Distro

* Fix tests and add removed extension point

* Skip failing notebook tests for now

* Disable broken tests and cleanup build folder

* Update yarn.lock and fix smoke tests

* Bump sqlite

* fix contributed actions and file spacing

* Fix user data path

* Update yarn.locks

Co-authored-by: ADS Merger <karlb@microsoft.com>
This commit is contained in:
Charles Gagnon
2021-06-17 08:17:11 -07:00
committed by GitHub
parent fdcb97c7f7
commit 3cb2f552a6
2582 changed files with 124827 additions and 87099 deletions

View File

@@ -3,9 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-action-bar .action-item.menu-entry .action-label.icon {
width: 16px;
height: 16px;
background-repeat: no-repeat;
background-position: 50%;
}
.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,

View File

@@ -6,31 +6,32 @@
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 { IAction, Separator, SubmenuAction } from 'vs/base/common/actions';
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';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { UILabelProvider } from 'vs/base/common/keybindingLabels';
import { INotificationService } from 'vs/platform/notification/common/notification';
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 { isWindows, isLinux, OS } 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 {
export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string): IDisposable {
const groups = menu.getActions(options);
const modifierKeyEmitter = ModifierKeyEmitter.getInstance();
const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey);
fillInActions(groups, target, useAlternativeActions, isPrimaryGroup);
fillInActions(groups, target, useAlternativeActions, primaryGroup);
return asDisposable(groups);
}
export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean, primaryMaxCount?: number): IDisposable {
export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string, primaryMaxCount?: number, shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean): IDisposable {
const groups = menu.getActions(options);
// Action bars handle alternative actions on their own so the alternative actions should be ignored
fillInActions(groups, target, false, isPrimaryGroup, primaryMaxCount);
fillInActions(groups, target, false, primaryGroup, primaryMaxCount, shouldInlineSubmenu);
return asDisposable(groups);
}
@@ -44,7 +45,13 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemActio
return disposables;
}
export function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemAction | SubmenuItemAction>]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation', primaryMaxCount: number = Number.MAX_SAFE_INTEGER): void { // {{SQL CARBON EDIT}} add export modifier
export function fillInActions( // {{SQL CARBON EDIT}} add export modifier
groups: ReadonlyArray<[string, ReadonlyArray<MenuItemAction | SubmenuItemAction>]>, target: IAction[] | { primary: IAction[]; secondary: IAction[]; },
useAlternativeActions: boolean,
primaryGroup = 'navigation',
primaryMaxCount: number = Number.MAX_SAFE_INTEGER,
shouldInlineSubmenu: (action: SubmenuAction, group: string, groupSize: number) => boolean = () => false
): void {
let primaryBucket: IAction[];
let secondaryBucket: IAction[];
@@ -56,18 +63,42 @@ export function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray<MenuI
secondaryBucket = target.secondary;
}
for (let [group, actions] of groups) {
if (useAlternativeActions) {
actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a);
const submenuInfo = new Set<{ group: string, action: SubmenuAction, index: number }>();
for (const [group, actions] of groups) {
let target: IAction[];
if (group === primaryGroup) {
target = primaryBucket;
} else {
target = secondaryBucket;
if (target.length > 0) {
target.push(new Separator());
}
}
if (isPrimaryGroup(group)) {
primaryBucket.unshift(...actions);
} else {
if (secondaryBucket.length > 0) {
secondaryBucket.push(new Separator());
for (let action of actions) {
if (useAlternativeActions) {
action = action instanceof MenuItemAction && action.alt ? action.alt : action;
}
secondaryBucket.push(...actions);
const newLen = target.push(action);
// keep submenu info for later inlining
if (action instanceof SubmenuAction) {
submenuInfo.add({ group, action, index: newLen - 1 });
}
}
}
// ask the outside if submenu should be inlined or not. only ask when
// there would be enough space
for (const { group, action, index } of submenuInfo) {
const target = group === primaryGroup ? primaryBucket : secondaryBucket;
// inlining submenus with length 0 or 1 is easy,
// larger submenus need to be checked with the overall limit
const submenuActions = action.actions;
if ((submenuActions.length <= 1 || target.length + submenuActions.length - 2 <= primaryMaxCount) && shouldInlineSubmenu(action, group, target.length)) {
target.splice(index, 1, ...submenuActions);
}
}
@@ -85,7 +116,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
private readonly _altKey: ModifierKeyEmitter;
constructor(
readonly _action: MenuItemAction,
_action: MenuItemAction,
@IKeybindingService protected readonly _keybindingService: IKeybindingService,
@INotificationService protected _notificationService: INotificationService
) {
@@ -93,11 +124,15 @@ export class MenuEntryActionViewItem extends ActionViewItem {
this._altKey = ModifierKeyEmitter.getInstance();
}
protected get _commandAction(): MenuItemAction {
return this._wantsAltCommand && (<MenuItemAction>this._action).alt || this._action;
protected get _menuItemAction(): MenuItemAction {
return <MenuItemAction>this._action;
}
onClick(event: MouseEvent): void {
protected get _commandAction(): MenuItemAction {
return this._wantsAltCommand && this._menuItemAction.alt || this._menuItemAction;
}
override onClick(event: MouseEvent): void {
event.preventDefault();
event.stopPropagation();
@@ -106,11 +141,11 @@ export class MenuEntryActionViewItem extends ActionViewItem {
.catch(err => this._notificationService.error(err));
}
render(container: HTMLElement): void {
override render(container: HTMLElement): void {
super.render(container);
container.classList.add('menu-entry');
this._updateItemClass(this._action.item);
this._updateItemClass(this._menuItemAction.item);
let mouseOver = false;
@@ -126,7 +161,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
}
};
if (this._action.alt) {
if (this._menuItemAction.alt) {
this._register(this._altKey.event(value => {
alternativeKeyDown = value.altKey || ((isWindows || isLinux) && value.shiftKey);
updateAltState();
@@ -144,32 +179,42 @@ export class MenuEntryActionViewItem extends ActionViewItem {
}));
}
updateLabel(): void {
override updateLabel(): void {
if (this.options.label && this.label) {
this.label.textContent = this._commandAction.label;
}
}
updateTooltip(): void {
override updateTooltip(): void {
if (this.label) {
const keybinding = this._keybindingService.lookupKeybinding(this._commandAction.id);
const keybindingLabel = keybinding && keybinding.getLabel();
const tooltip = this._commandAction.tooltip || this._commandAction.label;
this.label.title = keybindingLabel
let title = keybindingLabel
? localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel)
: tooltip;
if (!this._wantsAltCommand && this._menuItemAction.alt) {
const altTooltip = this._menuItemAction.alt.tooltip || this._menuItemAction.alt.label;
const altKeybinding = this._keybindingService.lookupKeybinding(this._menuItemAction.alt.id);
const altKeybindingLabel = altKeybinding && altKeybinding.getLabel();
const altTitleSection = altKeybindingLabel
? localize('titleAndKb', "{0} ({1})", altTooltip, altKeybindingLabel)
: altTooltip;
title += `\n[${UILabelProvider.modifierLabels[OS].altKey}] ${altTitleSection}`;
}
this.label.title = title;
}
}
updateClass(): void {
override updateClass(): void {
if (this.options.icon) {
if (this._commandAction !== this._action) {
if (this._action.alt) {
this._updateItemClass(this._action.alt.item);
if (this._commandAction !== this._menuItemAction) {
if (this._menuItemAction.alt) {
this._updateItemClass(this._menuItemAction.alt.item);
}
} else if ((<MenuItemAction>this._action).alt) {
this._updateItemClass(this._action.item);
} else if (this._menuItemAction.alt) {
this._updateItemClass(this._menuItemAction.item);
}
}
}
@@ -190,10 +235,10 @@ export class MenuEntryActionViewItem extends ActionViewItem {
if (ThemeIcon.isThemeIcon(icon)) {
// theme icons
const iconClass = ThemeIcon.asClassName(icon);
label.classList.add(...iconClass.split(' '));
const iconClasses = ThemeIcon.asClassNameArray(icon);
label.classList.add(...iconClasses);
this._itemClassDispose.value = toDisposable(() => {
label.classList.remove(...iconClass.split(' '));
label.classList.remove(...iconClasses);
});
} else {
@@ -226,7 +271,7 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem {
});
}
render(container: HTMLElement): void {
override render(container: HTMLElement): void {
super.render(container);
if (this.element) {
container.classList.add('menu-entry');

View File

@@ -45,7 +45,7 @@ export interface ICommandAction {
tooltip?: string;
icon?: Icon;
precondition?: ContextKeyExpression;
toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string; };
toggled?: ContextKeyExpression | { condition: ContextKeyExpression, icon?: Icon, tooltip?: string, title?: string | ILocalizedString };
}
export type ISerializableCommandAction = UriDto<ICommandAction>;
@@ -87,8 +87,10 @@ export class MenuId {
static readonly DebugWatchContext = new MenuId('DebugWatchContext');
static readonly DebugToolBar = new MenuId('DebugToolBar');
static readonly EditorContext = new MenuId('EditorContext');
static readonly EditorContextCopy = new MenuId('EditorContextCopy');
static readonly EditorContextPeek = new MenuId('EditorContextPeek');
static readonly EditorTitle = new MenuId('EditorTitle');
static readonly EditorTitleRun = new MenuId('EditorTitleRun');
static readonly EditorTitleContext = new MenuId('EditorTitleContext');
static readonly EmptyEditorGroupContext = new MenuId('EmptyEditorGroupContext');
static readonly ExplorerContext = new MenuId('ExplorerContext');
@@ -97,6 +99,7 @@ export class MenuId {
static readonly MenubarAppearanceMenu = new MenuId('MenubarAppearanceMenu');
static readonly MenubarDebugMenu = new MenuId('MenubarDebugMenu');
static readonly MenubarEditMenu = new MenuId('MenubarEditMenu');
static readonly MenubarCopy = new MenuId('MenubarCopy');
static readonly MenubarFileMenu = new MenuId('MenubarFileMenu');
static readonly MenubarGoMenu = new MenuId('MenubarGoMenu');
static readonly MenubarHelpMenu = new MenuId('MenubarHelpMenu');
@@ -120,11 +123,15 @@ export class MenuId {
static readonly SCMTitle = new MenuId('SCMTitle');
static readonly SearchContext = new MenuId('SearchContext');
static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu');
static readonly StatusBarRemoteIndicatorMenu = new MenuId('StatusBarRemoteIndicatorMenu');
static readonly TestItem = new MenuId('TestItem');
static readonly TouchBarContext = new MenuId('TouchBarContext');
static readonly TitleBarContext = new MenuId('TitleBarContext');
static readonly TunnelContext = new MenuId('TunnelContext');
static readonly TunnelInline = new MenuId('TunnelInline');
static readonly TunnelPortInline = new MenuId('TunnelInline');
static readonly TunnelTitle = new MenuId('TunnelTitle');
static readonly TunnelLocalAddressInline = new MenuId('TunnelLocalAddressInline');
static readonly TunnelOriginInline = new MenuId('TunnelOriginInline');
static readonly ViewItemContext = new MenuId('ViewItemContext');
static readonly ViewContainerTitle = new MenuId('ViewContainerTitle');
static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext');
@@ -134,10 +141,12 @@ export class MenuId {
static readonly CommentThreadActions = new MenuId('CommentThreadActions');
static readonly CommentTitle = new MenuId('CommentTitle');
static readonly CommentActions = new MenuId('CommentActions');
// static readonly NotebookToolbar = new MenuId('NotebookToolbar'); {{SQL CARBON EDIT}} We have our own toolbar
static readonly NotebookCellTitle = new MenuId('NotebookCellTitle');
static readonly NotebookCellInsert = new MenuId('NotebookCellInsert');
static readonly NotebookCellBetween = new MenuId('NotebookCellBetween');
static readonly NotebookCellListTop = new MenuId('NotebookCellTop');
static readonly NotebookCellExecute = new MenuId('NotebookCellExecute');
static readonly NotebookDiffCellInputTitle = new MenuId('NotebookDiffCellInputTitle');
static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle');
static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle');
@@ -157,6 +166,12 @@ export class MenuId {
static readonly TimelineTitleContext = new MenuId('TimelineTitleContext');
static readonly AccountsContext = new MenuId('AccountsContext');
static readonly PanelTitle = new MenuId('PanelTitle');
static readonly TerminalContainerContext = new MenuId('TerminalContainerContext');
static readonly TerminalToolbarContext = new MenuId('TerminalToolbarContext');
static readonly TerminalTabsWidgetContext = new MenuId('TerminalTabsWidgetContext');
static readonly TerminalTabsWidgetEmptyContext = new MenuId('TerminalTabsWidgetEmptyContext');
static readonly TerminalSingleTabContext = new MenuId('TerminalSingleTabContext');
static readonly TerminalTabInlineActions = new MenuId('TerminalTabInlineActions');
readonly id: number;
readonly _debugName: string;
@@ -183,7 +198,7 @@ export interface IMenuService {
readonly _serviceBrand: undefined;
createMenu(id: MenuId, scopedKeybindingService: IContextKeyService): IMenu;
createMenu(id: MenuId, contextKeyService: IContextKeyService, emitEventsForSubmenuChanges?: boolean): IMenu;
}
export type ICommandsMap = Map<string, ICommandAction>;
@@ -321,41 +336,37 @@ export class ExecuteCommandAction extends Action {
super(id, label);
}
run(...args: any[]): Promise<any> {
override run(...args: any[]): Promise<any> {
return this._commandService.executeCommand(this.id, ...args);
}
}
export class SubmenuItemAction extends SubmenuAction {
readonly item: ISubmenuItem;
constructor(
item: ISubmenuItem,
menuService: IMenuService,
contextKeyService: IContextKeyService,
options?: IMenuActionOptions
readonly item: ISubmenuItem,
private readonly _menuService: IMenuService,
private readonly _contextKeyService: IContextKeyService,
private readonly _options?: IMenuActionOptions
) {
super(`submenuitem.${item.submenu.id}`, typeof item.title === 'string' ? item.title : item.title.value, [], 'submenu');
}
override get actions(): readonly IAction[] {
const result: IAction[] = [];
const menu = menuService.createMenu(item.submenu, contextKeyService);
const groups = menu.getActions(options);
const menu = this._menuService.createMenu(this.item.submenu, this._contextKeyService);
const groups = menu.getActions(this._options);
menu.dispose();
for (let group of groups) {
const [, actions] = group;
for (const [, actions] of groups) {
if (actions.length > 0) {
result.push(...actions);
result.push(new Separator());
}
}
if (result.length) {
result.pop(); // remove last separator
}
super(`submenuitem.${item.submenu.id}`, typeof item.title === 'string' ? item.title : item.title.value, result, 'submenu');
this.item = item;
return result;
}
}
@@ -391,13 +402,17 @@ export class MenuItemAction implements IAction {
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;
const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as {
condition: ContextKeyExpression, icon?: Icon, tooltip?: string | ILocalizedString, title?: string | ILocalizedString
};
this.checked = contextKeyService.contextMatchesRules(toggled.condition);
if (this.checked && toggled.tooltip) {
this.tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value;
}
if (toggled.title) {
this.label = typeof toggled.title === 'string' ? toggled.title : toggled.title.value;
}
}
this.item = item;

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RunOnceScheduler } from 'vs/base/common/async';
@@ -20,8 +20,14 @@ export class MenuService implements IMenuService {
//
}
createMenu(id: MenuId, contextKeyService: IContextKeyService): IMenu {
return new Menu(id, this._commandService, contextKeyService, this);
/**
* Create a new menu for the given menu identifier. A menu sends events when it's entries
* have changed (placement, enablement, checked-state). By default it does send events for
* sub menu entries. That is more expensive and must be explicitly enabled with the
* `emitEventsForSubmenuChanges` flag.
*/
createMenu(id: MenuId, contextKeyService: IContextKeyService, emitEventsForSubmenuChanges: boolean = false): IMenu {
return new Menu(id, emitEventsForSubmenuChanges, this._commandService, contextKeyService, this);
}
}
@@ -40,6 +46,7 @@ class Menu implements IMenu {
constructor(
private readonly _id: MenuId,
private readonly _fireEventsForSubmenuChanges: boolean,
@ICommandService private readonly _commandService: ICommandService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IMenuService private readonly _menuService: IMenuService
@@ -93,23 +100,33 @@ class Menu implements IMenu {
group![1].push(item);
// keep keys for eventing
Menu._fillInKbExprKeys(item.when, 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: any = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled;
Menu._fillInKbExprKeys(toggledExpression, this._contextKeys);
}
}
this._collectContextKeys(item);
}
this._onDidChange.fire(this);
}
private _collectContextKeys(item: IMenuItem | ISubmenuItem): void {
Menu._fillInKbExprKeys(item.when, 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 as ContextKeyExpression;
Menu._fillInKbExprKeys(toggledExpression, this._contextKeys);
}
} else if (this._fireEventsForSubmenuChanges) {
// recursively collect context keys from submenus so that this
// menu fires events when context key changes affect submenus
MenuRegistry.getMenuItems(item.submenu).forEach(this._collectContextKeys, this);
}
}
getActions(options?: IMenuActionOptions): [string, Array<MenuItemAction | SubmenuItemAction>][] {
const result: [string, Array<MenuItemAction | SubmenuItemAction>][] = [];
for (let group of this._menuGroups) {

View File

@@ -13,7 +13,7 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe
// --- service instances
const contextKeyService = new class extends MockContextKeyService {
contextMatchesRules() {
override contextMatchesRules() {
return true;
}
};
@@ -65,14 +65,14 @@ suite('MenuService', function () {
const groups = menuService.createMenu(testMenuId, contextKeyService).getActions();
assert.equal(groups.length, 5);
assert.strictEqual(groups.length, 5);
const [one, two, three, four, five] = groups;
assert.equal(one[0], 'navigation');
assert.equal(two[0], '0_hello');
assert.equal(three[0], 'hello');
assert.equal(four[0], 'Hello');
assert.equal(five[0], '');
assert.strictEqual(one[0], 'navigation');
assert.strictEqual(two[0], '0_hello');
assert.strictEqual(three[0], 'hello');
assert.strictEqual(four[0], 'Hello');
assert.strictEqual(five[0], '');
});
test('in group sorting, by title', function () {
@@ -94,14 +94,14 @@ suite('MenuService', function () {
const groups = menuService.createMenu(testMenuId, contextKeyService).getActions();
assert.equal(groups.length, 1);
assert.strictEqual(groups.length, 1);
const [, actions] = groups[0];
assert.equal(actions.length, 3);
assert.strictEqual(actions.length, 3);
const [one, two, three] = actions;
assert.equal(one.id, 'a');
assert.equal(two.id, 'b');
assert.equal(three.id, 'c');
assert.strictEqual(one.id, 'a');
assert.strictEqual(two.id, 'b');
assert.strictEqual(three.id, 'c');
});
test('in group sorting, by title and order', function () {
@@ -131,15 +131,15 @@ suite('MenuService', function () {
const groups = menuService.createMenu(testMenuId, contextKeyService).getActions();
assert.equal(groups.length, 1);
assert.strictEqual(groups.length, 1);
const [, actions] = groups[0];
assert.equal(actions.length, 4);
assert.strictEqual(actions.length, 4);
const [one, two, three, four] = actions;
assert.equal(one.id, 'd');
assert.equal(two.id, 'c');
assert.equal(three.id, 'b');
assert.equal(four.id, 'a');
assert.strictEqual(one.id, 'd');
assert.strictEqual(two.id, 'c');
assert.strictEqual(three.id, 'b');
assert.strictEqual(four.id, 'a');
});
@@ -165,14 +165,14 @@ suite('MenuService', function () {
const groups = menuService.createMenu(testMenuId, contextKeyService).getActions();
assert.equal(groups.length, 1);
assert.strictEqual(groups.length, 1);
const [[, actions]] = groups;
assert.equal(actions.length, 3);
assert.strictEqual(actions.length, 3);
const [one, two, three] = actions;
assert.equal(one.id, 'c');
assert.equal(two.id, 'b');
assert.equal(three.id, 'a');
assert.strictEqual(one.id, 'c');
assert.strictEqual(two.id, 'b');
assert.strictEqual(three.id, 'a');
});
test('special MenuId palette', function () {
@@ -188,16 +188,16 @@ suite('MenuService', function () {
for (const item of MenuRegistry.getMenuItems(MenuId.CommandPalette)) {
if (isIMenuItem(item)) {
if (item.command.id === 'a') {
assert.equal(item.command.title, 'Explicit');
assert.strictEqual(item.command.title, 'Explicit');
foundA = true;
}
if (item.command.id === 'b') {
assert.equal(item.command.title, 'Implicit');
assert.strictEqual(item.command.title, 'Implicit');
foundB = true;
}
}
}
assert.equal(foundA, true);
assert.equal(foundB, true);
assert.strictEqual(foundA, true);
assert.strictEqual(foundB, true);
});
});

View File

@@ -38,12 +38,12 @@ export class BackupMainService implements IBackupMainService {
private readonly backupPathComparer = { isEqual: (pathA: string, pathB: string) => isEqual(pathA, pathB, !isLinux) };
constructor(
@IEnvironmentMainService environmentService: IEnvironmentMainService,
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ILogService private readonly logService: ILogService
) {
this.backupHome = environmentService.backupHome;
this.workspacesJsonPath = environmentService.backupWorkspacesPath;
this.backupHome = environmentMainService.backupHome;
this.workspacesJsonPath = environmentMainService.backupWorkspacesPath;
}
async initialize(): Promise<void> {

View File

@@ -17,12 +17,13 @@ import { IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
import { IBackupWorkspacesFormat, ISerializedWorkspace } from 'vs/platform/backup/node/backup';
import { HotExitConfiguration } from 'vs/platform/files/common/files';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { ConsoleLogMainService } from 'vs/platform/log/common/log';
import { ConsoleMainLogger, LogService } from 'vs/platform/log/common/log';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { createHash } from 'crypto';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { Schemas } from 'vs/base/common/network';
import { isEqual } from 'vs/base/common/resources';
import product from 'vs/platform/product/common/product';
flakySuite('BackupMainService', () => {
@@ -104,14 +105,14 @@ flakySuite('BackupMainService', () => {
backupWorkspacesPath = path.join(backupHome, 'workspaces.json');
existingTestFolder1 = URI.file(path.join(testDir, 'folder1'));
environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS));
environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS), { _serviceBrand: undefined, ...product });
await fs.promises.mkdir(backupHome, { recursive: true });
configService = new TestConfigurationService();
service = new class TestBackupMainService extends BackupMainService {
constructor() {
super(environmentService, configService, new ConsoleLogMainService());
super(environmentService, configService, new LogService(new ConsoleMainLogger()));
this.backupHome = backupHome;
this.workspacesJsonPath = backupWorkspacesPath;
@@ -122,7 +123,7 @@ flakySuite('BackupMainService', () => {
return path.join(this.backupHome, id);
}
getFolderHash(folderUri: URI): string {
override getFolderHash(folderUri: URI): string {
return super.getFolderHash(folderUri);
}
};

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* 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';
import { URI } from 'vs/base/common/uri';
export const IChecksumService = createDecorator<IChecksumService>('checksumService');
export interface IChecksumService {
readonly _serviceBrand: undefined;
/**
* Computes the checksum of the contents of the resource.
*/
checksum(resource: URI): Promise<string>;
}

View File

@@ -3,9 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services';
import { IChecksumService } from 'vs/platform/checksum/common/checksumService';
export interface IDisplayMainService {
readonly _serviceBrand: undefined;
readonly onDidDisplayChanged: Event<void>;
}
registerSharedProcessRemoteService(IChecksumService, 'checksum', { supportsDelayedInstantiation: true });

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createHash } from 'crypto';
import { listenStream } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri';
import { IChecksumService } from 'vs/platform/checksum/common/checksumService';
import { IFileService } from 'vs/platform/files/common/files';
export class ChecksumService implements IChecksumService {
declare readonly _serviceBrand: undefined;
constructor(@IFileService private readonly fileService: IFileService) { }
checksum(resource: URI): Promise<string> {
return new Promise<string>(async (resolve, reject) => {
const hash = createHash('md5');
const stream = (await this.fileService.readFileStream(resource)).value;
listenStream(stream, {
onData: data => hash.update(data.buffer),
onError: error => reject(error),
onEnd: () => resolve(hash.digest('base64').replace(/=+$/, ''))
});
});
}
}

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* 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 { getPathFromAmdModule } from 'vs/base/test/node/testUtils';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { ChecksumService } from 'vs/platform/checksum/node/checksumService';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { NullLogService } from 'vs/platform/log/common/log';
suite('Checksum Service', () => {
let diskFileSystemProvider: DiskFileSystemProvider;
let fileService: IFileService;
setup(() => {
const logService = new NullLogService();
fileService = new FileService(logService);
diskFileSystemProvider = new DiskFileSystemProvider(logService);
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
});
teardown(() => {
diskFileSystemProvider.dispose();
fileService.dispose();
});
test('checksum', async () => {
const checksumService = new ChecksumService(fileService);
const checksum = await checksumService.checksum(URI.file(getPathFromAmdModule(require, './fixtures/lorem.txt')));
assert.ok(checksum === '8mi5KF8kcb817zmlal1kZA' || checksum === 'DnUKbJ1bHPPNZoHgHV25sg'); // depends on line endings git config
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -41,6 +41,7 @@ export interface ICommandHandlerDescription {
readonly description: string;
readonly args: ReadonlyArray<{
readonly name: string;
readonly isOptional?: boolean;
readonly description?: string;
readonly constraint?: TypeConstraint;
readonly schema?: IJSONSchema;
@@ -147,3 +148,5 @@ export const NullCommandService: ICommandService = {
return Promise.resolve(undefined);
}
};
CommandsRegistry.registerCommand('noop', () => { });

View File

@@ -71,7 +71,7 @@ suite('Command Tests', function () {
CommandsRegistry.getCommands().get('test')!.handler.apply(undefined, [undefined!, 'string']);
CommandsRegistry.getCommands().get('test2')!.handler.apply(undefined, [undefined!, 'string']);
assert.throws(() => CommandsRegistry.getCommands().get('test3')!.handler.apply(undefined, [undefined!, 'string']));
assert.equal(CommandsRegistry.getCommands().get('test3')!.handler.apply(undefined, [undefined!, 1]), true);
assert.strictEqual(CommandsRegistry.getCommands().get('test3')!.handler.apply(undefined, [undefined!, 1]), true);
});
});

View File

@@ -214,42 +214,53 @@ export class DefaultConfigurationModel extends ConfigurationModel {
}
}
export interface ConfigurationParseOptions {
scopes: ConfigurationScope[] | undefined;
skipRestricted?: boolean;
}
export class ConfigurationModelParser {
private _raw: any = null;
private _configurationModel: ConfigurationModel | null = null;
private _restrictedConfigurations: string[] = [];
private _parseErrors: any[] = [];
constructor(protected readonly _name: string, private _scopes?: ConfigurationScope[]) { }
constructor(protected readonly _name: string) { }
get configurationModel(): ConfigurationModel {
return this._configurationModel || new ConfigurationModel();
}
get restrictedConfigurations(): string[] {
return this._restrictedConfigurations;
}
get errors(): any[] {
return this._parseErrors;
}
public parseContent(content: string | null | undefined): void {
public parse(content: string | null | undefined, options?: ConfigurationParseOptions): void {
if (!types.isUndefinedOrNull(content)) {
const raw = this.doParseContent(content);
this.parseRaw(raw);
this.parseRaw(raw, options);
}
}
public parseRaw(raw: any): void {
this._raw = raw;
const configurationModel = this.doParseRaw(raw);
this._configurationModel = new ConfigurationModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides);
}
public parse(): void {
public reparse(options: ConfigurationParseOptions): void {
if (this._raw) {
this.parseRaw(this._raw);
this.parseRaw(this._raw, options);
}
}
protected doParseContent(content: string): any {
public parseRaw(raw: any, options?: ConfigurationParseOptions): void {
this._raw = raw;
const { contents, keys, overrides, restricted } = this.doParseRaw(raw, options);
this._configurationModel = new ConfigurationModel(contents, keys, overrides);
this._restrictedConfigurations = restricted || [];
}
private doParseContent(content: string): any {
let raw: any = {};
let currentProperty: string | null = null;
let currentParent: any = [];
@@ -306,42 +317,50 @@ export class ConfigurationModelParser {
return raw;
}
protected doParseRaw(raw: any): IConfigurationModel {
if (this._scopes) {
const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
raw = this.filterByScope(raw, configurationProperties, true, this._scopes);
}
protected doParseRaw(raw: any, options?: ConfigurationParseOptions): IConfigurationModel & { restricted?: string[] } {
const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
const filtered = this.filter(raw, configurationProperties, true, options);
raw = filtered.raw;
const contents = toValuesTree(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`));
const keys = Object.keys(raw);
const overrides: IOverrides[] = toOverrides(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`));
return { contents, keys, overrides };
return { contents, keys, overrides, restricted: filtered.restricted };
}
private filterByScope(properties: any, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }, filterOverriddenProperties: boolean, scopes: ConfigurationScope[]): {} {
const result: any = {};
private filter(properties: any, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema | undefined }, filterOverriddenProperties: boolean, options?: ConfigurationParseOptions): { raw: {}, restricted: string[] } {
if (!options?.scopes && !options?.skipRestricted) {
return { raw: properties, restricted: [] };
}
const raw: any = {};
const restricted: string[] = [];
for (let key in properties) {
if (OVERRIDE_PROPERTY_PATTERN.test(key) && filterOverriddenProperties) {
result[key] = this.filterByScope(properties[key], configurationProperties, false, scopes);
const result = this.filter(properties[key], configurationProperties, false, options);
raw[key] = result.raw;
restricted.push(...result.restricted);
} else {
const scope = this.getScope(key, configurationProperties);
const propertySchema = configurationProperties[key];
const scope = propertySchema ? typeof propertySchema.scope !== 'undefined' ? propertySchema.scope : ConfigurationScope.WINDOW : undefined;
if (propertySchema?.restricted) {
restricted.push(key);
}
// Load unregistered configurations always.
if (scope === undefined || scopes.indexOf(scope) !== -1) {
result[key] = properties[key];
if (scope === undefined || options.scopes === undefined || options.scopes.includes(scope)) {
if (!(options.skipRestricted && propertySchema?.restricted)) {
raw[key] = properties[key];
}
}
}
}
return result;
return { raw, restricted };
}
private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope | undefined {
const propertySchema = configurationProperties[key];
return propertySchema ? typeof propertySchema.scope !== 'undefined' ? propertySchema.scope : ConfigurationScope.WINDOW : undefined;
}
}
export class UserSettings extends Disposable {
private readonly parser: ConfigurationModelParser;
private readonly parseOptions: ConfigurationParseOptions;
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
@@ -352,25 +371,32 @@ export class UserSettings extends Disposable {
private readonly fileService: IFileService
) {
super();
this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes);
this.parser = new ConfigurationModelParser(this.userSettingsResource.toString());
this.parseOptions = { scopes: this.scopes };
this._register(this.fileService.watch(extUri.dirname(this.userSettingsResource)));
// Also listen to the resource incase the resource is a symlink - https://github.com/microsoft/vscode/issues/118134
this._register(this.fileService.watch(this.userSettingsResource));
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire()));
}
async loadConfiguration(): Promise<ConfigurationModel> {
try {
const content = await this.fileService.readFile(this.userSettingsResource);
this.parser.parseContent(content.value.toString() || '{}');
this.parser.parse(content.value.toString() || '{}', this.parseOptions);
return this.parser.configurationModel;
} catch (e) {
return new ConfigurationModel();
}
}
reprocess(): ConfigurationModel {
this.parser.parse();
reparse(): ConfigurationModel {
this.parser.reparse(this.parseOptions);
return this.parser.configurationModel;
}
getRestrictedSettings(): string[] {
return this.parser.restrictedConfigurations;
}
}
@@ -797,8 +823,9 @@ export class ConfigurationChangeEvent implements IConfigurationChangeEvent {
}
export class AllKeysConfigurationChangeEvent extends ConfigurationChangeEvent {
constructor(configuration: Configuration, workspace: Workspace, public source: ConfigurationTarget, public sourceConfig: any) {
constructor(configuration: Configuration, workspace: Workspace, source: ConfigurationTarget, sourceConfig: any) {
super({ keys: configuration.allKeys(), overrides: [] }, undefined, configuration, workspace);
this.source = source;
this.sourceConfig = sourceConfig;
}
}

View File

@@ -109,22 +109,35 @@ export const enum ConfigurationScope {
}
export interface IConfigurationPropertySchema extends IJSONSchema {
scope?: ConfigurationScope;
/**
* When restricted, value of this configuration will be read only from trusted sources.
* For eg., If the workspace is not trusted, then the value of this configuration is not read from workspace settings file.
*/
restricted?: boolean;
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[];
}
export interface IConfigurationExtensionInfo {
id: string;
restrictedConfigurations?: string[];
}
export interface IConfigurationNode {
@@ -139,14 +152,12 @@ export interface IConfigurationNode {
extensionInfo?: IConfigurationExtensionInfo;
}
type SettingProperties = { [key: string]: any };
export const allSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const applicationSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const machineSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const machineOverridableSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const windowSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const resourceSettings: { properties: SettingProperties, patternProperties: SettingProperties } = { properties: {}, patternProperties: {} };
export const allSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
export const applicationSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
export const machineSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
export const machineOverridableSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
export const windowSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
export const resourceSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
export const resourceLanguageSettingsSchemaId = 'vscode://schemas/settings/resourceLanguage';
@@ -190,7 +201,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
public registerConfigurations(configurations: IConfigurationNode[], validate: boolean = true): void {
const properties: string[] = [];
configurations.forEach(configuration => {
properties.push(...this.validateAndRegisterProperties(configuration, validate)); // fills in defaults
properties.push(...this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo)); // fills in defaults
this.configurationContributors.push(configuration);
this.registerJSONConfiguration(configuration);
});
@@ -297,7 +308,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
this.updateOverridePropertyPatternKey();
}
private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, scope: ConfigurationScope = ConfigurationScope.WINDOW): string[] {
private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, extensionInfo?: IConfigurationExtensionInfo, scope: ConfigurationScope = ConfigurationScope.WINDOW): string[] {
scope = types.isUndefinedOrNull(configuration.scope) ? scope : configuration.scope;
let propertyKeys: string[] = [];
let properties = configuration.properties;
@@ -318,6 +329,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
property.scope = undefined; // No scope for overridable properties `[${identifier}]`
} else {
property.scope = types.isUndefinedOrNull(property.scope) ? scope : property.scope;
property.restricted = types.isUndefinedOrNull(property.restricted) ? !!extensionInfo?.restrictedConfigurations?.includes(key) : property.restricted;
}
// Add to properties maps
@@ -341,7 +353,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
let subNodes = configuration.allOf;
if (subNodes) {
for (let node of subNodes) {
propertyKeys.push(...this.validateAndRegisterProperties(node, validate, scope));
propertyKeys.push(...this.validateAndRegisterProperties(node, validate, extensionInfo, scope));
}
}
return propertyKeys;

View File

@@ -11,10 +11,10 @@ suite('Configuration', () => {
test('simple merge', () => {
let base = { 'a': 1, 'b': 2 };
merge(base, { 'a': 3, 'c': 4 }, true);
assert.deepEqual(base, { 'a': 3, 'b': 2, 'c': 4 });
assert.deepStrictEqual(base, { 'a': 3, 'b': 2, 'c': 4 });
base = { 'a': 1, 'b': 2 };
merge(base, { 'a': 3, 'c': 4 }, false);
assert.deepEqual(base, { 'a': 1, 'b': 2, 'c': 4 });
assert.deepStrictEqual(base, { 'a': 1, 'b': 2, 'c': 4 });
});
test('removeFromValueTree: remove a non existing key', () => {
@@ -22,7 +22,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'c');
assert.deepEqual(target, { 'a': { 'b': 2 } });
assert.deepStrictEqual(target, { 'a': { 'b': 2 } });
});
test('removeFromValueTree: remove a multi segmented key from an object that has only sub sections of the key', () => {
@@ -30,7 +30,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'a.b.c');
assert.deepEqual(target, { 'a': { 'b': 2 } });
assert.deepStrictEqual(target, { 'a': { 'b': 2 } });
});
test('removeFromValueTree: remove a single segmented key', () => {
@@ -38,7 +38,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'a');
assert.deepEqual(target, {});
assert.deepStrictEqual(target, {});
});
test('removeFromValueTree: remove a single segmented key when its value is undefined', () => {
@@ -46,7 +46,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'a');
assert.deepEqual(target, {});
assert.deepStrictEqual(target, {});
});
test('removeFromValueTree: remove a multi segmented key when its value is undefined', () => {
@@ -54,7 +54,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'a.b');
assert.deepEqual(target, {});
assert.deepStrictEqual(target, {});
});
test('removeFromValueTree: remove a multi segmented key when its value is array', () => {
@@ -62,7 +62,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'a.b');
assert.deepEqual(target, {});
assert.deepStrictEqual(target, {});
});
test('removeFromValueTree: remove a multi segmented key first segment value is array', () => {
@@ -70,7 +70,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'a.0');
assert.deepEqual(target, { 'a': [1] });
assert.deepStrictEqual(target, { 'a': [1] });
});
test('removeFromValueTree: remove when key is the first segmenet', () => {
@@ -78,7 +78,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'a');
assert.deepEqual(target, {});
assert.deepStrictEqual(target, {});
});
test('removeFromValueTree: remove a multi segmented key when the first node has more values', () => {
@@ -86,7 +86,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'a.b.c');
assert.deepEqual(target, { 'a': { 'd': 1 } });
assert.deepStrictEqual(target, { 'a': { 'd': 1 } });
});
test('removeFromValueTree: remove a multi segmented key when in between node has more values', () => {
@@ -94,7 +94,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'a.b.c.d');
assert.deepEqual(target, { 'a': { 'b': { 'd': 1 } } });
assert.deepStrictEqual(target, { 'a': { 'b': { 'd': 1 } } });
});
test('removeFromValueTree: remove a multi segmented key when the last but one node has more values', () => {
@@ -102,7 +102,7 @@ suite('Configuration', () => {
removeFromValueTree(target, 'a.b.c');
assert.deepEqual(target, { 'a': { 'b': { 'd': 1 } } });
assert.deepStrictEqual(target, { 'a': { 'b': { 'd': 1 } } });
});
});
@@ -111,37 +111,37 @@ suite('Configuration Changes: Merge', () => {
test('merge only keys', () => {
const actual = mergeChanges({ keys: ['a', 'b'], overrides: [] }, { keys: ['c', 'd'], overrides: [] });
assert.deepEqual(actual, { keys: ['a', 'b', 'c', 'd'], overrides: [] });
assert.deepStrictEqual(actual, { keys: ['a', 'b', 'c', 'd'], overrides: [] });
});
test('merge only keys with duplicates', () => {
const actual = mergeChanges({ keys: ['a', 'b'], overrides: [] }, { keys: ['c', 'd'], overrides: [] }, { keys: ['a', 'd', 'e'], overrides: [] });
assert.deepEqual(actual, { keys: ['a', 'b', 'c', 'd', 'e'], overrides: [] });
assert.deepStrictEqual(actual, { keys: ['a', 'b', 'c', 'd', 'e'], overrides: [] });
});
test('merge only overrides', () => {
const actual = mergeChanges({ keys: [], overrides: [['a', ['1', '2']]] }, { keys: [], overrides: [['b', ['3', '4']]] });
assert.deepEqual(actual, { keys: [], overrides: [['a', ['1', '2']], ['b', ['3', '4']]] });
assert.deepStrictEqual(actual, { keys: [], overrides: [['a', ['1', '2']], ['b', ['3', '4']]] });
});
test('merge only overrides with duplicates', () => {
const actual = mergeChanges({ keys: [], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }, { keys: [], overrides: [['b', ['3', '4']]] }, { keys: [], overrides: [['c', ['1', '4']], ['a', ['2', '3']]] });
assert.deepEqual(actual, { keys: [], overrides: [['a', ['1', '2', '3']], ['b', ['5', '4', '3']], ['c', ['1', '4']]] });
assert.deepStrictEqual(actual, { keys: [], overrides: [['a', ['1', '2', '3']], ['b', ['5', '4', '3']], ['c', ['1', '4']]] });
});
test('merge', () => {
const actual = mergeChanges({ keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }, { keys: ['b'], overrides: [['b', ['3', '4']]] }, { keys: ['c', 'a'], overrides: [['c', ['1', '4']], ['a', ['2', '3']]] });
assert.deepEqual(actual, { keys: ['b', 'c', 'a'], overrides: [['a', ['1', '2', '3']], ['b', ['5', '4', '3']], ['c', ['1', '4']]] });
assert.deepStrictEqual(actual, { keys: ['b', 'c', 'a'], overrides: [['a', ['1', '2', '3']], ['b', ['5', '4', '3']], ['c', ['1', '4']]] });
});
test('merge single change', () => {
const actual = mergeChanges({ keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] });
assert.deepEqual(actual, { keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] });
assert.deepStrictEqual(actual, { keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] });
});
test('merge no changes', () => {
const actual = mergeChanges();
assert.deepEqual(actual, { keys: [], overrides: [] });
assert.deepStrictEqual(actual, { keys: [], overrides: [] });
});
});

View File

@@ -19,8 +19,8 @@ suite('ConfigurationModel', () => {
testObject.setValue('f', 1);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1 }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b', 'f']);
assert.deepStrictEqual(testObject.contents, { 'a': { 'b': 1 }, 'f': 1 });
assert.deepStrictEqual(testObject.keys, ['a.b', 'f']);
});
test('setValue for a key that has no sections and defined', () => {
@@ -28,8 +28,8 @@ suite('ConfigurationModel', () => {
testObject.setValue('f', 3);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1 }, 'f': 3 });
assert.deepEqual(testObject.keys, ['a.b', 'f']);
assert.deepStrictEqual(testObject.contents, { 'a': { 'b': 1 }, 'f': 3 });
assert.deepStrictEqual(testObject.keys, ['a.b', 'f']);
});
test('setValue for a key that has sections and not defined', () => {
@@ -37,8 +37,13 @@ suite('ConfigurationModel', () => {
testObject.setValue('b.c', 1);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1 }, 'b': { 'c': 1 }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b', 'f', 'b.c']);
const expected: any = {};
expected['a'] = { 'b': 1 };
expected['f'] = 1;
expected['b'] = Object.create(null);
expected['b']['c'] = 1;
assert.deepStrictEqual(testObject.contents, expected);
assert.deepStrictEqual(testObject.keys, ['a.b', 'f', 'b.c']);
});
test('setValue for a key that has sections and defined', () => {
@@ -46,8 +51,8 @@ suite('ConfigurationModel', () => {
testObject.setValue('b.c', 3);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1 }, 'b': { 'c': 3 }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b', 'b.c', 'f']);
assert.deepStrictEqual(testObject.contents, { 'a': { 'b': 1 }, 'b': { 'c': 3 }, 'f': 1 });
assert.deepStrictEqual(testObject.keys, ['a.b', 'b.c', 'f']);
});
test('setValue for a key that has sections and sub section not defined', () => {
@@ -55,8 +60,8 @@ suite('ConfigurationModel', () => {
testObject.setValue('a.c', 1);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1, 'c': 1 }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b', 'f', 'a.c']);
assert.deepStrictEqual(testObject.contents, { 'a': { 'b': 1, 'c': 1 }, 'f': 1 });
assert.deepStrictEqual(testObject.keys, ['a.b', 'f', 'a.c']);
});
test('setValue for a key that has sections and sub section defined', () => {
@@ -64,8 +69,8 @@ suite('ConfigurationModel', () => {
testObject.setValue('a.c', 3);
assert.deepEqual(testObject.contents, { 'a': { 'b': 1, 'c': 3 }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b', 'a.c', 'f']);
assert.deepStrictEqual(testObject.contents, { 'a': { 'b': 1, 'c': 3 }, 'f': 1 });
assert.deepStrictEqual(testObject.keys, ['a.b', 'a.c', 'f']);
});
test('setValue for a key that has sections and last section is added', () => {
@@ -73,8 +78,8 @@ suite('ConfigurationModel', () => {
testObject.setValue('a.b.c', 1);
assert.deepEqual(testObject.contents, { 'a': { 'b': { 'c': 1 } }, 'f': 1 });
assert.deepEqual(testObject.keys, ['a.b.c', 'f']);
assert.deepStrictEqual(testObject.contents, { 'a': { 'b': { 'c': 1 } }, 'f': 1 });
assert.deepStrictEqual(testObject.keys, ['a.b.c', 'f']);
});
test('removeValue: remove a non existing key', () => {
@@ -82,8 +87,8 @@ suite('ConfigurationModel', () => {
testObject.removeValue('a.b.c');
assert.deepEqual(testObject.contents, { 'a': { 'b': 2 } });
assert.deepEqual(testObject.keys, ['a.b']);
assert.deepStrictEqual(testObject.contents, { 'a': { 'b': 2 } });
assert.deepStrictEqual(testObject.keys, ['a.b']);
});
test('removeValue: remove a single segmented key', () => {
@@ -91,8 +96,8 @@ suite('ConfigurationModel', () => {
testObject.removeValue('a');
assert.deepEqual(testObject.contents, {});
assert.deepEqual(testObject.keys, []);
assert.deepStrictEqual(testObject.contents, {});
assert.deepStrictEqual(testObject.keys, []);
});
test('removeValue: remove a multi segmented key', () => {
@@ -100,8 +105,8 @@ suite('ConfigurationModel', () => {
testObject.removeValue('a.b');
assert.deepEqual(testObject.contents, {});
assert.deepEqual(testObject.keys, []);
assert.deepStrictEqual(testObject.contents, {});
assert.deepStrictEqual(testObject.keys, []);
});
test('get overriding configuration model for an existing identifier', () => {
@@ -109,7 +114,7 @@ suite('ConfigurationModel', () => {
{ 'a': { 'b': 1 }, 'f': 1 }, [],
[{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1 });
assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1 });
});
test('get overriding configuration model for an identifier that does not exist', () => {
@@ -117,7 +122,7 @@ suite('ConfigurationModel', () => {
{ 'a': { 'b': 1 }, 'f': 1 }, [],
[{ identifiers: ['c'], contents: { 'a': { 'd': 1 } }, keys: ['a'] }]);
assert.deepEqual(testObject.override('xyz').contents, { 'a': { 'b': 1 }, 'f': 1 });
assert.deepStrictEqual(testObject.override('xyz').contents, { 'a': { 'b': 1 }, 'f': 1 });
});
test('get overriding configuration when one of the keys does not exist in base', () => {
@@ -125,7 +130,7 @@ suite('ConfigurationModel', () => {
{ 'a': { 'b': 1 }, 'f': 1 }, [],
[{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'g': 1 }, keys: ['a', 'g'] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1, 'g': 1 });
assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1, 'g': 1 });
});
test('get overriding configuration when one of the key in base is not of object type', () => {
@@ -133,7 +138,7 @@ suite('ConfigurationModel', () => {
{ 'a': { 'b': 1 }, 'f': 1 }, [],
[{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'f': { 'g': 1 } }, keys: ['a', 'f'] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': { 'g': 1 } });
assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': { 'g': 1 } });
});
test('get overriding configuration when one of the key in overriding contents is not of object type', () => {
@@ -141,7 +146,7 @@ suite('ConfigurationModel', () => {
{ 'a': { 'b': 1 }, 'f': { 'g': 1 } }, [],
[{ identifiers: ['c'], contents: { 'a': { 'd': 1 }, 'f': 1 }, keys: ['a', 'f'] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1 });
assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1, 'd': 1 }, 'f': 1 });
});
test('get overriding configuration if the value of overriding identifier is not object', () => {
@@ -149,7 +154,7 @@ suite('ConfigurationModel', () => {
{ 'a': { 'b': 1 }, 'f': { 'g': 1 } }, [],
[{ identifiers: ['c'], contents: 'abc', keys: [] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1 }, 'f': { 'g': 1 } });
assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1 }, 'f': { 'g': 1 } });
});
test('get overriding configuration if the value of overriding identifier is an empty object', () => {
@@ -157,7 +162,7 @@ suite('ConfigurationModel', () => {
{ 'a': { 'b': 1 }, 'f': { 'g': 1 } }, [],
[{ identifiers: ['c'], contents: {}, keys: [] }]);
assert.deepEqual(testObject.override('c').contents, { 'a': { 'b': 1 }, 'f': { 'g': 1 } });
assert.deepStrictEqual(testObject.override('c').contents, { 'a': { 'b': 1 }, 'f': { 'g': 1 } });
});
test('simple merge', () => {
@@ -165,8 +170,8 @@ suite('ConfigurationModel', () => {
let add = new ConfigurationModel({ 'a': 3, 'c': 4 }, ['a', 'c']);
let result = base.merge(add);
assert.deepEqual(result.contents, { 'a': 3, 'b': 2, 'c': 4 });
assert.deepEqual(result.keys, ['a', 'b', 'c']);
assert.deepStrictEqual(result.contents, { 'a': 3, 'b': 2, 'c': 4 });
assert.deepStrictEqual(result.keys, ['a', 'b', 'c']);
});
test('recursive merge', () => {
@@ -174,9 +179,9 @@ suite('ConfigurationModel', () => {
let add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b']);
let result = base.merge(add);
assert.deepEqual(result.contents, { 'a': { 'b': 2 } });
assert.deepEqual(result.getValue('a'), { 'b': 2 });
assert.deepEqual(result.keys, ['a.b']);
assert.deepStrictEqual(result.contents, { 'a': { 'b': 2 } });
assert.deepStrictEqual(result.getValue('a'), { 'b': 2 });
assert.deepStrictEqual(result.keys, ['a.b']);
});
test('simple merge overrides', () => {
@@ -184,10 +189,10 @@ suite('ConfigurationModel', () => {
let add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'b': 2 }, keys: ['b'] }]);
let result = base.merge(add);
assert.deepEqual(result.contents, { 'a': { 'b': 2 } });
assert.deepEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': 2, 'b': 2 }, keys: ['a'] }]);
assert.deepEqual(result.override('c').contents, { 'a': 2, 'b': 2 });
assert.deepEqual(result.keys, ['a.b']);
assert.deepStrictEqual(result.contents, { 'a': { 'b': 2 } });
assert.deepStrictEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': 2, 'b': 2 }, keys: ['a'] }]);
assert.deepStrictEqual(result.override('c').contents, { 'a': 2, 'b': 2 });
assert.deepStrictEqual(result.keys, ['a.b']);
});
test('recursive merge overrides', () => {
@@ -195,10 +200,10 @@ suite('ConfigurationModel', () => {
let add = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'a': { 'e': 2 } }, keys: ['a'] }]);
let result = base.merge(add);
assert.deepEqual(result.contents, { 'a': { 'b': 2 }, 'f': 1 });
assert.deepEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': { 'd': 1, 'e': 2 } }, keys: ['a'] }]);
assert.deepEqual(result.override('c').contents, { 'a': { 'b': 2, 'd': 1, 'e': 2 }, 'f': 1 });
assert.deepEqual(result.keys, ['a.b', 'f']);
assert.deepStrictEqual(result.contents, { 'a': { 'b': 2 }, 'f': 1 });
assert.deepStrictEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': { 'd': 1, 'e': 2 } }, keys: ['a'] }]);
assert.deepStrictEqual(result.override('c').contents, { 'a': { 'b': 2, 'd': 1, 'e': 2 }, 'f': 1 });
assert.deepStrictEqual(result.keys, ['a.b', 'f']);
});
test('merge overrides when frozen', () => {
@@ -206,30 +211,30 @@ suite('ConfigurationModel', () => {
let model2 = new ConfigurationModel({ 'a': { 'b': 2 } }, ['a.b'], [{ identifiers: ['c'], contents: { 'a': { 'e': 2 } }, keys: ['a'] }]).freeze();
let result = new ConfigurationModel().merge(model1, model2);
assert.deepEqual(result.contents, { 'a': { 'b': 2 }, 'f': 1 });
assert.deepEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': { 'd': 1, 'e': 2 } }, keys: ['a'] }]);
assert.deepEqual(result.override('c').contents, { 'a': { 'b': 2, 'd': 1, 'e': 2 }, 'f': 1 });
assert.deepEqual(result.keys, ['a.b', 'f']);
assert.deepStrictEqual(result.contents, { 'a': { 'b': 2 }, 'f': 1 });
assert.deepStrictEqual(result.overrides, [{ identifiers: ['c'], contents: { 'a': { 'd': 1, 'e': 2 } }, keys: ['a'] }]);
assert.deepStrictEqual(result.override('c').contents, { 'a': { 'b': 2, 'd': 1, 'e': 2 }, 'f': 1 });
assert.deepStrictEqual(result.keys, ['a.b', 'f']);
});
test('Test contents while getting an existing property', () => {
let testObject = new ConfigurationModel({ 'a': 1 });
assert.deepEqual(testObject.getValue('a'), 1);
assert.deepStrictEqual(testObject.getValue('a'), 1);
testObject = new ConfigurationModel({ 'a': { 'b': 1 } });
assert.deepEqual(testObject.getValue('a'), { 'b': 1 });
assert.deepStrictEqual(testObject.getValue('a'), { 'b': 1 });
});
test('Test contents are undefined for non existing properties', () => {
const testObject = new ConfigurationModel({ awesome: true });
assert.deepEqual(testObject.getValue('unknownproperty'), undefined);
assert.deepStrictEqual(testObject.getValue('unknownproperty'), undefined);
});
test('Test override gives all content merged with overrides', () => {
const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, [], [{ identifiers: ['b'], contents: { 'a': 2 }, keys: ['a'] }]);
assert.deepEqual(testObject.override('b').contents, { 'a': 2, 'c': 1 });
assert.deepStrictEqual(testObject.override('b').contents, { 'a': 2, 'c': 1 });
});
});
@@ -237,96 +242,96 @@ suite('CustomConfigurationModel', () => {
test('simple merge using models', () => {
let base = new ConfigurationModelParser('base');
base.parseContent(JSON.stringify({ 'a': 1, 'b': 2 }));
base.parse(JSON.stringify({ 'a': 1, 'b': 2 }));
let add = new ConfigurationModelParser('add');
add.parseContent(JSON.stringify({ 'a': 3, 'c': 4 }));
add.parse(JSON.stringify({ 'a': 3, 'c': 4 }));
let result = base.configurationModel.merge(add.configurationModel);
assert.deepEqual(result.contents, { 'a': 3, 'b': 2, 'c': 4 });
assert.deepStrictEqual(result.contents, { 'a': 3, 'b': 2, 'c': 4 });
});
test('simple merge with an undefined contents', () => {
let base = new ConfigurationModelParser('base');
base.parseContent(JSON.stringify({ 'a': 1, 'b': 2 }));
base.parse(JSON.stringify({ 'a': 1, 'b': 2 }));
let add = new ConfigurationModelParser('add');
let result = base.configurationModel.merge(add.configurationModel);
assert.deepEqual(result.contents, { 'a': 1, 'b': 2 });
assert.deepStrictEqual(result.contents, { 'a': 1, 'b': 2 });
base = new ConfigurationModelParser('base');
add = new ConfigurationModelParser('add');
add.parseContent(JSON.stringify({ 'a': 1, 'b': 2 }));
add.parse(JSON.stringify({ 'a': 1, 'b': 2 }));
result = base.configurationModel.merge(add.configurationModel);
assert.deepEqual(result.contents, { 'a': 1, 'b': 2 });
assert.deepStrictEqual(result.contents, { 'a': 1, 'b': 2 });
base = new ConfigurationModelParser('base');
add = new ConfigurationModelParser('add');
result = base.configurationModel.merge(add.configurationModel);
assert.deepEqual(result.contents, {});
assert.deepStrictEqual(result.contents, {});
});
test('Recursive merge using config models', () => {
let base = new ConfigurationModelParser('base');
base.parseContent(JSON.stringify({ 'a': { 'b': 1 } }));
base.parse(JSON.stringify({ 'a': { 'b': 1 } }));
let add = new ConfigurationModelParser('add');
add.parseContent(JSON.stringify({ 'a': { 'b': 2 } }));
add.parse(JSON.stringify({ 'a': { 'b': 2 } }));
let result = base.configurationModel.merge(add.configurationModel);
assert.deepEqual(result.contents, { 'a': { 'b': 2 } });
assert.deepStrictEqual(result.contents, { 'a': { 'b': 2 } });
});
test('Test contents while getting an existing property', () => {
let testObject = new ConfigurationModelParser('test');
testObject.parseContent(JSON.stringify({ 'a': 1 }));
assert.deepEqual(testObject.configurationModel.getValue('a'), 1);
testObject.parse(JSON.stringify({ 'a': 1 }));
assert.deepStrictEqual(testObject.configurationModel.getValue('a'), 1);
testObject.parseContent(JSON.stringify({ 'a': { 'b': 1 } }));
assert.deepEqual(testObject.configurationModel.getValue('a'), { 'b': 1 });
testObject.parse(JSON.stringify({ 'a': { 'b': 1 } }));
assert.deepStrictEqual(testObject.configurationModel.getValue('a'), { 'b': 1 });
});
test('Test contents are undefined for non existing properties', () => {
const testObject = new ConfigurationModelParser('test');
testObject.parseContent(JSON.stringify({
testObject.parse(JSON.stringify({
awesome: true
}));
assert.deepEqual(testObject.configurationModel.getValue('unknownproperty'), undefined);
assert.deepStrictEqual(testObject.configurationModel.getValue('unknownproperty'), undefined);
});
test('Test contents are undefined for undefined config', () => {
const testObject = new ConfigurationModelParser('test');
assert.deepEqual(testObject.configurationModel.getValue('unknownproperty'), undefined);
assert.deepStrictEqual(testObject.configurationModel.getValue('unknownproperty'), undefined);
});
test('Test configWithOverrides gives all content merged with overrides', () => {
const testObject = new ConfigurationModelParser('test');
testObject.parseContent(JSON.stringify({ 'a': 1, 'c': 1, '[b]': { 'a': 2 } }));
testObject.parse(JSON.stringify({ 'a': 1, 'c': 1, '[b]': { 'a': 2 } }));
assert.deepEqual(testObject.configurationModel.override('b').contents, { 'a': 2, 'c': 1, '[b]': { 'a': 2 } });
assert.deepStrictEqual(testObject.configurationModel.override('b').contents, { 'a': 2, 'c': 1, '[b]': { 'a': 2 } });
});
test('Test configWithOverrides gives empty contents', () => {
const testObject = new ConfigurationModelParser('test');
assert.deepEqual(testObject.configurationModel.override('b').contents, {});
assert.deepStrictEqual(testObject.configurationModel.override('b').contents, {});
});
test('Test update with empty data', () => {
const testObject = new ConfigurationModelParser('test');
testObject.parseContent('');
testObject.parse('');
assert.deepEqual(testObject.configurationModel.contents, {});
assert.deepEqual(testObject.configurationModel.keys, []);
assert.deepStrictEqual(testObject.configurationModel.contents, Object.create(null));
assert.deepStrictEqual(testObject.configurationModel.keys, []);
testObject.parseContent(null!);
testObject.parse(null!);
assert.deepEqual(testObject.configurationModel.contents, {});
assert.deepEqual(testObject.configurationModel.keys, []);
assert.deepStrictEqual(testObject.configurationModel.contents, Object.create(null));
assert.deepStrictEqual(testObject.configurationModel.keys, []);
testObject.parseContent(undefined!);
testObject.parse(undefined!);
assert.deepEqual(testObject.configurationModel.contents, {});
assert.deepEqual(testObject.configurationModel.keys, []);
assert.deepStrictEqual(testObject.configurationModel.contents, Object.create(null));
assert.deepStrictEqual(testObject.configurationModel.keys, []);
});
test('Test registering the same property again', () => {
@@ -356,7 +361,7 @@ suite('CustomConfigurationModel', () => {
}
}
});
assert.equal(true, new DefaultConfigurationModel().getValue('a'));
assert.strictEqual(true, new DefaultConfigurationModel().getValue('a'));
});
});
@@ -370,28 +375,28 @@ suite('Configuration', () => {
const { overrideIdentifiers } = testObject.inspect('a', {}, undefined);
assert.deepEqual(overrideIdentifiers, ['l1', 'l3', 'l4']);
assert.deepStrictEqual(overrideIdentifiers, ['l1', 'l3', 'l4']);
});
test('Test update value', () => {
const parser = new ConfigurationModelParser('test');
parser.parseContent(JSON.stringify({ 'a': 1 }));
parser.parse(JSON.stringify({ 'a': 1 }));
const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel());
testObject.updateValue('a', 2);
assert.equal(testObject.getValue('a', {}, undefined), 2);
assert.strictEqual(testObject.getValue('a', {}, undefined), 2);
});
test('Test update value after inspect', () => {
const parser = new ConfigurationModelParser('test');
parser.parseContent(JSON.stringify({ 'a': 1 }));
parser.parse(JSON.stringify({ 'a': 1 }));
const testObject: Configuration = new Configuration(parser.configurationModel, new ConfigurationModel());
testObject.inspect('a', {}, undefined);
testObject.updateValue('a', 2);
assert.equal(testObject.getValue('a', {}, undefined), 2);
assert.strictEqual(testObject.getValue('a', {}, undefined), 2);
});
test('Test compare and update default configuration', () => {
@@ -407,7 +412,7 @@ suite('Configuration', () => {
}
}), ['editor.lineNumbers', '[markdown]']);
assert.deepEqual(actual, { keys: ['editor.lineNumbers', '[markdown]'], overrides: [['markdown', ['editor.wordWrap']]] });
assert.deepStrictEqual(actual, { keys: ['editor.lineNumbers', '[markdown]'], overrides: [['markdown', ['editor.wordWrap']]] });
});
@@ -430,7 +435,7 @@ suite('Configuration', () => {
}
}));
assert.deepEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] });
assert.deepStrictEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] });
});
@@ -453,7 +458,7 @@ suite('Configuration', () => {
}
}));
assert.deepEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] });
assert.deepStrictEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] });
});
@@ -476,7 +481,7 @@ suite('Configuration', () => {
}
}));
assert.deepEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] });
assert.deepStrictEqual(actual, { keys: ['window.zoomLevel', 'editor.lineNumbers', '[typescript]', 'editor.fontSize'], overrides: [['typescript', ['editor.insertSpaces', 'editor.wordWrap']]] });
});
@@ -492,13 +497,13 @@ suite('Configuration', () => {
const actual = testObject.compareAndDeleteFolderConfiguration(URI.file('file1'));
assert.deepEqual(actual, { keys: ['editor.lineNumbers', 'editor.fontSize', '[typescript]'], overrides: [['typescript', ['editor.wordWrap']]] });
assert.deepStrictEqual(actual, { keys: ['editor.lineNumbers', 'editor.fontSize', '[typescript]'], overrides: [['typescript', ['editor.wordWrap']]] });
});
function parseConfigurationModel(content: any): ConfigurationModel {
const parser = new ConfigurationModelParser('test');
parser.parseContent(JSON.stringify(content));
parser.parse(JSON.stringify(content));
return parser.configurationModel;
}
@@ -515,7 +520,7 @@ suite('ConfigurationChangeEvent', () => {
}));
let testObject = new ConfigurationChangeEvent(change, undefined, configuration);
assert.deepEqual(testObject.affectedKeys, ['window.zoomLevel', 'workbench.editor.enablePreview', 'files.autoSave']);
assert.deepStrictEqual(testObject.affectedKeys, ['window.zoomLevel', 'workbench.editor.enablePreview', 'files.autoSave']);
assert.ok(testObject.affectsConfiguration('window.zoomLevel'));
assert.ok(testObject.affectsConfiguration('window'));
@@ -547,7 +552,7 @@ suite('ConfigurationChangeEvent', () => {
}));
let testObject = new ConfigurationChangeEvent(change, { data }, configuration);
assert.deepEqual(testObject.affectedKeys, ['window.zoomLevel', 'workbench.editor.enablePreview']);
assert.deepStrictEqual(testObject.affectedKeys, ['window.zoomLevel', 'workbench.editor.enablePreview']);
assert.ok(testObject.affectsConfiguration('window.zoomLevel'));
assert.ok(testObject.affectsConfiguration('window'));
@@ -571,7 +576,7 @@ suite('ConfigurationChangeEvent', () => {
}));
let testObject = new ConfigurationChangeEvent(change, undefined, configuration);
assert.deepEqual(testObject.affectedKeys, ['files.autoSave', '[markdown]', 'editor.wordWrap']);
assert.deepStrictEqual(testObject.affectedKeys, ['files.autoSave', '[markdown]', 'editor.wordWrap']);
assert.ok(testObject.affectsConfiguration('files'));
assert.ok(testObject.affectsConfiguration('files.autoSave'));
@@ -613,7 +618,7 @@ suite('ConfigurationChangeEvent', () => {
}));
let testObject = new ConfigurationChangeEvent(change, { data }, configuration);
assert.deepEqual(testObject.affectedKeys, ['window.zoomLevel', '[markdown]', 'workbench.editor.enablePreview', 'editor.fontSize']);
assert.deepStrictEqual(testObject.affectedKeys, ['window.zoomLevel', '[markdown]', 'workbench.editor.enablePreview', 'editor.fontSize']);
assert.ok(!testObject.affectsConfiguration('files'));
@@ -657,7 +662,7 @@ suite('ConfigurationChangeEvent', () => {
);
let testObject = new ConfigurationChangeEvent(change, { data, workspace }, configuration, workspace);
assert.deepEqual(testObject.affectedKeys, ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']);
assert.deepStrictEqual(testObject.affectedKeys, ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']);
assert.ok(testObject.affectsConfiguration('window.zoomLevel'));
assert.ok(testObject.affectsConfiguration('window.zoomLevel', { resource: URI.file('folder1') }));
@@ -755,7 +760,7 @@ suite('ConfigurationChangeEvent', () => {
const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('file1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('file2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]);
const testObject = new ConfigurationChangeEvent(change, { data, workspace }, configuration, workspace);
assert.deepEqual(testObject.affectedKeys, ['editor.lineNumbers', '[markdown]', '[json]', 'window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows', 'editor.wordWrap']);
assert.deepStrictEqual(testObject.affectedKeys, ['editor.lineNumbers', '[markdown]', '[json]', 'window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows', 'editor.wordWrap']);
assert.ok(testObject.affectsConfiguration('window.title'));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('file1') }));
@@ -841,7 +846,7 @@ suite('ConfigurationChangeEvent', () => {
}));
let testObject = new ConfigurationChangeEvent(change, undefined, configuration);
assert.deepEqual(testObject.affectedKeys, ['launch', 'launch.version', 'tasks']);
assert.deepStrictEqual(testObject.affectedKeys, ['launch', 'launch.version', 'tasks']);
assert.ok(testObject.affectsConfiguration('launch'));
assert.ok(testObject.affectsConfiguration('launch.version'));
assert.ok(testObject.affectsConfiguration('tasks'));
@@ -870,7 +875,7 @@ suite('AllKeysConfigurationChangeEvent', () => {
const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('file1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('file2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]);
let testObject = new AllKeysConfigurationChangeEvent(configuration, workspace, ConfigurationTarget.USER, null);
assert.deepEqual(testObject.affectedKeys, ['editor.lineNumbers', '[markdown]', '[json]', 'window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']);
assert.deepStrictEqual(testObject.affectedKeys, ['editor.lineNumbers', '[markdown]', '[json]', 'window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']);
assert.ok(testObject.affectsConfiguration('window.title'));
assert.ok(testObject.affectsConfiguration('window.title', { resource: URI.file('file1') }));
@@ -946,6 +951,6 @@ suite('AllKeysConfigurationChangeEvent', () => {
function toConfigurationModel(obj: any): ConfigurationModel {
const parser = new ConfigurationModelParser('test');
parser.parseContent(JSON.stringify(obj));
parser.parse(JSON.stringify(obj));
return parser.configurationModel;
}

View File

@@ -24,15 +24,15 @@ suite('ConfigurationRegistry', () => {
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 1, b: 2 } }]);
configurationRegistry.registerDefaultConfigurations([{ '[lang]': { a: 2, c: 3 } }]);
assert.deepEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 1, b: 2 });
assert.deepEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, c: 3 });
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 1, b: 2 });
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, c: 3 });
});
test('configuration override defaults - merges defaults', async () => {
configurationRegistry.registerDefaultConfigurations([{ '[lang]': { a: 1, b: 2 } }]);
configurationRegistry.registerDefaultConfigurations([{ '[lang]': { a: 2, c: 3 } }]);
assert.deepEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, b: 2, c: 3 });
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, b: 2, c: 3 });
});
test('configuration defaults - overrides defaults', async () => {
@@ -48,6 +48,6 @@ suite('ConfigurationRegistry', () => {
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 1, b: 2 } }]);
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 2, c: 3 } }]);
assert.deepEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 });
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 });
});
});

View File

@@ -43,7 +43,7 @@ suite('ConfigurationService', () => {
}>();
assert.ok(config);
assert.equal(config.foo, 'bar');
assert.strictEqual(config.foo, 'bar');
});
test('config gets flattened', async () => {
@@ -62,7 +62,7 @@ suite('ConfigurationService', () => {
assert.ok(config);
assert.ok(config.testworkbench);
assert.ok(config.testworkbench.editor);
assert.equal(config.testworkbench.editor.tabs, true);
assert.strictEqual(config.testworkbench.editor.tabs, true);
});
test('error case does not explode', async () => {
@@ -91,7 +91,7 @@ suite('ConfigurationService', () => {
await testObject.initialize();
return new Promise<void>(async (c) => {
disposables.add(Event.filter(testObject.onDidChangeConfiguration, e => e.source === ConfigurationTarget.USER)(() => {
assert.equal(testObject.getValue('foo'), 'bar');
assert.strictEqual(testObject.getValue('foo'), 'bar');
c();
}));
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }'));
@@ -106,7 +106,7 @@ suite('ConfigurationService', () => {
return new Promise<void>((c) => {
disposables.add(Event.filter(testObject.onDidChangeConfiguration, e => e.source === ConfigurationTarget.USER)(async (e) => {
assert.equal(testObject.getValue('foo'), 'barz');
assert.strictEqual(testObject.getValue('foo'), 'barz');
c();
}));
fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "barz" }'));
@@ -122,7 +122,7 @@ suite('ConfigurationService', () => {
foo: string;
}>();
assert.ok(config);
assert.equal(config.foo, 'bar');
assert.strictEqual(config.foo, 'bar');
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "changed" }'));
// force a reload to get latest
@@ -131,7 +131,7 @@ suite('ConfigurationService', () => {
foo: string;
}>();
assert.ok(config);
assert.equal(config.foo, 'changed');
assert.strictEqual(config.foo, 'changed');
});
test('model defaults', async () => {
@@ -160,7 +160,7 @@ suite('ConfigurationService', () => {
let setting = testObject.getValue<ITestSetting>();
assert.ok(setting);
assert.equal(setting.configuration.service.testSetting, 'isSet');
assert.strictEqual(setting.configuration.service.testSetting, 'isSet');
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }'));
testObject = disposables.add(new ConfigurationService(settingsResource, fileService));
@@ -168,14 +168,14 @@ suite('ConfigurationService', () => {
setting = testObject.getValue<ITestSetting>();
assert.ok(setting);
assert.equal(setting.configuration.service.testSetting, 'isSet');
assert.strictEqual(setting.configuration.service.testSetting, 'isSet');
await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "configuration.service.testSetting": "isChanged" }'));
await testObject.reloadConfiguration();
setting = testObject.getValue<ITestSetting>();
assert.ok(setting);
assert.equal(setting.configuration.service.testSetting, 'isChanged');
assert.strictEqual(setting.configuration.service.testSetting, 'isChanged');
});
test('lookup', async () => {

View File

@@ -3,14 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, PauseableEmitter } from 'vs/base/common/event';
import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/map';
import { distinct } from 'vs/base/common/objects';
import { localize } from 'vs/nls';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, SET_CONTEXT_COMMAND_ID, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, SET_CONTEXT_COMMAND_ID, ContextKeyExpression, RawContextKey, ContextKeyInfo } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context';
@@ -74,19 +75,19 @@ class NullContext extends Context {
super(-1, null);
}
public setValue(key: string, value: any): boolean {
public override setValue(key: string, value: any): boolean {
return false;
}
public removeValue(key: string): boolean {
public override removeValue(key: string): boolean {
return false;
}
public getValue<T>(key: string): T | undefined {
public override getValue<T>(key: string): T | undefined {
return undefined;
}
collectAllValues(): { [key: string]: any; } {
override collectAllValues(): { [key: string]: any; } {
return Object.create(null);
}
}
@@ -136,7 +137,7 @@ class ConfigAwareContextValuesContainer extends Context {
this._listener.dispose();
}
getValue(key: string): any {
override getValue(key: string): any {
if (key.indexOf(ConfigAwareContextValuesContainer._keyPrefix) !== 0) {
return super.getValue(key);
@@ -167,15 +168,15 @@ class ConfigAwareContextValuesContainer extends Context {
return value;
}
setValue(key: string, value: any): boolean {
override setValue(key: string, value: any): boolean {
return super.setValue(key, value);
}
removeValue(key: string): boolean {
override removeValue(key: string): boolean {
return super.removeValue(key);
}
collectAllValues(): { [key: string]: any; } {
override collectAllValues(): { [key: string]: any; } {
const result: { [key: string]: any } = Object.create(null);
this._values.forEach((value, index) => result[index] = value);
return { ...result, ...super.collectAllValues() };
@@ -287,6 +288,13 @@ export abstract class AbstractContextKeyService implements IContextKeyService {
return new ScopedContextKeyService(this, domNode);
}
createOverlay(overlay: Iterable<[string, any]> = Iterable.empty()): IContextKeyService {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
return new OverlayContextKeyService(this, overlay);
}
public contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
@@ -405,27 +413,25 @@ export class ContextKeyService extends AbstractContextKeyService implements ICon
class ScopedContextKeyService extends AbstractContextKeyService {
private _parent: AbstractContextKeyService;
private _domNode: IContextKeyServiceTarget | undefined;
private _domNode: IContextKeyServiceTarget;
private readonly _parentChangeListener = new MutableDisposable();
constructor(parent: AbstractContextKeyService, domNode?: IContextKeyServiceTarget) {
constructor(parent: AbstractContextKeyService, domNode: IContextKeyServiceTarget) {
super(parent.createChildContext());
this._parent = parent;
this._updateParentChangeListener();
if (domNode) {
this._domNode = domNode;
if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
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 = domNode;
if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
let extraInfo = '';
if ((this._domNode as HTMLElement).classList) {
extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(', ');
}
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`);
}
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
}
private _updateParentChangeListener(): void {
@@ -434,14 +440,15 @@ class ScopedContextKeyService extends AbstractContextKeyService {
}
public dispose(): void {
this._onDidChangeContext.dispose();
this._isDisposed = true;
this._parent.disposeContext(this._myContextId);
this._parentChangeListener?.dispose();
if (this._domNode) {
this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);
this._domNode = undefined;
if (this._isDisposed) {
return;
}
this._onDidChangeContext.dispose();
this._parent.disposeContext(this._myContextId);
this._parentChangeListener.dispose();
this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);
this._isDisposed = true;
}
public getContextValuesContainer(contextId: number): Context {
@@ -484,6 +491,76 @@ class ScopedContextKeyService extends AbstractContextKeyService {
}
}
class OverlayContext implements IContext {
constructor(private parent: IContext, private overlay: ReadonlyMap<string, any>) { }
getValue<T>(key: string): T | undefined {
return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getValue(key);
}
}
class OverlayContextKeyService implements IContextKeyService {
declare _serviceBrand: undefined;
private overlay: Map<string, any>;
get contextId(): number {
return this.parent.contextId;
}
get onDidChangeContext(): Event<IContextKeyChangeEvent> {
return this.parent.onDidChangeContext;
}
constructor(private parent: AbstractContextKeyService | OverlayContextKeyService, overlay: Iterable<[string, any]>) {
this.overlay = new Map(overlay);
}
bufferChangeEvents(callback: Function): void {
this.parent.bufferChangeEvents(callback);
}
createKey<T>(): IContextKey<T> {
throw new Error('Not supported.');
}
getContext(target: IContextKeyServiceTarget | null): IContext {
return new OverlayContext(this.parent.getContext(target), this.overlay);
}
getContextValuesContainer(contextId: number): IContext {
const parentContext = this.parent.getContextValuesContainer(contextId);
return new OverlayContext(parentContext, this.overlay);
}
contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {
const context = this.getContextValuesContainer(this.contextId);
const result = KeybindingResolver.contextMatchesRules(context, rules);
return result;
}
getContextKeyValue<T>(key: string): T | undefined {
return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getContextKeyValue(key);
}
createScoped(): IContextKeyService {
throw new Error('Not supported.');
}
createOverlay(overlay: Iterable<[string, any]> = Iterable.empty()): IContextKeyService {
return new OverlayContextKeyService(this, overlay);
}
updateParent(): void {
throw new Error('Not supported.');
}
dispose(): void {
// noop
}
}
function findContextAttr(domNode: IContextKeyServiceTarget | null): number {
while (domNode) {
if (domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
@@ -501,3 +578,27 @@ function findContextAttr(domNode: IContextKeyServiceTarget | null): number {
CommandsRegistry.registerCommand(SET_CONTEXT_COMMAND_ID, function (accessor, contextKey: any, contextValue: any) {
accessor.get(IContextKeyService).createKey(String(contextKey), contextValue);
});
CommandsRegistry.registerCommand({
id: 'getContextKeyInfo',
handler() {
return [...RawContextKey.all()].sort((a, b) => a.key.localeCompare(b.key));
},
description: {
description: localize('getContextKeyInfo', "A command that returns information about context keys"),
args: []
}
});
CommandsRegistry.registerCommand('_generateContextKeyInfo', function () {
const result: ContextKeyInfo[] = [];
const seen = new Set<string>();
for (let info of RawContextKey.all()) {
if (!seen.has(info.key)) {
seen.add(info.key);
result.push(info);
}
}
result.sort((a, b) => a.key.localeCompare(b.key));
console.log(JSON.stringify(result, undefined, 2));
});

View File

@@ -339,7 +339,7 @@ export class ContextKeyDefinedExpr implements IContextKeyExpression {
public readonly type = ContextKeyExprType.Defined;
protected constructor(protected readonly key: string) {
protected constructor(readonly key: string) {
}
public cmp(other: ContextKeyExpression): number {
@@ -1257,13 +1257,32 @@ export class ContextKeyOrExpr implements IContextKeyExpression {
}
}
export interface ContextKeyInfo {
readonly key: string;
readonly type?: string;
readonly description?: string;
}
export class RawContextKey<T> extends ContextKeyDefinedExpr {
private static _info: ContextKeyInfo[] = [];
static all(): IterableIterator<ContextKeyInfo> {
return RawContextKey._info.values();
}
private readonly _defaultValue: T | undefined;
constructor(key: string, defaultValue: T | undefined) {
constructor(key: string, defaultValue: T | undefined, metaOrHide?: string | true | { type: string, description: string }) {
super(key);
this._defaultValue = defaultValue;
// collect all context keys into a central place
if (typeof metaOrHide === 'object') {
RawContextKey._info.push({ ...metaOrHide, key });
} else if (metaOrHide !== true) {
RawContextKey._info.push({ key, description: metaOrHide, type: defaultValue !== null && defaultValue !== undefined ? typeof defaultValue : undefined });
}
}
public bindTo(target: IContextKeyService): IContextKey<T> {
@@ -1326,7 +1345,8 @@ export interface IContextKeyService {
contextMatchesRules(rules: ContextKeyExpression | undefined): boolean;
getContextKeyValue<T>(key: string): T | undefined;
createScoped(target?: IContextKeyServiceTarget): IContextKeyService;
createScoped(target: IContextKeyServiceTarget): IContextKeyService;
createOverlay(overlay: Iterable<[string, any]>): IContextKeyService;
getContext(target: IContextKeyServiceTarget | null): IContext;
updateParent(parentContextKeyService: IContextKeyService): void;

View File

@@ -3,17 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform';
export const IsMacContext = new RawContextKey<boolean>('isMac', isMacintosh);
export const IsLinuxContext = new RawContextKey<boolean>('isLinux', isLinux);
export const IsWindowsContext = new RawContextKey<boolean>('isWindows', isWindows);
export const IsMacContext = new RawContextKey<boolean>('isMac', isMacintosh, localize('isMac', "Whether the operating system is macOS"));
export const IsLinuxContext = new RawContextKey<boolean>('isLinux', isLinux, localize('isLinux', "Whether the operating system is Linux"));
export const IsWindowsContext = new RawContextKey<boolean>('isWindows', isWindows, localize('isWindows', "Whether the operating system is Windows"));
export const IsWebContext = new RawContextKey<boolean>('isWeb', isWeb);
export const IsMacNativeContext = new RawContextKey<boolean>('isMacNative', isMacintosh && !isWeb);
export const IsWebContext = new RawContextKey<boolean>('isWeb', isWeb, localize('isWeb', "Whether the platform is a web browser"));
export const IsMacNativeContext = new RawContextKey<boolean>('isMacNative', isMacintosh && !isWeb, localize('isMacNative', "Whether the operating system is macOS on a non-browser platform"));
export const IsDevelopmentContext = new RawContextKey<boolean>('isDevelopment', false);
export const IsDevelopmentContext = new RawContextKey<boolean>('isDevelopment', false, true);
export const InputFocusedContextKey = 'inputFocus';
export const InputFocusedContext = new RawContextKey<boolean>(InputFocusedContextKey, false);
export const InputFocusedContext = new RawContextKey<boolean>(InputFocusedContextKey, false, localize('inputFocus', "Whether keyboard focus is inside an input box"));

View File

@@ -18,6 +18,7 @@ import { EventType, $, isHTMLElement } from 'vs/base/browser/dom';
import { attachMenuStyler } from 'vs/platform/theme/common/styler';
import { domEvent } from 'vs/base/browser/event';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { isPromiseCanceledError } from 'vs/base/common/errors';
export interface IContextMenuHandlerOptions {
blockMouse: boolean;
@@ -145,9 +146,7 @@ export class ContextMenuHandler {
}
private onActionRun(e: IRunEvent): void {
if (this.telemetryService) {
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' });
}
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' });
this.contextViewService.hideContextView(false);
@@ -158,7 +157,7 @@ export class ContextMenuHandler {
}
private onDidActionRun(e: IRunEvent): void {
if (e.error) {
if (e.error && !isPromiseCanceledError(e.error)) {
this.notificationService.error(e.error);
}
}

View File

@@ -24,7 +24,7 @@ export class ContextViewService extends Disposable implements IContextViewServic
this.contextView = this._register(new ContextView(this.container, ContextViewDOMPosition.ABSOLUTE));
this.layout();
this._register(layoutService.onLayout(() => this.layout()));
this._register(layoutService.onDidLayout(() => this.layout()));
}
// ContextView

View File

@@ -5,7 +5,6 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { IProcessEnvironment } from 'vs/base/common/platform';
export const IExtensionHostDebugService = createDecorator<IExtensionHostDebugService>('extensionHostDebugService');
@@ -30,6 +29,14 @@ export interface ICloseSessionEvent {
export interface IOpenExtensionWindowResult {
rendererDebugPort?: number;
success: boolean;
}
/**
* Like a IProcessEnvironment, but the value "null" deletes an environment variable
*/
export interface INullableProcessEnvironment {
[key: string]: string | null;
}
export interface IExtensionHostDebugService {
@@ -47,5 +54,5 @@ export interface IExtensionHostDebugService {
terminateSession(sessionId: string, subId?: string): void;
readonly onTerminateSession: Event<ITerminateSessionEvent>;
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult>;
openExtensionDevelopmentHostWindow(args: string[], env: INullableProcessEnvironment | undefined, debugRenderer: boolean): Promise<IOpenExtensionWindowResult>;
}

View File

@@ -4,10 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult, INullableProcessEnvironment } from 'vs/platform/debug/common/extensionHostDebug';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IProcessEnvironment } from 'vs/base/common/platform';
export class ExtensionHostDebugBroadcastChannel<TContext> implements IServerChannel<TContext> {
@@ -87,7 +86,7 @@ export class ExtensionHostDebugChannelClient extends Disposable implements IExte
return this.channel.listen('terminate');
}
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {
return this.channel.call('openExtensionDevelopmentHostWindow', [args, env, debugRenderer]);
openExtensionDevelopmentHostWindow(args: string[], env: INullableProcessEnvironment | undefined, debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {
return this.channel.call('openExtensionDevelopmentHostWindow', [args, env || {}, debugRenderer]);
}
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
import { INullableProcessEnvironment, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { createServer, AddressInfo } from 'net';
@@ -16,7 +16,7 @@ export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends Extens
super();
}
call(ctx: TContext, command: string, arg?: any): Promise<any> {
override call(ctx: TContext, command: string, arg?: any): Promise<any> {
if (command === 'openExtensionDevelopmentHostWindow') {
return this.openExtensionDevelopmentHostWindow(arg[0], arg[1], arg[2]);
} else {
@@ -24,26 +24,48 @@ export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends Extens
}
}
private async openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {
private async openExtensionDevelopmentHostWindow(args: string[], env: INullableProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {
const pargs = parseArgs(args, OPTIONS);
pargs.debugRenderer = debugRenderer;
const extDevPaths = pargs.extensionDevelopmentPath;
if (!extDevPaths) {
return {};
return { success: false };
}
// split INullableProcessEnvironment into a IProcessEnvironment and an array of keys to be deleted
// TODO: support to delete env vars; currently the "deletes" are ignored
let userEnv: IProcessEnvironment | undefined;
//let userEnvDeletes: string[] = [];
const keys = Object.keys(env);
for (let k of keys) {
let value = env[k];
if (value === null) {
//userEnvDeletes.push(k);
} else {
if (!userEnv) {
userEnv = Object.create(null) as IProcessEnvironment;
}
userEnv[k] = value;
}
}
const [codeWindow] = this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, {
context: OpenContext.API,
cli: pargs,
userEnv: Object.keys(env).length > 0 ? env : undefined
userEnv: userEnv
});
if (!debugRenderer) {
return {};
return { success: true };
}
const debug = codeWindow.win.webContents.debugger;
const win = codeWindow.win;
if (!win) {
return { success: true };
}
const debug = win.webContents.debugger;
let listeners = debug.isAttached() ? Infinity : 0;
const server = createServer(listener => {
@@ -61,7 +83,7 @@ export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends Extens
const onMessage = (_event: Event, method: string, params: unknown, sessionId?: string) =>
writeMessage(({ method, params, sessionId }));
codeWindow.win.on('close', () => {
win.on('close', () => {
debug.removeListener('message', onMessage);
listener.end();
closed = true;
@@ -103,8 +125,8 @@ export class ElectronExtensionHostDebugBroadcastChannel<TContext> extends Extens
});
await new Promise<void>(r => server.listen(0, r));
codeWindow.win.on('close', () => server.close());
win.on('close', () => server.close());
return { rendererDebugPort: (server.address() as AddressInfo).port };
return { rendererDebugPort: (server.address() as AddressInfo).port, success: true };
}
}

View File

@@ -7,6 +7,20 @@ import { UriComponents } from 'vs/base/common/uri';
import { ProcessItem } from 'vs/base/common/processes';
import { IWorkspace } from 'vs/platform/workspace/common/workspace';
import { IStringDictionary } from 'vs/base/common/collections';
import { IMainProcessInfo } from 'vs/platform/launch/common/launch';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const ID = 'diagnosticsService';
export const IDiagnosticsService = createDecorator<IDiagnosticsService>(ID);
export interface IDiagnosticsService {
readonly _serviceBrand: undefined;
getPerformanceInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<PerformanceInfo>;
getSystemInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<SystemInfo>;
getDiagnostics(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<string>;
reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void>;
}
export interface IMachineInfo {
os: string;

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services';
import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics';
registerSharedProcessRemoteService(IDiagnosticsService, 'diagnostics', { supportsDelayedInstantiation: true });

View File

@@ -4,35 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import * as osLib from 'os';
import { virtualMachineHint } from 'vs/base/node/id';
import { IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics';
import { IDiagnosticsService, IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics';
import { exists, readFile } from 'fs';
import { join, basename } from 'vs/base/common/path';
import { parse, ParseError, getNodeType } from 'vs/base/common/json';
import { listProcesses } from 'vs/base/node/ps';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { isWindows, isLinux } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { ProcessItem } from 'vs/base/common/processes';
import { IMainProcessInfo } from 'vs/platform/launch/node/launch';
import { IMainProcessInfo } from 'vs/platform/launch/common/launch';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Iterable } from 'vs/base/common/iterator';
import { Schemas } from 'vs/base/common/network';
import { ByteSize } from 'vs/platform/files/common/files';
import { IDirent, readdir } from 'vs/base/node/pfs';
export const ID = 'diagnosticsService';
export const IDiagnosticsService = createDecorator<IDiagnosticsService>(ID);
export interface IDiagnosticsService {
readonly _serviceBrand: undefined;
getPerformanceInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<PerformanceInfo>;
getSystemInfo(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<SystemInfo>;
getDiagnostics(mainProcessInfo: IMainProcessInfo, remoteInfo: (IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]): Promise<string>;
reportWorkspaceStats(workspace: IWorkspaceInformation): Promise<void>;
}
export interface VersionInfo {
vscodeVersion: string;
os: string;
@@ -226,7 +213,10 @@ export class DiagnosticsService implements IDiagnosticsService {
declare readonly _serviceBrand: undefined;
constructor(@ITelemetryService private readonly telemetryService: ITelemetryService) { }
constructor(
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IProductService private readonly productService: IProductService
) { }
private formatMachineInfo(info: IMachineInfo): string {
const output: string[] = [];
@@ -240,7 +230,7 @@ export class DiagnosticsService implements IDiagnosticsService {
private formatEnvironment(info: IMainProcessInfo): string {
const output: string[] = [];
output.push(`Version: ${product.nameShort} ${product.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`);
output.push(`Version: ${this.productService.nameShort} ${this.productService.version} (${this.productService.commit || 'Commit unknown'}, ${this.productService.date || 'Date unknown'})`);
output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`);
const cpus = osLib.cpus();
if (cpus && cpus.length > 0) {
@@ -494,7 +484,7 @@ export class DiagnosticsService implements IDiagnosticsService {
// Format name with indent
let name: string;
if (isRoot) {
name = item.pid === mainPid ? `${product.applicationName} main` : 'remote agent';
name = item.pid === mainPid ? `${this.productService.applicationName} main` : 'remote agent';
} else {
name = `${' '.repeat(indent)} ${item.name}`;

View File

@@ -9,6 +9,8 @@ import { URI } from 'vs/base/common/uri';
import { basename } from 'vs/base/common/resources';
import { localize } from 'vs/nls';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { Codicon } from 'vs/base/common/codicons';
import { IMarkdownString } from 'vs/base/common/htmlContent';
export interface FileFilter {
extensions: string[];
@@ -100,6 +102,7 @@ export interface IPickAndOpenOptions {
defaultUri?: URI;
telemetryExtraData?: ITelemetryData;
availableFileSystems?: string[];
remoteAuthority?: string | null;
}
export interface ISaveDialogOptions {
@@ -177,11 +180,24 @@ export interface IOpenDialogOptions {
export const IDialogService = createDecorator<IDialogService>('dialogService');
export interface ICustomDialogOptions {
buttonDetails?: string[];
markdownDetails?: ICustomDialogMarkdown[];
classes?: string[];
icon?: Codicon;
disableCloseAction?: boolean;
}
export interface ICustomDialogMarkdown {
markdown: IMarkdownString,
classes?: string[]
}
export interface IDialogOptions {
cancelId?: number;
detail?: string;
checkbox?: ICheckbox;
useCustom?: boolean;
custom?: boolean | ICustomDialogOptions;
}
export interface IInput {

View File

@@ -1,47 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisplayMainService as ICommonDisplayMainService } from 'vs/platform/display/common/displayMainService';
import { Emitter } from 'vs/base/common/event';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { app, Display, screen } from 'electron';
import { RunOnceScheduler } from 'vs/base/common/async';
export const IDisplayMainService = createDecorator<IDisplayMainService>('displayMainService');
export interface IDisplayMainService extends ICommonDisplayMainService { }
export class DisplayMainService extends Disposable implements ICommonDisplayMainService {
declare readonly _serviceBrand: undefined;
private readonly _onDidDisplayChanged = this._register(new Emitter<void>());
readonly onDidDisplayChanged = this._onDidDisplayChanged.event;
constructor() {
super();
const displayChangedScheduler = this._register(new RunOnceScheduler(() => {
this._onDidDisplayChanged.fire();
}, 100));
app.whenReady().then(() => {
const displayChangedListener = (event: Event, display: Display, changedMetrics?: string[]) => {
displayChangedScheduler.schedule();
};
screen.on('display-metrics-changed', displayChangedListener);
this._register(toDisposable(() => screen.removeListener('display-metrics-changed', displayChangedListener)));
screen.on('display-added', displayChangedListener);
this._register(toDisposable(() => screen.removeListener('display-added', displayChangedListener)));
screen.on('display-removed', displayChangedListener);
this._register(toDisposable(() => screen.removeListener('display-removed', displayChangedListener)));
});
}
}

View File

@@ -54,3 +54,12 @@ export interface IWindowDriver {
getTerminalBuffer(selector: string): Promise<string[]>;
writeInTerminal(selector: string, text: string): Promise<void>;
}
export interface IDriverOptions {
verbose: boolean;
}
export interface IWindowDriverRegistry {
registerWindowDriver(windowId: number): Promise<IDriverOptions>;
reloadWindowDriver(windowId: number): Promise<void>;
}

View File

@@ -0,0 +1,96 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IDriverOptions, IElement, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver';
export class WindowDriverChannel implements IServerChannel {
constructor(private driver: IWindowDriver) { }
listen<T>(_: unknown, event: string): Event<T> {
throw new Error(`No event found: ${event}`);
}
call(_: unknown, command: string, arg?: any): Promise<any> {
switch (command) {
case 'click': return this.driver.click(arg[0], arg[1], arg[2]);
case 'doubleClick': return this.driver.doubleClick(arg);
case 'setValue': return this.driver.setValue(arg[0], arg[1]);
case 'getTitle': return this.driver.getTitle();
case 'isActiveElement': return this.driver.isActiveElement(arg);
case 'getElements': return this.driver.getElements(arg[0], arg[1]);
case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]);
case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]);
case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg);
case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1]);
}
throw new Error(`Call not found: ${command}`);
}
}
export class WindowDriverChannelClient implements IWindowDriver {
declare readonly _serviceBrand: undefined;
constructor(private channel: IChannel) { }
click(selector: string, xoffset?: number, yoffset?: number): Promise<void> {
return this.channel.call('click', [selector, xoffset, yoffset]);
}
doubleClick(selector: string): Promise<void> {
return this.channel.call('doubleClick', selector);
}
setValue(selector: string, text: string): Promise<void> {
return this.channel.call('setValue', [selector, text]);
}
getTitle(): Promise<string> {
return this.channel.call('getTitle');
}
isActiveElement(selector: string): Promise<boolean> {
return this.channel.call('isActiveElement', selector);
}
getElements(selector: string, recursive: boolean): Promise<IElement[]> {
return this.channel.call('getElements', [selector, recursive]);
}
getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number, y: number }> {
return this.channel.call('getElementXY', [selector, xoffset, yoffset]);
}
typeInEditor(selector: string, text: string): Promise<void> {
return this.channel.call('typeInEditor', [selector, text]);
}
getTerminalBuffer(selector: string): Promise<string[]> {
return this.channel.call('getTerminalBuffer', selector);
}
writeInTerminal(selector: string, text: string): Promise<void> {
return this.channel.call('writeInTerminal', [selector, text]);
}
}
export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry {
declare readonly _serviceBrand: undefined;
constructor(private channel: IChannel) { }
registerWindowDriver(windowId: number): Promise<IDriverOptions> {
return this.channel.call('registerWindowDriver', windowId);
}
reloadWindowDriver(windowId: number): Promise<void> {
return this.channel.call('reloadWindowDriver', windowId);
}
}

View File

@@ -3,7 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DriverChannel, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IDriverOptions } from 'vs/platform/driver/node/driver';
import { DriverChannel, WindowDriverRegistryChannel } from 'vs/platform/driver/node/driver';
import { WindowDriverChannelClient } from 'vs/platform/driver/common/driverIpc';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net';
import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle';
@@ -17,7 +18,7 @@ import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/e
import { ScanCodeBinding } from 'vs/base/common/scanCode';
import { KeybindingParser } from 'vs/base/common/keybindingParser';
import { timeout } from 'vs/base/common/async';
import { IDriver, IElement, IWindowDriver } from 'vs/platform/driver/common/driver';
import { IDriver, IDriverOptions, IElement, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
@@ -62,7 +63,7 @@ export class Driver implements IDriver, IWindowDriverRegistry {
await this.whenUnfrozen(windowId);
const window = this.windowsMainService.getWindowById(windowId);
if (!window) {
if (!window?.win) {
throw new Error('Invalid window');
}
const webContents = window.win.webContents;
@@ -101,7 +102,7 @@ export class Driver implements IDriver, IWindowDriverRegistry {
}
const window = this.windowsMainService.getWindowById(windowId);
if (!window) {
if (!window?.win) {
throw new Error('Invalid window');
}
const webContents = window.win.webContents;
@@ -207,10 +208,10 @@ export class Driver implements IDriver, IWindowDriverRegistry {
export async function serve(
windowServer: IPCServer,
handle: string,
environmentService: IEnvironmentMainService,
environmentMainService: IEnvironmentMainService,
instantiationService: IInstantiationService
): Promise<IDisposable> {
const verbose = environmentService.driverVerbose;
const verbose = environmentMainService.driverVerbose;
const driver = instantiationService.createInstance(Driver as any, windowServer, { verbose }) as Driver; // {{SQL CARBON EDIT}} strict-null-check...i guess?
const windowDriverRegistryChannel = new WindowDriverRegistryChannel(driver);

View File

@@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/node/driver';
import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/common/driverIpc';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { timeout } from 'vs/base/common/async';
import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';

View File

@@ -7,7 +7,7 @@ import { Client } from 'vs/base/parts/ipc/common/ipc.net';
import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IDriver, IElement, IWindowDriver } from 'vs/platform/driver/common/driver';
import { IDriver, IElement, IWindowDriverRegistry } from 'vs/platform/driver/common/driver';
export class DriverChannel implements IServerChannel {
@@ -107,15 +107,6 @@ export class DriverChannelClient implements IDriver {
}
}
export interface IDriverOptions {
verbose: boolean;
}
export interface IWindowDriverRegistry {
registerWindowDriver(windowId: number): Promise<IDriverOptions>;
reloadWindowDriver(windowId: number): Promise<void>;
}
export class WindowDriverRegistryChannel implements IServerChannel {
constructor(private registry: IWindowDriverRegistry) { }
@@ -134,94 +125,6 @@ export class WindowDriverRegistryChannel implements IServerChannel {
}
}
export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry {
declare readonly _serviceBrand: undefined;
constructor(private channel: IChannel) { }
registerWindowDriver(windowId: number): Promise<IDriverOptions> {
return this.channel.call('registerWindowDriver', windowId);
}
reloadWindowDriver(windowId: number): Promise<void> {
return this.channel.call('reloadWindowDriver', windowId);
}
}
export class WindowDriverChannel implements IServerChannel {
constructor(private driver: IWindowDriver) { }
listen<T>(_: unknown, event: string): Event<T> {
throw new Error(`No event found: ${event}`);
}
call(_: unknown, command: string, arg?: any): Promise<any> {
switch (command) {
case 'click': return this.driver.click(arg[0], arg[1], arg[2]);
case 'doubleClick': return this.driver.doubleClick(arg);
case 'setValue': return this.driver.setValue(arg[0], arg[1]);
case 'getTitle': return this.driver.getTitle();
case 'isActiveElement': return this.driver.isActiveElement(arg);
case 'getElements': return this.driver.getElements(arg[0], arg[1]);
case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]);
case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]);
case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg);
case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1]);
}
throw new Error(`Call not found: ${command}`);
}
}
export class WindowDriverChannelClient implements IWindowDriver {
declare readonly _serviceBrand: undefined;
constructor(private channel: IChannel) { }
click(selector: string, xoffset?: number, yoffset?: number): Promise<void> {
return this.channel.call('click', [selector, xoffset, yoffset]);
}
doubleClick(selector: string): Promise<void> {
return this.channel.call('doubleClick', selector);
}
setValue(selector: string, text: string): Promise<void> {
return this.channel.call('setValue', [selector, text]);
}
getTitle(): Promise<string> {
return this.channel.call('getTitle');
}
isActiveElement(selector: string): Promise<boolean> {
return this.channel.call('isActiveElement', selector);
}
getElements(selector: string, recursive: boolean): Promise<IElement[]> {
return this.channel.call('getElements', [selector, recursive]);
}
getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number, y: number }> {
return this.channel.call('getElementXY', [selector, xoffset, yoffset]);
}
typeInEditor(selector: string, text: string): Promise<void> {
return this.channel.call('typeInEditor', [selector, text]);
}
getTerminalBuffer(selector: string): Promise<string[]> {
return this.channel.call('getTerminalBuffer', selector);
}
writeInTerminal(selector: string, text: string): Promise<void> {
return this.channel.call('writeInTerminal', [selector, text]);
}
}
export async function connect(handle: string): Promise<{ client: Client, driver: IDriver }> {
const client = await connectNet(handle, 'driverClient');
const channel = client.getChannel('driver');

View File

@@ -9,14 +9,19 @@ import { Event } from 'vs/base/common/event';
export interface IEditorModel {
/**
* Emitted when the model is disposed.
* Emitted when the model is about to be disposed.
*/
readonly onDispose: Event<void>;
readonly onWillDispose: Event<void>;
/**
* Loads the model.
* Resolves the model.
*/
load(): Promise<IEditorModel>;
resolve(): Promise<void>;
/**
* Find out if the editor model was resolved or not.
*/
isResolved(): boolean;
/**
* Find out if this model has been disposed.
@@ -65,6 +70,23 @@ export interface IBaseResourceEditorInput {
readonly forceUntitled?: boolean;
}
/**
* This identifier allows to uniquely identify an editor with a
* resource and type identifier.
*/
export interface IResourceEditorInputIdentifier {
/**
* The resource URI of the editor.
*/
readonly resource: URI;
/**
* The type of the editor.
*/
readonly typeId: string;
}
export interface IResourceEditorInput extends IBaseResourceEditorInput {
/**
@@ -111,6 +133,19 @@ export enum EditorActivation {
PRESERVE
}
export enum EditorOverride {
/**
* Displays a picker and allows the user to decide which editor to use
*/
PICK = 1,
/**
* Disables overrides
*/
DISABLED
}
export enum EditorOpenContext {
/**
@@ -204,10 +239,10 @@ export interface IEditorOptions {
/**
* Allows to override the editor that should be used to display the input:
* - `undefined`: let the editor decide for itself
* - `false`: disable overrides
* - `string`: specific override by id
* - `EditorOverride`: specific override handling
*/
readonly override?: false | string;
readonly override?: string | EditorOverride;
/**
* A optional hint to signal in which context the editor opens.

View File

@@ -39,8 +39,9 @@ export interface NativeParsedArgs {
'extensions-dir'?: string;
'extensions-download-dir'?: string;
'builtin-extensions-dir'?: string;
extensionDevelopmentPath?: string[]; // // undefined or array of 1 or more local paths or URIs
extensionDevelopmentPath?: string[]; // undefined or array of 1 or more local paths or URIs
extensionTestsPath?: string; // either a local path or a URI
extensionDevelopmentKind?: string[];
'inspect-extensions'?: string;
'inspect-brk-extensions'?: string;
debugId?: string;
@@ -63,6 +64,7 @@ export interface NativeParsedArgs {
'export-default-configuration'?: string;
'install-source'?: string;
'disable-updates'?: boolean;
'disable-keytar'?: boolean;
'disable-crash-reporter'?: boolean;
'crash-reporter-directory'?: string;
'crash-reporter-id'?: string;
@@ -105,4 +107,5 @@ export interface NativeParsedArgs {
'ignore-certificate-errors'?: boolean;
'allow-insecure-localhost'?: boolean;
'log-net-log'?: string;
'vmodule'?: string;
}

View File

@@ -3,12 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { createDecorator, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { ExtensionKind } from 'vs/platform/extensions/common/extensions';
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
export const INativeEnvironmentService = createDecorator<INativeEnvironmentService>('nativeEnvironmentService');
export const INativeEnvironmentService = refineServiceDecorator<IEnvironmentService, INativeEnvironmentService>(IEnvironmentService);
export interface IDebugParams {
port: number | null;
@@ -62,6 +63,7 @@ export interface IEnvironmentService {
isExtensionDevelopment: boolean;
disableExtensions: boolean | string[];
extensionDevelopmentLocationURI?: URI[];
extensionDevelopmentKind?: ExtensionKind[];
extensionTestsLocationURI?: URI;
// --- logging
@@ -106,7 +108,7 @@ export interface INativeEnvironmentService extends IEnvironmentService {
// --- CLI Arguments
args: NativeParsedArgs;
// --- paths
// --- data paths
appRoot: string;
userHome: URI;
appSettingsHome: URI;
@@ -115,15 +117,12 @@ export interface INativeEnvironmentService extends IEnvironmentService {
machineSettingsResource: URI;
installSourcePath: string;
// --- IPC Handles
sharedIPCHandle: string;
// --- Extensions
// --- extensions
extensionsPath: string;
extensionsDownloadPath: string;
builtinExtensionsPath: string;
// --- Smoke test support
// --- smoke test support
driverHandle?: string;
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

View File

@@ -0,0 +1,257 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IProductService } from 'vs/platform/product/common/productService';
import { IDebugParams, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { dirname, join, normalize, resolve } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { memoize } from 'vs/base/common/decorators';
import { toLocalISOString } from 'vs/base/common/date';
import { FileAccess } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { ExtensionKind } from 'vs/platform/extensions/common/extensions';
import { env } from 'vs/base/common/process';
export interface INativeEnvironmentPaths {
/**
* The user data directory to use for anything that should be
* persisted except for the content that is meant for the `homeDir`.
*
* Only one instance of VSCode can use the same `userDataDir`.
*/
userDataDir: string
/**
* The user home directory mainly used for persisting extensions
* and global configuration that should be shared across all
* versions.
*/
homeDir: string;
/**
* OS tmp dir.
*/
tmpDir: string,
}
export abstract class AbstractNativeEnvironmentService implements INativeEnvironmentService {
declare readonly _serviceBrand: undefined;
@memoize
get appRoot(): string { return dirname(FileAccess.asFileUri('', require).fsPath); }
@memoize
get userHome(): URI { return URI.file(this.paths.homeDir); }
@memoize
get userDataPath(): string { return this.paths.userDataDir; }
@memoize
get appSettingsHome(): URI { return URI.file(join(this.userDataPath, 'User')); }
@memoize
get tmpDir(): URI { return URI.file(this.paths.tmpDir); }
@memoize
get userRoamingDataHome(): URI { return this.appSettingsHome; }
@memoize
get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); }
@memoize
get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); }
get logsPath(): string {
if (!this.args.logsPath) {
const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '');
this.args.logsPath = join(this.userDataPath, 'logs', key);
}
return this.args.logsPath;
}
@memoize
get userDataSyncLogResource(): URI { return URI.file(join(this.logsPath, 'userDataSync.log')); }
@memoize
get sync(): 'on' | 'off' | undefined { return this.args.sync; }
@memoize
get machineSettingsResource(): URI { return joinPath(URI.file(join(this.userDataPath, 'Machine')), 'settings.json'); }
@memoize
get globalStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'globalStorage'); }
@memoize
get workspaceStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'workspaceStorage'); }
@memoize
get keybindingsResource(): URI { return joinPath(this.userRoamingDataHome, 'keybindings.json'); }
@memoize
get keyboardLayoutResource(): URI { return joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); }
@memoize
get argvResource(): URI {
const vscodePortable = env['VSCODE_PORTABLE'];
if (vscodePortable) {
return URI.file(join(vscodePortable, 'argv.json'));
}
return joinPath(this.userHome, this.productService.dataFolderName, 'argv.json');
}
@memoize
get snippetsHome(): URI { return joinPath(this.userRoamingDataHome, 'snippets'); }
@memoize
get isExtensionDevelopment(): boolean { return !!this.args.extensionDevelopmentPath; }
@memoize
get untitledWorkspacesHome(): URI { return URI.file(join(this.userDataPath, 'Workspaces')); }
@memoize
get installSourcePath(): string { return join(this.userDataPath, 'installSource'); }
@memoize
get builtinExtensionsPath(): string {
const cliBuiltinExtensionsDir = this.args['builtin-extensions-dir'];
if (cliBuiltinExtensionsDir) {
return resolve(cliBuiltinExtensionsDir);
}
return normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions'));
}
get extensionsDownloadPath(): string {
const cliExtensionsDownloadDir = this.args['extensions-download-dir'];
if (cliExtensionsDownloadDir) {
return resolve(cliExtensionsDownloadDir);
}
return join(this.userDataPath, 'CachedExtensionVSIXs');
}
@memoize
get extensionsPath(): string {
const cliExtensionsDir = this.args['extensions-dir'];
if (cliExtensionsDir) {
return resolve(cliExtensionsDir);
}
const vscodeExtensions = env['VSCODE_EXTENSIONS'];
if (vscodeExtensions) {
return vscodeExtensions;
}
const vscodePortable = env['VSCODE_PORTABLE'];
if (vscodePortable) {
return join(vscodePortable, 'extensions');
}
return joinPath(this.userHome, this.productService.dataFolderName, 'extensions').fsPath;
}
@memoize
get extensionDevelopmentLocationURI(): URI[] | undefined {
const extensionDevelopmentPaths = this.args.extensionDevelopmentPath;
if (Array.isArray(extensionDevelopmentPaths)) {
return extensionDevelopmentPaths.map(extensionDevelopmentPath => {
if (/^[^:/?#]+?:\/\//.test(extensionDevelopmentPath)) {
return URI.parse(extensionDevelopmentPath);
}
return URI.file(normalize(extensionDevelopmentPath));
});
}
return undefined;
}
@memoize
get extensionDevelopmentKind(): ExtensionKind[] | undefined {
return this.args.extensionDevelopmentKind?.map(kind => kind === 'ui' || kind === 'workspace' || kind === 'web' ? kind : 'workspace');
}
@memoize
get extensionTestsLocationURI(): URI | undefined {
const extensionTestsPath = this.args.extensionTestsPath;
if (extensionTestsPath) {
if (/^[^:/?#]+?:\/\//.test(extensionTestsPath)) {
return URI.parse(extensionTestsPath);
}
return URI.file(normalize(extensionTestsPath));
}
return undefined;
}
get disableExtensions(): boolean | string[] {
if (this.args['disable-extensions']) {
return true;
}
const disableExtensions = this.args['disable-extension'];
if (disableExtensions) {
if (typeof disableExtensions === 'string') {
return [disableExtensions];
}
if (Array.isArray(disableExtensions) && disableExtensions.length > 0) {
return disableExtensions;
}
}
return false;
}
@memoize
get debugExtensionHost(): IExtensionHostDebugParams { return parseExtensionHostPort(this.args, this.isBuilt); }
get debugRenderer(): boolean { return !!this.args.debugRenderer; }
get isBuilt(): boolean { return !env['VSCODE_DEV']; }
get verbose(): boolean { return !!this.args.verbose; }
get logLevel(): string | undefined { return this.args.log; }
@memoize
get serviceMachineIdResource(): URI { return joinPath(URI.file(this.userDataPath), 'machineid'); }
get crashReporterId(): string | undefined { return this.args['crash-reporter-id']; }
get crashReporterDirectory(): string | undefined { return this.args['crash-reporter-directory']; }
get driverHandle(): string | undefined { return this.args['driver']; }
@memoize
get telemetryLogResource(): URI { return URI.file(join(this.logsPath, 'telemetry.log')); }
get disableTelemetry(): boolean { return !!this.args['disable-telemetry']; }
get args(): NativeParsedArgs { return this._args; }
constructor(
private readonly _args: NativeParsedArgs,
private readonly paths: INativeEnvironmentPaths,
protected readonly productService: IProductService
) { }
}
export function parseExtensionHostPort(args: NativeParsedArgs, isBuild: boolean): IExtensionHostDebugParams {
return parseDebugPort(args['inspect-extensions'], args['inspect-brk-extensions'], 5870, isBuild, args.debugId);
}
export function parseSearchPort(args: NativeParsedArgs, isBuild: boolean): IDebugParams {
return parseDebugPort(args['inspect-search'], args['inspect-brk-search'], 5876, isBuild);
}
function parseDebugPort(debugArg: string | undefined, debugBrkArg: string | undefined, defaultBuildPort: number, isBuild: boolean, debugId?: string): IExtensionHostDebugParams {
const portStr = debugBrkArg || debugArg;
const port = Number(portStr) || (!isBuild ? defaultBuildPort : null);
const brk = port ? Boolean(!!debugBrkArg) : false;
return { port, break: brk, debugId };
}

View File

@@ -5,13 +5,12 @@
import { join } from 'vs/base/common/path';
import { memoize } from 'vs/base/common/decorators';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { createStaticIPCHandle } from 'vs/base/parts/ipc/node/ipc.net';
import product from 'vs/platform/product/common/product';
export const IEnvironmentMainService = createDecorator<IEnvironmentMainService>('nativeEnvironmentService');
export const IEnvironmentMainService = refineServiceDecorator<IEnvironmentService, IEnvironmentMainService>(IEnvironmentService);
/**
* A subclass of the `INativeEnvironmentService` to be used only in electron-main
@@ -26,9 +25,12 @@ export interface IEnvironmentMainService extends INativeEnvironmentService {
backupHome: string;
backupWorkspacesPath: string;
// --- V8 script cache path
// --- V8 script cache path (ours)
nodeCachedDataDir?: string;
// --- V8 script cache path (chrome)
chromeCachedDataDir: string;
// --- IPC
mainIPCHandle: string;
@@ -36,6 +38,7 @@ export interface IEnvironmentMainService extends INativeEnvironmentService {
sandbox: boolean;
driverVerbose: boolean;
disableUpdates: boolean;
disableKeytar: boolean;
}
export class EnvironmentMainService extends NativeEnvironmentService implements IEnvironmentMainService {
@@ -50,17 +53,23 @@ export class EnvironmentMainService extends NativeEnvironmentService implements
get backupWorkspacesPath(): string { return join(this.backupHome, 'workspaces.json'); }
@memoize
get mainIPCHandle(): string { return createStaticIPCHandle(this.userDataPath, 'main', product.version); }
get mainIPCHandle(): string { return createStaticIPCHandle(this.userDataPath, 'main', this.productService.version); }
@memoize
get sandbox(): boolean { return !!this._args['__sandbox']; }
get sandbox(): boolean { return !!this.args['__sandbox']; }
@memoize
get driverVerbose(): boolean { return !!this._args['driver-verbose']; }
get driverVerbose(): boolean { return !!this.args['driver-verbose']; }
@memoize
get disableUpdates(): boolean { return !!this._args['disable-updates']; }
get disableUpdates(): boolean { return !!this.args['disable-updates']; }
@memoize
get disableKeytar(): boolean { return !!this.args['disable-keytar']; }
@memoize
get nodeCachedDataDir(): string | undefined { return process.env['VSCODE_NODE_CACHED_DATA_DIR'] || undefined; }
@memoize
get chromeCachedDataDir(): string { return join(this.userDataPath, 'Code Cache'); }
}

View File

@@ -83,6 +83,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'locate-extension': { type: 'string[]' },
'extensionDevelopmentPath': { type: 'string[]' },
'extensionDevelopmentKind': { type: 'string[]' },
'extensionTestsPath': { type: 'string' },
'debugId': { type: 'string' },
'debugRenderer': { type: 'boolean' },
@@ -95,6 +96,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'skip-release-notes': { type: 'boolean' },
'disable-telemetry': { type: 'boolean' },
'disable-updates': { type: 'boolean' },
'disable-keytar': { type: 'boolean' },
'disable-crash-reporter': { type: 'boolean' },
'crash-reporter-directory': { type: 'string' },
'crash-reporter-id': { type: 'string' },
@@ -139,6 +141,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'ignore-certificate-errors': { type: 'boolean' },
'allow-insecure-localhost': { type: 'boolean' },
'log-net-log': { type: 'string' },
'vmodule': { type: 'string' },
'_urls': { type: 'string[]' },
_: { type: 'string[]' } // main arguments
@@ -269,7 +272,7 @@ export function formatOptions(options: OptionDescriptions<any>, columns: number)
}
function indent(count: number): string {
return (<any>' ').repeat(count);
return ' '.repeat(count);
}
function wrapText(text: string, columns: number): string[] {

View File

@@ -8,6 +8,7 @@ import { localize } from 'vs/nls';
import { MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files';
import { parseArgs, ErrorReporter, OPTIONS } from 'vs/platform/environment/node/argv';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { IProcessEnvironment } from 'vs/base/common/platform';
function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): NativeParsedArgs {
const errorReporter: ErrorReporter = {
@@ -79,6 +80,6 @@ export function addArg(argv: string[], ...args: string[]): string[] {
return argv;
}
export function isLaunchedFromCli(env: NodeJS.ProcessEnv): boolean {
export function isLaunchedFromCli(env: IProcessEnvironment): boolean {
return env['VSCODE_CLI'] === '1';
}

View File

@@ -3,254 +3,19 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import { IDebugParams, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { homedir, tmpdir } from 'os';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import * as paths from 'vs/base/node/paths';
import * as path from 'vs/base/common/path';
import * as resources from 'vs/base/common/resources';
import { memoize } from 'vs/base/common/decorators';
import product from 'vs/platform/product/common/product';
import { toLocalISOString } from 'vs/base/common/date';
import { FileAccess } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { createStaticIPCHandle } from 'vs/base/parts/ipc/node/ipc.net';
import { getUserDataPath } from 'vs/platform/environment/node/userDataPath';
import { AbstractNativeEnvironmentService } from 'vs/platform/environment/common/environmentService';
import { IProductService } from 'vs/platform/product/common/productService';
export class NativeEnvironmentService implements INativeEnvironmentService {
export class NativeEnvironmentService extends AbstractNativeEnvironmentService {
declare readonly _serviceBrand: undefined;
get args(): NativeParsedArgs { return this._args; }
@memoize
get appRoot(): string { return path.dirname(FileAccess.asFileUri('', require).fsPath); }
readonly logsPath: string;
@memoize
get userHome(): URI { return URI.file(os.homedir()); }
@memoize
get userDataPath(): string {
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return path.join(vscodePortable, 'user-data');
}
return parseUserDataDir(this._args, process);
}
@memoize
get appSettingsHome(): URI { return URI.file(path.join(this.userDataPath, 'User')); }
@memoize
get tmpDir(): URI { return URI.file(os.tmpdir()); }
@memoize
get userRoamingDataHome(): URI { return this.appSettingsHome; }
@memoize
get settingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'settings.json'); }
@memoize
get userDataSyncHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'sync'); }
@memoize
get userDataSyncLogResource(): URI { return URI.file(path.join(this.logsPath, 'userDataSync.log')); }
@memoize
get sync(): 'on' | 'off' | undefined { return this.args.sync; }
@memoize
get machineSettingsResource(): URI { return resources.joinPath(URI.file(path.join(this.userDataPath, 'Machine')), 'settings.json'); }
@memoize
get globalStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'globalStorage'); }
@memoize
get workspaceStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'workspaceStorage'); }
@memoize
get keybindingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'keybindings.json'); }
@memoize
get keyboardLayoutResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); }
@memoize
get argvResource(): URI {
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return URI.file(path.join(vscodePortable, 'argv.json'));
}
return resources.joinPath(this.userHome, product.dataFolderName, 'argv.json');
}
@memoize
get snippetsHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'snippets'); }
@memoize
get isExtensionDevelopment(): boolean { return !!this._args.extensionDevelopmentPath; }
@memoize
get untitledWorkspacesHome(): URI { return URI.file(path.join(this.userDataPath, 'Workspaces')); }
@memoize
get installSourcePath(): string { return path.join(this.userDataPath, 'installSource'); }
@memoize
get builtinExtensionsPath(): string {
const fromArgs = parsePathArg(this._args['builtin-extensions-dir'], process);
if (fromArgs) {
return fromArgs;
} else {
return path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions'));
}
}
get extensionsDownloadPath(): string {
const fromArgs = parsePathArg(this._args['extensions-download-dir'], process);
if (fromArgs) {
return fromArgs;
} else {
return path.join(this.userDataPath, 'CachedExtensionVSIXs');
}
}
@memoize
get extensionsPath(): string {
const fromArgs = parsePathArg(this._args['extensions-dir'], process);
if (fromArgs) {
return fromArgs;
}
const vscodeExtensions = process.env['VSCODE_EXTENSIONS'];
if (vscodeExtensions) {
return vscodeExtensions;
}
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return path.join(vscodePortable, 'extensions');
}
return resources.joinPath(this.userHome, product.dataFolderName, 'extensions').fsPath;
}
@memoize
get extensionDevelopmentLocationURI(): URI[] | undefined {
const s = this._args.extensionDevelopmentPath;
if (Array.isArray(s)) {
return s.map(p => {
if (/^[^:/?#]+?:\/\//.test(p)) {
return URI.parse(p);
}
return URI.file(path.normalize(p));
});
}
return undefined;
}
@memoize
get extensionTestsLocationURI(): URI | undefined {
const s = this._args.extensionTestsPath;
if (s) {
if (/^[^:/?#]+?:\/\//.test(s)) {
return URI.parse(s);
}
return URI.file(path.normalize(s));
}
return undefined;
}
get disableExtensions(): boolean | string[] {
if (this._args['disable-extensions']) {
return true;
}
const disableExtensions = this._args['disable-extension'];
if (disableExtensions) {
if (typeof disableExtensions === 'string') {
return [disableExtensions];
}
if (Array.isArray(disableExtensions) && disableExtensions.length > 0) {
return disableExtensions;
}
}
return false;
}
@memoize
get debugExtensionHost(): IExtensionHostDebugParams { return parseExtensionHostPort(this._args, this.isBuilt); }
get debugRenderer(): boolean { return !!this._args.debugRenderer; }
get isBuilt(): boolean { return !process.env['VSCODE_DEV']; }
get verbose(): boolean { return !!this._args.verbose; }
get logLevel(): string | undefined { return this._args.log; }
@memoize
get sharedIPCHandle(): string { return createStaticIPCHandle(this.userDataPath, 'shared', product.version); }
@memoize
get serviceMachineIdResource(): URI { return resources.joinPath(URI.file(this.userDataPath), 'machineid'); }
get crashReporterId(): string | undefined { return this._args['crash-reporter-id']; }
get crashReporterDirectory(): string | undefined { return this._args['crash-reporter-directory']; }
get driverHandle(): string | undefined { return this._args['driver']; }
@memoize
get telemetryLogResource(): URI { return URI.file(path.join(this.logsPath, 'telemetry.log')); }
get disableTelemetry(): boolean { return !!this._args['disable-telemetry']; }
constructor(protected _args: NativeParsedArgs) {
if (!_args.logsPath) {
const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '');
_args.logsPath = path.join(this.userDataPath, 'logs', key);
}
// {{SQL CARBON EDIT}} Note we keep the VSCODE_LOGS var above in case merges come in that use that so we don't
// break functionality. ADS code should always use ADS_LOGS when referring to the log path
if (!process.env['ADS_LOGS']) {
const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '');
process.env['ADS_LOGS'] = path.join(this.userDataPath, 'logs', key);
}
this.logsPath = process.env['ADS_LOGS']!;
constructor(args: NativeParsedArgs, productService: IProductService) {
super(args, {
homeDir: homedir(),
tmpDir: tmpdir(),
userDataDir: getUserDataPath(args)
}, productService);
}
}
export function parseExtensionHostPort(args: NativeParsedArgs, isBuild: boolean): IExtensionHostDebugParams {
return parseDebugPort(args['inspect-extensions'], args['inspect-brk-extensions'], 5870, isBuild, args.debugId);
}
export function parseSearchPort(args: NativeParsedArgs, isBuild: boolean): IDebugParams {
return parseDebugPort(args['inspect-search'], args['inspect-brk-search'], 5876, isBuild);
}
function parseDebugPort(debugArg: string | undefined, debugBrkArg: string | undefined, defaultBuildPort: number, isBuild: boolean, debugId?: string): IExtensionHostDebugParams {
const portStr = debugBrkArg || debugArg;
const port = Number(portStr) || (!isBuild ? defaultBuildPort : null);
const brk = port ? Boolean(!!debugBrkArg) : false;
return { port, break: brk, debugId };
}
export function parsePathArg(arg: string | undefined, process: NodeJS.Process): string | undefined {
if (!arg) {
return undefined;
}
// Determine if the arg is relative or absolute, if relative use the original CWD
// (VSCODE_CWD), not the potentially overridden one (process.cwd()).
const resolved = path.resolve(arg);
if (path.normalize(arg) === resolved) {
return resolved;
}
return path.resolve(process.env['VSCODE_CWD'] || process.cwd(), arg);
}
export function parseUserDataDir(args: NativeParsedArgs, process: NodeJS.Process): string {
return parsePathArg(args['user-data-dir'], process) || path.resolve(paths.getDefaultUserDataPath());
}

View File

@@ -0,0 +1,165 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { spawn } from 'child_process';
import { generateUuid } from 'vs/base/common/uuid';
import { IProcessEnvironment, isWindows, OS } from 'vs/base/common/platform';
import { ILogService } from 'vs/platform/log/common/log';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { getSystemShell } from 'vs/base/node/shell';
/**
* We need to get the environment from a user's shell.
* This should only be done when Code itself is not launched
* from within a shell.
*/
export async function resolveShellEnv(logService: ILogService, args: NativeParsedArgs, env: IProcessEnvironment): Promise<typeof process.env> {
// Skip if --force-disable-user-env
if (args['force-disable-user-env']) {
logService.trace('resolveShellEnv(): skipped (--force-disable-user-env)');
return {};
}
// Skip on windows
else if (isWindows) {
logService.trace('resolveShellEnv(): skipped (Windows)');
return {};
}
// Skip if running from CLI already
else if (isLaunchedFromCli(env) && !args['force-user-env']) {
logService.trace('resolveShellEnv(): skipped (VSCODE_CLI is set)');
return {};
}
// Otherwise resolve (macOS, Linux)
else {
if (isLaunchedFromCli(env)) {
logService.trace('resolveShellEnv(): running (--force-user-env)');
} else {
logService.trace('resolveShellEnv(): running (macOS/Linux)');
}
if (!unixShellEnvPromise) {
unixShellEnvPromise = doResolveUnixShellEnv(logService);
}
return unixShellEnvPromise;
}
}
let unixShellEnvPromise: Promise<typeof process.env> | undefined = undefined;
async function doResolveUnixShellEnv(logService: ILogService): Promise<typeof process.env> {
const promise = new Promise<typeof process.env>(async (resolve, reject) => {
const runAsNode = process.env['ELECTRON_RUN_AS_NODE'];
logService.trace('getUnixShellEnvironment#runAsNode', runAsNode);
const noAttach = process.env['ELECTRON_NO_ATTACH_CONSOLE'];
logService.trace('getUnixShellEnvironment#noAttach', noAttach);
const mark = generateUuid().replace(/-/g, '').substr(0, 12);
const regex = new RegExp(mark + '(.*)' + mark);
const env = {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
ELECTRON_NO_ATTACH_CONSOLE: '1'
};
logService.trace('getUnixShellEnvironment#env', env);
const systemShellUnix = await getSystemShell(OS, env);
logService.trace('getUnixShellEnvironment#shell', systemShellUnix);
// handle popular non-POSIX shells
const name = path.basename(systemShellUnix);
let command: string, shellArgs: Array<string>;
if (/^pwsh(-preview)?$/.test(name)) {
// Older versions of PowerShell removes double quotes sometimes so we use "double single quotes" which is how
// you escape single quotes inside of a single quoted string.
command = `& '${process.execPath}' -p '''${mark}'' + JSON.stringify(process.env) + ''${mark}'''`;
shellArgs = ['-Login', '-Command'];
} else {
command = `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`;
shellArgs = ['-ilc'];
}
logService.trace('getUnixShellEnvironment#spawn', JSON.stringify(shellArgs), command);
const child = spawn(systemShellUnix, [...shellArgs, command], {
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
env
});
child.on('error', err => {
logService.error('getUnixShellEnvironment#errorChildProcess', toErrorMessage(err));
resolve({});
});
const buffers: Buffer[] = [];
child.stdout.on('data', b => buffers.push(b));
const stderr: Buffer[] = [];
child.stderr.on('data', b => stderr.push(b));
child.on('close', (code, signal) => {
const raw = Buffer.concat(buffers).toString('utf8');
logService.trace('getUnixShellEnvironment#raw', raw);
const stderrStr = Buffer.concat(stderr).toString('utf8');
if (stderrStr.trim()) {
logService.trace('getUnixShellEnvironment#stderr', stderrStr);
}
if (code || signal) {
return reject(new Error(`Failed to get environment (code ${code}, signal ${signal})`));
}
const match = regex.exec(raw);
const rawStripped = match ? match[1] : '{}';
try {
const env = JSON.parse(rawStripped);
if (runAsNode) {
env['ELECTRON_RUN_AS_NODE'] = runAsNode;
} else {
delete env['ELECTRON_RUN_AS_NODE'];
}
if (noAttach) {
env['ELECTRON_NO_ATTACH_CONSOLE'] = noAttach;
} else {
delete env['ELECTRON_NO_ATTACH_CONSOLE'];
}
// https://github.com/microsoft/vscode/issues/22593#issuecomment-336050758
delete env['XDG_RUNTIME_DIR'];
logService.trace('getUnixShellEnvironment#result', env);
resolve(env);
} catch (err) {
logService.error('getUnixShellEnvironment#errorCaught', toErrorMessage(err));
reject(err);
}
});
});
try {
return await promise;
} catch (error) {
logService.error('getUnixShellEnvironment#error', toErrorMessage(error));
return {}; // ignore any errors
}
}

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
/**
* Returns the user data path to use with some rules:
* - respect portable mode
* - respect --user-data-dir CLI argument
* - respect VSCODE_APPDATA environment variable
*/
export function getUserDataPath(args: NativeParsedArgs): string;

View File

@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path="../../../../typings/require.d.ts" />
//@ts-check
(function () {
'use strict';
/**
* @typedef {import('../../environment/common/argv').NativeParsedArgs} NativeParsedArgs
*
* @param {typeof import('path')} path
* @param {typeof import('os')} os
* @param {string} productName
* @param {string} cwd
*/
function factory(path, os, productName, cwd) {
/**
* @param {NativeParsedArgs} cliArgs
*
* @returns {string}
*/
function getUserDataPath(cliArgs) {
const userDataPath = doGetUserDataPath(cliArgs);
const pathsToResolve = [userDataPath];
// If the user-data-path is not absolute, make
// sure to resolve it against the passed in
// current working directory. We cannot use the
// node.js `path.resolve()` logic because it will
// not pick up our `VSCODE_CWD` environment variable
// (https://github.com/microsoft/vscode/issues/120269)
if (!path.isAbsolute(userDataPath)) {
pathsToResolve.unshift(cwd);
}
return path.resolve(...pathsToResolve);
}
/**
* @param {NativeParsedArgs} cliArgs
*
* @returns {string}
*/
function doGetUserDataPath(cliArgs) {
// 1. Support portable mode
const portablePath = process.env['VSCODE_PORTABLE'];
if (portablePath) {
return path.join(portablePath, 'user-data');
}
// 2. Support explicit --user-data-dir
const cliPath = cliArgs['user-data-dir'];
if (cliPath) {
return cliPath;
}
// 3. Support global VSCODE_APPDATA environment variable
let appDataPath = process.env['VSCODE_APPDATA'];
// 4. Otherwise check per platform
if (!appDataPath) {
switch (process.platform) {
case 'win32':
appDataPath = process.env['APPDATA'];
if (!appDataPath) {
const userProfile = process.env['USERPROFILE'];
if (typeof userProfile !== 'string') {
throw new Error('Windows: Unexpected undefined %USERPROFILE% environment variable');
}
appDataPath = path.join(userProfile, 'AppData', 'Roaming');
}
break;
case 'darwin':
appDataPath = path.join(os.homedir(), 'Library', 'Application Support');
break;
case 'linux':
appDataPath = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
break;
default:
throw new Error('Platform not supported');
}
}
return path.join(appDataPath, 'azuredatastudio'); // {{SQL CARBON EDIT}} hard-code Azure Data Studio
}
return {
getUserDataPath
};
}
if (typeof define === 'function') {
define(['require', 'path', 'os', 'vs/base/common/network', 'vs/base/common/resources', 'vs/base/common/process'], function (
require,
/** @type {typeof import('path')} */ path,
/** @type {typeof import('os')} */ os,
/** @type {typeof import('../../../base/common/network')} */ network,
/** @type {typeof import("../../../base/common/resources")} */ resources,
/** @type {typeof import("../../../base/common/process")} */ process
) {
const rootPath = resources.dirname(network.FileAccess.asFileUri('', require));
const pkg = require.__$__nodeRequire(resources.joinPath(rootPath, 'package.json').fsPath);
return factory(path, os, pkg.name, process.cwd());
}); // amd
} else if (typeof module === 'object' && typeof module.exports === 'object') {
const pkg = require('../../../../../package.json');
const path = require('path');
const os = require('os');
module.exports = factory(path, os, pkg.name, process.env['VSCODE_CWD'] || process.cwd()); // commonjs
} else {
throw new Error('Unknown context');
}
}());

View File

@@ -3,15 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small.
*/
import * as path from 'vs/base/common/path';
import * as os from 'os';
import * as fs from 'fs';
import { tmpdir } from 'os';
import { join } from 'vs/base/common/path';
export function createWaitMarkerFile(verbose?: boolean): string | undefined {
const randomWaitMarkerPath = path.join(os.tmpdir(), Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10));
const randomWaitMarkerPath = join(tmpdir(), Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10));
try {
fs.writeFileSync(randomWaitMarkerPath, ''); // use built-in fs to avoid dragging in more dependencies

View File

@@ -16,13 +16,13 @@ suite('formatOptions', () => {
}
test('Text should display small columns correctly', () => {
assert.deepEqual(
assert.deepStrictEqual(
formatOptions({
'add': o('bar')
}, 80),
[' --add bar']
);
assert.deepEqual(
assert.deepStrictEqual(
formatOptions({
'add': o('bar'),
'wait': o('ba'),
@@ -36,7 +36,7 @@ suite('formatOptions', () => {
});
test('Text should wrap', () => {
assert.deepEqual(
assert.deepStrictEqual(
formatOptions({
'add': o((<any>'bar ').repeat(9))
}, 40),
@@ -47,7 +47,7 @@ suite('formatOptions', () => {
});
test('Text should revert to the condensed view when the terminal is too narrow', () => {
assert.deepEqual(
assert.deepStrictEqual(
formatOptions({
'add': o((<any>'bar ').repeat(9))
}, 30),
@@ -58,11 +58,11 @@ suite('formatOptions', () => {
});
test('addArg', () => {
assert.deepEqual(addArg([], 'foo'), ['foo']);
assert.deepEqual(addArg([], 'foo', 'bar'), ['foo', 'bar']);
assert.deepEqual(addArg(['foo'], 'bar'), ['foo', 'bar']);
assert.deepEqual(addArg(['--wait'], 'bar'), ['--wait', 'bar']);
assert.deepEqual(addArg(['--wait', '--', '--foo'], 'bar'), ['--wait', 'bar', '--', '--foo']);
assert.deepEqual(addArg(['--', '--foo'], 'bar'), ['bar', '--', '--foo']);
assert.deepStrictEqual(addArg([], 'foo'), ['foo']);
assert.deepStrictEqual(addArg([], 'foo', 'bar'), ['foo', 'bar']);
assert.deepStrictEqual(addArg(['foo'], 'bar'), ['foo', 'bar']);
assert.deepStrictEqual(addArg(['--wait'], 'bar'), ['--wait', 'bar']);
assert.deepStrictEqual(addArg(['--wait', '--', '--foo'], 'bar'), ['--wait', 'bar', '--', '--foo']);
assert.deepStrictEqual(addArg(['--', '--foo'], 'bar'), ['bar', '--', '--foo']);
});
});

View File

@@ -4,9 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as path from 'vs/base/common/path';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { parseExtensionHostPort, parseUserDataDir } from 'vs/platform/environment/node/environmentService';
import { parseExtensionHostPort } from 'vs/platform/environment/common/environmentService';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import product from 'vs/platform/product/common/product';
suite('EnvironmentService', () => {
@@ -44,15 +45,6 @@ suite('EnvironmentService', () => {
assert.deepStrictEqual(parse(['--inspect-extensions=1234', '--inspect-brk-extensions=5678', '--debugId=7']), { port: 5678, break: true, debugId: '7' });
});
test('userDataPath', () => {
const parse = (a: string[], b: { cwd: () => string, env: { [key: string]: string } }) => parseUserDataDir(parseArgs(a, OPTIONS), <any>b);
assert.equal(parse(['--user-data-dir', './dir'], { cwd: () => '/foo', env: {} }), path.resolve('/foo/dir'),
'should use cwd when --user-data-dir is specified');
assert.equal(parse(['--user-data-dir', './dir'], { cwd: () => '/foo', env: { 'VSCODE_CWD': '/bar' } }), path.resolve('/bar/dir'),
'should use VSCODE_CWD as the cwd when --user-data-dir is specified');
});
// https://github.com/microsoft/vscode/issues/78440
test('careful with boolean file names', function () {
let actual = parseArgs(['-r', 'arg.txt'], OPTIONS);
@@ -63,4 +55,15 @@ suite('EnvironmentService', () => {
assert(actual['reuse-window']);
assert.deepStrictEqual(actual._, ['true.txt']);
});
test('userDataDir', () => {
const service1 = new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), { _serviceBrand: undefined, ...product });
assert.ok(service1.userDataPath.length > 0);
const args = parseArgs(process.argv, OPTIONS);
args['user-data-dir'] = '/userDataDir/folder';
const service2 = new NativeEnvironmentService(args, { _serviceBrand: undefined, ...product });
assert.notStrictEqual(service1.userDataPath, service2.userDataPath);
});
});

View File

@@ -37,9 +37,9 @@ suite('Native Modules (all platforms)', () => {
assert.ok(typeof spdlog.createRotatingLogger === 'function', testErrorMessage('spdlog'));
});
test('vscode-nsfw', async () => {
const nsfWatcher = await import('vscode-nsfw');
assert.ok(typeof nsfWatcher === 'function', testErrorMessage('vscode-nsfw'));
test('nsfw', async () => {
const nsfWatcher = await import('nsfw');
assert.ok(typeof nsfWatcher === 'function', testErrorMessage('nsfw'));
});
test('vscode-sqlite3', async () => {

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* 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 { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv';
import { getUserDataPath } from 'vs/platform/environment/node/userDataPath';
suite('User data path', () => {
test('getUserDataPath - default', () => {
const path = getUserDataPath(parseArgs(process.argv, OPTIONS));
assert.ok(path.length > 0);
});
test('getUserDataPath - portable mode', () => {
const origPortable = process.env['VSCODE_PORTABLE'];
try {
const portableDir = 'portable-dir';
process.env['VSCODE_PORTABLE'] = portableDir;
const path = getUserDataPath(parseArgs(process.argv, OPTIONS));
assert.ok(path.includes(portableDir));
} finally {
if (typeof origPortable === 'string') {
process.env['VSCODE_PORTABLE'] = origPortable;
} else {
delete process.env['VSCODE_PORTABLE'];
}
}
});
test('getUserDataPath - --user-data-dir', () => {
const cliUserDataDir = 'cli-data-dir';
const args = parseArgs(process.argv, OPTIONS);
args['user-data-dir'] = cliUserDataDir;
const path = getUserDataPath(args);
assert.ok(path.includes(cliUserDataDir));
});
test('getUserDataPath - VSCODE_APPDATA', () => {
const origAppData = process.env['VSCODE_APPDATA'];
try {
const appDataDir = 'appdata-dir';
process.env['VSCODE_APPDATA'] = appDataDir;
const path = getUserDataPath(parseArgs(process.argv, OPTIONS));
assert.ok(path.includes(appDataDir));
} finally {
if (typeof origAppData === 'string') {
process.env['VSCODE_APPDATA'] = origAppData;
} else {
delete process.env['VSCODE_APPDATA'];
}
}
});
});

View File

@@ -108,6 +108,7 @@ export interface ILocalExtension extends IExtension {
isMachineScoped: boolean;
publisherId: string | null;
publisherDisplayName: string | null;
installedTimestamp?: number;
}
export const enum SortBy {

View File

@@ -10,11 +10,10 @@ 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 { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest } 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');
@@ -47,8 +46,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
constructor(
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@ILocalizationsService private readonly localizationsService: ILocalizationsService
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService
) { }
protected get location(): string | undefined {
@@ -135,7 +133,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
if (vsixs.length) {
await Promise.all(vsixs.map(async vsix => {
try {
const manifest = await this.installVSIX(vsix, force, output);
const manifest = await this.installVSIX(vsix, { isBuiltin: false, isMachineScoped }, force, output);
if (manifest) {
installedExtensionsManifests.push(manifest);
}
@@ -170,16 +168,12 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
}
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> {
private async installVSIX(vsix: URI, installOptions: InstallOptions, force: boolean, output: CLIOutput): Promise<IExtensionManifest | null> {
const manifest = await this.extensionManagementService.getManifest(vsix);
if (!manifest) {
@@ -189,7 +183,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
const valid = await this.validateVSIX(manifest, force, output);
if (valid) {
try {
await this.extensionManagementService.install(vsix);
await this.extensionManagementService.install(vsix, installOptions);
output.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix)));
return manifest;
} catch (error) {
@@ -315,10 +309,6 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
}
}
if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) {
await this.updateLocalizationsCache();
}
}
public async locateExtension(extensions: string[], output: CLIOutput = console): Promise<void> {
@@ -335,11 +325,6 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
});
}
private updateLocalizationsCache(): Promise<boolean> {
return this.localizationsService.update();
}
private notInstalled(id: string) {
return this.location ? localize('notInstalleddOnLocation', "Extension '{0}' is not installed on {1}.", id, this.location) : localize('notInstalled', "Extension '{0}' is not installed.", id);
}

View File

@@ -4,7 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IProductService, IConfigBasedExtensionTip as IRawConfigBasedExtensionTip } from 'vs/platform/product/common/productService';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConfigBasedExtensionTip as IRawConfigBasedExtensionTip } from 'vs/base/common/product';
import { IFileService } from 'vs/platform/files/common/files';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IExtensionTipsService, IExecutableBasedExtensionTip, IWorkspaceTips, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';

View File

@@ -7,7 +7,7 @@ import { URI } from 'vs/base/common/uri';
import { basename, join, } from 'vs/base/common/path';
import { IProductService } from 'vs/platform/product/common/productService';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { env } from 'vs/base/common/process';
import { IFileService } from 'vs/platform/files/common/files';
import { isWindows } from 'vs/base/common/platform';
import { isNonEmptyArray } from 'vs/base/common/arrays';
@@ -40,7 +40,7 @@ const lastPromptedMediumImpExeTimeStorageKey = 'extensionTips/lastPromptedMedium
export class ExtensionTipsService extends BaseExtensionTipsService {
_serviceBrand: any;
override _serviceBrand: any;
private readonly highImportanceExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
private readonly mediumImportanceExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
@@ -101,13 +101,13 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
});
}
async getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
override async getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
const highImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.highImportanceExecutableTips);
const mediumImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.mediumImportanceExecutableTips);
return [...highImportanceExeTips, ...mediumImportanceExeTips];
}
getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
override getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return this.getValidExecutableBasedExtensionTips(this.allOtherExecutableTips);
}
@@ -294,11 +294,11 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
const exePaths: string[] = [];
if (isWindows) {
if (extensionTip.windowsPath) {
exePaths.push(extensionTip.windowsPath.replace('%USERPROFILE%', process.env['USERPROFILE']!)
.replace('%ProgramFiles(x86)%', process.env['ProgramFiles(x86)']!)
.replace('%ProgramFiles%', process.env['ProgramFiles']!)
.replace('%APPDATA%', process.env['APPDATA']!)
.replace('%WINDIR%', process.env['WINDIR']!));
exePaths.push(extensionTip.windowsPath.replace('%USERPROFILE%', env['USERPROFILE']!)
.replace('%ProgramFiles(x86)%', env['ProgramFiles(x86)']!)
.replace('%ProgramFiles%', env['ProgramFiles']!)
.replace('%APPDATA%', env['APPDATA']!)
.replace('%WINDIR%', env['WINDIR']!));
}
} else {
exePaths.push(join('/usr/local/bin', exeName));

View File

@@ -16,6 +16,7 @@ import { generateUuid } from 'vs/base/common/uuid';
import * as semver from 'vs/base/common/semver/semver';
import { isWindows } from 'vs/base/common/platform';
import { Promises } from 'vs/base/common/async';
import { getErrorMessage } from 'vs/base/common/errors';
const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/;
@@ -45,13 +46,26 @@ export class ExtensionsDownloader extends Disposable {
// Download only if vsix does not exist
if (!await this.fileService.exists(location)) {
// Download to temporary location first only if vsix does not exist
const tempLocation = joinPath(this.extensionsDownloadDir, `.${vsixName}`);
const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`);
if (!await this.fileService.exists(tempLocation)) {
await this.extensionGalleryService.download(extension, tempLocation, operation);
}
// Rename temp location to original
await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
try {
// Rename temp location to original
await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
} catch (error) {
try {
await this.fileService.del(tempLocation);
} catch (e) { /* ignore */ }
if (error.code === 'ENOTEMPTY') {
this.logService.info(`Rename failed because vsix was downloaded by another source. So ignoring renaming.`, extension.identifier.id);
} else {
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the vsix from downloaded location`, tempLocation.path);
throw error;
}
}
}
return location;

View File

@@ -47,6 +47,8 @@ import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
import { ExtensionsScanner, ILocalExtensionManifest, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner';
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher';
import { IFileService } from 'vs/platform/files/common/files';
const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled';
const INSTALL_ERROR_DOWNLOADING = 'downloading';
@@ -91,12 +93,19 @@ export class ExtensionManagementService extends Disposable implements IExtension
@optional(IDownloadService) private downloadService: IDownloadService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IInstantiationService instantiationService: IInstantiationService,
@IFileService fileService: IFileService,
) {
super();
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension)));
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader));
const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService));
this._register(extensionsWatcher.onDidChangeExtensionsByAnotherSource(({ added, removed }) => {
added.forEach(extension => this._onDidInstallExtension.fire({ identifier: extension.identifier, operation: InstallOperation.None, local: extension }));
removed.forEach(extension => this._onDidUninstallExtension.fire({ identifier: extension }));
}));
this._register(toDisposable(() => {
this.installingExtensions.forEach(promise => promise.cancel());
@@ -327,7 +336,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
if (existingExtension && semver.neq(existingExtension.manifest.version, extension.version)) {
await this.setUninstalled(existingExtension);
await this.extensionsScanner.setUninstalled(existingExtension);
}
this.logService.info(`Extensions installed successfully:`, extension.identifier.id);
@@ -371,7 +380,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
throw new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled"));
}
await this.setUninstalled(extension);
await this.extensionsScanner.setUninstalled(extension);
try {
await this.extensionsScanner.removeUninstalledExtension(extension);
} catch (e) {
@@ -397,12 +406,11 @@ export class ExtensionManagementService extends Disposable implements IExtension
publisherDisplayName: extension.publisherDisplayName,
};
let zipPath;
let zipPath: string | undefined;
try {
this.logService.trace('Started downloading extension:', extension.identifier.id);
const zip = await this.extensionsDownloader.downloadExtension(extension, operation);
zipPath = (await this.extensionsDownloader.downloadExtension(extension, operation)).fsPath;
this.logService.info('Downloaded extension:', extension.identifier.id, zipPath);
zipPath = zip.fsPath;
} catch (error) {
throw new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_DOWNLOADING);
}
@@ -439,11 +447,10 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.logService.trace('Removing the extension from uninstalled list:', identifierWithVersion.id);
// If the same version of extension is marked as uninstalled, remove it from there and return the local.
await this.unsetUninstalled(identifierWithVersion);
const local = await this.extensionsScanner.setInstalled(identifierWithVersion);
this.logService.info('Removed the extension from uninstalled list:', identifierWithVersion.id);
const installed = await this.getInstalled(ExtensionType.User);
return installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null;
return local;
}
private async extractAndInstall({ zipPath, identifierWithVersion, metadata }: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
@@ -664,7 +671,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
// Set all versions of the extension as uninstalled
promise = createCancelablePromise(async () => {
const userExtensions = await this.extensionsScanner.scanUserExtensions(false);
await this.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier)));
await this.extensionsScanner.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier)));
});
this.uninstallingExtensions.set(local.identifier.id, promise);
promise.finally(() => this.uninstallingExtensions.delete(local.identifier.id));
@@ -702,28 +709,15 @@ export class ExtensionManagementService extends Disposable implements IExtension
return uninstalled.length === 1;
}
private filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise<string[]> {
return this.extensionsScanner.withUninstalledExtensions(allUninstalled => {
const uninstalled: string[] = [];
for (const identifier of identifiers) {
if (!!allUninstalled[identifier.key()]) {
uninstalled.push(identifier.key());
}
private async filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise<string[]> {
const uninstalled: string[] = [];
const allUninstalled = await this.extensionsScanner.getUninstalledExtensions();
for (const identifier of identifiers) {
if (!!allUninstalled[identifier.key()]) {
uninstalled.push(identifier.key());
}
return uninstalled;
});
}
private setUninstalled(...extensions: ILocalExtension[]): Promise<{ [id: string]: boolean }> {
const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version));
return this.extensionsScanner.withUninstalledExtensions(uninstalled => {
ids.forEach(id => uninstalled[id.key()] = true);
return uninstalled;
});
}
private unsetUninstalled(extensionIdentifier: ExtensionIdentifierWithVersion): Promise<void> {
return this.extensionsScanner.withUninstalledExtensions<void>(uninstalled => delete uninstalled[extensionIdentifier.key()]);
}
return uninstalled;
}
getExtensionsReport(): Promise<IReportedExtension[]> {

View File

@@ -25,8 +25,6 @@ type IExeBasedExtensionTips = {
export class ExtensionTipsService extends BaseExtensionTipsService {
_serviceBrand: any;
private readonly allImportantExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
private readonly allOtherExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
@@ -59,11 +57,11 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
}
}
getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
override getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return this.getValidExecutableBasedExtensionTips(this.allImportantExecutableTips);
}
getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
override getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return this.getValidExecutableBasedExtensionTips(this.allOtherExecutableTips);
}

View File

@@ -24,6 +24,10 @@ import { isWindows } from 'vs/base/common/platform';
import { flatten } from 'vs/base/common/arrays';
import { IStringDictionary } from 'vs/base/common/collections';
import { FileAccess } from 'vs/base/common/network';
import { IFileService } from 'vs/platform/files/common/files';
import { basename } from 'vs/base/common/resources';
import { generateUuid } from 'vs/base/common/uuid';
import { getErrorMessage } from 'vs/base/common/errors';
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser';
@@ -31,7 +35,8 @@ const INSTALL_ERROR_EXTRACTING = 'extracting';
const INSTALL_ERROR_DELETING = 'deleting';
const INSTALL_ERROR_RENAMING = 'renaming';
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean }>;
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; }>;
type IStoredMetadata = IMetadata & { installedTimestamp: number | undefined };
export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: IMetadata };
type IRelaxedLocalExtension = Omit<ILocalExtension, 'isBuiltin'> & { isBuiltin: boolean };
@@ -44,6 +49,7 @@ export class ExtensionsScanner extends Disposable {
constructor(
private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>,
@IFileService private readonly fileService: IFileService,
@ILogService private readonly logService: ILogService,
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IProductService private readonly productService: IProductService,
@@ -97,7 +103,7 @@ export class ExtensionsScanner extends Disposable {
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise<ILocalExtension> {
const folderName = identifierWithVersion.key();
const tempPath = path.join(this.extensionsPath, `.${folderName}`);
const tempPath = path.join(this.extensionsPath, `.${generateUuid()}`);
const extensionPath = path.join(this.extensionsPath, folderName);
try {
@@ -110,20 +116,29 @@ export class ExtensionsScanner extends Disposable {
}
await this.extractAtLocation(identifierWithVersion, zipPath, tempPath, token);
let local = await this.scanExtension(URI.file(tempPath), ExtensionType.User);
if (!local) {
throw new Error(localize('cannot read', "Cannot read the extension from {0}", tempPath));
}
await this.storeMetadata(local, { installedTimestamp: Date.now() });
try {
await this.rename(identifierWithVersion, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
this.logService.info('Renamed to', extensionPath);
} catch (error) {
this.logService.info('Rename failed. Deleting from extracted location', tempPath);
try {
pfs.rimraf(tempPath);
await pfs.rimraf(tempPath);
} catch (e) { /* ignore */ }
throw error;
if (error.code === 'ENOTEMPTY') {
this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, identifierWithVersion.id);
} else {
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath);
throw error;
}
}
let local: ILocalExtension | null = null;
try {
local = await this.scanExtension(folderName, this.extensionsPath, ExtensionType.User);
local = await this.scanExtension(URI.file(extensionPath), ExtensionType.User);
} catch (e) { /*ignore */ }
if (local) {
@@ -134,23 +149,46 @@ export class ExtensionsScanner extends Disposable {
async saveMetadataForLocalExtension(local: ILocalExtension, metadata: IMetadata): Promise<ILocalExtension> {
this.setMetadata(local, metadata);
await this.storeMetadata(local, { ...metadata, installedTimestamp: local.installedTimestamp });
return local;
}
private async storeMetadata(local: ILocalExtension, storedMetadata: IStoredMetadata): Promise<ILocalExtension> {
// unset if false
metadata.isMachineScoped = metadata.isMachineScoped || undefined;
metadata.isBuiltin = metadata.isBuiltin || undefined;
storedMetadata.isMachineScoped = storedMetadata.isMachineScoped || undefined;
storedMetadata.isBuiltin = storedMetadata.isBuiltin || undefined;
storedMetadata.installedTimestamp = storedMetadata.installedTimestamp || undefined;
const manifestPath = path.join(local.location.fsPath, 'package.json');
const raw = await fs.promises.readFile(manifestPath, 'utf8');
const { manifest } = await this.parseManifest(raw);
(manifest as ILocalExtensionManifest).__metadata = metadata;
(manifest as ILocalExtensionManifest).__metadata = storedMetadata;
await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
return local;
}
getUninstalledExtensions(): Promise<{ [id: string]: boolean; }> {
return this.withUninstalledExtensions(uninstalled => uninstalled);
getUninstalledExtensions(): Promise<IStringDictionary<boolean>> {
return this.withUninstalledExtensions();
}
async withUninstalledExtensions<T>(fn: (uninstalled: IStringDictionary<boolean>) => T): Promise<T> {
async setUninstalled(...extensions: ILocalExtension[]): Promise<void> {
const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version));
await this.withUninstalledExtensions(uninstalled => {
ids.forEach(id => uninstalled[id.key()] = true);
});
}
async setInstalled(identifierWithVersion: ExtensionIdentifierWithVersion): Promise<ILocalExtension | null> {
await this.withUninstalledExtensions(uninstalled => delete uninstalled[identifierWithVersion.key()]);
const installed = await this.scanExtensions(ExtensionType.User);
const localExtension = installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null;
if (!localExtension) {
return null;
}
await this.storeMetadata(localExtension, { installedTimestamp: Date.now() });
return this.scanExtension(localExtension.location, ExtensionType.User);
}
private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary<boolean>) => void): Promise<IStringDictionary<boolean>> {
return this.uninstalledFileLimiter.queue(async () => {
let raw: string | undefined;
try {
@@ -168,15 +206,16 @@ export class ExtensionsScanner extends Disposable {
} catch (e) { /* ignore */ }
}
const result = fn(uninstalled);
if (Object.keys(uninstalled).length) {
await pfs.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
} else {
await pfs.rimraf(this.uninstalledPath);
if (updateFn) {
updateFn(uninstalled);
if (Object.keys(uninstalled).length) {
await pfs.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
} else {
await pfs.rimraf(this.uninstalledPath);
}
}
return result;
return uninstalled;
});
}
@@ -237,33 +276,41 @@ export class ExtensionsScanner extends Disposable {
private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise<ILocalExtension[]> {
const limiter = new Limiter<any>(10);
const extensionsFolders = await pfs.readdir(dir);
const extensions = await Promise.all<ILocalExtension>(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, dir, type))));
return extensions.filter(e => e && e.identifier);
const stat = await this.fileService.resolve(URI.file(dir));
if (stat.children) {
const extensions = await Promise.all<ILocalExtension>(stat.children.filter(c => c.isDirectory)
.map(c => limiter.queue(async () => {
if (type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
return null;
}
return this.scanExtension(c.resource, type);
})));
return extensions.filter(e => e && e.identifier);
}
return [];
}
private async scanExtension(folderName: string, root: string, type: ExtensionType): Promise<ILocalExtension | null> {
if (type === ExtensionType.User && folderName.indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
return null;
}
const extensionPath = path.join(root, folderName);
private async scanExtension(extensionLocation: URI, type: ExtensionType): Promise<ILocalExtension | null> {
try {
const children = await pfs.readdir(extensionPath);
const { manifest, metadata } = await this.readManifest(extensionPath);
const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)) : undefined;
const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)) : undefined;
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
const local = <ILocalExtension>{ type, identifier, manifest, location: URI.file(extensionPath), readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
if (metadata) {
this.setMetadata(local, metadata);
const stat = await this.fileService.resolve(extensionLocation);
if (stat.children) {
const { manifest, metadata } = await this.readManifest(extensionLocation.fsPath);
const readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource;
const changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource;
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
const local = <ILocalExtension>{ type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
if (metadata) {
this.setMetadata(local, metadata);
local.installedTimestamp = metadata.installedTimestamp;
}
return local;
}
return local;
} catch (e) {
this.logService.trace(e);
return null;
if (type !== ExtensionType.System) {
this.logService.trace(e);
}
}
return null;
}
private async scanDefaultSystemExtensions(): Promise<ILocalExtension[]> {
@@ -344,7 +391,7 @@ export class ExtensionsScanner extends Disposable {
return this._devSystemExtensionsPath;
}
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IStoredMetadata | null; }> {
const promises = [
fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8')
.then(raw => this.parseManifest(raw)),

View File

@@ -0,0 +1,143 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { DidInstallExtensionEvent, DidUninstallExtensionEvent, IExtensionManagementService, ILocalExtension, InstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Emitter, Event } from 'vs/base/common/event';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { FileChangeType, FileSystemProviderCapabilities, IFileChange, IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtUri } from 'vs/base/common/resources';
import { ILogService } from 'vs/platform/log/common/log';
export class ExtensionsWatcher extends Disposable {
private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<{ added: ILocalExtension[], removed: IExtensionIdentifier[] }>());
readonly onDidChangeExtensionsByAnotherSource = this._onDidChangeExtensionsByAnotherSource.event;
private startTimestamp = 0;
private installingExtensions: IExtensionIdentifier[] = [];
private installedExtensions: IExtensionIdentifier[] | undefined;
constructor(
private readonly extensionsManagementService: IExtensionManagementService,
@IFileService fileService: IFileService,
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@ILogService private readonly logService: ILogService,
) {
super();
this.extensionsManagementService.getInstalled(ExtensionType.User).then(extensions => {
this.installedExtensions = extensions.map(e => e.identifier);
this.startTimestamp = Date.now();
});
this._register(extensionsManagementService.onInstallExtension(e => this.onInstallExtension(e)));
this._register(extensionsManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e)));
this._register(extensionsManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e)));
const extensionsResource = URI.file(environmentService.extensionsPath);
const extUri = new ExtUri(resource => !fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive));
this._register(fileService.watch(extensionsResource));
this._register(Event.filter(fileService.onDidFilesChange, e => e.changes.some(change => this.doesChangeAffects(change, extensionsResource, extUri)))(() => this.onDidChange()));
}
private doesChangeAffects(change: IFileChange, extensionsResource: URI, extUri: ExtUri): boolean {
// Is not immediate child of extensions resource
if (!extUri.isEqual(extUri.dirname(change.resource), extensionsResource)) {
return false;
}
// .obsolete file changed
if (extUri.isEqual(change.resource, extUri.joinPath(extensionsResource, '.obsolete'))) {
return true;
}
// Only interested in added/deleted changes
if (change.type !== FileChangeType.ADDED && change.type !== FileChangeType.DELETED) {
return false;
}
// Ingore changes to files starting with `.`
if (extUri.basename(change.resource).startsWith('.')) {
return false;
}
return true;
}
private onInstallExtension(e: InstallExtensionEvent): void {
this.addInstallingExtension(e.identifier);
}
private onDidInstallExtension(e: DidInstallExtensionEvent): void {
this.removeInstallingExtension(e.identifier);
if (!e.error) {
this.addInstalledExtension(e.identifier);
}
}
private onDidUninstallExtension(e: DidUninstallExtensionEvent): void {
if (!e.error) {
this.removeInstalledExtension(e.identifier);
}
}
private addInstallingExtension(extension: IExtensionIdentifier) {
this.removeInstallingExtension(extension);
this.installingExtensions.push(extension);
}
private removeInstallingExtension(identifier: IExtensionIdentifier) {
this.installingExtensions = this.installingExtensions.filter(e => !areSameExtensions(e, identifier));
}
private addInstalledExtension(extension: IExtensionIdentifier): void {
if (this.installedExtensions) {
this.removeInstalledExtension(extension);
this.installedExtensions.push(extension);
}
}
private removeInstalledExtension(identifier: IExtensionIdentifier): void {
if (this.installedExtensions) {
this.installedExtensions = this.installedExtensions.filter(e => !areSameExtensions(e, identifier));
}
}
private async onDidChange(): Promise<void> {
if (this.installedExtensions) {
const extensions = await this.extensionsManagementService.getInstalled(ExtensionType.User);
const added = extensions.filter(e => {
if ([...this.installingExtensions, ...this.installedExtensions!].some(identifier => areSameExtensions(identifier, e.identifier))) {
return false;
}
if (e.installedTimestamp && e.installedTimestamp > this.startTimestamp) {
this.logService.info('Detected extension installed from another source', e.identifier.id);
return true;
} else {
this.logService.info('Ignored extension installed by another source because of invalid timestamp', e.identifier.id);
return false;
}
});
const removed = this.installedExtensions.filter(identifier => {
// Extension being installed
if (this.installingExtensions.some(installingExtension => areSameExtensions(installingExtension, identifier))) {
return false;
}
if (extensions.every(e => !areSameExtensions(e.identifier, identifier))) {
this.logService.info('Detected extension removed from another source', identifier.id);
return true;
}
return false;
});
this.installedExtensions = extensions.map(e => e.identifier);
if (added.length || removed.length) {
this._onDidChangeExtensionsByAnotherSource.fire({ added, removed });
}
}
}
}

View File

@@ -19,8 +19,10 @@ 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) {
override readonly serviceMachineIdResource: URI;
constructor(serviceMachineIdResource: URI) {
super();
this.serviceMachineIdResource = serviceMachineIdResource;
}
}
@@ -43,6 +45,6 @@ suite('Extension Gallery Service', () => {
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']);
assert.strictEqual(headers['X-Market-User-Id'], headers2['X-Market-User-Id']);
});
});

View File

@@ -9,21 +9,21 @@ suite('Extension Identifier Pattern', () => {
test('extension identifier pattern', () => {
const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN);
assert.equal(true, regEx.test('publisher.name'));
assert.equal(true, regEx.test('publiSher.name'));
assert.equal(true, regEx.test('publisher.Name'));
assert.equal(true, regEx.test('PUBLISHER.NAME'));
assert.equal(true, regEx.test('PUBLISHEr.NAMe'));
assert.equal(true, regEx.test('PUBLISHEr.N-AMe'));
assert.equal(true, regEx.test('PUB-LISHEr.NAMe'));
assert.equal(true, regEx.test('PUB-LISHEr.N-AMe'));
assert.equal(true, regEx.test('PUBLISH12Er90.N-A54Me123'));
assert.equal(true, regEx.test('111PUBLISH12Er90.N-1111A54Me123'));
assert.equal(false, regEx.test('publishername'));
assert.equal(false, regEx.test('-publisher.name'));
assert.equal(false, regEx.test('publisher.-name'));
assert.equal(false, regEx.test('-publisher.-name'));
assert.equal(false, regEx.test('publ_isher.name'));
assert.equal(false, regEx.test('publisher._name'));
assert.strictEqual(true, regEx.test('publisher.name'));
assert.strictEqual(true, regEx.test('publiSher.name'));
assert.strictEqual(true, regEx.test('publisher.Name'));
assert.strictEqual(true, regEx.test('PUBLISHER.NAME'));
assert.strictEqual(true, regEx.test('PUBLISHEr.NAMe'));
assert.strictEqual(true, regEx.test('PUBLISHEr.N-AMe'));
assert.strictEqual(true, regEx.test('PUB-LISHEr.NAMe'));
assert.strictEqual(true, regEx.test('PUB-LISHEr.N-AMe'));
assert.strictEqual(true, regEx.test('PUBLISH12Er90.N-A54Me123'));
assert.strictEqual(true, regEx.test('111PUBLISH12Er90.N-1111A54Me123'));
assert.strictEqual(false, regEx.test('publishername'));
assert.strictEqual(false, regEx.test('-publisher.name'));
assert.strictEqual(false, regEx.test('publisher.-name'));
assert.strictEqual(false, regEx.test('-publisher.-name'));
assert.strictEqual(false, regEx.test('publ_isher.name'));
assert.strictEqual(false, regEx.test('publisher._name'));
});
});

View File

@@ -120,6 +120,34 @@ export interface IAuthenticationContribution {
readonly label: string;
}
export interface IWalkthroughStep {
readonly id: string;
readonly title: string;
readonly description: string;
readonly media:
| { path: string | { dark: string, light: string, hc: string }, altText: string }
| { path: string, },
readonly doneOn?: { command: string };
readonly when?: string;
}
export interface IWalkthrough {
readonly id: string,
readonly title: string;
readonly description: string;
readonly steps: IWalkthroughStep[];
readonly primary?: boolean;
readonly when?: string;
}
export interface IStartEntry {
readonly title: string;
readonly description: string;
readonly command: string;
readonly type?: 'sample-folder' | 'sample-notebook' | string;
readonly when?: string;
}
export interface IExtensionContributions {
commands?: ICommand[];
configuration?: IConfiguration | IConfiguration[];
@@ -132,6 +160,7 @@ export interface IExtensionContributions {
snippets?: ISnippet[];
themes?: ITheme[];
iconThemes?: ITheme[];
productIconThemes?: ITheme[];
viewsContainers?: { [location: string]: IViewContainer[] };
views?: { [location: string]: IView[] };
colors?: IColor[];
@@ -139,9 +168,18 @@ export interface IExtensionContributions {
readonly customEditors?: readonly IWebviewEditor[];
readonly codeActions?: readonly ICodeActionContribution[];
authentication?: IAuthenticationContribution[];
walkthroughs?: IWalkthrough[];
startEntries?: IStartEntry[];
}
export interface IExtensionCapabilities {
readonly virtualWorkspaces?: boolean;
readonly untrustedWorkspaces?: ExtensionUntrustedWorkspaceSupport;
}
export type ExtensionKind = 'ui' | 'workspace' | 'web';
export type ExtensionUntrustedWorkpaceSupportType = boolean | 'limited';
export type ExtensionUntrustedWorkspaceSupport = { supported: true; } | { supported: false, description: string } | { supported: 'limited', description: string, restrictedConfigurations?: string[] };
export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier {
return thing
@@ -161,6 +199,7 @@ export const EXTENSION_CATEGORIES = [
// 'Data Science',
// 'Debuggers',
// 'Extension Packs',
// 'Education',
// 'Formatters',
// 'Keymaps',
'Language Packs',
@@ -199,6 +238,7 @@ export interface IExtensionManifest {
readonly enableProposedApi?: boolean;
readonly api?: string;
readonly scripts?: { [key: string]: string; };
readonly capabilities?: IExtensionCapabilities;
}
export const enum ExtensionType {
@@ -299,6 +339,7 @@ export interface IScannedExtension {
readonly packageNLSUrl?: URI;
readonly readmeUrl?: URI;
readonly changelogUrl?: URI;
readonly isUnderDevelopment: boolean;
}
export interface ITranslatedScannedExtension {
@@ -308,6 +349,7 @@ export interface ITranslatedScannedExtension {
readonly packageJSON: IExtensionManifest;
readonly readmeUrl?: URI;
readonly changelogUrl?: URI;
readonly isUnderDevelopment: boolean;
}
export const IBuiltinExtensionsScannerService = createDecorator<IBuiltinExtensionsScannerService>('IBuiltinExtensionsScannerService');

View File

@@ -8,22 +8,22 @@ import { INormalizedVersion, IParsedVersion, IReducedExtensionDescription, isVal
suite('Extension Version Validator', () => {
test('isValidVersionStr', () => {
assert.equal(isValidVersionStr('0.10.0-dev'), true);
assert.equal(isValidVersionStr('0.10.0'), true);
assert.equal(isValidVersionStr('0.10.1'), true);
assert.equal(isValidVersionStr('0.10.100'), true);
assert.equal(isValidVersionStr('0.11.0'), true);
assert.strictEqual(isValidVersionStr('0.10.0-dev'), true);
assert.strictEqual(isValidVersionStr('0.10.0'), true);
assert.strictEqual(isValidVersionStr('0.10.1'), true);
assert.strictEqual(isValidVersionStr('0.10.100'), true);
assert.strictEqual(isValidVersionStr('0.11.0'), true);
assert.equal(isValidVersionStr('x.x.x'), true);
assert.equal(isValidVersionStr('0.x.x'), true);
assert.equal(isValidVersionStr('0.10.0'), true);
assert.equal(isValidVersionStr('0.10.x'), true);
assert.equal(isValidVersionStr('^0.10.0'), true);
assert.equal(isValidVersionStr('*'), true);
assert.strictEqual(isValidVersionStr('x.x.x'), true);
assert.strictEqual(isValidVersionStr('0.x.x'), true);
assert.strictEqual(isValidVersionStr('0.10.0'), true);
assert.strictEqual(isValidVersionStr('0.10.x'), true);
assert.strictEqual(isValidVersionStr('^0.10.0'), true);
assert.strictEqual(isValidVersionStr('*'), true);
assert.equal(isValidVersionStr('0.x.x.x'), false);
assert.equal(isValidVersionStr('0.10'), false);
assert.equal(isValidVersionStr('0.10.'), false);
assert.strictEqual(isValidVersionStr('0.x.x.x'), false);
assert.strictEqual(isValidVersionStr('0.10'), false);
assert.strictEqual(isValidVersionStr('0.10.'), false);
});
test('parseVersion', () => {
@@ -31,7 +31,7 @@ suite('Extension Version Validator', () => {
const actual = parseVersion(version);
const expected: IParsedVersion = { hasCaret, hasGreaterEquals, majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, preRelease };
assert.deepEqual(actual, expected, 'parseVersion for ' + version);
assert.deepStrictEqual(actual, expected, 'parseVersion for ' + version);
}
assertParseVersion('0.10.0-dev', false, false, 0, true, 10, true, 0, true, '-dev');
@@ -56,7 +56,7 @@ suite('Extension Version Validator', () => {
function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean): void {
const actual = normalizeVersion(parseVersion(version));
const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum };
assert.deepEqual(actual, expected, 'parseVersion for ' + version);
assert.deepStrictEqual(actual, expected, 'parseVersion for ' + version);
}
assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false);
@@ -80,7 +80,7 @@ suite('Extension Version Validator', () => {
test('isValidVersion', () => {
function testIsValidVersion(version: string, desiredVersion: string, expectedResult: boolean): void {
let actual = isValidVersion(version, desiredVersion);
assert.equal(actual, expectedResult, 'extension - vscode: ' + version + ', desiredVersion: ' + desiredVersion + ' should be ' + expectedResult);
assert.strictEqual(actual, expectedResult, 'extension - vscode: ' + version + ', desiredVersion: ' + desiredVersion + ' should be ' + expectedResult);
}
testIsValidVersion('0.10.0-dev', 'x.x.x', true);
@@ -213,7 +213,7 @@ suite('Extension Version Validator', () => {
let reasons: string[] = [];
let actual = isValidExtensionVersion(version, desc, reasons);
assert.equal(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(desc) + ', reasons: ' + JSON.stringify(reasons));
assert.strictEqual(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(desc) + ', reasons: ' + JSON.stringify(reasons));
}
function testIsInvalidExtensionVersion(version: string, desiredVersion: string, isBuiltin: boolean, hasMain: boolean): void {

View File

@@ -0,0 +1,210 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions } from 'vs/platform/files/common/files';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { extUri } from 'vs/base/common/resources';
function split(path: string): [string, string] | undefined {
const match = /^(.*)\/([^/]+)$/.exec(path);
if (!match) {
return undefined;
}
const [, parentPath, name] = match;
return [parentPath, name];
}
function getRootUUID(uri: URI): string | undefined {
const match = /^\/([^/]+)\/[^/]+\/?$/.exec(uri.path);
if (!match) {
return undefined;
}
return match[1];
}
export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability {
private readonly files = new Map<string, FileSystemFileHandle>();
private readonly directories = new Map<string, FileSystemDirectoryHandle>();
readonly capabilities: FileSystemProviderCapabilities =
FileSystemProviderCapabilities.FileReadWrite
| FileSystemProviderCapabilities.PathCaseSensitive;
readonly onDidChangeCapabilities = Event.None;
private readonly _onDidChangeFile = new Emitter<readonly IFileChange[]>();
readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidErrorOccur = new Emitter<string>();
readonly onDidErrorOccur = this._onDidErrorOccur.event;
async readFile(resource: URI): Promise<Uint8Array> {
const handle = await this.getFileHandle(resource);
if (!handle) {
throw new Error('File not found.');
}
const file = await handle.getFile();
return new Uint8Array(await file.arrayBuffer());
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
const handle = await this.getFileHandle(resource);
if (!handle) {
throw new Error('File not found.');
}
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}
watch(resource: URI, opts: IWatchOptions): IDisposable {
return Disposable.None;
}
async stat(resource: URI): Promise<IStat> {
const rootUUID = getRootUUID(resource);
if (rootUUID) {
const fileHandle = this.files.get(rootUUID);
if (fileHandle) {
const file = await fileHandle.getFile();
return {
type: FileType.File,
mtime: file.lastModified,
ctime: 0,
size: file.size
};
}
const directoryHandle = this.directories.get(rootUUID);
if (directoryHandle) {
return {
type: FileType.Directory,
mtime: 0,
ctime: 0,
size: 0
};
}
}
const parent = await this.getParentDirectoryHandle(resource);
if (!parent) {
throw new Error('Stat error: no parent found');
}
const name = extUri.basename(resource);
for await (const [childName, child] of parent) {
if (childName === name) {
if (child.kind === 'file') {
const file = await child.getFile();
return {
type: FileType.File,
mtime: file.lastModified,
ctime: 0,
size: file.size
};
} else {
return {
type: FileType.Directory,
mtime: 0,
ctime: 0,
size: 0
};
}
}
}
throw new Error('Stat error: entry not found');
}
mkdir(resource: URI): Promise<void> {
throw new Error('Method not implemented.');
}
async readdir(resource: URI): Promise<[string, FileType][]> {
const parent = await this.getDirectoryHandle(resource);
if (!parent) {
throw new Error('Stat error: no parent found');
}
const result: [string, FileType][] = [];
for await (const [name, child] of parent) {
result.push([name, child.kind === 'file' ? FileType.File : FileType.Directory]);
}
return result;
}
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
throw new Error('Method not implemented: delete');
}
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
throw new Error('Method not implemented: rename');
}
private async getDirectoryHandle(uri: URI): Promise<FileSystemDirectoryHandle | undefined> {
const rootUUID = getRootUUID(uri);
if (rootUUID) {
return this.directories.get(rootUUID);
}
const splitResult = split(uri.path);
if (!splitResult) {
return undefined;
}
const parent = await this.getDirectoryHandle(URI.from({ ...uri, path: splitResult[0] }));
return await parent?.getDirectoryHandle(extUri.basename(uri));
}
private async getParentDirectoryHandle(uri: URI): Promise<FileSystemDirectoryHandle | undefined> {
return this.getDirectoryHandle(URI.from({ ...uri, path: extUri.dirname(uri).path }));
}
private async getFileHandle(uri: URI): Promise<FileSystemFileHandle | undefined> {
const rootUUID = getRootUUID(uri);
if (rootUUID) {
return this.files.get(rootUUID);
}
const parent = await this.getParentDirectoryHandle(uri);
const name = extUri.basename(uri);
return await parent?.getFileHandle(name);
}
registerFileHandle(uuid: string, handle: FileSystemFileHandle): void {
this.files.set(uuid, handle);
}
registerDirectoryHandle(uuid: string, handle: FileSystemDirectoryHandle): void {
this.directories.set(uuid, handle);
}
dispose(): void {
this._onDidChangeFile.dispose();
}
}

View File

@@ -10,7 +10,6 @@ import { Event, Emitter } from 'vs/base/common/event';
import { VSBuffer } from 'vs/base/common/buffer';
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';
@@ -48,9 +47,6 @@ export class IndexedDB {
}
private openIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase | null> {
if (browser.isEdgeLegacy) {
return Promise.resolve(null);
}
return new Promise((c, e) => {
const request = window.indexedDB.open(name, version);
request.onerror = (err) => e(request.error);

View File

@@ -6,16 +6,16 @@
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 { 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, IReadFileStreamOptions, FileDeleteOptions } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { Emitter } from 'vs/base/common/event';
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 { ILogService } from 'vs/platform/log/common/log';
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 { Promises, Queue } from 'vs/base/common/async';
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, listenStream, consumeStream } from 'vs/base/common/stream';
import { Promises, ResourceQueue } from 'vs/base/common/async';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { Schemas } from 'vs/base/common/network';
import { readFileIntoStream } from 'vs/platform/files/common/io';
@@ -71,6 +71,10 @@ export class FileService extends Disposable implements IFileService {
});
}
getProvider(scheme: string): IFileSystemProvider | undefined {
return this.provider.get(scheme);
}
async activateProvider(scheme: string): Promise<void> {
// Emit an event that we are about to activate a provider with the given scheme.
@@ -79,9 +83,7 @@ export class FileService extends Disposable implements IFileService {
this._onWillActivateFileSystemProvider.fire({
scheme,
join(promise) {
if (promise) {
joiners.push(promise);
}
joiners.push(promise);
},
});
@@ -364,12 +366,12 @@ export class FileService extends Disposable implements IFileService {
// write file: unbuffered (only if data to write is a buffer, or the provider has no buffered write capability)
if (!hasOpenReadWriteCloseCapability(provider) || (hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer)) {
await this.doWriteUnbuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream);
await this.doWriteUnbuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream);
}
// write file: buffered
else {
await this.doWriteBuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
await this.doWriteBuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
}
} catch (error) {
throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
@@ -379,6 +381,14 @@ export class FileService extends Disposable implements IFileService {
}
private async validateWriteFile(provider: IFileSystemProvider, resource: URI, options?: IWriteFileOptions): Promise<IStat | undefined> {
// Validate unlock support
const unlock = !!options?.unlock;
if (unlock && !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)) {
throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));
}
// Validate via file stat meta data
let stat: IStat | undefined = undefined;
try {
stat = await provider.stat(resource);
@@ -386,7 +396,7 @@ export class FileService extends Disposable implements IFileService {
return undefined; // file might not exist
}
// file cannot be directory
// File cannot be directory
if ((stat.type & FileType.Directory) !== 0) {
throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
}
@@ -417,7 +427,28 @@ export class FileService extends Disposable implements IFileService {
async readFile(resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
const provider = await this.withReadProvider(resource);
const stream = await this.doReadAsFileStream(provider, resource, {
if (options?.atomic) {
return this.doReadFileAtomic(provider, resource, options);
}
return this.doReadFile(provider, resource, options);
}
private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
return new Promise<IFileContent>((resolve, reject) => {
this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
try {
const content = await this.doReadFile(provider, resource, options);
resolve(content);
} catch (error) {
reject(error);
}
});
});
}
private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
const stream = await this.doReadFileStream(provider, resource, {
...options,
// optimization: since we know that the caller does not
// care about buffering, we indicate this to the reader.
@@ -433,13 +464,13 @@ export class FileService extends Disposable implements IFileService {
};
}
async readFileStream(resource: URI, options?: IReadFileOptions): Promise<IFileStreamContent> {
async readFileStream(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStreamContent> {
const provider = await this.withReadProvider(resource);
return this.doReadAsFileStream(provider, resource, options);
return this.doReadFileStream(provider, resource, options);
}
private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean; }): Promise<IFileStreamContent> {
private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileStreamOptions & { preferUnbuffered?: boolean; }): Promise<IFileStreamContent> {
// install a cancellation token that gets cancelled
// when any error occurs. this allows us to resolve
@@ -454,20 +485,17 @@ export class FileService extends Disposable implements IFileService {
throw error;
});
let fileStreamObserver: IReadableStreamObservable | undefined = undefined;
let fileStream: VSBufferReadableStream | undefined = undefined;
try {
// if the etag is provided, we await the result of the validation
// due to the likelyhood of hitting a NOT_MODIFIED_SINCE result.
// due to the likelihood of hitting a NOT_MODIFIED_SINCE result.
// otherwise, we let it run in parallel to the file reading for
// optimal startup performance.
if (options && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED) {
await statPromise;
}
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)) {
fileStream = this.readFileUnbuffered(provider, resource, options);
@@ -483,9 +511,6 @@ export class FileService extends Disposable implements IFileService {
fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options);
}
// observe the stream for the error case below
fileStreamObserver = observe(fileStream);
const fileStat = await statPromise;
return {
@@ -497,15 +522,15 @@ export class FileService extends Disposable implements IFileService {
// 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();
if (fileStream) {
await consumeStream(fileStream);
}
throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
}
}
private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileOptions = Object.create(null)): VSBufferReadableStream {
private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
const fileStream = provider.readFileStream(resource, options, token);
return transform(fileStream, {
@@ -514,7 +539,7 @@ export class FileService extends Disposable implements IFileService {
}, data => VSBuffer.concat(data));
}
private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileOptions = Object.create(null)): VSBufferReadableStream {
private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
const stream = newWriteableBufferStream();
readFileIntoStream(provider, resource, stream, data => data, {
@@ -526,7 +551,7 @@ export class FileService extends Disposable implements IFileService {
return stream;
}
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): VSBufferReadableStream {
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileStreamOptions): VSBufferReadableStream {
const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));
// Read the file into the stream async but do not wait for
@@ -552,13 +577,14 @@ export class FileService extends Disposable implements IFileService {
stream.end(VSBuffer.wrap(buffer));
} catch (err) {
stream.error(err);
stream.end();
}
})();
return stream;
}
private async validateReadFile(resource: URI, options?: IReadFileOptions): Promise<IFileStatWithMetadata> {
private async validateReadFile(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStatWithMetadata> {
const stat = await this.resolve(resource, { resolveMetadata: true });
// Throw if resource is a directory
@@ -577,7 +603,7 @@ export class FileService extends Disposable implements IFileService {
return stat;
}
private validateReadFileLimits(resource: URI, size: number, options?: IReadFileOptions): void {
private validateReadFileLimits(resource: URI, size: number, options?: IReadFileStreamOptions): void {
if (options?.limits) {
let tooLargeErrorResult: FileOperationResult | undefined = undefined;
@@ -866,7 +892,7 @@ export class FileService extends Disposable implements IFileService {
}
}
async canDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<Error | true> {
async canDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<Error | true> {
try {
await this.doValidateDelete(resource, options);
} catch (error) {
@@ -876,7 +902,7 @@ export class FileService extends Disposable implements IFileService {
return true;
}
private async doValidateDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<IFileSystemProvider> {
private async doValidateDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<IFileSystemProvider> {
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
// Validate trash support
@@ -903,7 +929,7 @@ export class FileService extends Disposable implements IFileService {
return provider;
}
async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<void> {
async del(resource: URI, options?: Partial<FileDeleteOptions>): Promise<void> {
const provider = await this.doValidateDelete(resource, options);
const useTrash = !!options?.useTrash;
@@ -978,7 +1004,7 @@ export class FileService extends Disposable implements IFileService {
].join();
}
dispose(): void {
override dispose(): void {
super.dispose();
this.activeWatchers.forEach(watcher => dispose(watcher.disposable));
@@ -989,35 +1015,13 @@ export class FileService extends Disposable implements IFileService {
//#region Helpers
private readonly writeQueues: Map<string, Queue<void>> = new Map();
private readonly writeQueue = this._register(new ResourceQueue());
private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue<void> {
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
// (even with error) before another write is done.
let writeQueue = this.writeQueues.get(queueKey);
if (!writeQueue) {
writeQueue = new Queue<void>();
this.writeQueues.set(queueKey, writeQueue);
const onFinish = Event.once(writeQueue.onFinished);
onFinish(() => {
this.writeQueues.delete(queueKey);
dispose(writeQueue);
});
}
return writeQueue;
}
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.ensureWriteQueue(provider, resource).queue(async () => {
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
// open handle
const handle = await provider.open(resource, { create: true });
const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false });
// write into handle until all bytes from buffer have been written
try {
@@ -1065,28 +1069,29 @@ export class FileService extends Disposable implements IFileService {
return new Promise(async (resolve, reject) => {
stream.on('data', async chunk => {
listenStream(stream, {
onData: async chunk => {
// pause stream to perform async write operation
stream.pause();
// pause stream to perform async write operation
stream.pause();
try {
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
} catch (error) {
return reject(error);
}
try {
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
} catch (error) {
return reject(error);
}
posInFile += chunk.byteLength;
posInFile += chunk.byteLength;
// resume stream now that we have successfully written
// run this on the next tick to prevent increasing the
// execution stack because resume() may call the event
// handler again before finishing.
setTimeout(() => stream.resume());
// resume stream now that we have successfully written
// run this on the next tick to prevent increasing the
// execution stack because resume() may call the event
// handler again before finishing.
setTimeout(() => stream.resume());
},
onError: error => reject(error),
onEnd: () => resolve()
});
stream.on('error', error => reject(error));
stream.on('end', () => resolve());
});
}
@@ -1111,11 +1116,11 @@ export class FileService extends Disposable implements IFileService {
}
}
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.ensureWriteQueue(provider, resource).queue(() => this.doWriteUnbufferedQueued(provider, resource, bufferOrReadableOrStreamOrBufferedStream));
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(() => this.doWriteUnbufferedQueued(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream));
}
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
let buffer: VSBuffer;
if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {
buffer = bufferOrReadableOrStreamOrBufferedStream;
@@ -1128,11 +1133,11 @@ export class FileService extends Disposable implements IFileService {
}
// Write through the provider
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true });
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false });
}
private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target));
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
@@ -1143,7 +1148,7 @@ export class FileService extends Disposable implements IFileService {
// Open handles
sourceHandle = await sourceProvider.open(source, { create: false });
targetHandle = await targetProvider.open(target, { create: true });
targetHandle = await targetProvider.open(target, { create: true, unlock: false });
const buffer = VSBuffer.alloc(this.BUFFER_SIZE);
@@ -1178,21 +1183,21 @@ export class FileService extends Disposable implements IFileService {
}
private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target));
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true });
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false });
}
private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target));
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
// Open handle
const targetHandle = await targetProvider.open(target, { create: true });
const targetHandle = await targetProvider.open(target, { create: true, unlock: false });
// Read entire buffer from source and write buffered
try {
@@ -1211,7 +1216,7 @@ export class FileService extends Disposable implements IFileService {
const buffer = await streamToBuffer(this.readFileBuffered(sourceProvider, source, CancellationToken.None));
// Write buffer into target at once
await this.doWriteUnbuffered(targetProvider, target, buffer);
await this.doWriteUnbuffered(targetProvider, target, undefined, buffer);
}
protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {

View File

@@ -6,7 +6,7 @@
import { localize } from 'vs/nls';
import { sep } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import * as glob from 'vs/base/common/glob';
import { IExpression } from 'vs/base/common/glob';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { startsWithIgnoreCase } from 'vs/base/common/strings';
@@ -17,6 +17,8 @@ import { ReadableStreamEvents } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
import { TernarySearchTree } from 'vs/base/common/map';
//#region file service & providers
export const IFileService = createDecorator<IFileService>('fileService');
export interface IFileService {
@@ -44,6 +46,11 @@ export interface IFileService {
*/
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable;
/**
* Returns a file system provider for a certain scheme.
*/
getProvider(scheme: string): IFileSystemProvider | undefined;
/**
* Tries to activate a provider with the given scheme.
*/
@@ -112,7 +119,7 @@ export interface IFileService {
/**
* Read the contents of the provided resource buffered as stream.
*/
readFileStream(resource: URI, options?: IReadFileOptions): Promise<IFileStreamContent>;
readFileStream(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStreamContent>;
/**
* Updates the content replacing its previous value.
@@ -170,13 +177,13 @@ export interface IFileService {
* move the file to trash. The optional recursive parameter allows to delete
* non-empty folders recursively.
*/
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void>;
del(resource: URI, options?: Partial<FileDeleteOptions>): Promise<void>;
/**
* Find out if a delete operation is possible given the arguments. No changes on disk will
* be performed. Returns an Error if the operation cannot be done.
*/
canDelete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<Error | true>;
canDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<Error | true>;
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
@@ -192,7 +199,22 @@ export interface IFileService {
}
export interface FileOverwriteOptions {
overwrite: boolean;
/**
* Set to `true` to overwrite a file if it exists. Will
* throw an error otherwise if the file does exist.
*/
readonly overwrite: boolean;
}
export interface FileUnlockOptions {
/**
* Set to `true` to try to remove any write locks the file might
* have. A file that is write locked will throw an error for any
* attempt to write to unless `unlock: true` is provided.
*/
readonly unlock: boolean;
}
export interface FileReadStreamOptions {
@@ -218,59 +240,159 @@ export interface FileReadStreamOptions {
};
}
export interface FileWriteOptions {
overwrite: boolean;
create: boolean;
export interface FileWriteOptions extends FileOverwriteOptions, FileUnlockOptions {
/**
* Set to `true` to create a file when it does not exist. Will
* throw an error otherwise if the file does not exist.
*/
readonly create: boolean;
}
export interface FileOpenOptions {
create: boolean;
export type FileOpenOptions = FileOpenForReadOptions | FileOpenForWriteOptions;
export function isFileOpenForWriteOptions(options: FileOpenOptions): options is FileOpenForWriteOptions {
return options.create === true;
}
export interface FileOpenForReadOptions {
/**
* A hint that the file should be opened for reading only.
*/
readonly create: false;
}
export interface FileOpenForWriteOptions extends FileUnlockOptions {
/**
* A hint that the file should be opened for reading and writing.
*/
readonly create: true;
}
export interface FileDeleteOptions {
recursive: boolean;
useTrash: boolean;
/**
* Set to `true` to recursively delete any children of the file. This
* only applies to folders and can lead to an error unless provided
* if the folder is not empty.
*/
readonly recursive: boolean;
/**
* Set to `true` to attempt to move the file to trash
* instead of deleting it permanently from disk. This
* option maybe not be supported on all providers.
*/
readonly useTrash: boolean;
}
export enum FileType {
/**
* File is unknown (neither file, directory nor symbolic link).
*/
Unknown = 0,
/**
* File is a normal file.
*/
File = 1,
/**
* File is a directory.
*/
Directory = 2,
/**
* File is a symbolic link.
*
* Note: even when the file is a symbolic link, you can test for
* `FileType.File` and `FileType.Directory` to know the type of
* the target the link points to.
*/
SymbolicLink = 64
}
export interface IStat {
type: FileType;
/**
* The file type.
*/
readonly type: FileType;
/**
* The last modification date represented as millis from unix epoch.
*/
mtime: number;
readonly mtime: number;
/**
* The creation date represented as millis from unix epoch.
*/
ctime: number;
readonly ctime: number;
/**
* The size of the file in bytes.
*/
size: number;
}
export interface IWatchOptions {
recursive: boolean;
/**
* Set to `true` to watch for changes recursively in a folder
* and all of its children.
*/
readonly recursive: boolean;
/**
* A set of paths to exclude from watching.
*/
excludes: string[];
}
export const enum FileSystemProviderCapabilities {
/**
* Provider supports unbuffered read/write.
*/
FileReadWrite = 1 << 1,
/**
* Provider supports open/read/write/close low level file operations.
*/
FileOpenReadWriteClose = 1 << 2,
/**
* Provider supports stream based reading.
*/
FileReadStream = 1 << 4,
/**
* Provider supports copy operation.
*/
FileFolderCopy = 1 << 3,
/**
* Provider is path case sensitive.
*/
PathCaseSensitive = 1 << 10,
/**
* All files of the provider are readonly.
*/
Readonly = 1 << 11,
Trash = 1 << 12
/**
* Provider supports to delete via trash.
*/
Trash = 1 << 12,
/**
* Provider support to unlock files for writing.
*/
FileWriteUnlock = 1 << 13
}
export interface IFileSystemProvider {
@@ -345,6 +467,7 @@ export enum FileSystemProviderErrorCode {
FileIsADirectory = 'EntryIsADirectory',
FileExceedsMemoryLimit = 'EntryExceedsMemoryLimit',
FileTooLarge = 'EntryTooLarge',
FileWriteLocked = 'EntryWriteLocked',
NoPermissions = 'NoPermissions',
Unavailable = 'Unavailable',
Unknown = 'Unknown'
@@ -404,6 +527,7 @@ export function toFileSystemProviderErrorCode(error: Error | undefined | null):
case FileSystemProviderErrorCode.FileNotFound: return FileSystemProviderErrorCode.FileNotFound;
case FileSystemProviderErrorCode.FileExceedsMemoryLimit: return FileSystemProviderErrorCode.FileExceedsMemoryLimit;
case FileSystemProviderErrorCode.FileTooLarge: return FileSystemProviderErrorCode.FileTooLarge;
case FileSystemProviderErrorCode.FileWriteLocked: return FileSystemProviderErrorCode.FileWriteLocked;
case FileSystemProviderErrorCode.NoPermissions: return FileSystemProviderErrorCode.NoPermissions;
case FileSystemProviderErrorCode.Unavailable: return FileSystemProviderErrorCode.Unavailable;
}
@@ -426,6 +550,8 @@ export function toFileOperationResult(error: Error): FileOperationResult {
return FileOperationResult.FILE_IS_DIRECTORY;
case FileSystemProviderErrorCode.FileNotADirectory:
return FileOperationResult.FILE_NOT_DIRECTORY;
case FileSystemProviderErrorCode.FileWriteLocked:
return FileOperationResult.FILE_WRITE_LOCKED;
case FileSystemProviderErrorCode.NoPermissions:
return FileOperationResult.FILE_PERMISSION_DENIED;
case FileSystemProviderErrorCode.FileExists:
@@ -440,18 +566,18 @@ export function toFileOperationResult(error: Error): FileOperationResult {
}
export interface IFileSystemProviderRegistrationEvent {
added: boolean;
scheme: string;
provider?: IFileSystemProvider;
readonly added: boolean;
readonly scheme: string;
readonly provider?: IFileSystemProvider;
}
export interface IFileSystemProviderCapabilitiesChangeEvent {
provider: IFileSystemProvider;
scheme: string;
readonly provider: IFileSystemProvider;
readonly scheme: string;
}
export interface IFileSystemProviderActivationEvent {
scheme: string;
readonly scheme: string;
join(promise: Promise<void>): void;
}
@@ -479,9 +605,9 @@ export class FileOperationEvent {
* Possible changes that can occur to a file.
*/
export const enum FileChangeType {
UPDATED = 0,
ADDED = 1,
DELETED = 2
UPDATED,
ADDED,
DELETED
}
/**
@@ -702,13 +828,13 @@ interface IBaseStat {
/**
* The unified resource identifier of this file or folder.
*/
resource: URI;
readonly resource: URI;
/**
* The name which is the last segment
* of the {{path}}.
*/
name: string;
readonly name: string;
/**
* The size of the file.
@@ -716,7 +842,7 @@ interface IBaseStat {
* The value may or may not be resolved as
* it is optional.
*/
size?: number;
readonly size?: number;
/**
* The last modification date represented as millis from unix epoch.
@@ -724,7 +850,7 @@ interface IBaseStat {
* The value may or may not be resolved as
* it is optional.
*/
mtime?: number;
readonly mtime?: number;
/**
* The creation date represented as millis from unix epoch.
@@ -732,7 +858,7 @@ interface IBaseStat {
* The value may or may not be resolved as
* it is optional.
*/
ctime?: number;
readonly ctime?: number;
/**
* A unique identifier thet represents the
@@ -741,15 +867,10 @@ interface IBaseStat {
* The value may or may not be resolved as
* it is optional.
*/
etag?: string;
readonly etag?: string;
}
export interface IBaseStatWithMetadata extends IBaseStat {
mtime: number;
ctime: number;
etag: string;
size: number;
}
export interface IBaseStatWithMetadata extends Required<IBaseStat> { }
/**
* A file resource with meta information.
@@ -759,17 +880,20 @@ export interface IFileStat extends IBaseStat {
/**
* The resource is a file.
*/
isFile: boolean;
readonly isFile: boolean;
/**
* The resource is a directory.
*/
isDirectory: boolean;
readonly isDirectory: boolean;
/**
* The resource is a symbolic link.
* The resource is a symbolic link. Note: even when the
* file is a symbolic link, you can test for `FileType.File`
* and `FileType.Directory` to know the type of the target
* the link points to.
*/
isSymbolicLink: boolean;
readonly isSymbolicLink: boolean;
/**
* The children of the file stat or undefined if none.
@@ -778,20 +902,20 @@ export interface IFileStat extends IBaseStat {
}
export interface IFileStatWithMetadata extends IFileStat, IBaseStatWithMetadata {
mtime: number;
ctime: number;
etag: string;
size: number;
children?: IFileStatWithMetadata[];
readonly mtime: number;
readonly ctime: number;
readonly etag: string;
readonly size: number;
readonly children?: IFileStatWithMetadata[];
}
export interface IResolveFileResult {
stat?: IFileStat;
success: boolean;
readonly stat?: IFileStat;
readonly success: boolean;
}
export interface IResolveFileResultWithMetadata extends IResolveFileResult {
stat?: IFileStatWithMetadata;
readonly stat?: IFileStatWithMetadata;
}
export interface IFileContent extends IBaseStatWithMetadata {
@@ -799,7 +923,7 @@ export interface IFileContent extends IBaseStatWithMetadata {
/**
* The content of a file as buffer.
*/
value: VSBuffer;
readonly value: VSBuffer;
}
export interface IFileStreamContent extends IBaseStatWithMetadata {
@@ -807,10 +931,10 @@ export interface IFileStreamContent extends IBaseStatWithMetadata {
/**
* The content of a file as stream.
*/
value: VSBufferReadableStream;
readonly value: VSBufferReadableStream;
}
export interface IReadFileOptions extends FileReadStreamOptions {
export interface IBaseReadFileOptions extends FileReadStreamOptions {
/**
* The optional etag parameter allows to return early from resolving the resource if
@@ -821,6 +945,28 @@ export interface IReadFileOptions extends FileReadStreamOptions {
readonly etag?: string;
}
export interface IReadFileStreamOptions extends IBaseReadFileOptions { }
export interface IReadFileOptions extends IBaseReadFileOptions {
/**
* The optional `atomic` flag can be used to make sure
* the `readFile` method is not running in parallel with
* any `write` operations in the same process.
*
* Typically you should not need to use this flag but if
* for example you are quickly reading a file right after
* a file event occured and the file changes a lot, there
* is a chance that a read returns an empty or partial file
* because a pending write has not finished yet.
*
* Note: this does not prevent the file from being written
* to from a different process. If you need such atomic
* operations, you better use a real database as storage.
*/
readonly atomic?: boolean;
}
export interface IWriteFileOptions {
/**
@@ -832,6 +978,11 @@ export interface IWriteFileOptions {
* The etag of the file. This can be used to prevent dirty writes.
*/
readonly etag?: string;
/**
* Whether to attempt to unlock a file before writing.
*/
readonly unlock?: boolean;
}
export interface IResolveFileOptions {
@@ -883,7 +1034,7 @@ export const enum FileOperationResult {
FILE_NOT_MODIFIED_SINCE,
FILE_MODIFIED_SINCE,
FILE_MOVE_CONFLICT,
FILE_READ_ONLY,
FILE_WRITE_LOCKED,
FILE_PERMISSION_DENIED,
FILE_TOO_LARGE,
FILE_INVALID_PATH,
@@ -892,6 +1043,10 @@ export const enum FileOperationResult {
FILE_OTHER_ERROR
}
//#endregion
//#region Settings
export const AutoSaveConfiguration = {
OFF: 'off',
AFTER_DELAY: 'afterDelay',
@@ -911,7 +1066,7 @@ export const FILES_EXCLUDE_CONFIG = 'files.exclude';
export interface IFilesConfiguration {
files: {
associations: { [filepattern: string]: string };
exclude: glob.IExpression;
exclude: IExpression;
watcherExclude: { [filepattern: string]: boolean };
encoding: string;
autoGuessEncoding: boolean;
@@ -926,6 +1081,10 @@ export interface IFilesConfiguration {
};
}
//#endregion
//#region Utilities
export enum FileKind {
FILE,
FOLDER,
@@ -972,6 +1131,7 @@ export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096;
* Helper to format a raw byte size into a human readable label.
*/
export class ByteSize {
static readonly KB = 1024;
static readonly MB = ByteSize.KB * ByteSize.KB;
static readonly GB = ByteSize.MB * ByteSize.KB;
@@ -1001,3 +1161,24 @@ export class ByteSize {
return localize('sizeTB', "{0}TB", (size / ByteSize.TB).toFixed(2));
}
}
// Native only: Arch limits
export interface IArchLimits {
readonly maxFileSize: number;
readonly maxHeapSize: number;
}
export const enum Arch {
IA32,
OTHER
}
export function getPlatformLimits(arch: Arch): IArchLimits {
return {
maxFileSize: arch === Arch.IA32 ? 300 * ByteSize.MB : 16 * ByteSize.GB, // https://github.com/microsoft/vscode/issues/30180
maxHeapSize: arch === Arch.IA32 ? 700 * ByteSize.MB : 2 * 700 * ByteSize.MB, // https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149
};
}
//#endregion

View File

@@ -10,6 +10,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, createFileSystemProviderError, FileSystemProviderErrorCode, ensureFileSystemProviderError } from 'vs/platform/files/common/files';
import { canceled } from 'vs/base/common/errors';
import { IErrorTransformer, IDataTransformer, WriteableStream } from 'vs/base/common/stream';
import product from 'vs/platform/product/common/product';
export interface ICreateReadStreamOptions extends FileReadStreamOptions {
@@ -46,7 +47,11 @@ export async function readFileIntoStream<T>(
error = options.errorTransformer(error);
}
target.end(error);
if (typeof error !== 'undefined') {
target.error(error);
}
target.end();
}
}
@@ -123,7 +128,7 @@ function throwIfTooLarge(totalBytesRead: number, options: ICreateReadStreamOptio
// Return early if file is too large to load and we have configured limits
if (options?.limits) {
if (typeof options.limits.memory === 'number' && totalBytesRead > options.limits.memory) {
throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), FileSystemProviderErrorCode.FileExceedsMemoryLimit);
throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", product.nameShort), FileSystemProviderErrorCode.FileExceedsMemoryLimit);
}
if (typeof options.limits.size === 'number' && totalBytesRead > options.limits.size) {

View File

@@ -0,0 +1,197 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, IFileChange, IStat, IWatchOptions, FileOpenOptions, IFileSystemProviderWithFileReadWriteCapability, FileWriteOptions, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, FileReadStreamOptions, IFileSystemProviderWithOpenReadWriteCloseCapability } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { newWriteableStream, ReadableStreamEvents, ReadableStreamEventPayload } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
import { canceled } from 'vs/base/common/errors';
import { toErrorMessage } from 'vs/base/common/errorMessage';
interface IFileChangeDto {
resource: UriComponents;
type: FileChangeType;
}
/**
* An abstract file system provider that delegates all calls to a provided
* `IChannel` via IPC communication.
*/
export abstract class IPCFileSystemProvider extends Disposable implements
IFileSystemProviderWithFileReadWriteCapability,
IFileSystemProviderWithOpenReadWriteCloseCapability,
IFileSystemProviderWithFileReadStreamCapability,
IFileSystemProviderWithFileFolderCopyCapability {
private readonly session: string = generateUuid();
private readonly _onDidChange = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile = this._onDidChange.event;
private _onDidWatchErrorOccur = this._register(new Emitter<string>());
readonly onDidErrorOccur = this._onDidWatchErrorOccur.event;
private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
private _capabilities = FileSystemProviderCapabilities.FileReadWrite
| FileSystemProviderCapabilities.FileOpenReadWriteClose
| FileSystemProviderCapabilities.FileReadStream
| FileSystemProviderCapabilities.FileFolderCopy
| FileSystemProviderCapabilities.FileWriteUnlock;
get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }
constructor(private readonly channel: IChannel) {
super();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.channel.listen<IFileChangeDto[] | string>('filechange', [this.session])(eventsOrError => {
if (Array.isArray(eventsOrError)) {
const events = eventsOrError;
this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type })));
} else {
const error = eventsOrError;
this._onDidWatchErrorOccur.fire(error);
}
}));
}
protected setCaseSensitive(isCaseSensitive: boolean) {
if (isCaseSensitive) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
} else {
this._capabilities &= ~FileSystemProviderCapabilities.PathCaseSensitive;
}
this._onDidChangeCapabilities.fire(undefined);
}
// --- forwarding calls
stat(resource: URI): Promise<IStat> {
return this.channel.call('stat', [resource]);
}
open(resource: URI, opts: FileOpenOptions): Promise<number> {
return this.channel.call('open', [resource, opts]);
}
close(fd: number): Promise<void> {
return this.channel.call('close', [fd]);
}
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]);
// copy back the data that was written into the buffer on the remote
// side. we need to do this because buffers are not referenced by
// pointer, but only by value and as such cannot be directly written
// to from the other process.
data.set(bytes.buffer.slice(0, bytesRead), offset);
return bytesRead;
}
async readFile(resource: URI): Promise<Uint8Array> {
const buff = <VSBuffer>await this.channel.call('readFile', [resource]);
return buff.buffer;
}
readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
// Reading as file stream goes through an event to the remote side
const listener = this.channel.listen<ReadableStreamEventPayload<VSBuffer>>('readFileStream', [resource, opts])(dataOrErrorOrEnd => {
// data
if (dataOrErrorOrEnd instanceof VSBuffer) {
stream.write(dataOrErrorOrEnd.buffer);
}
// end or error
else {
if (dataOrErrorOrEnd === 'end') {
stream.end();
} else {
// Since we receive data through a IPC channel, it is likely
// that the error was not serialized, or only partially. To
// ensure our API use is correct, we convert the data to an
// error here to forward it properly.
let error = dataOrErrorOrEnd;
if (!(error instanceof Error)) {
error = new Error(toErrorMessage(error));
}
stream.error(error);
stream.end();
}
// Signal to the remote side that we no longer listen
listener.dispose();
}
});
// Support cancellation
token.onCancellationRequested(() => {
// Ensure to end the stream properly with an error
// to indicate the cancellation.
stream.error(canceled());
stream.end();
// Ensure to dispose the listener upon cancellation. This will
// bubble through the remote side as event and allows to stop
// reading the file.
listener.dispose();
});
return stream;
}
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);
}
writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]);
}
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
return this.channel.call('delete', [resource, opts]);
}
mkdir(resource: URI): Promise<void> {
return this.channel.call('mkdir', [resource]);
}
readdir(resource: URI): Promise<[string, FileType][]> {
return this.channel.call('readdir', [resource]);
}
rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
return this.channel.call('rename', [resource, target, opts]);
}
copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
return this.channel.call('copy', [resource, target, opts]);
}
watch(resource: URI, opts: IWatchOptions): IDisposable {
const req = Math.random();
this.channel.call('watch', [this.session, req, resource, opts]);
return toDisposable(() => this.channel.call('unwatch', [this.session, req]));
}
}

View File

@@ -21,7 +21,7 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
super(logService, options);
}
get capabilities(): FileSystemProviderCapabilities {
override get capabilities(): FileSystemProviderCapabilities {
if (!this._capabilities) {
this._capabilities = super.capabilities | FileSystemProviderCapabilities.Trash;
}
@@ -29,7 +29,7 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
return this._capabilities;
}
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
protected override async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
if (!opts.useTrash) {
return super.doDelete(filePath, opts);
}

View File

@@ -6,7 +6,7 @@
import { open, close, read, write, fdatasync, Stats, promises } from 'fs';
import { promisify } from 'util';
import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files';
import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability, isFileOpenForWriteOptions } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isLinux, isWindows } from 'vs/base/common/platform';
@@ -30,7 +30,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
export interface IWatcherOptions {
pollingInterval?: number;
usePolling: boolean;
usePolling: boolean | string[];
}
export interface IDiskFileSystemProviderOptions {
@@ -64,7 +64,8 @@ export class DiskFileSystemProvider extends Disposable implements
FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileReadStream |
FileSystemProviderCapabilities.FileFolderCopy;
FileSystemProviderCapabilities.FileFolderCopy |
FileSystemProviderCapabilities.FileWriteUnlock;
if (isLinux) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
@@ -188,12 +189,12 @@ export class DiskFileSystemProvider extends Disposable implements
}
// Open
handle = await this.open(resource, { create: true });
handle = await this.open(resource, { create: true, unlock: opts.unlock });
// Write content at once
await this.write(handle, 0, content, 0, content.byteLength);
} catch (error) {
throw this.toFileSystemProviderError(error);
throw await this.toFileSystemProviderWriteError(resource, error);
} finally {
if (typeof handle === 'number') {
await this.close(handle);
@@ -203,15 +204,28 @@ export class DiskFileSystemProvider extends Disposable implements
private readonly mapHandleToPos: Map<number, number> = new Map();
private readonly writeHandles: Set<number> = new Set();
private readonly writeHandles = new Map<number, URI>();
private canFlush: boolean = true;
async open(resource: URI, opts: FileOpenOptions): Promise<number> {
try {
const filePath = this.toFilePath(resource);
// Determine wether to unlock the file (write only)
if (isFileOpenForWriteOptions(opts) && opts.unlock) {
try {
const { stat } = await SymlinkSupport.stat(filePath);
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
await promises.chmod(filePath, stat.mode | 0o200);
}
} catch (error) {
this.logService.trace(error); // ignore any errors here and try to just write
}
}
// Determine file flags for opening (read vs write)
let flags: string | undefined = undefined;
if (opts.create) {
if (isFileOpenForWriteOptions(opts)) {
if (isWindows) {
try {
// On Windows and if the file exists, we use a different strategy of saving the file
@@ -252,13 +266,17 @@ export class DiskFileSystemProvider extends Disposable implements
this.mapHandleToPos.set(handle, 0);
// remember that this handle was used for writing
if (opts.create) {
this.writeHandles.add(handle);
if (isFileOpenForWriteOptions(opts)) {
this.writeHandles.set(handle, resource);
}
return handle;
} catch (error) {
throw this.toFileSystemProviderError(error);
if (isFileOpenForWriteOptions(opts)) {
throw await this.toFileSystemProviderWriteError(resource, error);
} else {
throw this.toFileSystemProviderError(error);
}
}
}
@@ -388,7 +406,7 @@ export class DiskFileSystemProvider extends Disposable implements
return bytesWritten;
} catch (error) {
throw this.toFileSystemProviderError(error);
throw await this.toFileSystemProviderWriteError(this.writeHandles.get(fd), error);
} finally {
this.updatePos(fd, normalizedPos, bytesWritten);
}
@@ -690,9 +708,29 @@ export class DiskFileSystemProvider extends Disposable implements
return createFileSystemProviderError(error, code);
}
private async toFileSystemProviderWriteError(resource: URI | undefined, error: NodeJS.ErrnoException): Promise<FileSystemProviderError> {
let fileSystemProviderWriteError = this.toFileSystemProviderError(error);
// If the write error signals permission issues, we try
// to read the file's mode to see if the file is write
// locked.
if (resource && fileSystemProviderWriteError.code === FileSystemProviderErrorCode.NoPermissions) {
try {
const { stat } = await SymlinkSupport.stat(this.toFilePath(resource));
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
fileSystemProviderWriteError = createFileSystemProviderError(error, FileSystemProviderErrorCode.FileWriteLocked);
}
} catch (error) {
this.logService.trace(error); // ignore - return original error
}
}
return fileSystemProviderWriteError;
}
//#endregion
dispose(): void {
override dispose(): void {
super.dispose();
dispose(this.recursiveWatcher);

View File

@@ -124,7 +124,7 @@ export class FileWatcher extends Disposable {
}
}
dispose(): void {
override dispose(): void {
this.isDisposed = true;
super.dispose();

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nsfw from 'vscode-nsfw';
import * as nsfw from 'nsfw';
import * as glob from 'vs/base/common/glob';
import { join } from 'vs/base/common/path';
import { isMacintosh } from 'vs/base/common/platform';
@@ -61,9 +61,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
});
// Logging
if (this.verboseLogging) {
this.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
}
this.debug(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
// Stop watching some roots
rootsToStopWatching.forEach(root => {
@@ -133,9 +131,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
}
}
if (this.verboseLogging) {
this.log(`Start watching with nsfw: ${request.path}`);
}
this.debug(`Start watching with nsfw: ${request.path}`);
nsfw(request.path, events => {
for (const e of events) {
@@ -249,4 +245,8 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
private error(message: string) {
this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ` + message });
}
private debug(message: string) {
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (nsfw)] ` + message });
}
}

View File

@@ -10,7 +10,7 @@ import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
suite('NSFW Watcher Service', async () => {
// Load `nsfwWatcherService` within the suite to prevent all tests
// from failing to start if `vscode-nsfw` was not properly installed
// from failing to start if `nsfw` was not properly installed
const { NsfwWatcherService } = await import('vs/platform/files/node/watcher/nsfw/nsfwWatcherService');
class TestNsfwWatcherService extends NsfwWatcherService {

View File

@@ -5,8 +5,8 @@
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
const server = new Server('watcher');
const service = new NsfwWatcherService();
server.registerChannel('watcher', createChannelReceiver(service));
server.registerChannel('watcher', ProxyChannel.fromService(service));

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
@@ -61,7 +61,7 @@ export class FileWatcher extends Disposable {
}));
// Initialize watcher
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service.setVerboseLogging(this.verboseLogging);
@@ -91,7 +91,7 @@ export class FileWatcher extends Disposable {
}
}
dispose(): void {
override dispose(): void {
this.isDisposed = true;
super.dispose();

View File

@@ -49,7 +49,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
get wacherCount() { return this._watcherCount; }
private pollingInterval?: number;
private usePolling?: boolean;
private usePolling?: boolean | string[];
private verboseLogging: boolean | undefined;
private spamCheckStartTime: number | undefined;
@@ -101,7 +101,11 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
private watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
const pollingInterval = this.pollingInterval || 5000;
const usePolling = this.usePolling;
let usePolling = this.usePolling; // boolean or a list of path patterns
if (Array.isArray(usePolling)) {
// switch to polling if one of the paths matches with a watched path
usePolling = usePolling.some(pattern => requests.some(r => glob.match(pattern, r.path)));
}
const watcherOpts: chokidar.WatchOptions = {
ignoreInitial: true,
@@ -142,9 +146,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
this.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`);
}
if (this.verboseLogging) {
this.log(`Start watching with chokidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
}
this.debug(`Start watching with chokidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts);
this._watcherCount++;
@@ -297,6 +299,10 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (chokidar)] ` + message });
}
private debug(message: string) {
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (chokidar)] ` + message });
}
private warn(message: string) {
this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (chokidar)] ` + message });
}

View File

@@ -13,7 +13,7 @@ export interface IWatcherRequest {
export interface IWatcherOptions {
pollingInterval?: number;
usePolling?: boolean;
usePolling?: boolean | string[]; // boolean or a set of glob patterns matching folders that need polling
verboseLogging?: boolean;
}

View File

@@ -5,8 +5,8 @@
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { ChokidarWatcherService } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService';
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
const server = new Server('watcher');
const service = new ChokidarWatcherService();
server.registerChannel('watcher', createChannelReceiver(service));
server.registerChannel('watcher', ProxyChannel.fromService(service));

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
@@ -62,7 +62,7 @@ export class FileWatcher extends Disposable {
}));
// Initialize watcher
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service.init({ ...this.watcherOptions, verboseLogging: this.verboseLogging });
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));
@@ -92,7 +92,7 @@ export class FileWatcher extends Disposable {
}
}
dispose(): void {
override dispose(): void {
this.isDisposed = true;
super.dispose();

View File

@@ -13,7 +13,7 @@ export interface IDiskFileChange {
}
export interface ILogMessage {
type: 'trace' | 'warn' | 'error';
type: 'trace' | 'warn' | 'error' | 'info' | 'debug';
message: string;
}

View File

@@ -6,11 +6,13 @@
import * as assert from 'assert';
import { FileService } from 'vs/platform/files/common/fileService';
import { URI } from 'vs/base/common/uri';
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files';
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities, IFileSystemProviderCapabilitiesChangeEvent, FileOpenOptions, FileReadStreamOptions, IStat, FileType } from 'vs/platform/files/common/files';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { NullLogService } from 'vs/platform/log/common/log';
import { timeout } from 'vs/base/common/async';
import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider';
import { consumeStream, newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
suite('File Service', () => {
@@ -20,6 +22,7 @@ suite('File Service', () => {
const provider = new NullFileSystemProvider();
assert.strictEqual(service.canHandleResource(resource), false);
assert.strictEqual(service.getProvider(resource.scheme), undefined);
const registrations: IFileSystemProviderRegistrationEvent[] = [];
service.onDidChangeFileSystemProviderRegistrations(e => {
@@ -31,7 +34,7 @@ suite('File Service', () => {
capabilityChanges.push(e);
});
let registrationDisposable: IDisposable | undefined = undefined;
let registrationDisposable: IDisposable | undefined;
let callCount = 0;
service.onWillActivateFileSystemProvider(e => {
callCount++;
@@ -48,6 +51,7 @@ suite('File Service', () => {
await service.activateProvider('test');
assert.strictEqual(service.canHandleResource(resource), true);
assert.strictEqual(service.getProvider(resource.scheme), provider);
assert.strictEqual(registrations.length, 1);
assert.strictEqual(registrations[0].scheme, 'test');
@@ -126,4 +130,82 @@ suite('File Service', () => {
service.dispose();
});
test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060) - async', async () => {
testReadErrorBubbles(true);
});
test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060)', async () => {
testReadErrorBubbles(false);
});
async function testReadErrorBubbles(async: boolean) {
const service = new FileService(new NullLogService());
const provider = new class extends NullFileSystemProvider {
override async stat(resource: URI): Promise<IStat> {
return {
mtime: Date.now(),
ctime: Date.now(),
size: 100,
type: FileType.File
};
}
override readFile(resource: URI): Promise<Uint8Array> {
if (async) {
return timeout(5).then(() => { throw new Error('failed'); });
}
throw new Error('failed');
}
override open(resource: URI, opts: FileOpenOptions): Promise<number> {
if (async) {
return timeout(5).then(() => { throw new Error('failed'); });
}
throw new Error('failed');
}
readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
if (async) {
const stream = newWriteableStream<Uint8Array>(chunk => chunk[0]);
timeout(5).then(() => stream.error(new Error('failed')));
return stream;
}
throw new Error('failed');
}
};
const disposable = service.registerProvider('test', provider);
for (const capabilities of [FileSystemProviderCapabilities.FileReadWrite, FileSystemProviderCapabilities.FileReadStream, FileSystemProviderCapabilities.FileOpenReadWriteClose]) {
provider.setCapabilities(capabilities);
let e1;
try {
await service.readFile(URI.parse('test://foo/bar'));
} catch (error) {
e1 = error;
}
assert.ok(e1);
let e2;
try {
const stream = await service.readFileStream(URI.parse('test://foo/bar'));
await consumeStream(stream.value, chunk => chunk[0]);
} catch (error) {
e2 = error;
}
assert.ok(e2);
}
disposable.dispose();
}
});

View File

@@ -78,7 +78,8 @@ suite('IndexedDB File Service', function () {
disposables.add(userdataFileProvider);
};
setup(async () => {
setup(async function () {
this.timeout(15000);
await reload();
});

View File

@@ -8,13 +8,12 @@ import { tmpdir } from 'os';
import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { getRandomTestPath, getPathFromAmdModule } from 'vs/base/test/node/testUtils';
import { join, basename, dirname, posix } from 'vs/base/common/path';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { copy, rimraf, rimrafSync } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream, promises } from 'fs';
import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata, IReadFileOptions } from 'vs/platform/files/common/files';
import { NullLogService } from 'vs/platform/log/common/log';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { DisposableStore } from 'vs/base/common/lifecycle';
@@ -59,13 +58,14 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
private smallStatSize: boolean = false;
private _testCapabilities!: FileSystemProviderCapabilities;
get capabilities(): FileSystemProviderCapabilities {
override get capabilities(): FileSystemProviderCapabilities {
if (!this._testCapabilities) {
this._testCapabilities =
FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileReadStream |
FileSystemProviderCapabilities.Trash |
FileSystemProviderCapabilities.FileWriteUnlock |
FileSystemProviderCapabilities.FileFolderCopy;
if (isLinux) {
@@ -76,7 +76,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
return this._testCapabilities;
}
set capabilities(capabilities: FileSystemProviderCapabilities) {
override set capabilities(capabilities: FileSystemProviderCapabilities) {
this._testCapabilities = capabilities;
}
@@ -88,7 +88,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
this.smallStatSize = enabled;
}
async stat(resource: URI): Promise<IStat> {
override async stat(resource: URI): Promise<IStat> {
const res = await super.stat(resource);
if (this.invalidStatSize) {
@@ -100,7 +100,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
return res;
}
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
override async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const bytesRead = await super.read(fd, pos, data, offset, length);
this.totalBytesRead += bytesRead;
@@ -108,7 +108,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
return bytesRead;
}
async readFile(resource: URI): Promise<Uint8Array> {
override async readFile(resource: URI): Promise<Uint8Array> {
const res = await super.readFile(resource);
this.totalBytesRead += res.byteLength;
@@ -1181,8 +1181,14 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
return testReadFile(URI.file(join(testDir, 'lorem.txt')));
});
async function testReadFile(resource: URI): Promise<void> {
const content = await service.readFile(resource);
test('readFile - atomic', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadStream);
return testReadFile(URI.file(join(testDir, 'lorem.txt')), { atomic: true });
});
async function testReadFile(resource: URI, options?: IReadFileOptions): Promise<void> {
const content = await service.readFile(resource, options);
assert.strictEqual(content.value.toString(), readFileSync(resource.fsPath).toString());
}
@@ -1584,6 +1590,20 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE);
}
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => {
const link = URI.file(join(testDir, 'small.js-link'));
await promises.symlink(join(testDir, 'small.js'), link.fsPath);
let error: FileOperationError | undefined = undefined;
try {
await service.readFile(link);
} catch (err) {
error = err;
}
assert.ok(error);
});
test('createFile', async () => {
return assertCreateFile(contents => VSBuffer.fromString(contents));
});
@@ -1740,19 +1760,23 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
assert.ok(error!);
}
test('writeFile (large file) - multiple parallel writes queue up', async () => {
test('writeFile (large file) - multiple parallel writes queue up and atomic read support', async () => {
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = readFileSync(resource.fsPath);
const newContent = content.toString() + content.toString();
await Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => {
const writePromises = Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => {
const fileStat = await service.writeFile(resource, VSBuffer.fromString(offset + newContent));
assert.strictEqual(fileStat.name, 'lorem.txt');
}));
const fileContent = readFileSync(resource.fsPath).toString();
assert.ok(['0', '00', '000', '0000', '00000'].some(offset => fileContent === offset + newContent));
const readPromises = Promise.all(['0', '00', '000', '0000', '00000'].map(async () => {
const fileContent = await service.readFile(resource, { atomic: true });
assert.ok(fileContent.value.byteLength > 0); // `atomic: true` ensures we never read a truncated file
}));
await Promise.all([writePromises, readPromises]);
});
test('writeFile (readable) - default', async () => {
@@ -1875,6 +1899,63 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
assert.strictEqual(readFileSync(resource.fsPath).toString(), content);
});
test('writeFile - locked files and unlocking', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileWriteUnlock);
return testLockedFiles(false);
});
test('writeFile (stream) - locked files and unlocking', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileWriteUnlock);
return testLockedFiles(false);
});
test('writeFile - locked files and unlocking throws error when missing capability', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
return testLockedFiles(true);
});
test('writeFile (stream) - locked files and unlocking throws error when missing capability', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
return testLockedFiles(true);
});
async function testLockedFiles(expectError: boolean) {
const lockedFile = URI.file(join(testDir, 'my-locked-file'));
await service.writeFile(lockedFile, VSBuffer.fromString('Locked File'));
const stats = await promises.stat(lockedFile.fsPath);
await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200);
let error;
const newContent = 'Updates to locked file';
try {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent));
} catch (e) {
error = e;
}
assert.ok(error);
error = undefined;
if (expectError) {
try {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
} catch (e) {
error = e;
}
assert.ok(error);
} else {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
assert.strictEqual(readFileSync(lockedFile.fsPath).toString(), newContent);
}
}
test('writeFile (error when folder is encountered)', async () => {
const resource = URI.file(testDir);
@@ -2272,7 +2353,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
const resource = URI.file(join(testDir, 'lorem.txt'));
const buffer = VSBuffer.alloc(1024);
const fdWrite = await fileProvider.open(resource, { create: true });
const fdWrite = await fileProvider.open(resource, { create: true, unlock: false });
const fdRead = await fileProvider.open(resource, { create: false });
let posInFileWrite = 0;

View File

@@ -8,8 +8,14 @@ import { ServiceIdentifier, BrandedService } from './instantiation';
const _registry: [ServiceIdentifier<any>, SyncDescriptor<any>][] = [];
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctor: new (...services: Services) => T, supportsDelayedInstantiation?: boolean): void {
_registry.push([id, new SyncDescriptor<T>(ctor as new (...args: any[]) => T, [], supportsDelayedInstantiation)]);
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctor: new (...services: Services) => T, supportsDelayedInstantiation?: boolean): void;
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, descriptor: SyncDescriptor<any>): void;
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctorOrDescriptor: { new(...services: Services): T } | SyncDescriptor<any>, supportsDelayedInstantiation?: boolean): void {
if (!(ctorOrDescriptor instanceof SyncDescriptor)) {
ctorOrDescriptor = new SyncDescriptor<T>(ctorOrDescriptor as new (...args: any[]) => T, [], supportsDelayedInstantiation);
}
_registry.push([id, ctorOrDescriptor]);
}
export function getSingletonServiceDescriptors(): [ServiceIdentifier<any>, SyncDescriptor<any>][] {

View File

@@ -77,4 +77,34 @@ export class Graph<T> {
}
return data.join('\n');
}
/**
* This is brute force and slow and **only** be used
* to trouble shoot.
*/
findCycleSlow() {
for (let [id, node] of this._nodes) {
const seen = new Set<string>([id]);
const res = this._findCycle(node, seen);
if (res) {
return res;
}
}
return undefined;
}
private _findCycle(node: Node<T>, seen: Set<string>): string | undefined {
for (let [id, outgoing] of node.outgoing) {
if (seen.has(id)) {
return [...seen, id].join(' -> ');
}
seen.add(id);
const value = this._findCycle(outgoing, seen);
if (value) {
return value;
}
seen.delete(id);
}
return undefined;
}
}

View File

@@ -155,6 +155,10 @@ export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
return id;
}
export function refineServiceDecorator<T1, T extends T1>(serviceIdentifier: ServiceIdentifier<T1>): ServiceIdentifier<T> {
return <ServiceIdentifier<T>>serviceIdentifier;
}
/**
* Mark a service dependency as optional.
*/

View File

@@ -16,7 +16,7 @@ const _enableTracing = false;
class CyclicDependencyError extends Error {
constructor(graph: Graph<any>) {
super('cyclic dependency between services');
this.message = graph.toString();
this.message = graph.findCycleSlow() ?? `UNABLE to detect cycle, dumping graph: \n${graph.toString()}`;
}
}
@@ -268,8 +268,8 @@ export class Trace {
private static readonly _None = new class extends Trace {
constructor() { super(-1, null); }
stop() { }
branch() { return this; }
override stop() { }
override branch() { return this; }
};
static traceInvocation(ctor: any): Trace {

View File

@@ -13,34 +13,34 @@ suite('Graph', () => {
});
test('is possible to lookup nodes that don\'t exist', function () {
assert.deepEqual(graph.lookup('ddd'), null);
assert.strictEqual(graph.lookup('ddd'), undefined);
});
test('inserts nodes when not there yet', function () {
assert.deepEqual(graph.lookup('ddd'), null);
assert.deepEqual(graph.lookupOrInsertNode('ddd').data, 'ddd');
assert.deepEqual(graph.lookup('ddd')!.data, 'ddd');
assert.strictEqual(graph.lookup('ddd'), undefined);
assert.strictEqual(graph.lookupOrInsertNode('ddd').data, 'ddd');
assert.strictEqual(graph.lookup('ddd')!.data, 'ddd');
});
test('can remove nodes and get length', function () {
assert.ok(graph.isEmpty());
assert.deepEqual(graph.lookup('ddd'), null);
assert.deepEqual(graph.lookupOrInsertNode('ddd').data, 'ddd');
assert.strictEqual(graph.lookup('ddd'), undefined);
assert.strictEqual(graph.lookupOrInsertNode('ddd').data, 'ddd');
assert.ok(!graph.isEmpty());
graph.removeNode('ddd');
assert.deepEqual(graph.lookup('ddd'), null);
assert.strictEqual(graph.lookup('ddd'), undefined);
assert.ok(graph.isEmpty());
});
test('root', () => {
graph.insertEdge('1', '2');
let roots = graph.roots();
assert.equal(roots.length, 1);
assert.equal(roots[0].data, '2');
assert.strictEqual(roots.length, 1);
assert.strictEqual(roots[0].data, '2');
graph.insertEdge('2', '1');
roots = graph.roots();
assert.equal(roots.length, 0);
assert.strictEqual(roots.length, 0);
});
test('root complex', function () {
@@ -49,7 +49,7 @@ suite('Graph', () => {
graph.insertEdge('3', '4');
let roots = graph.roots();
assert.equal(roots.length, 2);
assert.strictEqual(roots.length, 2);
assert(['2', '4'].every(n => roots.some(node => node.data === n)));
});
});

View File

@@ -55,7 +55,7 @@ interface IDependentService {
class DependentService implements IDependentService {
declare readonly _serviceBrand: undefined;
constructor(@IService1 service: IService1) {
assert.equal(service.c, 1);
assert.strictEqual(service.c, 1);
}
name = 'farboo';
@@ -65,7 +65,7 @@ class Service1Consumer {
constructor(@IService1 service1: IService1) {
assert.ok(service1);
assert.equal(service1.c, 1);
assert.strictEqual(service1.c, 1);
}
}
@@ -81,7 +81,7 @@ class TargetWithStaticParam {
constructor(v: boolean, @IService1 service1: IService1) {
assert.ok(v);
assert.ok(service1);
assert.equal(service1.c, 1);
assert.strictEqual(service1.c, 1);
}
}
@@ -93,7 +93,7 @@ class TargetNotOptional {
class TargetOptional {
constructor(@IService1 service1: IService1, @optional(IService2) service2: IService2) {
assert.ok(service1);
assert.equal(service1.c, 1);
assert.strictEqual(service1.c, 1);
assert.ok(service2 === undefined);
}
}
@@ -101,16 +101,16 @@ class TargetOptional {
class DependentServiceTarget {
constructor(@IDependentService d: IDependentService) {
assert.ok(d);
assert.equal(d.name, 'farboo');
assert.strictEqual(d.name, 'farboo');
}
}
class DependentServiceTarget2 {
constructor(@IDependentService d: IDependentService, @IService1 s: IService1) {
assert.ok(d);
assert.equal(d.name, 'farboo');
assert.strictEqual(d.name, 'farboo');
assert.ok(s);
assert.equal(s.c, 1);
assert.strictEqual(s.c, 1);
}
}
@@ -138,9 +138,9 @@ suite('Instantiation Service', () => {
test('service collection, cannot overwrite', function () {
let collection = new ServiceCollection();
let result = collection.set(IService1, null!);
assert.equal(result, undefined);
assert.strictEqual(result, undefined);
result = collection.set(IService1, new Service1());
assert.equal(result, null);
assert.strictEqual(result, null);
});
test('service collection, add/has', function () {
@@ -237,7 +237,7 @@ suite('Instantiation Service', () => {
let service1 = accessor.get(IService1);
assert.ok(service1);
assert.equal(service1.c, 1);
assert.strictEqual(service1.c, 1);
let service2 = accessor.get(IService1);
assert.ok(service1 === service2);
@@ -253,7 +253,7 @@ suite('Instantiation Service', () => {
service.invokeFunction(accessor => {
let d = accessor.get(IDependentService);
assert.ok(d);
assert.equal(d.name, 'farboo');
assert.strictEqual(d.name, 'farboo');
});
});
@@ -305,12 +305,12 @@ suite('Instantiation Service', () => {
function test(accessor: ServicesAccessor) {
assert.ok(accessor.get(IService1) instanceof Service1);
assert.equal(accessor.get(IService1).c, 1);
assert.strictEqual(accessor.get(IService1).c, 1);
return true;
}
assert.equal(service.invokeFunction(test), true);
assert.strictEqual(service.invokeFunction(test), true);
});
test('Invoke - get service, optional', function () {
@@ -320,10 +320,10 @@ suite('Instantiation Service', () => {
function test(accessor: ServicesAccessor) {
assert.ok(accessor.get(IService1) instanceof Service1);
assert.throws(() => accessor.get(IService2));
assert.equal(accessor.get(IService2, optional), undefined);
assert.strictEqual(accessor.get(IService2, optional), undefined);
return true;
}
assert.equal(service.invokeFunction(test), true);
assert.strictEqual(service.invokeFunction(test), true);
});
test('Invoke - keeping accessor NOT allowed', function () {
@@ -336,12 +336,12 @@ suite('Instantiation Service', () => {
function test(accessor: ServicesAccessor) {
assert.ok(accessor.get(IService1) instanceof Service1);
assert.equal(accessor.get(IService1).c, 1);
assert.strictEqual(accessor.get(IService1).c, 1);
cached = accessor;
return true;
}
assert.equal(service.invokeFunction(test), true);
assert.strictEqual(service.invokeFunction(test), true);
assert.throws(() => cached.get(IService2));
});
@@ -379,7 +379,7 @@ suite('Instantiation Service', () => {
let child = service.createChild(new ServiceCollection([IService2, new Service2()]));
child.createInstance(Service1Consumer);
assert.equal(serviceInstanceCount, 1);
assert.strictEqual(serviceInstanceCount, 1);
// creating the service instance AFTER the child service
serviceInstanceCount = 0;
@@ -390,7 +390,7 @@ suite('Instantiation Service', () => {
service.createInstance(Service1Consumer);
child.createInstance(Service1Consumer);
assert.equal(serviceInstanceCount, 1);
assert.strictEqual(serviceInstanceCount, 1);
});
test('Remote window / integration tests is broken #105562', function () {

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-browser/ipc.mp';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
/**
* An implementation of `IMainProcessService` that leverages MessagePorts.
*/
export class MessagePortMainProcessService implements IMainProcessService {
declare readonly _serviceBrand: undefined;
constructor(
private server: MessagePortServer,
private router: StaticRouter
) { }
getChannel(channelName: string): IChannel {
return this.server.getChannel(channelName, this.router);
}
registerChannel(channelName: string, channel: IServerChannel<string>): void {
this.server.registerChannel(channelName, channel);
}
}

View File

@@ -1,68 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
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');
export interface ISharedProcessService {
readonly _serviceBrand: undefined;
getChannel(channelName: string): IChannel;
registerChannel(channelName: string, channel: IServerChannel<string>): void;
}
export class SharedProcessService extends Disposable implements ISharedProcessService {
declare readonly _serviceBrand: undefined;
private readonly withSharedProcessConnection: Promise<MessagePortClient>;
constructor(
@INativeHostService private readonly nativeHostService: INativeHostService,
@ILogService private readonly logService: ILogService
) {
super();
this.withSharedProcessConnection = this.connect();
}
private async connect(): Promise<MessagePortClient> {
this.logService.trace('Renderer->SharedProcess#connect');
// Ask to create message channel inside the window
// and send over a UUID to correlate the response
const nonce = generateUuid();
ipcRenderer.send('vscode:createSharedProcessMessageChannel', nonce);
// Wait until the main side has returned the `MessagePort`
// We need to filter by the `nonce` to ensure we listen
// to the right response.
const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePort }>(ipcRenderer, 'vscode:createSharedProcessMessageChannelResult', (e: IpcRendererEvent, nonce: string) => ({ nonce, port: e.ports[0] }));
const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce)));
this.logService.trace('Renderer->SharedProcess#connect: connection established');
return this._register(new MessagePortClient(port, `window:${this.nativeHostService.windowId}`));
}
getChannel(channelName: string): IChannel {
return getDelayedChannel(this.withSharedProcessConnection.then(connection => connection.getChannel(channelName)));
}
registerChannel(channelName: string, channel: IServerChannel<string>): void {
this.withSharedProcessConnection.then(connection => connection.registerChannel(channelName, channel));
}
}

View File

@@ -3,22 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
import { IChannel, IServerChannel } 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');
export interface IMainProcessService {
readonly _serviceBrand: undefined;
getChannel(channelName: string): IChannel;
registerChannel(channelName: string, channel: IServerChannel<string>): void;
}
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
/**
* An implementation of `IMainProcessService` that leverages Electron's IPC.
@@ -45,24 +33,3 @@ export class ElectronIPCMainProcessService extends Disposable implements IMainPr
this.mainProcessConnection.registerChannel(channelName, channel);
}
}
/**
* An implementation of `IMainProcessService` that leverages MessagePorts.
*/
export class MessagePortMainProcessService implements IMainProcessService {
declare readonly _serviceBrand: undefined;
constructor(
private server: MessagePortServer,
private router: StaticRouter
) { }
getChannel(channelName: string): IChannel {
return this.server.getChannel(channelName, this.router);
}
registerChannel(channelName: string, channel: IServerChannel<string>): void {
this.server.registerChannel(channelName, channel);
}
}

View File

@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
type ChannelClientCtor<T> = { new(channel: IChannel): T };
type Remote = { getChannel(channelName: string): IChannel; };
abstract class RemoteServiceStub<T> {
constructor(
channelName: string,
options: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions | undefined,
remote: Remote
) {
const channel = remote.getChannel(channelName);
if (isRemoteServiceWithChannelClientOptions(options)) {
return new options.channelClientCtor(channel);
}
return ProxyChannel.toService(channel, options?.proxyOptions);
}
}
export interface IBaseRemoteServiceOptions {
readonly supportsDelayedInstantiation?: boolean;
}
export interface IRemoteServiceWithChannelClientOptions<T> extends IBaseRemoteServiceOptions {
readonly channelClientCtor: ChannelClientCtor<T>;
}
export interface IRemoteServiceWithProxyOptions extends IBaseRemoteServiceOptions {
readonly proxyOptions?: ProxyChannel.ICreateProxyServiceOptions;
}
function isRemoteServiceWithChannelClientOptions<T>(obj: unknown): obj is IRemoteServiceWithChannelClientOptions<T> {
const candidate = obj as IRemoteServiceWithChannelClientOptions<T> | undefined;
return !!candidate?.channelClientCtor;
}
//#region Main Process
export const IMainProcessService = createDecorator<IMainProcessService>('mainProcessService');
export interface IMainProcessService {
readonly _serviceBrand: undefined;
getChannel(channelName: string): IChannel;
registerChannel(channelName: string, channel: IServerChannel<string>): void;
}
class MainProcessRemoteServiceStub<T> extends RemoteServiceStub<T> {
constructor(channelName: string, options: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions | undefined, @IMainProcessService ipcService: IMainProcessService) {
super(channelName, options, ipcService);
}
}
export function registerMainProcessRemoteService<T>(id: ServiceIdentifier<T>, channelName: string, options?: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions): void {
registerSingleton(id, new SyncDescriptor(MainProcessRemoteServiceStub, [channelName, options], options?.supportsDelayedInstantiation));
}
//#endregion
//#region Shared Process
export const ISharedProcessService = createDecorator<ISharedProcessService>('sharedProcessService');
export interface ISharedProcessService {
readonly _serviceBrand: undefined;
getChannel(channelName: string): IChannel;
registerChannel(channelName: string, channel: IServerChannel<string>): void;
}
class SharedProcessRemoteServiceStub<T> extends RemoteServiceStub<T> {
constructor(channelName: string, options: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions | undefined, @ISharedProcessService ipcService: ISharedProcessService) {
super(channelName, options, ipcService);
}
}
export function registerSharedProcessRemoteService<T>(id: ServiceIdentifier<T>, channelName: string, options?: IRemoteServiceWithChannelClientOptions<T> | IRemoteServiceWithProxyOptions): void {
registerSingleton(id, new SyncDescriptor(SharedProcessRemoteServiceStub, [channelName, options], options?.supportsDelayedInstantiation));
}
//#endregion

View File

@@ -3,6 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes';
// Since data sent through the service is serialized to JSON, functions will be lost, so Color objects
// should not be sent as their 'toString' method will be stripped. Instead convert to strings before sending.
export interface WindowStyles {
@@ -40,7 +42,7 @@ export interface IssueReporterStyles extends WindowStyles {
export interface IssueReporterExtensionData {
name: string;
publisher: string;
publisher: string | undefined;
version: string;
id: string;
isTheme: boolean;
@@ -67,9 +69,6 @@ export interface ISettingSearchResult {
score: number;
}
export interface IssueReporterFeatures {
}
export interface ProcessExplorerStyles extends WindowStyles {
hoverBackground?: string;
hoverForeground?: string;
@@ -78,7 +77,7 @@ export interface ProcessExplorerStyles extends WindowStyles {
export interface ProcessExplorerData extends WindowData {
pid: number;
styles: ProcessExplorerStyles;
platform: 'win32' | 'darwin' | 'linux';
platform: string;
applicationName: string;
}
@@ -88,3 +87,17 @@ export interface ICommonIssueService {
openProcessExplorer(data: ProcessExplorerData): Promise<void>;
getSystemStatus(): Promise<string>;
}
export interface IssueReporterWindowConfiguration extends ISandboxConfiguration {
disableExtensions: boolean;
data: IssueReporterData;
os: {
type: string;
arch: string;
release: string;
}
}
export interface ProcessExplorerWindowConfiguration extends ISandboxConfiguration {
data: ProcessExplorerData;
}

View File

@@ -4,16 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import * as os from 'os';
import { arch, release, type } from 'os';
import product from 'vs/platform/product/common/product';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { ICommonIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/common/issue';
import { ICommonIssueService, IssueReporterWindowConfiguration, IssueReporterData, ProcessExplorerData, ProcessExplorerWindowConfiguration } from 'vs/platform/issue/common/issue';
import { BrowserWindow, ipcMain, screen, IpcMainEvent, Display } from 'electron';
import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService';
import { PerformanceInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics';
import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
import { IDiagnosticsService, PerformanceInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform';
import { isMacintosh, IProcessEnvironment, browserCodeLoadingCacheStrategy } 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';
@@ -22,66 +20,69 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows';
import { FileAccess } from 'vs/base/common/network';
import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
const DEFAULT_BACKGROUND_COLOR = '#1E1E1E';
import { IIPCObjectUrl, IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol';
import { DisposableStore } from 'vs/base/common/lifecycle';
export const IIssueMainService = createDecorator<IIssueMainService>('issueMainService');
export interface IIssueMainService extends ICommonIssueService { }
export class IssueMainService implements ICommonIssueService {
declare readonly _serviceBrand: undefined;
_issueWindow: BrowserWindow | null = null;
_issueParentWindow: BrowserWindow | null = null;
_processExplorerWindow: BrowserWindow | null = null;
_processExplorerParentWindow: BrowserWindow | null = null;
private static readonly DEFAULT_BACKGROUND_COLOR = '#1E1E1E';
private issueReporterWindow: BrowserWindow | null = null;
private issueReporterParentWindow: BrowserWindow | null = null;
private processExplorerWindow: BrowserWindow | null = null;
private processExplorerParentWindow: BrowserWindow | null = null;
constructor(
private machineId: string,
private userEnv: IProcessEnvironment,
@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@ILaunchMainService private readonly launchMainService: ILaunchMainService,
@ILogService private readonly logService: ILogService,
@IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService,
@IDialogMainService private readonly dialogMainService: IDialogMainService,
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IProtocolMainService private readonly protocolMainService: IProtocolMainService
) {
this.registerListeners();
}
private registerListeners(): void {
ipcMain.on('vscode:issueSystemInfoRequest', async (event: IpcMainEvent) => {
Promise.all([this.launchMainService.getMainProcessInfo(), this.launchMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })])
.then(result => {
const [info, remoteData] = result;
this.diagnosticsService.getSystemInfo(info, remoteData).then(msg => {
this.safeSend(event, 'vscode:issueSystemInfoResponse', msg);
});
});
ipcMain.on('vscode:issueSystemInfoRequest', async event => {
const [info, remoteData] = await Promise.all([this.launchMainService.getMainProcessInfo(), this.launchMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]);
const msg = await this.diagnosticsService.getSystemInfo(info, remoteData);
this.safeSend(event, 'vscode:issueSystemInfoResponse', msg);
});
ipcMain.on('vscode:listProcesses', async (event: IpcMainEvent) => {
ipcMain.on('vscode:listProcesses', async event => {
const processes = [];
try {
const mainPid = await this.launchMainService.getMainProcessId();
processes.push({ name: localize('local', "Local"), rootProcess: await listProcesses(mainPid) });
(await this.launchMainService.getRemoteDiagnostics({ includeProcesses: true }))
.forEach(data => {
if (isRemoteDiagnosticError(data)) {
const remoteDiagnostics = await this.launchMainService.getRemoteDiagnostics({ includeProcesses: true });
remoteDiagnostics.forEach(data => {
if (isRemoteDiagnosticError(data)) {
processes.push({
name: data.hostName,
rootProcess: data
});
} else {
if (data.processes) {
processes.push({
name: data.hostName,
rootProcess: data
rootProcess: data.processes
});
} else {
if (data.processes) {
processes.push({
name: data.hostName,
rootProcess: data.processes
});
}
}
});
}
});
} catch (e) {
this.logService.error(`Listing processes failed: ${e}`);
}
@@ -89,7 +90,7 @@ export class IssueMainService implements ICommonIssueService {
this.safeSend(event, 'vscode:listProcessesResponse', processes);
});
ipcMain.on('vscode:issueReporterClipboard', (event: IpcMainEvent) => {
ipcMain.on('vscode:issueReporterClipboard', async event => {
const messageOptions = {
message: localize('issueReporterWriteToClipboard', "There is too much data to send to GitHub directly. The data will be copied to the clipboard, please paste it into the GitHub issue page that is opened."),
type: 'warning',
@@ -99,21 +100,18 @@ export class IssueMainService implements ICommonIssueService {
]
};
if (this._issueWindow) {
this.dialogMainService.showMessageBox(messageOptions, this._issueWindow)
.then(result => {
this.safeSend(event, 'vscode:issueReporterClipboardResponse', result.response === 0);
});
if (this.issueReporterWindow) {
const result = await this.dialogMainService.showMessageBox(messageOptions, this.issueReporterWindow);
this.safeSend(event, 'vscode:issueReporterClipboardResponse', result.response === 0);
}
});
ipcMain.on('vscode:issuePerformanceInfoRequest', (event: IpcMainEvent) => {
this.getPerformanceInfo().then(msg => {
this.safeSend(event, 'vscode:issuePerformanceInfoResponse', msg);
});
ipcMain.on('vscode:issuePerformanceInfoRequest', async event => {
const performanceInfo = await this.getPerformanceInfo();
this.safeSend(event, 'vscode:issuePerformanceInfoResponse', performanceInfo);
});
ipcMain.on('vscode:issueReporterConfirmClose', () => {
ipcMain.on('vscode:issueReporterConfirmClose', async () => {
const messageOptions = {
message: localize('confirmCloseIssueReporter', "Your input will not be saved. Are you sure you want to close this window?"),
type: 'warning',
@@ -123,16 +121,14 @@ export class IssueMainService implements ICommonIssueService {
]
};
if (this._issueWindow) {
this.dialogMainService.showMessageBox(messageOptions, this._issueWindow)
.then(result => {
if (result.response === 0) {
if (this._issueWindow) {
this._issueWindow.destroy();
this._issueWindow = null;
}
}
});
if (this.issueReporterWindow) {
const result = await this.dialogMainService.showMessageBox(messageOptions, this.issueReporterWindow);
if (result.response === 0) {
if (this.issueReporterWindow) {
this.issueReporterWindow.destroy();
this.issueReporterWindow = null;
}
}
}
});
@@ -142,10 +138,10 @@ export class IssueMainService implements ICommonIssueService {
let parentWindow: BrowserWindow | null;
switch (from) {
case 'issueReporter':
parentWindow = this._issueParentWindow;
parentWindow = this.issueReporterParentWindow;
break;
case 'processExplorer':
parentWindow = this._processExplorerParentWindow;
parentWindow = this.processExplorerParentWindow;
break;
default:
throw new Error(`Unexpected command source: ${from}`);
@@ -160,22 +156,21 @@ export class IssueMainService implements ICommonIssueService {
this.nativeHostMainService.openExternal(undefined, arg);
});
ipcMain.on('vscode:closeIssueReporter', (event: IpcMainEvent) => {
if (this._issueWindow) {
this._issueWindow.close();
ipcMain.on('vscode:closeIssueReporter', event => {
if (this.issueReporterWindow) {
this.issueReporterWindow.close();
}
});
ipcMain.on('vscode:closeProcessExplorer', (event: IpcMainEvent) => {
if (this._processExplorerWindow) {
this._processExplorerWindow.close();
ipcMain.on('vscode:closeProcessExplorer', event => {
if (this.processExplorerWindow) {
this.processExplorerWindow.close();
}
});
ipcMain.on('vscode:windowsInfoRequest', (event: IpcMainEvent) => {
this.launchMainService.getMainProcessInfo().then(info => {
this.safeSend(event, 'vscode:windowsInfoResponse', info.windows);
});
ipcMain.on('vscode:windowsInfoRequest', async event => {
const mainProcessInfo = await this.launchMainService.getMainProcessInfo();
this.safeSend(event, 'vscode:windowsInfoResponse', mainProcessInfo.windows);
});
}
@@ -186,128 +181,138 @@ export class IssueMainService implements ICommonIssueService {
}
async openReporter(data: IssueReporterData): Promise<void> {
if (!this._issueWindow) {
this._issueParentWindow = BrowserWindow.getFocusedWindow();
if (this._issueParentWindow) {
const position = this.getWindowPosition(this._issueParentWindow, 700, 800);
if (!this.issueReporterWindow) {
this.issueReporterParentWindow = BrowserWindow.getFocusedWindow();
if (this.issueReporterParentWindow) {
const issueReporterDisposables = new DisposableStore();
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
}
const issueReporterWindowConfigUrl = issueReporterDisposables.add(this.protocolMainService.createIPCObjectUrl<IssueReporterWindowConfiguration>());
const position = this.getWindowPosition(this.issueReporterParentWindow, 700, 800);
this.issueReporterWindow = this.createBrowserWindow(position, issueReporterWindowConfigUrl, data.styles.backgroundColor, localize('issueReporter', "Issue Reporter"), data.zoomLevel);
// Store into config object URL
issueReporterWindowConfigUrl.update({
appRoot: this.environmentMainService.appRoot,
windowId: this.issueReporterWindow.id,
userEnv: this.userEnv,
data,
disableExtensions: !!this.environmentMainService.disableExtensions,
os: {
type: type(),
arch: arch(),
release: release(),
},
product
});
this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented
this.issueReporterWindow.loadURL(
FileAccess.asBrowserUri('vs/code/electron-sandbox/issue/issueReporter.html', require, true).toString(true)
);
// Modified when testing UI
const features: IssueReporterFeatures = {};
this.issueReporterWindow.on('close', () => {
this.issueReporterWindow = null;
this.logService.trace('issueService#openReporter: opening issue reporter');
this._issueWindow.loadURL(this.getIssueReporterPath(data, features));
issueReporterDisposables.dispose();
});
this._issueWindow.on('close', () => this._issueWindow = null);
this.issueReporterParentWindow.on('closed', () => {
if (this.issueReporterWindow) {
this.issueReporterWindow.close();
this.issueReporterWindow = null;
this._issueParentWindow.on('closed', () => {
if (this._issueWindow) {
this._issueWindow.close();
this._issueWindow = null;
issueReporterDisposables.dispose();
}
});
}
}
if (this._issueWindow) {
this._issueWindow.focus();
}
this.issueReporterWindow?.focus();
}
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
}
if (!this.processExplorerWindow) {
this.processExplorerParentWindow = BrowserWindow.getFocusedWindow();
if (this.processExplorerParentWindow) {
const processExplorerDisposables = new DisposableStore();
const processExplorerWindowConfigUrl = processExplorerDisposables.add(this.protocolMainService.createIPCObjectUrl<ProcessExplorerWindowConfiguration>());
const position = this.getWindowPosition(this.processExplorerParentWindow, 800, 500);
this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, data.styles.backgroundColor, localize('processExplorer', "Process Explorer"), data.zoomLevel);
// Store into config object URL
processExplorerWindowConfigUrl.update({
appRoot: this.environmentMainService.appRoot,
windowId: this.processExplorerWindow.id,
userEnv: this.userEnv,
data,
product
});
this._processExplorerWindow.setMenuBarVisibility(false);
this.processExplorerWindow.loadURL(
FileAccess.asBrowserUri('vs/code/electron-sandbox/processExplorer/processExplorer.html', require, true).toString(true)
);
const windowConfiguration = {
appRoot: this.environmentService.appRoot,
windowId: this._processExplorerWindow.id,
userEnv: this.userEnv,
machineId: this.machineId,
data
};
this.processExplorerWindow.on('close', () => {
this.processExplorerWindow = null;
processExplorerDisposables.dispose();
});
this._processExplorerWindow.loadURL(
toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration));
this.processExplorerParentWindow.on('close', () => {
if (this.processExplorerWindow) {
this.processExplorerWindow.close();
this.processExplorerWindow = null;
this._processExplorerWindow.on('close', () => this._processExplorerWindow = null);
this._processExplorerParentWindow.on('close', () => {
if (this._processExplorerWindow) {
this._processExplorerWindow.close();
this._processExplorerWindow = null;
processExplorerDisposables.dispose();
}
});
}
}
// Focus
if (this._processExplorerWindow) {
this._processExplorerWindow.focus();
}
this.processExplorerWindow?.focus();
}
public async getSystemStatus(): Promise<string> {
return Promise.all([this.launchMainService.getMainProcessInfo(), this.launchMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })])
.then(result => {
const [info, remoteData] = result;
return this.diagnosticsService.getDiagnostics(info, remoteData);
});
private createBrowserWindow<T>(position: IWindowState, ipcObjectUrl: IIPCObjectUrl<T>, backgroundColor: string | undefined, title: string, zoomLevel: number): BrowserWindow {
const window = new BrowserWindow({
fullscreen: false,
skipTaskbar: true,
resizable: true,
width: position.width,
height: position.height,
minWidth: 300,
minHeight: 200,
x: position.x,
y: position.y,
title,
backgroundColor: backgroundColor || IssueMainService.DEFAULT_BACKGROUND_COLOR,
webPreferences: {
preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath,
additionalArguments: [`--vscode-window-config=${ipcObjectUrl.resource.toString()}`, '--context-isolation' /* TODO@bpasero: Use process.contextIsolateed when 13-x-y is adopted (https://github.com/electron/electron/pull/28030) */],
v8CacheOptions: browserCodeLoadingCacheStrategy,
enableWebSQL: false,
enableRemoteModule: false,
spellcheck: false,
nativeWindowOpen: true,
zoomFactor: zoomLevelToZoomFactor(zoomLevel),
sandbox: true,
contextIsolation: true
}
});
window.setMenuBarVisibility(false);
return window;
}
async getSystemStatus(): Promise<string> {
const [info, remoteData] = await Promise.all([this.launchMainService.getMainProcessInfo(), this.launchMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]);
return this.diagnosticsService.getDiagnostics(info, remoteData);
}
private getWindowPosition(parentWindow: BrowserWindow, defaultWidth: number, defaultHeight: number): IWindowState {
// We want the new window to open on the same display that the parent is in
let displayToUse: Display | undefined;
const displays = screen.getAllDisplays();
@@ -375,66 +380,14 @@ export class IssueMainService implements ICommonIssueService {
return state;
}
private getPerformanceInfo(): Promise<PerformanceInfo> {
return new Promise(async (resolve, reject) => {
Promise.all([this.launchMainService.getMainProcessInfo(), this.launchMainService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true })])
.then(result => {
const [info, remoteData] = result;
this.diagnosticsService.getPerformanceInfo(info, remoteData)
.then(diagnosticInfo => {
resolve(diagnosticInfo);
})
.catch(err => {
this.logService.warn('issueService#getPerformanceInfo ', err.message);
reject(err);
});
});
});
}
private async getPerformanceInfo(): Promise<PerformanceInfo> {
try {
const [info, remoteData] = await Promise.all([this.launchMainService.getMainProcessInfo(), this.launchMainService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true })]);
return await this.diagnosticsService.getPerformanceInfo(info, remoteData);
} catch (error) {
this.logService.warn('issueService#getPerformanceInfo ', error.message);
private getIssueReporterPath(data: IssueReporterData, features: IssueReporterFeatures): string {
if (!this._issueWindow) {
throw new Error('Issue window has been disposed');
}
const windowConfiguration = {
appRoot: this.environmentService.appRoot,
windowId: this._issueWindow.id,
machineId: this.machineId,
userEnv: this.userEnv,
data,
features,
disableExtensions: this.environmentService.disableExtensions,
os: {
type: os.type(),
arch: os.arch(),
release: os.release(),
},
product: {
nameShort: product.nameShort,
version: !!product.darwinUniversalAssetId ? `${product.version} (Universal)` : product.version,
commit: product.commit,
date: product.date,
reportIssueUrl: product.reportIssueUrl
}
};
return toWindowUrl('vs/code/electron-sandbox/issue/issueReporter.html', windowConfiguration);
}
}
function toWindowUrl<T>(modulePathToHtml: string, windowConfiguration: T): string {
const environment = parseArgs(process.argv, OPTIONS);
const config = Object.assign(environment, windowConfiguration);
for (const keyValue of Object.keys(config)) {
const key = keyValue as keyof typeof config;
if (config[key] === undefined || config[key] === null || config[key] === '') {
delete config[key]; // only send over properties that have a true value
throw error;
}
}
return FileAccess
.asBrowserUri(modulePathToHtml, require, true)
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` })
.toString(true);
}

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