mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-01 01:25:38 -05:00
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:
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
19
src/vs/platform/checksum/common/checksumService.ts
Normal file
19
src/vs/platform/checksum/common/checksumService.ts
Normal 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>;
|
||||
}
|
||||
@@ -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 });
|
||||
30
src/vs/platform/checksum/node/checksumService.ts
Normal file
30
src/vs/platform/checksum/node/checksumService.ts
Normal 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(/=+$/, ''))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
40
src/vs/platform/checksum/test/node/checksumService.test.ts
Normal file
40
src/vs/platform/checksum/test/node/checksumService.test.ts
Normal 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
|
||||
});
|
||||
});
|
||||
1135
src/vs/platform/checksum/test/node/fixtures/lorem.txt
Normal file
1135
src/vs/platform/checksum/test/node/fixtures/lorem.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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', () => { });
|
||||
|
||||
@@ -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);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: [] });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
96
src/vs/platform/driver/common/driverIpc.ts
Normal file
96
src/vs/platform/driver/common/driverIpc.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
@@ -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');
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
257
src/vs/platform/environment/common/environmentService.ts
Normal file
257
src/vs/platform/environment/common/environmentService.ts
Normal 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 };
|
||||
}
|
||||
@@ -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'); }
|
||||
}
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
165
src/vs/platform/environment/node/shellEnv.ts
Normal file
165
src/vs/platform/environment/node/shellEnv.ts
Normal 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
|
||||
}
|
||||
}
|
||||
14
src/vs/platform/environment/node/userDataPath.d.ts
vendored
Normal file
14
src/vs/platform/environment/node/userDataPath.d.ts
vendored
Normal 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;
|
||||
122
src/vs/platform/environment/node/userDataPath.js
Normal file
122
src/vs/platform/environment/node/userDataPath.js
Normal 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');
|
||||
}
|
||||
}());
|
||||
@@ -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
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
59
src/vs/platform/environment/test/node/userDataPath.test.ts
Normal file
59
src/vs/platform/environment/test/node/userDataPath.test.ts
Normal 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'];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -108,6 +108,7 @@ export interface ILocalExtension extends IExtension {
|
||||
isMachineScoped: boolean;
|
||||
publisherId: string | null;
|
||||
publisherDisplayName: string | null;
|
||||
installedTimestamp?: number;
|
||||
}
|
||||
|
||||
export const enum SortBy {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
143
src/vs/platform/extensionManagement/node/extensionsWatcher.ts
Normal file
143
src/vs/platform/extensionManagement/node/extensionsWatcher.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
210
src/vs/platform/files/browser/htmlFileSystemProvider.ts
Normal file
210
src/vs/platform/files/browser/htmlFileSystemProvider.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
197
src/vs/platform/files/common/ipcFileSystemProvider.ts
Normal file
197
src/vs/platform/files/common/ipcFileSystemProvider.ts
Normal 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]));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -124,7 +124,7 @@ export class FileWatcher extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
this.isDisposed = true;
|
||||
|
||||
super.dispose();
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface IDiskFileChange {
|
||||
}
|
||||
|
||||
export interface ILogMessage {
|
||||
type: 'trace' | 'warn' | 'error';
|
||||
type: 'trace' | 'warn' | 'error' | 'info' | 'debug';
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,7 +78,8 @@ suite('IndexedDB File Service', function () {
|
||||
disposables.add(userdataFileProvider);
|
||||
};
|
||||
|
||||
setup(async () => {
|
||||
setup(async function () {
|
||||
this.timeout(15000);
|
||||
await reload();
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>][] {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
29
src/vs/platform/ipc/electron-browser/mainProcessService.ts
Normal file
29
src/vs/platform/ipc/electron-browser/mainProcessService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
90
src/vs/platform/ipc/electron-sandbox/services.ts
Normal file
90
src/vs/platform/ipc/electron-sandbox/services.ts
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user