Merge from vscode e3c4990c67c40213af168300d1cfeb71d680f877 (#16569)

This commit is contained in:
Cory Rivera
2021-08-25 16:28:29 -07:00
committed by GitHub
parent ab1112bfb3
commit cb7b7da0a4
1752 changed files with 59525 additions and 33878 deletions

View File

@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ActionViewItem, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
import { IAction } from 'vs/base/common/actions';
import { Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { MenuItemAction } from 'vs/platform/actions/common/actions';
import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem {
private _primaryAction: ActionViewItem;
private _dropdown: DropdownMenuActionViewItem;
private _container: HTMLElement | null = null;
private _dropdownContainer: HTMLElement | null = null;
get onDidChangeDropdownVisibility(): Event<boolean> {
return this._dropdown.onDidChangeVisibility;
}
constructor(
primaryAction: MenuItemAction,
dropdownAction: IAction,
dropdownMenuActions: IAction[],
className: string,
private readonly _contextMenuProvider: IContextMenuProvider,
_keybindingService: IKeybindingService,
_notificationService: INotificationService
) {
super(null, primaryAction);
this._primaryAction = new MenuEntryActionViewItem(primaryAction, _keybindingService, _notificationService);
this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, {
menuAsChild: true,
classNames: ['codicon', 'codicon-chevron-down']
});
}
override setActionContext(newContext: unknown): void {
super.setActionContext(newContext);
this._primaryAction.setActionContext(newContext);
this._dropdown.setActionContext(newContext);
}
override render(container: HTMLElement): void {
this._container = container;
super.render(this._container);
this._container.classList.add('monaco-dropdown-with-primary');
const primaryContainer = DOM.$('.action-container');
this._primaryAction.render(DOM.append(this._container, primaryContainer));
this._dropdownContainer = DOM.$('.dropdown-action-container');
this._dropdown.render(DOM.append(this._container, this._dropdownContainer));
this._register(DOM.addDisposableListener(primaryContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.RightArrow)) {
this._primaryAction.element!.tabIndex = -1;
this._dropdown.focus();
event.stopPropagation();
}
}));
this._register(DOM.addDisposableListener(this._dropdownContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.LeftArrow)) {
this._primaryAction.element!.tabIndex = 0;
this._dropdown.setFocusable(false);
this._primaryAction.element?.focus();
event.stopPropagation();
}
}));
}
override focus(fromRight?: boolean): void {
if (fromRight) {
this._dropdown.focus();
} else {
this._primaryAction.element!.tabIndex = 0;
this._primaryAction.element!.focus();
}
}
override blur(): void {
this._primaryAction.element!.tabIndex = -1;
this._dropdown.blur();
this._container!.blur();
}
override setFocusable(focusable: boolean): void {
if (focusable) {
this._primaryAction.element!.tabIndex = 0;
} else {
this._primaryAction.element!.tabIndex = -1;
this._dropdown.setFocusable(false);
}
}
update(dropdownAction: IAction, dropdownMenuActions: IAction[], dropdownIcon?: string): void {
this._dropdown.dispose();
this._dropdown = new DropdownMenuActionViewItem(dropdownAction, dropdownMenuActions, this._contextMenuProvider, {
menuAsChild: true,
classNames: ['codicon', dropdownIcon || 'codicon-chevron-down']
});
if (this._dropdownContainer) {
this._dropdown.render(this._dropdownContainer);
}
}
override dispose() {
this._primaryAction.dispose();
this._dropdown.dispose();
super.dispose();
}
}

View File

@@ -24,14 +24,16 @@ export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuAct
const groups = menu.getActions(options);
const modifierKeyEmitter = ModifierKeyEmitter.getInstance();
const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey);
fillInActions(groups, target, useAlternativeActions, primaryGroup);
fillInActions(groups, target, useAlternativeActions, primaryGroup ? actionGroup => actionGroup === primaryGroup : actionGroup => actionGroup === 'navigation');
return asDisposable(groups);
}
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 {
export function createAndFillInActionBarActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, primaryGroup?: string | ((actionGroup: string) => boolean), primaryMaxCount?: number, shouldInlineSubmenu?: (action: SubmenuAction, group: string, groupSize: number) => boolean, useSeparatorsInPrimaryActions?: boolean): IDisposable {
const groups = menu.getActions(options);
const isPrimaryAction = typeof primaryGroup === 'string' ? (actionGroup: string) => actionGroup === primaryGroup : primaryGroup;
// Action bars handle alternative actions on their own so the alternative actions should be ignored
fillInActions(groups, target, false, primaryGroup, primaryMaxCount, shouldInlineSubmenu);
fillInActions(groups, target, false, isPrimaryAction, primaryMaxCount, shouldInlineSubmenu, useSeparatorsInPrimaryActions);
return asDisposable(groups);
}
@@ -48,9 +50,10 @@ function asDisposable(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemActio
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',
isPrimaryAction: (actionGroup: string) => boolean = actionGroup => actionGroup === 'navigation',
primaryMaxCount: number = Number.MAX_SAFE_INTEGER,
shouldInlineSubmenu: (action: SubmenuAction, group: string, groupSize: number) => boolean = () => false
shouldInlineSubmenu: (action: SubmenuAction, group: string, groupSize: number) => boolean = () => false,
useSeparatorsInPrimaryActions: boolean = false
): void {
let primaryBucket: IAction[];
@@ -68,8 +71,11 @@ export function fillInActions( // {{SQL CARBON EDIT}} add export modifier
for (const [group, actions] of groups) {
let target: IAction[];
if (group === primaryGroup) {
if (isPrimaryAction(group)) {
target = primaryBucket;
if (target.length > 0 && useSeparatorsInPrimaryActions) {
target.push(new Separator());
}
} else {
target = secondaryBucket;
if (target.length > 0) {
@@ -92,7 +98,7 @@ export function fillInActions( // {{SQL CARBON EDIT}} add export modifier
// 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;
const target = isPrimaryAction(group) ? primaryBucket : secondaryBucket;
// inlining submenus with length 0 or 1 is easy,
// larger submenus need to be checked with the overall limit
@@ -132,13 +138,15 @@ export class MenuEntryActionViewItem extends ActionViewItem {
return this._wantsAltCommand && this._menuItemAction.alt || this._menuItemAction;
}
override onClick(event: MouseEvent): void {
override async onClick(event: MouseEvent): Promise<void> {
event.preventDefault();
event.stopPropagation();
this.actionRunner
.run(this._commandAction, this._context)
.catch(err => this._notificationService.error(err));
try {
await this.actionRunner.run(this._commandAction, this._context);
} catch (err) {
this._notificationService.error(err);
}
}
override render(container: HTMLElement): void {

View File

@@ -41,6 +41,7 @@ export type Icon = { dark?: URI; light?: URI; } | ThemeIcon;
export interface ICommandAction {
id: string;
title: string | ICommandActionTitle;
shortTitle?: string | ICommandActionTitle;
category?: string | ILocalizedString;
tooltip?: string;
icon?: Icon;
@@ -87,6 +88,7 @@ export class MenuId {
static readonly DebugWatchContext = new MenuId('DebugWatchContext');
static readonly DebugToolBar = new MenuId('DebugToolBar');
static readonly EditorContext = new MenuId('EditorContext');
static readonly SimpleEditorContext = new MenuId('SimpleEditorContext');
static readonly EditorContextCopy = new MenuId('EditorContextCopy');
static readonly EditorContextPeek = new MenuId('EditorContextPeek');
static readonly EditorTitle = new MenuId('EditorTitle');
@@ -96,6 +98,7 @@ export class MenuId {
static readonly ExplorerContext = new MenuId('ExplorerContext');
static readonly ExtensionContext = new MenuId('ExtensionContext');
static readonly GlobalActivity = new MenuId('GlobalActivity');
static readonly MenubarMainMenu = new MenuId('MenubarMainMenu');
static readonly MenubarAppearanceMenu = new MenuId('MenubarAppearanceMenu');
static readonly MenubarDebugMenu = new MenuId('MenubarDebugMenu');
static readonly MenubarEditMenu = new MenuId('MenubarEditMenu');
@@ -125,9 +128,12 @@ export class MenuId {
static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu');
static readonly StatusBarRemoteIndicatorMenu = new MenuId('StatusBarRemoteIndicatorMenu');
static readonly TestItem = new MenuId('TestItem');
static readonly TestPeekElement = new MenuId('TestPeekElement');
static readonly TestPeekTitle = new MenuId('TestPeekTitle');
static readonly TouchBarContext = new MenuId('TouchBarContext');
static readonly TitleBarContext = new MenuId('TitleBarContext');
static readonly TunnelContext = new MenuId('TunnelContext');
static readonly TunnelProtocol = new MenuId('TunnelProtocol');
static readonly TunnelPortInline = new MenuId('TunnelInline');
static readonly TunnelTitle = new MenuId('TunnelTitle');
static readonly TunnelLocalAddressInline = new MenuId('TunnelLocalAddressInline');
@@ -142,6 +148,7 @@ export class MenuId {
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 NotebookRightToolbar = new MenuId('NotebookRightToolbar');
static readonly NotebookCellTitle = new MenuId('NotebookCellTitle');
static readonly NotebookCellInsert = new MenuId('NotebookCellInsert');
static readonly NotebookCellBetween = new MenuId('NotebookCellBetween');
@@ -150,6 +157,7 @@ export class MenuId {
static readonly NotebookDiffCellInputTitle = new MenuId('NotebookDiffCellInputTitle');
static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle');
static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle');
static readonly NotebookOutputToolbar = new MenuId('NotebookOutputToolbar');
static readonly BulkEditTitle = new MenuId('BulkEditTitle');
static readonly BulkEditContext = new MenuId('BulkEditContext');
static readonly ObjectExplorerItemContext = new MenuId('ObjectExplorerItemContext'); // {{SQL CARBON EDIT}}
@@ -166,12 +174,13 @@ 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');
static readonly TerminalInstanceContext = new MenuId('TerminalInstanceContext');
static readonly TerminalNewDropdownContext = new MenuId('TerminalNewDropdownContext');
static readonly TerminalTabContext = new MenuId('TerminalTabContext');
static readonly TerminalTabEmptyAreaContext = new MenuId('TerminalTabEmptyAreaContext');
static readonly TerminalInlineTabContext = new MenuId('TerminalInlineTabContext');
static readonly WebviewContext = new MenuId('WebviewContext');
static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions');
readonly id: number;
readonly _debugName: string;
@@ -185,6 +194,7 @@ export class MenuId {
export interface IMenuActionOptions {
arg?: any;
shouldForwardArgs?: boolean;
renderShortTitle?: boolean;
}
export interface IMenu extends IDisposable {
@@ -213,7 +223,7 @@ export interface IMenuRegistry {
addCommand(userCommand: ICommandAction): IDisposable;
getCommand(id: string): ICommandAction | undefined;
getCommands(): ICommandsMap;
appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem; }>): IDisposable;
appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem }>): IDisposable;
appendMenuItem(menu: MenuId, item: IMenuItem | ISubmenuItem): IDisposable;
getMenuItems(loc: MenuId): Array<IMenuItem | ISubmenuItem>;
}
@@ -264,7 +274,7 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
return this.appendMenuItems(Iterable.single({ id, item }));
}
appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem; }>): IDisposable {
appendMenuItems(items: Iterable<{ id: MenuId, item: IMenuItem | ISubmenuItem }>): IDisposable {
const changedIds = new Set<MenuId>();
const toRemove = new LinkedList<Function>();
@@ -336,7 +346,7 @@ export class ExecuteCommandAction extends Action {
super(id, label);
}
override run(...args: any[]): Promise<any> {
override run(...args: any[]): Promise<void> {
return this._commandService.executeCommand(this.id, ...args);
}
}
@@ -386,7 +396,7 @@ export class MenuItemAction implements IAction {
readonly class: string | undefined;
readonly enabled: boolean;
readonly checked: boolean;
readonly expanded: boolean = false;
readonly expanded: boolean = false; // {{SQL CARBON EDIT}}
constructor(
item: ICommandAction,
@@ -396,7 +406,9 @@ export class MenuItemAction implements IAction {
@ICommandService private _commandService: ICommandService
) {
this.id = item.id;
this.label = typeof item.title === 'string' ? item.title : item.title.value;
this.label = options?.renderShortTitle && item.shortTitle
? (typeof item.shortTitle === 'string' ? item.shortTitle : item.shortTitle.value)
: (typeof item.title === 'string' ? item.title : item.title.value);
this.tooltip = item.tooltip ?? '';
this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition);
this.checked = false;
@@ -429,7 +441,7 @@ export class MenuItemAction implements IAction {
// to bridge into the rendering world.
}
run(...args: any[]): Promise<any> {
run(...args: any[]): Promise<void> {
let runArgs: any[] = [];
if (this._options?.arg) {
@@ -442,8 +454,6 @@ export class MenuItemAction implements IAction {
return this._commandService.executeCommand(this.id, ...runArgs);
}
}
export class SyncActionDescriptor {
@@ -456,7 +466,7 @@ export class SyncActionDescriptor {
private readonly _keybindingContext: ContextKeyExpression | undefined;
private readonly _keybindingWeight: number | undefined;
public static create<Services extends BrandedService[]>(ctor: { new(id: string, label: string, ...services: Services): Action; },
public static create<Services extends BrandedService[]>(ctor: { new(id: string, label: string, ...services: Services): Action },
id: string, label: string | undefined, keybindings?: IKeybindings, keybindingContext?: ContextKeyExpression, keybindingWeight?: number
): SyncActionDescriptor {
return new SyncActionDescriptor(ctor as IConstructorSignature2<string, string | undefined, Action>, id, label, keybindings, keybindingContext, keybindingWeight);
@@ -523,7 +533,7 @@ export interface IAction2Options extends ICommandAction {
/**
* One or many menu items.
*/
menu?: OneOrN<{ id: MenuId; } & Omit<IMenuItem, 'command'>>;
menu?: OneOrN<{ id: MenuId } & Omit<IMenuItem, 'command'>>;
/**
* One keybinding.
@@ -539,10 +549,10 @@ export interface IAction2Options extends ICommandAction {
export abstract class Action2 {
constructor(readonly desc: Readonly<IAction2Options>) { }
abstract run(accessor: ServicesAccessor, ...args: any[]): any;
abstract run(accessor: ServicesAccessor, ...args: any[]): void;
}
export function registerAction2(ctor: { new(): Action2; }): IDisposable {
export function registerAction2(ctor: { new(): Action2 }): IDisposable {
const disposables = new DisposableStore();
const action = new ctor();

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RunOnceScheduler } from 'vs/base/common/async';
@@ -116,7 +116,7 @@ class Menu implements IMenu {
}
// 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;
const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled as ContextKeyExpression; // {{SQL CARBON EDIT}} Cast to ContextKeyExpression
Menu._fillInKbExprKeys(toggledExpression, this._contextKeys);
}

View File

@@ -7,7 +7,7 @@ import * as fs from 'fs';
import { createHash } from 'crypto';
import { join } from 'vs/base/common/path';
import { isLinux } from 'vs/base/common/platform';
import { writeFileSync, writeFile, readdir, exists, rimraf, RimRafMode } from 'vs/base/node/pfs';
import { writeFileSync, RimRafMode, Promises } from 'vs/base/node/pfs';
import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
@@ -49,26 +49,28 @@ export class BackupMainService implements IBackupMainService {
async initialize(): Promise<void> {
let backups: IBackupWorkspacesFormat;
try {
backups = JSON.parse(await fs.promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here
backups = JSON.parse(await Promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here
} catch (error) {
backups = Object.create(null);
}
// read empty workspaces backups first
if (backups.emptyWorkspaceInfos) {
this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos);
}
// validate empty workspaces backups first
this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos);
// read workspace backups
let rootWorkspaces: IWorkspaceBackupInfo[] = [];
try {
if (Array.isArray(backups.rootURIWorkspaces)) {
rootWorkspaces = backups.rootURIWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) }, remoteAuthority: workspace.remoteAuthority }));
rootWorkspaces = backups.rootURIWorkspaces.map(workspace => ({
workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) },
remoteAuthority: workspace.remoteAuthority
}));
}
} catch (e) {
// ignore URI parsing exceptions
}
// validate workspace backups
this.workspaces = await this.validateWorkspaces(rootWorkspaces);
// read folder backups
@@ -81,6 +83,7 @@ export class BackupMainService implements IBackupMainService {
// ignore URI parsing exceptions
}
// validate folder backups
this.folders = await this.validateFolders(workspaceFolders);
// save again in case some workspaces or folders have been removed
@@ -230,7 +233,7 @@ export class BackupMainService implements IBackupMainService {
// If the workspace has no backups, ignore it
if (hasBackups) {
if (workspace.configPath.scheme !== Schemas.file || await exists(workspace.configPath.fsPath)) {
if (workspace.configPath.scheme !== Schemas.file || await Promises.exists(workspace.configPath.fsPath)) {
result.push(workspaceInfo);
} else {
// If the workspace has backups, but the target workspace is missing, convert backups to empty ones
@@ -262,7 +265,7 @@ export class BackupMainService implements IBackupMainService {
// If the folder has no backups, ignore it
if (hasBackups) {
if (folderURI.scheme !== Schemas.file || await exists(folderURI.fsPath)) {
if (folderURI.scheme !== Schemas.file || await Promises.exists(folderURI.fsPath)) {
result.push(folderURI);
} else {
// If the folder has backups, but the target workspace is missing, convert backups to empty ones
@@ -309,8 +312,8 @@ export class BackupMainService implements IBackupMainService {
private async deleteStaleBackup(backupPath: string): Promise<void> {
try {
if (await exists(backupPath)) {
await rimraf(backupPath, RimRafMode.MOVE);
if (await Promises.exists(backupPath)) {
await Promises.rm(backupPath, RimRafMode.MOVE);
}
} catch (error) {
this.logService.error(`Backup: Could not delete stale backup: ${error.toString()}`);
@@ -328,7 +331,7 @@ export class BackupMainService implements IBackupMainService {
// Rename backupPath to new empty window backup path
const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder);
try {
await fs.promises.rename(backupPath, newEmptyWindowBackupPath);
await Promises.rename(backupPath, newEmptyWindowBackupPath);
} catch (error) {
this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`);
return false;
@@ -402,11 +405,11 @@ export class BackupMainService implements IBackupMainService {
private async doHasBackups(backupPath: string): Promise<boolean> {
try {
const backupSchemas = await readdir(backupPath);
const backupSchemas = await Promises.readdir(backupPath);
for (const backupSchema of backupSchemas) {
try {
const backupSchemaChildren = await readdir(join(backupPath, backupSchema));
const backupSchemaChildren = await Promises.readdir(join(backupPath, backupSchema));
if (backupSchemaChildren.length > 0) {
return true;
}
@@ -431,7 +434,7 @@ export class BackupMainService implements IBackupMainService {
private async save(): Promise<void> {
try {
await writeFile(this.workspacesJsonPath, JSON.stringify(this.serializeBackups()));
await Promises.writeFile(this.workspacesJsonPath, JSON.stringify(this.serializeBackups()));
} catch (error) {
this.logService.error(`Backup: Could not save workspaces.json: ${error.toString()}`);
}

View File

@@ -66,7 +66,7 @@ flakySuite('BackupMainService', () => {
async function ensureWorkspaceExists(workspace: IWorkspaceIdentifier): Promise<IWorkspaceIdentifier> {
if (!fs.existsSync(workspace.configPath.fsPath)) {
await pfs.writeFile(workspace.configPath.fsPath, 'Hello');
await pfs.Promises.writeFile(workspace.configPath.fsPath, 'Hello');
}
const backupFolder = service.toBackupPath(workspace.id);
@@ -79,7 +79,7 @@ flakySuite('BackupMainService', () => {
if (!fs.existsSync(backupFolder)) {
fs.mkdirSync(backupFolder);
fs.mkdirSync(path.join(backupFolder, Schemas.file));
await pfs.writeFile(path.join(backupFolder, Schemas.file, 'foo.txt'), 'Hello');
await pfs.Promises.writeFile(path.join(backupFolder, Schemas.file, 'foo.txt'), 'Hello');
}
}
@@ -107,7 +107,7 @@ flakySuite('BackupMainService', () => {
environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS), { _serviceBrand: undefined, ...product });
await fs.promises.mkdir(backupHome, { recursive: true });
await pfs.Promises.mkdir(backupHome, { recursive: true });
configService = new TestConfigurationService();
service = new class TestBackupMainService extends BackupMainService {
@@ -132,7 +132,7 @@ flakySuite('BackupMainService', () => {
});
teardown(() => {
return pfs.rimraf(testDir);
return pfs.Promises.rm(testDir);
});
test('service validates backup workspaces on startup and cleans up (folder workspaces)', async function () {
@@ -443,10 +443,10 @@ flakySuite('BackupMainService', () => {
folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString()],
emptyWorkspaceInfos: []
};
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
@@ -460,9 +460,9 @@ flakySuite('BackupMainService', () => {
folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString().toLowerCase()],
emptyWorkspaceInfos: []
};
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
@@ -481,10 +481,10 @@ flakySuite('BackupMainService', () => {
folderURIWorkspaces: [],
emptyWorkspaceInfos: []
};
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.strictEqual(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1);
if (platform.isLinux) {
@@ -500,7 +500,7 @@ flakySuite('BackupMainService', () => {
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]);
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]);
});
@@ -515,7 +515,7 @@ flakySuite('BackupMainService', () => {
assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id);
assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id);
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]);
@@ -528,7 +528,7 @@ flakySuite('BackupMainService', () => {
service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase()));
assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]);
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]);
});
@@ -538,7 +538,7 @@ flakySuite('BackupMainService', () => {
service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath));
assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [URI.file(upperFooPath)]);
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]);
});
@@ -549,12 +549,12 @@ flakySuite('BackupMainService', () => {
service.registerFolderBackupSync(barFile);
service.unregisterFolderBackupSync(fooFile);
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]);
service.unregisterFolderBackupSync(barFile);
const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepStrictEqual(json2.folderURIWorkspaces, []);
});
@@ -566,12 +566,12 @@ flakySuite('BackupMainService', () => {
service.registerWorkspaceBackupSync(ws2);
service.unregisterWorkspaceBackupSync(ws1.workspace);
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]);
service.unregisterWorkspaceBackupSync(ws2.workspace);
const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepStrictEqual(json2.rootURIWorkspaces, []);
});
@@ -581,12 +581,12 @@ flakySuite('BackupMainService', () => {
service.registerEmptyWindowBackupSync('bar');
service.unregisterEmptyWindowBackupSync('foo');
const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]);
service.unregisterEmptyWindowBackupSync('bar');
const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepStrictEqual(json2.emptyWorkspaceInfos, []);
});
@@ -596,11 +596,11 @@ flakySuite('BackupMainService', () => {
await ensureFolderExists(existingTestFolder1); // make sure backup folder exists, so the folder is not removed on loadSync
const workspacesJson: IBackupWorkspacesFormat = { rootURIWorkspaces: [], folderURIWorkspaces: [existingTestFolder1.toString()], emptyWorkspaceInfos: [] };
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await pfs.Promises.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
service.unregisterFolderBackupSync(barFile);
service.unregisterEmptyWindowBackupSync('test');
const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8');
const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
@@ -670,8 +670,8 @@ flakySuite('BackupMainService', () => {
assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0);
try {
await fs.promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true });
await fs.promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true });
await pfs.Promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true });
await pfs.Promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true });
} catch (error) {
// ignore - folder might exist already
}

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createHash } from 'crypto';

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';

View File

@@ -112,7 +112,7 @@ export interface IConfigurationService {
updateValue(key: string, value: any, target: ConfigurationTarget): Promise<void>;
updateValue(key: string, value: any, overrides: IConfigurationOverrides, target: ConfigurationTarget, donotNotifyError?: boolean): Promise<void>;
inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T>;
inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<Readonly<T>>;
reloadConfiguration(target?: ConfigurationTarget | IWorkspaceFolder): Promise<void>;

View File

@@ -21,7 +21,6 @@ STATIC_VALUES.set('isEdge', _userAgent.indexOf('Edg/') >= 0);
STATIC_VALUES.set('isFirefox', _userAgent.indexOf('Firefox') >= 0);
STATIC_VALUES.set('isChrome', _userAgent.indexOf('Chrome') >= 0);
STATIC_VALUES.set('isSafari', _userAgent.indexOf('Safari') >= 0);
STATIC_VALUES.set('isIPad', _userAgent.indexOf('iPad') >= 0);
const hasOwnProperty = Object.prototype.hasOwnProperty;

View File

@@ -5,7 +5,7 @@
import { localize } from 'vs/nls';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform';
import { isMacintosh, isLinux, isWindows, isWeb, isIOS } from 'vs/base/common/platform';
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"));
@@ -13,6 +13,7 @@ export const IsWindowsContext = new RawContextKey<boolean>('isWindows', isWindow
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 IsIOSContext = new RawContextKey<boolean>('isIOS', isIOS, localize('isIOS', "Whether the operating system is IOS"));
export const IsDevelopmentContext = new RawContextKey<boolean>('isDevelopment', false, true);

View File

@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService';
import * as assert from 'assert';
suite('ContextKeyService', () => {
test('updateParent', () => {
const root = new ContextKeyService(new TestConfigurationService());
const parent1 = root.createScoped(document.createElement('div'));
const parent2 = root.createScoped(document.createElement('div'));
const child = parent1.createScoped(document.createElement('div'));
parent1.createKey('testA', 1);
parent1.createKey('testB', 2);
parent1.createKey('testD', 0);
parent2.createKey('testA', 3);
parent2.createKey('testC', 4);
parent2.createKey('testD', 0);
let complete: () => void;
let reject: (err: Error) => void;
const p = new Promise<void>((_complete, _reject) => {
complete = _complete;
reject = _reject;
});
child.onDidChangeContext(e => {
try {
assert.ok(e.affectsSome(new Set(['testA'])), 'testA changed');
assert.ok(e.affectsSome(new Set(['testB'])), 'testB changed');
assert.ok(e.affectsSome(new Set(['testC'])), 'testC changed');
assert.ok(!e.affectsSome(new Set(['testD'])), 'testD did not change');
assert.strictEqual(child.getContextKeyValue('testA'), 3);
assert.strictEqual(child.getContextKeyValue('testB'), undefined);
assert.strictEqual(child.getContextKeyValue('testC'), 4);
assert.strictEqual(child.getContextKeyValue('testD'), 0);
} catch (err) {
reject(err);
return;
}
complete();
});
child.updateParent(parent2);
return p;
});
});

View File

@@ -27,7 +27,7 @@ suite('ContextKeyExpr', () => {
ContextKeyExpr.notEquals('c1', 'cc1'),
ContextKeyExpr.notEquals('c2', 'cc2'),
ContextKeyExpr.not('d1'),
ContextKeyExpr.not('d2'),
ContextKeyExpr.not('d2')
)!;
let b = ContextKeyExpr.and(
ContextKeyExpr.equals('b2', 'bb2'),

View File

@@ -12,12 +12,15 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { Disposable } from 'vs/base/common/lifecycle';
import { ModifierKeyEmitter } from 'vs/base/browser/dom';
import { Emitter } from 'vs/base/common/event';
export class ContextMenuService extends Disposable implements IContextMenuService {
declare readonly _serviceBrand: undefined;
private contextMenuHandler: ContextMenuHandler;
readonly onDidShowContextMenu = new Emitter<void>().event;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@INotificationService notificationService: INotificationService,

View File

@@ -7,6 +7,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IContextMenuDelegate } from 'vs/base/browser/contextmenu';
import { AnchorAlignment, AnchorAxisAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { Event } from 'vs/base/common/event';
export const IContextViewService = createDecorator<IContextViewService>('contextViewService');
@@ -40,5 +41,7 @@ export interface IContextMenuService {
readonly _serviceBrand: undefined;
readonly onDidShowContextMenu: Event<void>;
showContextMenu(delegate: IContextMenuDelegate): void;
}

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services';

View File

@@ -5,7 +5,6 @@
import * as osLib from 'os';
import { virtualMachineHint } from 'vs/base/node/id';
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';
@@ -18,7 +17,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
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';
import { IDirent, Promises } from 'vs/base/node/pfs';
export interface VersionInfo {
vscodeVersion: string;
@@ -70,7 +69,7 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P
return new Promise(async resolve => {
let files: IDirent[];
try {
files = await readdir(dir, { withFileTypes: true });
files = await Promises.readdir(dir, { withFileTypes: true });
} catch (error) {
// Ignore folders that can't be read
resolve();
@@ -168,45 +167,37 @@ export function getMachineInfo(): IMachineInfo {
return machineInfo;
}
export function collectLaunchConfigs(folder: string): Promise<WorkspaceStatItem[]> {
let launchConfigs = new Map<string, number>();
export async function collectLaunchConfigs(folder: string): Promise<WorkspaceStatItem[]> {
try {
const launchConfigs = new Map<string, number>();
const launchConfig = join(folder, '.vscode', 'launch.json');
let launchConfig = join(folder, '.vscode', 'launch.json');
return new Promise((resolve, reject) => {
exists(launchConfig, (doesExist) => {
if (doesExist) {
readFile(launchConfig, (err, contents) => {
if (err) {
return resolve([]);
const contents = await Promises.readFile(launchConfig);
const errors: ParseError[] = [];
const json = parse(contents.toString(), errors);
if (errors.length) {
console.log(`Unable to parse ${launchConfig}`);
return [];
}
if (getNodeType(json) === 'object' && json['configurations']) {
for (const each of json['configurations']) {
const type = each['type'];
if (type) {
if (launchConfigs.has(type)) {
launchConfigs.set(type, launchConfigs.get(type)! + 1);
} else {
launchConfigs.set(type, 1);
}
const errors: ParseError[] = [];
const json = parse(contents.toString(), errors);
if (errors.length) {
console.log(`Unable to parse ${launchConfig}`);
return resolve([]);
}
if (getNodeType(json) === 'object' && json['configurations']) {
for (const each of json['configurations']) {
const type = each['type'];
if (type) {
if (launchConfigs.has(type)) {
launchConfigs.set(type, launchConfigs.get(type)! + 1);
} else {
launchConfigs.set(type, 1);
}
}
}
}
return resolve(asSortedItems(launchConfigs));
});
} else {
return resolve([]);
}
}
});
});
}
return asSortedItems(launchConfigs);
} catch (error) {
return [];
}
}
export class DiagnosticsService implements IDiagnosticsService {

View File

@@ -31,12 +31,13 @@ export interface IConfirmDialogArgs {
export interface IShowDialogArgs {
severity: Severity;
message: string;
buttons: string[];
buttons?: string[];
options?: IDialogOptions;
}
export interface IInputDialogArgs extends IShowDialogArgs {
inputs: IInput[],
buttons: string[];
inputs: IInput[];
}
export interface IDialog {
@@ -222,7 +223,7 @@ export interface IDialogHandler {
* then a promise with index of `cancelId` option is returned. If there is no such
* option then promise with index `0` is returned.
*/
show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult>;
show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise<IShowResult>;
/**
* Present a modal dialog to the user asking for input.
@@ -262,7 +263,7 @@ export interface IDialogService {
* then a promise with index of `cancelId` option is returned. If there is no such
* option then promise with index `0` is returned.
*/
show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult>;
show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise<IShowResult>;
/**
* Present a modal dialog to the user asking for input.

View File

@@ -6,11 +6,11 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { MessageBoxOptions, MessageBoxReturnValue, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, dialog, FileFilter, BrowserWindow } from 'electron';
import { Queue } from 'vs/base/common/async';
import { IStateService } from 'vs/platform/state/node/state';
import { IStateMainService } from 'vs/platform/state/electron-main/state';
import { isMacintosh } from 'vs/base/common/platform';
import { dirname } from 'vs/base/common/path';
import { normalizeNFC } from 'vs/base/common/normalization';
import { exists } from 'vs/base/node/pfs';
import { Promises } from 'vs/base/node/pfs';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { withNullAsUndefined } from 'vs/base/common/types';
import { localize } from 'vs/nls';
@@ -55,7 +55,7 @@ export class DialogMainService implements IDialogMainService {
private readonly noWindowDialogueQueue = new Queue<MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>();
constructor(
@IStateService private readonly stateService: IStateService
@IStateMainService private readonly stateMainService: IStateMainService
) {
}
@@ -89,8 +89,7 @@ export class DialogMainService implements IDialogMainService {
};
// Ensure defaultPath
dialogOptions.defaultPath = options.defaultPath || this.stateService.getItem<string>(DialogMainService.workingDirPickerStorageKey);
dialogOptions.defaultPath = options.defaultPath || this.stateMainService.getItem<string>(DialogMainService.workingDirPickerStorageKey);
// Ensure properties
if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') {
@@ -116,12 +115,12 @@ export class DialogMainService implements IDialogMainService {
if (result && result.filePaths && result.filePaths.length > 0) {
// Remember path in storage for next time
this.stateService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0]));
this.stateMainService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0]));
return result.filePaths;
}
return undefined;
return undefined; // {{SQL CARBON EDIT}} Strict nulls
}
private getWindowDialogQueue<T extends MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>(window?: BrowserWindow): Queue<T> {
@@ -195,7 +194,7 @@ export class DialogMainService implements IDialogMainService {
// Ensure the path exists (if provided)
if (options.defaultPath) {
const pathExists = await exists(options.defaultPath);
const pathExists = await Promises.exists(options.defaultPath);
if (!pathExists) {
options.defaultPath = undefined;
}

View File

@@ -10,8 +10,23 @@ export class TestDialogService implements IDialogService {
declare readonly _serviceBrand: undefined;
confirm(_confirmation: IConfirmation): Promise<IConfirmationResult> { return Promise.resolve({ confirmed: false }); }
show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise<IShowResult> { return Promise.resolve({ choice: 0 }); }
input(): Promise<IInputResult> { { return Promise.resolve({ choice: 0, values: [] }); } }
about(): Promise<void> { return Promise.resolve(); }
private confirmResult: IConfirmationResult | undefined = undefined;
setConfirmResult(result: IConfirmationResult) {
this.confirmResult = result;
}
async confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
if (this.confirmResult) {
const confirmResult = this.confirmResult;
this.confirmResult = undefined;
return confirmResult;
}
return { confirmed: false };
}
async show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise<IShowResult> { return { choice: 0 }; }
async input(): Promise<IInputResult> { { return { choice: 0, values: [] }; } }
async about(): Promise<void> { }
}

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';

View File

@@ -37,17 +37,17 @@ export interface IEditorModel {
export interface IBaseResourceEditorInput {
/**
* Optional options to use when opening the text input.
* Optional options to use when opening the input.
*/
options?: ITextEditorOptions;
options?: IEditorOptions;
/**
* Label to show for the diff editor
* Label to show for the input.
*/
readonly label?: string;
/**
* Description to show for the diff editor
* Description to show for the input.
*/
readonly description?: string;
@@ -70,6 +70,48 @@ export interface IBaseResourceEditorInput {
readonly forceUntitled?: boolean;
}
export interface IBaseTextResourceEditorInput extends IBaseResourceEditorInput {
/**
* Optional options to use when opening the text input.
*/
options?: ITextEditorOptions;
/**
* The contents of the text input if known. If provided,
* the input will not attempt to load the contents from
* disk and may appear dirty.
*/
contents?: string;
/**
* The encoding of the text input if known.
*/
encoding?: string;
/**
* The identifier of the language mode of the text input
* if known to use when displaying the contents.
*/
mode?: string;
}
export interface IResourceEditorInput extends IBaseResourceEditorInput {
/**
* The resource URI of the resource to open.
*/
readonly resource: URI;
}
export interface ITextResourceEditorInput extends IResourceEditorInput, IBaseTextResourceEditorInput {
/**
* Optional options to use when opening the text input.
*/
options?: ITextEditorOptions;
}
/**
* This identifier allows to uniquely identify an editor with a
* resource and type identifier.
@@ -87,25 +129,6 @@ export interface IResourceEditorInputIdentifier {
readonly typeId: string;
}
export interface IResourceEditorInput extends IBaseResourceEditorInput {
/**
* The resource URI of the resource to open.
*/
readonly resource: URI;
/**
* The encoding of the text input if known.
*/
readonly encoding?: string;
/**
* The identifier of the language mode of the text input
* if known to use when displaying the contents.
*/
readonly mode?: string;
}
export enum EditorActivation {
/**
@@ -169,7 +192,7 @@ export interface IEditorOptions {
* Will also not activate the group the editor opens in unless the group is already
* the active one. This behaviour can be overridden via the `activation` option.
*/
readonly preserveFocus?: boolean;
preserveFocus?: boolean;
/**
* This option is only relevant if an editor is opened into a group that is not active
@@ -179,14 +202,14 @@ export interface IEditorOptions {
* By default, the editor group will become active unless `preserveFocus` or `inactive`
* is specified.
*/
readonly activation?: EditorActivation;
activation?: EditorActivation;
/**
* Tells the editor to reload the editor input in the editor even if it is identical to the one
* already showing. By default, the editor will not reload the input if it is identical to the
* one showing.
*/
readonly forceReload?: boolean;
forceReload?: boolean;
/**
* Will reveal the editor if it is already opened and visible in any of the opened editor groups.
@@ -194,7 +217,7 @@ export interface IEditorOptions {
* Note that this option is just a hint that might be ignored if the user wants to open an editor explicitly
* to the side of another one or into a specific editor group.
*/
readonly revealIfVisible?: boolean;
revealIfVisible?: boolean;
/**
* Will reveal the editor if it is already opened (even when not visible) in any of the opened editor groups.
@@ -202,24 +225,24 @@ export interface IEditorOptions {
* Note that this option is just a hint that might be ignored if the user wants to open an editor explicitly
* to the side of another one or into a specific editor group.
*/
readonly revealIfOpened?: boolean;
revealIfOpened?: boolean;
/**
* An editor that is pinned remains in the editor stack even when another editor is being opened.
* An editor that is not pinned will always get replaced by another editor that is not pinned.
*/
readonly pinned?: boolean;
pinned?: boolean;
/**
* An editor that is sticky moves to the beginning of the editors list within the group and will remain
* there unless explicitly closed. Operations such as "Close All" will not close sticky editors.
*/
readonly sticky?: boolean;
sticky?: boolean;
/**
* The index in the document stack where to insert the editor into when opening.
*/
readonly index?: number;
index?: number;
/**
* An active editor that is opened will show its contents directly. Set to true to open an editor
@@ -228,13 +251,13 @@ export interface IEditorOptions {
* Will also not activate the group the editor opens in unless the group is already
* the active one. This behaviour can be overridden via the `activation` option.
*/
readonly inactive?: boolean;
inactive?: boolean;
/**
* Will not show an error in case opening the editor fails and thus allows to show a custom error
* message as needed. By default, an error will be presented as notification if opening was not possible.
*/
readonly ignoreError?: boolean;
ignoreError?: boolean;
/**
* Allows to override the editor that should be used to display the input:
@@ -242,7 +265,7 @@ export interface IEditorOptions {
* - `string`: specific override by id
* - `EditorOverride`: specific override handling
*/
readonly override?: string | EditorOverride;
override?: string | EditorOverride;
/**
* A optional hint to signal in which context the editor opens.
@@ -254,7 +277,7 @@ export interface IEditorOptions {
* some background task, the notification would show in the background,
* not as a modal dialog.
*/
readonly context?: EditorOpenContext;
context?: EditorOpenContext;
}
export interface ITextEditorSelection {
@@ -269,14 +292,17 @@ export const enum TextEditorSelectionRevealType {
* Option to scroll vertically or horizontally as necessary and reveal a range centered vertically.
*/
Center = 0,
/**
* Option to scroll vertically or horizontally as necessary and reveal a range centered vertically only if it lies outside the viewport.
*/
CenterIfOutsideViewport = 1,
/**
* Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top.
*/
NearTop = 2,
/**
* Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top.
* Only if it lies outside the viewport
@@ -289,16 +315,16 @@ export interface ITextEditorOptions extends IEditorOptions {
/**
* Text editor selection.
*/
readonly selection?: ITextEditorSelection;
selection?: ITextEditorSelection;
/**
* Text editor view state.
*/
readonly viewState?: object;
viewState?: object;
/**
* Option to control the text editor selection reveal type.
* Defaults to TextEditorSelectionRevealType.Center
*/
readonly selectionRevealType?: TextEditorSelectionRevealType;
selectionRevealType?: TextEditorSelectionRevealType;
}

View File

@@ -65,6 +65,7 @@ export interface NativeParsedArgs {
'install-source'?: string;
'disable-updates'?: boolean;
'disable-keytar'?: boolean;
'disable-workspace-trust'?: boolean;
'disable-crash-reporter'?: boolean;
'crash-reporter-directory'?: string;
'crash-reporter-id'?: string;

View File

@@ -66,6 +66,9 @@ export interface IEnvironmentService {
extensionDevelopmentKind?: ExtensionKind[];
extensionTestsLocationURI?: URI;
// --- workspace trust
disableWorkspaceTrust: boolean;
// --- logging
logsPath: string;
logLevel?: string;

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IProductService } from 'vs/platform/product/common/productService';
@@ -231,6 +231,9 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron
get telemetryLogResource(): URI { return URI.file(join(this.logsPath, 'telemetry.log')); }
get disableTelemetry(): boolean { return !!this.args['disable-telemetry']; }
@memoize
get disableWorkspaceTrust(): boolean { return !!this.args['disable-workspace-trust']; }
get args(): NativeParsedArgs { return this._args; }
constructor(

View File

@@ -25,11 +25,8 @@ export interface IEnvironmentMainService extends INativeEnvironmentService {
backupHome: string;
backupWorkspacesPath: string;
// --- V8 script cache path (ours)
nodeCachedDataDir?: string;
// --- V8 script cache path (chrome)
chromeCachedDataDir: string;
// --- V8 code cache path
codeCachePath?: string;
// --- IPC
mainIPCHandle: string;
@@ -68,8 +65,5 @@ export class EnvironmentMainService extends NativeEnvironmentService implements
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'); }
get codeCachePath(): string | undefined { return process.env['VSCODE_CODE_CACHE_PATH'] || undefined; }
}

View File

@@ -54,7 +54,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'builtin-extensions-dir': { type: 'string' },
'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") },
'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") },
'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions.") },
'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions."), args: 'category' },
'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. The identifier of an extension is always `${publisher}.${name}`. Use `--force` argument to update to latest version. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") },
'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") },
'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") },
@@ -63,18 +63,18 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'verbose': { type: 'boolean', cat: 't', description: localize('verbose', "Print verbose output (implies --wait).") },
'log': { type: 'string', cat: 't', args: 'level', description: localize('log', "Log level to use. Default is 'info'. Allowed values are 'critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'.") },
'status': { type: 'boolean', alias: 's', cat: 't', description: localize('status', "Print process usage and diagnostics information.") },
'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup") },
'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup.") },
'prof-append-timers': { type: 'string' },
'prof-startup-prefix': { type: 'string' },
'prof-v8-extensions': { type: 'boolean' },
'disable-extensions': { type: 'boolean', deprecates: 'disableExtensions', cat: 't', description: localize('disableExtensions', "Disable all installed extensions.") },
'disable-extension': { type: 'string[]', cat: 't', args: 'extension-id', description: localize('disableExtension', "Disable an extension.") },
'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] },
'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off."), args: ['on', 'off'] },
'inspect-extensions': { type: 'string', deprecates: 'debugPluginHost', args: 'port', cat: 't', description: localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.") },
'inspect-brk-extensions': { type: 'string', deprecates: 'debugBrkPluginHost', args: 'port', cat: 't', description: localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.") },
'disable-gpu': { type: 'boolean', cat: 't', description: localize('disableGPU', "Disable GPU hardware acceleration.") },
'max-memory': { type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes).") },
'max-memory': { type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes)."), args: 'memory' },
'telemetry': { type: 'boolean', cat: 't', description: localize('telemetry', "Shows all telemetry events which VS code collects.") },
'remote': { type: 'string' },
@@ -97,6 +97,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'disable-telemetry': { type: 'boolean' },
'disable-updates': { type: 'boolean' },
'disable-keytar': { type: 'boolean' },
'disable-workspace-trust': { type: 'boolean' },
'disable-crash-reporter': { type: 'boolean' },
'crash-reporter-directory': { type: 'string' },
'crash-reporter-id': { type: 'string' },

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path="../../../../typings/require.d.ts" />

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';

View File

@@ -9,7 +9,7 @@ import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGallery
import { getOrDefault } from 'vs/base/common/objects';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IPager } from 'vs/base/common/paging';
import { IRequestService, asJson, asText } from 'vs/platform/request/common/request';
import { IRequestService, asJson, asText, isSuccess } from 'vs/platform/request/common/request';
import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -26,48 +26,48 @@ import { optional } from 'vs/platform/instantiation/common/instantiation';
import { joinPath } from 'vs/base/common/resources';
interface IRawGalleryExtensionFile {
assetType: string;
source: string;
readonly assetType: string;
readonly source: string;
}
interface IRawGalleryExtensionProperty {
key: string;
value: string;
readonly key: string;
readonly value: string;
}
interface IRawGalleryExtensionVersion {
version: string;
lastUpdated: string;
assetUri: string;
fallbackAssetUri: string;
files: IRawGalleryExtensionFile[];
properties?: IRawGalleryExtensionProperty[];
readonly version: string;
readonly lastUpdated: string;
readonly assetUri: string;
readonly fallbackAssetUri: string;
readonly files: IRawGalleryExtensionFile[];
readonly properties?: IRawGalleryExtensionProperty[];
}
interface IRawGalleryExtensionStatistics {
statisticName: string;
value: number;
readonly statisticName: string;
readonly value: number;
}
interface IRawGalleryExtension {
extensionId: string;
extensionName: string;
displayName: string;
shortDescription: string;
publisher: { displayName: string, publisherId: string, publisherName: string; };
versions: IRawGalleryExtensionVersion[];
statistics: IRawGalleryExtensionStatistics[];
flags: string;
readonly extensionId: string;
readonly extensionName: string;
readonly displayName: string;
readonly shortDescription: string;
readonly publisher: { displayName: string, publisherId: string, publisherName: string; };
readonly versions: IRawGalleryExtensionVersion[];
readonly statistics: IRawGalleryExtensionStatistics[];
readonly flags: string;
}
interface IRawGalleryQueryResult {
results: {
extensions: IRawGalleryExtension[];
resultMetadata: {
metadataType: string;
metadataItems: {
name: string;
count: number;
readonly results: {
readonly extensions: IRawGalleryExtension[];
readonly resultMetadata: {
readonly metadataType: string;
readonly metadataItems: {
readonly name: string;
readonly count: number;
}[];
}[]
}[];
@@ -126,20 +126,20 @@ const PropertyType = {
};
interface ICriterium {
filterType: FilterType;
value?: string;
readonly filterType: FilterType;
readonly value?: string;
}
const DefaultPageSize = 10;
interface IQueryState {
pageNumber: number;
pageSize: number;
sortBy: SortBy;
sortOrder: SortOrder;
flags: Flags;
criteria: ICriterium[];
assetTypes: string[];
readonly pageNumber: number;
readonly pageSize: number;
readonly sortBy: SortBy;
readonly sortOrder: SortOrder;
readonly flags: Flags;
readonly criteria: ICriterium[];
readonly assetTypes: string[];
}
const DefaultQueryState: IQueryState = {
@@ -152,10 +152,33 @@ const DefaultQueryState: IQueryState = {
assetTypes: []
};
type GalleryServiceQueryClassification = {
readonly filterTypes: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly sortBy: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly sortOrder: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', 'isMeasurement': true };
readonly success: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly requestBodySize: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly responseBodySize?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly statusCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly errorCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly count?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
type QueryTelemetryData = {
filterTypes: string[];
sortBy: string;
sortOrder: string;
readonly filterTypes: string[];
readonly sortBy: string;
readonly sortOrder: string;
};
type GalleryServiceQueryEvent = QueryTelemetryData & {
readonly duration: number;
readonly success: boolean;
readonly requestBodySize: string;
readonly responseBodySize?: string;
readonly statusCode?: string;
readonly errorCode?: string;
readonly count?: string;
};
class Query {
@@ -239,7 +262,7 @@ function getCoreTranslationAssets(version: IRawGalleryExtensionVersion): [string
function getRepositoryAsset(version: IRawGalleryExtensionVersion): IGalleryExtensionAsset | null {
if (version.properties) {
const results = version.properties.filter(p => p.key === AssetType.Repository);
const gitRegExp = new RegExp('((git|ssh|http(s)?)|(git@[\w.]+))(:(//)?)([\w.@\:/\-~]+)(.git)(/)?');
const gitRegExp = new RegExp('((git|ssh|http(s)?)|(git@[\\w.]+))(:(//)?)([\\w.@\:/\\-~]+)(.git)(/)?');
const uri = results.filter(r => gitRegExp.test(r.value))[0];
return uri ? { uri: uri.value, fallbackUri: uri.value } : null;
@@ -380,13 +403,11 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
/* __GDPR__FRAGMENT__
"GalleryExtensionTelemetryData2" : {
"index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"searchText": { "classification": "CustomerContent", "purpose": "FeatureInsight" },
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
telemetryData: {
index: ((query.pageNumber - 1) * query.pageSize) + index,
searchText: query.searchText,
querySource
},
preview: getIsPreview(galleryExtension.flags)
@@ -489,7 +510,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
const versionAsset = rawExtension.versions.filter(v => v.version === version)[0];
if (versionAsset) {
const extension = toExtension(rawExtension, versionAsset, 0, query);
if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version)) {
if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version, this.productService.date)) {
return extension;
}
}
@@ -513,20 +534,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
throw new Error('No extension gallery service configured.');
}
const type = options.names ? 'ids' : (options.text ? 'text' : 'all');
let text = options.text || '';
const pageSize = getOrDefault(options, o => o.pageSize, 50);
type GalleryServiceQueryClassification = {
type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
text: { classification: 'CustomerContent', purpose: 'FeatureInsight' };
};
type GalleryServiceQueryEvent = {
type: string;
text: string;
};
this.telemetryService.publicLog2<GalleryServiceQueryEvent, GalleryServiceQueryClassification>('galleryService:query', { type, text });
let query = new Query()
.withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, pageSize)
@@ -676,6 +686,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
extension.extensionId && extension.extensionId.toLocaleLowerCase().indexOf(text) > -1);
}
// {{SQL CARBON EDIT}}
public static compareByField(a: any, b: any, fieldName: string): number {
if (a && !b) {
return 1;
@@ -702,6 +713,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
}
// Always exclude non validated and unpublished extensions
query = query
.withFlags(query.flags, Flags.ExcludeNonValidated)
@@ -717,34 +729,56 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
'Content-Length': String(data.length)
};
const context = await this.requestService.request({
// {{SQL CARBON EDIT}}
type: 'GET',
url: this.api('/extensionquery'),
data,
headers
}, token);
const startTime = new Date().getTime();
let context: IRequestContext | undefined, error: any, total: number = 0;
// {{SQL CARBON EDIT}}
let extensionPolicy: string = this.configurationService.getValue<string>(ExtensionsPolicyKey);
if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500 || extensionPolicy === ExtensionsPolicy.allowNone) {
return { galleryExtensions: [], total: 0 };
}
const result = await asJson<IRawGalleryQueryResult>(context);
if (result) {
const r = result.results[0];
const galleryExtensions = r.extensions;
// const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; {{SQL CARBON EDIT}} comment out for no unused
// const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; {{SQL CARBON EDIT}} comment out for no unused
try {
context = await this.requestService.request({
// {{SQL CARBON EDIT}}
type: 'GET',
url: this.api('/extensionquery'),
data,
headers
}, token);
// {{SQL CARBON EDIT}}
let filteredExtensionsResult = this.createQueryResult(query, galleryExtensions);
let extensionPolicy: string = this.configurationService.getValue<string>(ExtensionsPolicyKey);
if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500 || extensionPolicy === ExtensionsPolicy.allowNone) {
return { galleryExtensions: [], total: 0 };
}
return { galleryExtensions: filteredExtensionsResult.galleryExtensions, total: filteredExtensionsResult.total };
// {{SQL CARBON EDIT}} - End
const result = await asJson<IRawGalleryQueryResult>(context);
if (result) {
const r = result.results[0];
const galleryExtensions = r.extensions;
// const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; {{SQL CARBON EDIT}} comment out for no unused
// const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; {{SQL CARBON EDIT}} comment out for no unused
// {{SQL CARBON EDIT}}
let filteredExtensionsResult = this.createQueryResult(query, galleryExtensions);
return { galleryExtensions: filteredExtensionsResult.galleryExtensions, total: filteredExtensionsResult.total };
// {{SQL CARBON EDIT}} - End
}
return { galleryExtensions: [], total };
} catch (e) {
error = e;
throw e;
} finally {
this.telemetryService.publicLog2<GalleryServiceQueryEvent, GalleryServiceQueryClassification>('galleryService:query', {
...query.telemetryData,
requestBodySize: String(data.length),
duration: new Date().getTime() - startTime,
success: !!context && isSuccess(context),
responseBodySize: context?.res.headers['Content-Length'],
statusCode: context ? String(context.res.statusCode) : undefined,
errorCode: error
? isPromiseCanceledError(error) ? 'canceled' : getErrorMessage(error).startsWith('XHR timeout') ? 'timeout' : 'failed'
: undefined,
count: String(total)
});
}
return { galleryExtensions: [], total: 0 };
}
async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void> {
@@ -848,7 +882,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
try {
engine = await this.getEngine(v);
} catch (error) { /* Ignore error and skip version */ }
if (engine && isEngineValid(engine, this.productService.version)) {
if (engine && isEngineValid(engine, this.productService.version, this.productService.date)) {
result.push({ version: v!.version, date: v!.lastUpdated });
}
}));
@@ -914,8 +948,8 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
if (!vsCodeEngine && !azDataEngine) {
return null;
}
const vsCodeEngineValid = !vsCodeEngine || (vsCodeEngine && isEngineValid(vsCodeEngine, this.productService.vscodeVersion));
const azDataEngineValid = !azDataEngine || (azDataEngine && isEngineValid(azDataEngine, this.productService.version));
const vsCodeEngineValid = !vsCodeEngine || (vsCodeEngine && isEngineValid(vsCodeEngine, this.productService.vscodeVersion, this.productService.date));
const azDataEngineValid = !azDataEngine || (azDataEngine && isEngineValid(azDataEngine, this.productService.version, this.productService.date));
if (vsCodeEngineValid && azDataEngineValid) {
return version;
}
@@ -951,13 +985,14 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
const version = versions[0];
const engine = await this.getEngine(version);
if (!isEngineValid(engine, this.productService.version)) {
if (!isEngineValid(engine, this.productService.version, this.productService.date)) {
return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1));
}
version.properties = version.properties || [];
version.properties.push({ key: PropertyType.Engine, value: engine });
return version;
return {
...version,
properties: [...(version.properties || []), { key: PropertyType.Engine, value: engine }]
};
}
async getExtensionsReport(): Promise<IReportedExtension[]> {

View File

@@ -207,6 +207,7 @@ export class ExtensionManagementError extends Error {
}
export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean };
export type InstallVSIXOptions = InstallOptions & { installOnlyNewlyAddedFromExtensionPack?: boolean };
export type UninstallOptions = { donotIncludePack?: boolean, donotCheckDependents?: boolean };
export const IExtensionManagementService = createDecorator<IExtensionManagementService>('extensionManagementService');
@@ -221,7 +222,7 @@ export interface IExtensionManagementService {
zip(extension: ILocalExtension): Promise<URI>;
unzip(zipLocation: URI): Promise<IExtensionIdentifier>;
getManifest(vsix: URI): Promise<IExtensionManifest>;
install(vsix: URI, options?: InstallOptions): Promise<ILocalExtension>;
install(vsix: URI, options?: InstallVSIXOptions): Promise<ILocalExtension>;
canInstall(extension: IGalleryExtension): Promise<boolean>;
installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise<ILocalExtension>;
uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void>;

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService, InstallOptions, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService, InstallOptions, UninstallOptions, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Emitter, Event } from 'vs/base/common/event';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc';
@@ -62,7 +62,7 @@ export class ExtensionManagementChannel implements IServerChannel {
switch (command) {
case 'zip': return this.service.zip(transformIncomingExtension(args[0], uriTransformer)).then(uri => transformOutgoingURI(uri, uriTransformer));
case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer));
case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer));
case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer), args[1]);
case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer));
case 'canInstall': return this.service.canInstall(args[0]);
case 'installFromGallery': return this.service.installFromGallery(args[0], args[1]);
@@ -112,8 +112,8 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
return Promise.resolve(this.channel.call('unzip', [zipLocation]));
}
install(vsix: URI): Promise<ILocalExtension> {
return Promise.resolve(this.channel.call<ILocalExtension>('install', [vsix])).then(local => transformIncomingExtension(local, null));
install(vsix: URI, options?: InstallVSIXOptions): Promise<ILocalExtension> {
return Promise.resolve(this.channel.call<ILocalExtension>('install', [vsix, options])).then(local => transformIncomingExtension(local, null));
}
getManifest(vsix: URI): Promise<IExtensionManifest> {

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises } from 'fs';
import { Promises as FSPromises } from 'vs/base/node/pfs';
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
@@ -77,7 +77,7 @@ export class ExtensionsDownloader extends Disposable {
private async rename(from: URI, to: URI, retryUntil: number): Promise<void> {
try {
await promises.rename(from.fsPath, to.fsPath);
await FSPromises.rename(from.fsPath, to.fsPath);
} catch (error) {
if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) {
this.logService.info(`Failed renaming ${from} to ${to} with 'EPERM' error. Trying again...`);

View File

@@ -12,7 +12,7 @@ import { join } from 'vs/base/common/path';
import { Limiter } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { Schemas } from 'vs/base/common/network';
import { rimraf } from 'vs/base/node/pfs';
import { Promises } from 'vs/base/node/pfs';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
export class ExtensionsLifecycle extends Disposable {
@@ -34,7 +34,7 @@ export class ExtensionsLifecycle extends Disposable {
this.runLifecycleHook(script.script, 'uninstall', script.args, true, extension)
.then(() => this.logService.info(extension.identifier.id, extension.manifest.version, `Finished running post uninstall script`), err => this.logService.error(extension.identifier.id, extension.manifest.version, `Failed to run post uninstall script: ${err}`)));
}
return rimraf(this.getExtensionStoragePath(extension)).then(undefined, e => this.logService.error('Error while removing extension storage path', e));
return Promises.rm(this.getExtensionStoragePath(extension)).then(undefined, e => this.logService.error('Error while removing extension storage path', e));
}
private parseScript(extension: ILocalExtension, type: string): { script: string, args: string[] } | null {

View File

@@ -3,11 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as nls from 'vs/nls';
import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
// import { isNonEmptyArray } from 'vs/base/common/arrays'; {{SQL CARBON EDIT}}
import { zip, IFile } from 'vs/base/node/zip';
import {
IExtensionManagementService, IExtensionGalleryService, ILocalExtension,
@@ -21,7 +21,8 @@ import {
INSTALL_ERROR_INCOMPATIBLE,
ExtensionManagementError,
InstallOptions,
UninstallOptions
UninstallOptions,
InstallVSIXOptions
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, getGalleryExtensionId, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -137,9 +138,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
private async collectFiles(extension: ILocalExtension): Promise<IFile[]> {
const collectFilesFromDirectory = async (dir: string): Promise<string[]> => {
let entries = await pfs.readdir(dir);
let entries = await pfs.Promises.readdir(dir);
entries = entries.map(e => path.join(dir, e));
const stats = await Promise.all(entries.map(e => fs.promises.stat(e)));
const stats = await Promise.all(entries.map(e => pfs.Promises.stat(e)));
let promise: Promise<string[]> = Promise.resolve([]);
stats.forEach((stat, index) => {
const entry = entries[index];
@@ -159,7 +160,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
return files.map(f => (<IFile>{ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f }));
}
async install(vsix: URI, options: InstallOptions = {}): Promise<ILocalExtension> {
async install(vsix: URI, options: InstallVSIXOptions = {}): Promise<ILocalExtension> {
// {{SQL CARBON EDIT}}
let startTime = new Date().getTime();
this.logService.trace('ExtensionManagementService#install', vsix.toString());
@@ -172,10 +173,10 @@ export class ExtensionManagementService extends Disposable implements IExtension
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
// let operation: InstallOperation = InstallOperation.Install; {{SQL CARBON EDIT}}
// {{SQL CARBON EDIT}}
if (manifest.engines?.vscode && !isEngineValid(manifest.engines.vscode, product.vscodeVersion)) {
if (manifest.engines?.vscode && !isEngineValid(manifest.engines.vscode, product.vscodeVersion, product.date)) {
throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with the current VS Code engine version '{1}'.", identifier.id, product.vscodeVersion));
}
if (manifest.engines?.azdata && !isEngineValid(manifest.engines.azdata, product.version)) {
if (manifest.engines?.azdata && !isEngineValid(manifest.engines.azdata, product.version, product.date)) {
throw new Error(nls.localize('incompatibleAzdata', "Unable to install extension '{0}' as it is not compatible with Azure Data Studio '{1}'.", identifier.id, product.version));
}
@@ -228,8 +229,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
// try {
// metadata = await this.getGalleryMetadata(getGalleryExtensionId(manifest.publisher, manifest.name));
// } catch (e) { /* Ignore */ }
// try {
// const local = await this.installFromZipPath(identifierWithVersion, zipPath, isMachineScoped ? { ...(metadata || {}), isMachineScoped } : metadata, operation, token);
// const local = await this.installFromZipPath(identifierWithVersion, zipPath, options.installOnlyNewlyAddedFromExtensionPack ? existing : undefined, { ...(metadata || {}), ...options }, options, operation, token);
// this.logService.info('Successfully installed the extension:', identifier.id);
// return local;
// } catch (e) {
@@ -253,11 +255,13 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
// {{SQL CARBON EDIT}}
/*private async installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, operation: InstallOperation, token: CancellationToken): Promise<ILocalExtension> {
/*private async installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, existing: ILocalExtension | undefined, metadata: IMetadata | undefined, options: InstallOptions, operation: InstallOperation, token: CancellationToken): Promise<ILocalExtension> {
try {
const local = await this.installExtension({ zipPath, identifierWithVersion, metadata }, token);
try {
await this.installDependenciesAndPackExtensions(local, undefined, options);
if (!options.donotIncludePackAndDependencies) {
await this.installDependenciesAndPackExtensions(local, existing, options);
}
} catch (error) {
if (isNonEmptyArray(local.manifest.extensionDependencies)) {
this.logService.warn(`Cannot install dependencies of extension:`, local.identifier.id, error.message);
@@ -657,7 +661,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
private async preUninstallExtension(extension: ILocalExtension): Promise<void> {
const exists = await pfs.exists(extension.location.fsPath);
const exists = await pfs.Promises.exists(extension.location.fsPath);
if (!exists) {
throw new Error(nls.localize('notExists', "Could not find extension"));
}

View File

@@ -16,4 +16,4 @@ export function getManifest(vsix: string): Promise<IExtensionManifest> {
throw new Error(localize('invalidManifest', "VSIX invalid: package.json is not a JSON file."));
}
});
}
}

View File

@@ -36,6 +36,6 @@ export class ExtensionsManifestCache extends Disposable {
}
invalidate(): void {
pfs.rimraf(this.extensionsManifestCache, pfs.RimRafMode.MOVE).then(() => { }, () => { });
pfs.Promises.rm(this.extensionsManifestCache, pfs.RimRafMode.MOVE).then(() => { }, () => { });
}
}

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as semver from 'vs/base/common/semver/semver';
import { Disposable } from 'vs/base/common/lifecycle';
import * as pfs from 'vs/base/node/pfs';
@@ -107,10 +106,10 @@ export class ExtensionsScanner extends Disposable {
const extensionPath = path.join(this.extensionsPath, folderName);
try {
await pfs.rimraf(extensionPath);
await pfs.Promises.rm(extensionPath);
} catch (error) {
try {
await pfs.rimraf(extensionPath);
await pfs.Promises.rm(extensionPath);
} catch (e) { /* ignore */ }
throw new ExtensionManagementError(localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, identifierWithVersion.id), INSTALL_ERROR_DELETING);
}
@@ -127,7 +126,7 @@ export class ExtensionsScanner extends Disposable {
this.logService.info('Renamed to', extensionPath);
} catch (error) {
try {
await pfs.rimraf(tempPath);
await pfs.Promises.rm(tempPath);
} catch (e) { /* ignore */ }
if (error.code === 'ENOTEMPTY') {
this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, identifierWithVersion.id);
@@ -159,10 +158,10 @@ export class ExtensionsScanner extends Disposable {
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 raw = await pfs.Promises.readFile(manifestPath, 'utf8');
const { manifest } = await this.parseManifest(raw);
(manifest as ILocalExtensionManifest).__metadata = storedMetadata;
await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
await pfs.Promises.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
return local;
}
@@ -192,7 +191,7 @@ export class ExtensionsScanner extends Disposable {
return this.uninstalledFileLimiter.queue(async () => {
let raw: string | undefined;
try {
raw = await fs.promises.readFile(this.uninstalledPath, 'utf8');
raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8');
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
@@ -209,9 +208,9 @@ export class ExtensionsScanner extends Disposable {
if (updateFn) {
updateFn(uninstalled);
if (Object.keys(uninstalled).length) {
await pfs.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
} else {
await pfs.rimraf(this.uninstalledPath);
await pfs.Promises.rm(this.uninstalledPath);
}
}
@@ -221,7 +220,7 @@ export class ExtensionsScanner extends Disposable {
async removeExtension(extension: ILocalExtension, type: string): Promise<void> {
this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath);
await pfs.rimraf(extension.location.fsPath);
await pfs.Promises.rm(extension.location.fsPath);
this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath);
}
@@ -235,7 +234,7 @@ export class ExtensionsScanner extends Disposable {
// Clean the location
try {
await pfs.rimraf(location);
await pfs.Promises.rm(location);
} catch (e) {
throw new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING);
}
@@ -244,14 +243,14 @@ export class ExtensionsScanner extends Disposable {
await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token);
this.logService.info(`Extracted extension to ${location}:`, identifier.id);
} catch (e) {
try { await pfs.rimraf(location); } catch (e) { /* Ignore */ }
try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ }
throw new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING);
}
}
private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> {
try {
await fs.promises.rename(extractPath, renamePath);
await pfs.Promises.rename(extractPath, renamePath);
} catch (error) {
if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) {
this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id);
@@ -393,9 +392,9 @@ export class ExtensionsScanner extends Disposable {
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IStoredMetadata | null; }> {
const promises = [
fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8')
pfs.Promises.readFile(path.join(extensionPath, 'package.json'), 'utf8')
.then(raw => this.parseManifest(raw)),
fs.promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8')
pfs.Promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8')
.then(undefined, err => err.code !== 'ENOENT' ? Promise.reject<string>(err) : '{}')
.then(raw => JSON.parse(raw))
];

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';

View File

@@ -24,10 +24,12 @@ export interface INormalizedVersion {
minorMustEqual: boolean;
patchBase: number;
patchMustEqual: boolean;
notBefore: number; /* milliseconds timestamp, or 0 */
isMinimum: boolean;
}
const VERSION_REGEXP = /^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(\-.*)?$/;
const NOT_BEFORE_REGEXP = /^-(\d{4})(\d{2})(\d{2})$/;
export function isValidVersionStr(version: string): boolean {
version = version.trim();
@@ -93,6 +95,15 @@ export function normalizeVersion(version: IParsedVersion | null): INormalizedVer
}
}
let notBefore = 0;
if (version.preRelease) {
const match = NOT_BEFORE_REGEXP.exec(version.preRelease);
if (match) {
const [, year, month, day] = match;
notBefore = Date.UTC(Number(year), Number(month) - 1, Number(day));
}
}
return {
majorBase: majorBase,
majorMustEqual: majorMustEqual,
@@ -100,16 +111,24 @@ export function normalizeVersion(version: IParsedVersion | null): INormalizedVer
minorMustEqual: minorMustEqual,
patchBase: patchBase,
patchMustEqual: patchMustEqual,
isMinimum: version.hasGreaterEquals
isMinimum: version.hasGreaterEquals,
notBefore,
};
}
export function isValidVersion(_version: string | INormalizedVersion, _desiredVersion: string | INormalizedVersion): boolean {
export function isValidVersion(_inputVersion: string | INormalizedVersion, _inputDate: ProductDate, _desiredVersion: string | INormalizedVersion): boolean {
let version: INormalizedVersion | null;
if (typeof _version === 'string') {
version = normalizeVersion(parseVersion(_version));
if (typeof _inputVersion === 'string') {
version = normalizeVersion(parseVersion(_inputVersion));
} else {
version = _version;
version = _inputVersion;
}
let productTs: number | undefined;
if (_inputDate instanceof Date) {
productTs = _inputDate.getTime();
} else if (typeof _inputDate === 'string') {
productTs = new Date(_inputDate).getTime();
}
let desiredVersion: INormalizedVersion | null;
@@ -130,6 +149,7 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe
let desiredMajorBase = desiredVersion.majorBase;
let desiredMinorBase = desiredVersion.minorBase;
let desiredPatchBase = desiredVersion.patchBase;
let desiredNotBefore = desiredVersion.notBefore;
let majorMustEqual = desiredVersion.majorMustEqual;
let minorMustEqual = desiredVersion.minorMustEqual;
@@ -152,6 +172,10 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe
return false;
}
if (productTs && productTs < desiredNotBefore) {
return false;
}
return patchBase >= desiredPatchBase;
}
@@ -200,6 +224,11 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe
}
// at this point, patchBase are equal
if (productTs && productTs < desiredNotBefore) {
return false;
}
return true;
}
@@ -213,7 +242,9 @@ export interface IReducedExtensionDescription {
main?: string;
}
export function isValidExtensionVersion(version: string, extensionDesc: IReducedExtensionDescription, notices: string[]): boolean {
type ProductDate = string | Date | undefined;
export function isValidExtensionVersion(version: string, date: ProductDate, extensionDesc: IReducedExtensionDescription, notices: string[]): boolean {
if (extensionDesc.isBuiltin || typeof extensionDesc.main === 'undefined') {
// No version check for builtin or declarative extensions
@@ -221,16 +252,16 @@ export function isValidExtensionVersion(version: string, extensionDesc: IReduced
}
// {{SQL CARBON EDIT}}
return extensionDesc.engines.azdata ? extensionDesc.engines.azdata === '*' || isVersionValid(version, extensionDesc.engines.azdata, notices) : true;
return extensionDesc.engines.azdata ? extensionDesc.engines.azdata === '*' || isVersionValid(version, date, extensionDesc.engines.azdata, notices) : true;
}
// {{SQL CARBON EDIT}}
export function isEngineValid(engine: string, version: string): boolean {
export function isEngineValid(engine: string, version: string, date: ProductDate): boolean {
// TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version
return engine === '*' || isVersionValid(version, engine);
return engine === '*' || isVersionValid(version, date, engine);
}
export function isVersionValid(currentVersion: string, requestedVersion: string, notices: string[] = []): boolean {
function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean {
let desiredVersion = normalizeVersion(parseVersion(requestedVersion));
if (!desiredVersion) {
@@ -255,7 +286,7 @@ export function isVersionValid(currentVersion: string, requestedVersion: string,
}
}
if (!isValidVersion(currentVersion, desiredVersion)) {
if (!isValidVersion(currentVersion, date, desiredVersion)) {
notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
return false;
}

View File

@@ -123,10 +123,12 @@ export interface IAuthenticationContribution {
export interface IWalkthroughStep {
readonly id: string;
readonly title: string;
readonly description: string;
readonly description: string | undefined;
readonly media:
| { path: string | { dark: string, light: string, hc: string }, altText: string }
| { path: string, },
| { image: string | { dark: string, light: string, hc: string }, altText: string, markdown?: never }
| { markdown: string, image?: never }
readonly completionEvents?: string[];
/** @deprecated use `completionEvents: 'onCommand:...'` */
readonly doneOn?: { command: string };
readonly when?: string;
}
@@ -136,7 +138,6 @@ export interface IWalkthrough {
readonly title: string;
readonly description: string;
readonly steps: IWalkthroughStep[];
readonly primary?: boolean;
readonly when?: string;
}
@@ -173,13 +174,30 @@ export interface IExtensionContributions {
}
export interface IExtensionCapabilities {
readonly virtualWorkspaces?: boolean;
readonly virtualWorkspaces?: ExtensionVirtualWorkpaceSupport;
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 type LimitedWorkpaceSupportType = 'limited';
export type ExtensionUntrustedWorkpaceSupportType = boolean | LimitedWorkpaceSupportType;
export type ExtensionUntrustedWorkspaceSupport = { supported: true; } | { supported: false, description: string } | { supported: LimitedWorkpaceSupportType, description: string, restrictedConfigurations?: string[] };
export type ExtensionVirtualWorkpaceSupportType = boolean | LimitedWorkpaceSupportType;
export type ExtensionVirtualWorkpaceSupport = boolean | { supported: true; } | { supported: false | LimitedWorkpaceSupportType, description: string };
export function getWorkpaceSupportTypeMessage(supportType: ExtensionUntrustedWorkspaceSupport | ExtensionVirtualWorkpaceSupport | undefined): string | undefined {
if (typeof supportType === 'object' && supportType !== null) {
if (supportType.supported !== true) {
return supportType.description;
}
}
return undefined;
}
export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier {
return thing

View File

@@ -6,6 +6,7 @@ import * as assert from 'assert';
import { INormalizedVersion, IParsedVersion, IReducedExtensionDescription, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator';
suite('Extension Version Validator', () => {
const productVersion = '2021-05-11T21:54:30.577Z';
test('isValidVersionStr', () => {
assert.strictEqual(isValidVersionStr('0.10.0-dev'), true);
@@ -53,13 +54,16 @@ suite('Extension Version Validator', () => {
});
test('normalizeVersion', () => {
function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean): void {
function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean, notBefore = 0): void {
const actual = normalizeVersion(parseVersion(version));
const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum };
const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum, notBefore };
assert.deepStrictEqual(actual, expected, 'parseVersion for ' + version);
}
assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false);
assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false, 0);
assertNormalizeVersion('0.10.0-222222222', 0, true, 10, true, 0, true, false, 0);
assertNormalizeVersion('0.10.0-20210511', 0, true, 10, true, 0, true, false, new Date('2021-05-11T00:00:00Z').getTime());
assertNormalizeVersion('0.10.0', 0, true, 10, true, 0, true, false);
assertNormalizeVersion('0.10.1', 0, true, 10, true, 1, true, false);
assertNormalizeVersion('0.10.100', 0, true, 10, true, 100, true, false);
@@ -75,11 +79,12 @@ suite('Extension Version Validator', () => {
assertNormalizeVersion('>=0.0.1', 0, true, 0, true, 1, true, true);
assertNormalizeVersion('>=2.4.3', 2, true, 4, true, 3, true, true);
assertNormalizeVersion('>=2.4.3', 2, true, 4, true, 3, true, true);
});
test('isValidVersion', () => {
function testIsValidVersion(version: string, desiredVersion: string, expectedResult: boolean): void {
let actual = isValidVersion(version, desiredVersion);
let actual = isValidVersion(version, productVersion, desiredVersion);
assert.strictEqual(actual, expectedResult, 'extension - vscode: ' + version + ', desiredVersion: ' + desiredVersion + ' should be ' + expectedResult);
}
@@ -211,7 +216,7 @@ suite('Extension Version Validator', () => {
main: hasMain ? 'something' : undefined
};
let reasons: string[] = [];
let actual = isValidExtensionVersion(version, desc, reasons);
let actual = isValidExtensionVersion(version, productVersion, desc, reasons);
assert.strictEqual(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(desc) + ', reasons: ' + JSON.stringify(reasons));
}
@@ -390,5 +395,12 @@ suite('Extension Version Validator', () => {
testIsValidVersion('2.0.0', '^1.100.0', false);
testIsValidVersion('2.0.0', '^2.0.0', true);
testIsValidVersion('2.0.0', '*', false); // fails due to lack of specificity
// date tags
testIsValidVersion('1.10.0', '^1.10.0-20210511', true); // current date
testIsValidVersion('1.10.0', '^1.10.0-20210510', true); // before date
testIsValidVersion('1.10.0', '^1.10.0-20210512', false); // future date
testIsValidVersion('1.10.1', '^1.10.0-20200101', true); // before date, but ahead version
testIsValidVersion('1.11.0', '^1.10.0-20200101', true);
});
});

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* 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 { ITerminalEnvironment } from 'vs/platform/terminal/common/terminal';
export const IExternalTerminalService = createDecorator<IExternalTerminalService>('externalTerminal');
export interface IExternalTerminalSettings {
linuxExec?: string;
osxExec?: string;
windowsExec?: string;
}
export interface ITerminalForPlatform {
windows: string,
linux: string,
osx: string
}
export interface IExternalTerminalService {
readonly _serviceBrand: undefined;
openTerminal(path: string): Promise<void>;
runInTerminal(title: string, cwd: string, args: string[], env: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined>;
getDefaultTerminalForPlatforms(): Promise<ITerminalForPlatform>;
}
export interface IExternalTerminalConfiguration {
terminal: {
explorerKind: 'integrated' | 'external',
external: IExternalTerminalSettings;
};
}
export const DEFAULT_TERMINAL_OSX = 'Terminal.app';
export const IExternalTerminalMainService = createDecorator<IExternalTerminalMainService>('externalTerminal');
export interface IExternalTerminalMainService extends IExternalTerminalService {
readonly _serviceBrand: undefined;
}

View File

@@ -0,0 +1,243 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deepEqual, equal } from 'assert';
import { DEFAULT_TERMINAL_OSX } from 'vs/platform/externalTerminal/common/externalTerminal';
import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService';
suite('ExternalTerminalService', () => {
let mockOnExit: Function;
let mockOnError: Function;
let mockConfig: any;
setup(() => {
mockConfig = {
terminal: {
explorerKind: 'external',
external: {
windowsExec: 'testWindowsShell',
osxExec: 'testOSXShell',
linuxExec: 'testLinuxShell'
}
}
};
mockOnExit = (s: any) => s;
mockOnError = (e: any) => e;
});
test(`WinTerminalService - uses terminal from configuration`, done => {
let testShell = 'cmd';
let testCwd = 'path/to/workspace';
let mockSpawner = {
spawn: (command: any, args: any, opts: any) => {
// assert
equal(command, testShell, 'shell should equal expected');
equal(args[args.length - 1], mockConfig.terminal.external.windowsExec, 'terminal should equal expected');
equal(opts.cwd, testCwd, 'opts.cwd should equal expected');
done();
return {
on: (evt: any) => evt
};
}
};
let testService = new WindowsExternalTerminalService(mockConfig);
(<any>testService).spawnTerminal(
mockSpawner,
mockConfig,
testShell,
testCwd,
mockOnExit,
mockOnError
);
});
test(`WinTerminalService - uses default terminal when configuration.terminal.external.windowsExec is undefined`, done => {
let testShell = 'cmd';
let testCwd = 'path/to/workspace';
let mockSpawner = {
spawn: (command: any, args: any, opts: any) => {
// assert
equal(args[args.length - 1], WindowsExternalTerminalService.getDefaultTerminalWindows(), 'terminal should equal expected');
done();
return {
on: (evt: any) => evt
};
}
};
mockConfig.terminal.external.windowsExec = undefined;
let testService = new WindowsExternalTerminalService(mockConfig);
(<any>testService).spawnTerminal(
mockSpawner,
mockConfig,
testShell,
testCwd,
mockOnExit,
mockOnError
);
});
test(`WinTerminalService - uses default terminal when configuration.terminal.external.windowsExec is undefined`, done => {
let testShell = 'cmd';
let testCwd = 'c:/foo';
let mockSpawner = {
spawn: (command: any, args: any, opts: any) => {
// assert
equal(opts.cwd, 'C:/foo', 'cwd should be uppercase regardless of the case that\'s passed in');
done();
return {
on: (evt: any) => evt
};
}
};
let testService = new WindowsExternalTerminalService(mockConfig);
(<any>testService).spawnTerminal(
mockSpawner,
mockConfig,
testShell,
testCwd,
mockOnExit,
mockOnError
);
});
test(`WinTerminalService - cmder should be spawned differently`, done => {
let testShell = 'cmd';
mockConfig.terminal.external.windowsExec = 'cmder';
let testCwd = 'c:/foo';
let mockSpawner = {
spawn: (command: any, args: any, opts: any) => {
// assert
deepEqual(args, ['C:/foo']);
equal(opts, undefined);
done();
return { on: (evt: any) => evt };
}
};
let testService = new WindowsExternalTerminalService(mockConfig);
(<any>testService).spawnTerminal(
mockSpawner,
mockConfig,
testShell,
testCwd,
mockOnExit,
mockOnError
);
});
test(`WinTerminalService - windows terminal should open workspace directory`, done => {
let testShell = 'wt';
let testCwd = 'c:/foo';
let mockSpawner = {
spawn: (command: any, args: any, opts: any) => {
// assert
equal(opts.cwd, 'C:/foo');
done();
return { on: (evt: any) => evt };
}
};
let testService = new WindowsExternalTerminalService(mockConfig);
(<any>testService).spawnTerminal(
mockSpawner,
mockConfig,
testShell,
testCwd,
mockOnExit,
mockOnError
);
});
test(`MacTerminalService - uses terminal from configuration`, done => {
let testCwd = 'path/to/workspace';
let mockSpawner = {
spawn: (command: any, args: any, opts: any) => {
// assert
equal(args[1], mockConfig.terminal.external.osxExec, 'terminal should equal expected');
done();
return {
on: (evt: any) => evt
};
}
};
let testService = new MacExternalTerminalService(mockConfig);
(<any>testService).spawnTerminal(
mockSpawner,
mockConfig,
testCwd,
mockOnExit,
mockOnError
);
});
test(`MacTerminalService - uses default terminal when configuration.terminal.external.osxExec is undefined`, done => {
let testCwd = 'path/to/workspace';
let mockSpawner = {
spawn: (command: any, args: any, opts: any) => {
// assert
equal(args[1], DEFAULT_TERMINAL_OSX, 'terminal should equal expected');
done();
return {
on: (evt: any) => evt
};
}
};
mockConfig.terminal.external.osxExec = undefined;
let testService = new MacExternalTerminalService(mockConfig);
(<any>testService).spawnTerminal(
mockSpawner,
mockConfig,
testCwd,
mockOnExit,
mockOnError
);
});
test(`LinuxTerminalService - uses terminal from configuration`, done => {
let testCwd = 'path/to/workspace';
let mockSpawner = {
spawn: (command: any, args: any, opts: any) => {
// assert
equal(command, mockConfig.terminal.external.linuxExec, 'terminal should equal expected');
equal(opts.cwd, testCwd, 'opts.cwd should equal expected');
done();
return {
on: (evt: any) => evt
};
}
};
let testService = new LinuxExternalTerminalService(mockConfig);
(<any>testService).spawnTerminal(
mockSpawner,
mockConfig,
testCwd,
mockOnExit,
mockOnError
);
});
test(`LinuxTerminalService - uses default terminal when configuration.terminal.external.linuxExec is undefined`, done => {
LinuxExternalTerminalService.getDefaultTerminalLinuxReady().then(defaultTerminalLinux => {
let testCwd = 'path/to/workspace';
let mockSpawner = {
spawn: (command: any, args: any, opts: any) => {
// assert
equal(command, defaultTerminalLinux, 'terminal should equal expected');
done();
return {
on: (evt: any) => evt
};
}
};
mockConfig.terminal.external.linuxExec = undefined;
let testService = new LinuxExternalTerminalService(mockConfig);
(<any>testService).spawnTerminal(
mockSpawner,
mockConfig,
testCwd,
mockOnExit,
mockOnError
);
});
});
});

View File

@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* 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 { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services';
import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal';
export const IExternalTerminalMainService = createDecorator<IExternalTerminalMainService>('externalTerminal');
export interface IExternalTerminalMainService extends IExternalTerminalService {
readonly _serviceBrand: undefined;
}
registerMainProcessRemoteService(IExternalTerminalMainService, 'externalTerminal', { supportsDelayedInstantiation: true });

View File

@@ -0,0 +1,362 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import * as path from 'vs/base/common/path';
import * as processes from 'vs/base/node/processes';
import * as nls from 'vs/nls';
import * as pfs from 'vs/base/node/pfs';
import * as env from 'vs/base/common/platform';
import { IExternalTerminalConfiguration, IExternalTerminalSettings, DEFAULT_TERMINAL_OSX, ITerminalForPlatform, IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { optional } from 'vs/platform/instantiation/common/instantiation';
import { FileAccess } from 'vs/base/common/network';
import { ITerminalEnvironment } from 'vs/platform/terminal/common/terminal';
import { sanitizeProcessEnvironment } from 'vs/base/common/processes';
const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console");
abstract class ExternalTerminalService {
public _serviceBrand: undefined;
async getDefaultTerminalForPlatforms(): Promise<ITerminalForPlatform> {
const linuxTerminal = await LinuxExternalTerminalService.getDefaultTerminalLinuxReady();
return { windows: WindowsExternalTerminalService.getDefaultTerminalWindows(), linux: linuxTerminal, osx: 'xterm' };
}
}
export class WindowsExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService {
private static readonly CMD = 'cmd.exe';
private static _DEFAULT_TERMINAL_WINDOWS: string;
constructor(
@optional(IConfigurationService) private readonly _configurationService: IConfigurationService
) {
super();
}
public openTerminal(cwd?: string): Promise<void> {
const configuration = this._configurationService.getValue<IExternalTerminalConfiguration>();
return this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd);
}
public spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, command: string, cwd?: string): Promise<void> {
const terminalConfig = configuration.terminal.external;
const exec = terminalConfig?.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows();
// Make the drive letter uppercase on Windows (see #9448)
if (cwd && cwd[1] === ':') {
cwd = cwd[0].toUpperCase() + cwd.substr(1);
}
// cmder ignores the environment cwd and instead opts to always open in %USERPROFILE%
// unless otherwise specified
const basename = path.basename(exec).toLowerCase();
if (basename === 'cmder' || basename === 'cmder.exe') {
spawner.spawn(exec, cwd ? [cwd] : undefined);
return Promise.resolve(undefined);
}
const cmdArgs = ['/c', 'start', '/wait'];
if (exec.indexOf(' ') >= 0) {
// The "" argument is the window title. Without this, exec doesn't work when the path
// contains spaces
cmdArgs.push('""');
}
cmdArgs.push(exec);
// Add starting directory parameter for Windows Terminal (see #90734)
if (basename === 'wt' || basename === 'wt.exe') {
cmdArgs.push('-d .');
}
return new Promise<void>((c, e) => {
const env = getSanitizedEnvironment(process);
const child = spawner.spawn(command, cmdArgs, { cwd, env });
child.on('error', e);
child.on('exit', () => c());
});
}
public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
const exec = 'windowsExec' in settings && settings.windowsExec ? settings.windowsExec : WindowsExternalTerminalService.getDefaultTerminalWindows();
return new Promise<number | undefined>((resolve, reject) => {
const title = `"${dir} - ${TERMINAL_TITLE}"`;
const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code
const cmdArgs = [
'/c', 'start', title, '/wait', exec, '/c', command
];
// merge environment variables into a copy of the process.env
const env = Object.assign({}, getSanitizedEnvironment(process), envVars);
// delete environment variables that have a null value
Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);
const options: any = {
cwd: dir,
env: env,
windowsVerbatimArguments: true
};
const cmd = cp.spawn(WindowsExternalTerminalService.CMD, cmdArgs, options);
cmd.on('error', err => {
reject(improveError(err));
});
resolve(undefined);
});
}
public static getDefaultTerminalWindows(): string {
if (!WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS) {
const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`;
}
return WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS;
}
}
export class MacExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService {
private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X
constructor(
@optional(IConfigurationService) private readonly _configurationService: IConfigurationService
) {
super();
}
public openTerminal(cwd?: string): Promise<void> {
const configuration = this._configurationService.getValue<IExternalTerminalConfiguration>();
return this.spawnTerminal(cp, configuration, cwd);
}
public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
const terminalApp = settings.osxExec || DEFAULT_TERMINAL_OSX;
return new Promise<number | undefined>((resolve, reject) => {
if (terminalApp === DEFAULT_TERMINAL_OSX || terminalApp === 'iTerm.app') {
// On OS X we launch an AppleScript that creates (or reuses) a Terminal window
// and then launches the program inside that window.
const script = terminalApp === DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper';
const scriptpath = FileAccess.asFileUri(`vs/workbench/contrib/externalTerminal/node/${script}.scpt`, require).fsPath;
const osaArgs = [
scriptpath,
'-t', title || TERMINAL_TITLE,
'-w', dir,
];
for (let a of args) {
osaArgs.push('-a');
osaArgs.push(a);
}
if (envVars) {
for (let key in envVars) {
const value = envVars[key];
if (value === null) {
osaArgs.push('-u');
osaArgs.push(key);
} else {
osaArgs.push('-e');
osaArgs.push(`${key}=${value}`);
}
}
}
let stderr = '';
const osa = cp.spawn(MacExternalTerminalService.OSASCRIPT, osaArgs);
osa.on('error', err => {
reject(improveError(err));
});
osa.stderr.on('data', (data) => {
stderr += data.toString();
});
osa.on('exit', (code: number) => {
if (code === 0) { // OK
resolve(undefined);
} else {
if (stderr) {
const lines = stderr.split('\n', 1);
reject(new Error(lines[0]));
} else {
reject(new Error(nls.localize('mac.terminal.script.failed', "Script '{0}' failed with exit code {1}", script, code)));
}
}
});
} else {
reject(new Error(nls.localize('mac.terminal.type.not.supported', "'{0}' not supported", terminalApp)));
}
});
}
spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise<void> {
const terminalConfig = configuration.terminal.external;
const terminalApp = terminalConfig?.osxExec || DEFAULT_TERMINAL_OSX;
return new Promise<void>((c, e) => {
const args = ['-a', terminalApp];
if (cwd) {
args.push(cwd);
}
const child = spawner.spawn('/usr/bin/open', args);
child.on('error', e);
child.on('exit', () => c());
});
}
}
export class LinuxExternalTerminalService extends ExternalTerminalService implements IExternalTerminalMainService {
private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue...");
constructor(
@optional(IConfigurationService) private readonly _configurationService: IConfigurationService
) {
super();
}
public openTerminal(cwd?: string): Promise<void> {
const configuration = this._configurationService.getValue<IExternalTerminalConfiguration>();
return this.spawnTerminal(cp, configuration, cwd);
}
public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
const execPromise = settings.linuxExec ? Promise.resolve(settings.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady();
return new Promise<number | undefined>((resolve, reject) => {
let termArgs: string[] = [];
//termArgs.push('--title');
//termArgs.push(`"${TERMINAL_TITLE}"`);
execPromise.then(exec => {
if (exec.indexOf('gnome-terminal') >= 0) {
termArgs.push('-x');
} else {
termArgs.push('-e');
}
termArgs.push('bash');
termArgs.push('-c');
const bashCommand = `${quote(args)}; echo; read -p "${LinuxExternalTerminalService.WAIT_MESSAGE}" -n1;`;
termArgs.push(`''${bashCommand}''`); // wrapping argument in two sets of ' because node is so "friendly" that it removes one set...
// merge environment variables into a copy of the process.env
const env = Object.assign({}, process.env, envVars);
// delete environment variables that have a null value
Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);
const options: any = {
cwd: dir,
env: env
};
let stderr = '';
const cmd = cp.spawn(exec, termArgs, options);
cmd.on('error', err => {
reject(improveError(err));
});
cmd.stderr.on('data', (data) => {
stderr += data.toString();
});
cmd.on('exit', (code: number) => {
if (code === 0) { // OK
resolve(undefined);
} else {
if (stderr) {
const lines = stderr.split('\n', 1);
reject(new Error(lines[0]));
} else {
reject(new Error(nls.localize('linux.term.failed', "'{0}' failed with exit code {1}", exec, code)));
}
}
});
});
});
}
private static _DEFAULT_TERMINAL_LINUX_READY: Promise<string>;
public static async getDefaultTerminalLinuxReady(): Promise<string> {
if (!LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY) {
LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise(async r => {
if (env.isLinux) {
const isDebian = await pfs.Promises.exists('/etc/debian_version');
if (isDebian) {
r('x-terminal-emulator');
} else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') {
r('gnome-terminal');
} else if (process.env.DESKTOP_SESSION === 'kde-plasma') {
r('konsole');
} else if (process.env.COLORTERM) {
r(process.env.COLORTERM);
} else if (process.env.TERM) {
r(process.env.TERM);
} else {
r('xterm');
}
} else {
r('xterm');
}
});
}
return LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY;
}
spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise<void> {
const terminalConfig = configuration.terminal.external;
const execPromise = terminalConfig?.linuxExec ? Promise.resolve(terminalConfig.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady();
return new Promise<void>((c, e) => {
execPromise.then(exec => {
const env = getSanitizedEnvironment(process);
const child = spawner.spawn(exec, [], { cwd, env });
child.on('error', e);
child.on('exit', () => c());
});
});
}
}
function getSanitizedEnvironment(process: NodeJS.Process) {
const env = process.env;
sanitizeProcessEnvironment(env);
return env;
}
/**
* tries to turn OS errors into more meaningful error messages
*/
function improveError(err: Error & { errno?: string, path?: string }): Error {
if ('errno' in err && err['errno'] === 'ENOENT' && 'path' in err && typeof err['path'] === 'string') {
return new Error(nls.localize('ext.term.app.not.found', "can't find terminal application '{0}'", err['path']));
}
return err;
}
/**
* Quote args if necessary and combine into a space separated string.
*/
function quote(args: string[]): string {
let r = '';
for (let a of args) {
if (a.indexOf(' ') >= 0) {
r += '"' + a + '"';
} else {
r += a;
}
r += ' ';
}
return r;
}

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';

View File

@@ -23,7 +23,7 @@ const ERR_FILE_NOT_DIR = createFileSystemProviderError(localize('fileNotDirector
const ERR_DIR_NOT_EMPTY = createFileSystemProviderError(localize('dirIsNotEmpty', "Directory is not empty"), FileSystemProviderErrorCode.Unknown);
// Arbitrary Internal Errors (should never be thrown in production)
const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occured in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown);
const ERR_UNKNOWN_INTERNAL = (message: string) => createFileSystemProviderError(localize('internal', "Internal error occurred in IndexedDB File System Provider. ({0})", message), FileSystemProviderErrorCode.Unknown);
export class IndexedDB {
@@ -99,7 +99,7 @@ class IndexedDBFileSystemNode {
}
read(path: string) {
read(path: string): IndexedDBFileSystemEntry | undefined {
return this.doRead(path.split('/').filter(p => p.length));
}
@@ -283,10 +283,7 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
async readFile(resource: URI): Promise<Uint8Array> {
const buffer = await new Promise<Uint8Array>((c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.get(resource.path);
request.onerror = () => e(request.error);
request.onsuccess = () => {
transaction.oncomplete = () => {
if (request.result instanceof Uint8Array) {
c(request.result);
} else if (typeof request.result === 'string') {
@@ -300,6 +297,10 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
}
}
};
transaction.onerror = () => e(transaction.error);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.get(resource.path);
});
(await this.getFiletree()).add(resource.path, { type: 'file', size: buffer.byteLength });
@@ -374,10 +375,7 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
if (!this.cachedFiletree) {
this.cachedFiletree = new Promise((c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.getAllKeys();
request.onerror = () => e(request.error);
request.onsuccess = () => {
transaction.oncomplete = () => {
const rootNode = new IndexedDBFileSystemNode({
children: new Map(),
path: '',
@@ -387,6 +385,10 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
keys.forEach(key => rootNode.add(key, { type: 'file' }));
c(rootNode);
};
transaction.onerror = () => e(transaction.error);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.getAllKeys();
});
}
return this.cachedFiletree;
@@ -397,41 +399,44 @@ class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSy
return new Promise<void>((c, e) => {
const fileBatch = this.fileWriteBatch;
this.fileWriteBatch = [];
if (fileBatch.length === 0) { return c(); }
if (fileBatch.length === 0) {
return c();
}
const transaction = this.database.transaction([this.store], 'readwrite');
transaction.oncomplete = () => c();
transaction.onerror = () => e(transaction.error);
const objectStore = transaction.objectStore(this.store);
let request: IDBRequest = undefined!;
for (const entry of fileBatch) {
request = objectStore.put(entry.content, entry.resource.path);
objectStore.put(entry.content, entry.resource.path);
}
request.onsuccess = () => c();
});
}
private deleteKeys(keys: string[]): Promise<void> {
return new Promise(async (c, e) => {
if (keys.length === 0) { return c(); }
const transaction = this.database.transaction([this.store], 'readwrite');
transaction.onerror = () => e(transaction.error);
const objectStore = transaction.objectStore(this.store);
let request: IDBRequest = undefined!;
for (const key of keys) {
request = objectStore.delete(key);
if (keys.length === 0) {
return c();
}
request.onsuccess = () => c();
const transaction = this.database.transaction([this.store], 'readwrite');
transaction.oncomplete = () => c();
transaction.onerror = () => e(transaction.error);
const objectStore = transaction.objectStore(this.store);
for (const key of keys) {
objectStore.delete(key);
}
});
}
reset(): Promise<void> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store], 'readwrite');
transaction.oncomplete = () => c();
transaction.onerror = () => e(transaction.error);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.clear();
request.onerror = () => e(request.error);
request.onsuccess = () => c();
objectStore.clear();
});
}
}

View File

@@ -6,7 +6,7 @@
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, IReadFileStreamOptions, FileDeleteOptions } 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, FilePermission, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Emitter } from 'vs/base/common/event';
import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources';
@@ -234,6 +234,7 @@ export class FileService extends Disposable implements IFileService {
mtime: stat.mtime,
ctime: stat.ctime,
size: stat.size,
readonly: Boolean((stat.permissions ?? 0) & FilePermission.Readonly) || Boolean(provider.capabilities & FileSystemProviderCapabilities.Readonly),
etag: etag({ mtime: stat.mtime, size: stat.size })
};
@@ -401,6 +402,9 @@ export class FileService extends Disposable implements IFileService {
throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
}
// File cannot be readonly
this.throwIfFileIsReadonly(resource, stat);
// Dirty write prevention: if the file on disk has been changed and does not match our expected
// mtime and etag, we bail out to prevent dirty writing.
//
@@ -526,7 +530,14 @@ export class FileService extends Disposable implements IFileService {
await consumeStream(fileStream);
}
throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
// Re-throw errors as file operation errors but preserve
// specific errors (such as not modified since)
const message = localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString());
if (error instanceof NotModifiedSinceFileOperationError) {
throw new NotModifiedSinceFileOperationError(message, error.stat, options);
} else {
throw new FileOperationError(message, toFileOperationResult(error), options);
}
}
}
@@ -594,7 +605,7 @@ export class FileService extends Disposable implements IFileService {
// Throw if file not modified since (unless disabled)
if (options && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED && options.etag === stat.etag) {
throw new FileOperationError(localize('fileNotModifiedError', "File not modified since"), FileOperationResult.FILE_NOT_MODIFIED_SINCE, options);
throw new NotModifiedSinceFileOperationError(localize('fileNotModifiedError', "File not modified since"), stat, options);
}
// Throw if file is too large to load
@@ -912,14 +923,22 @@ export class FileService extends Disposable implements IFileService {
}
// Validate delete
const exists = await this.exists(resource);
if (!exists) {
let stat: IStat | undefined = undefined;
try {
stat = await provider.stat(resource);
} catch (error) {
// Handled later
}
if (stat) {
this.throwIfFileIsReadonly(resource, stat);
} else {
throw new FileOperationError(localize('deleteFailedNotFound', "Unable to delete non-existing file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND);
}
// Validate recursive
const recursive = !!options?.recursive;
if (!recursive && exists) {
if (!recursive) {
const stat = await this.resolve(resource);
if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) {
throw new Error(localize('deleteFailedNonEmptyFolder', "Unable to delete non-empty folder '{0}'.", this.resourceForError(resource)));
@@ -1227,6 +1246,12 @@ export class FileService extends Disposable implements IFileService {
return provider;
}
private throwIfFileIsReadonly(resource: URI, stat: IStat): void {
if ((stat.permissions ?? 0) & FilePermission.Readonly) {
throw new FileOperationError(localize('err.readonly', "Unable to modify readonly file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED);
}
}
private resourceForError(resource: URI): string {
if (resource.scheme === Schemas.file) {
return resource.fsPath;

View File

@@ -11,7 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { Event } from 'vs/base/common/event';
import { startsWithIgnoreCase } from 'vs/base/common/strings';
import { IDisposable } from 'vs/base/common/lifecycle';
import { isNumber, isUndefinedOrNull } from 'vs/base/common/types';
import { isNumber } from 'vs/base/common/types';
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
import { ReadableStreamEvents } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
@@ -315,6 +315,14 @@ export enum FileType {
SymbolicLink = 64
}
export enum FilePermission {
/**
* File is readonly.
*/
Readonly = 1
}
export interface IStat {
/**
@@ -335,7 +343,12 @@ export interface IStat {
/**
* The size of the file in bytes.
*/
size: number;
readonly size: number;
/**
* The file permissions.
*/
readonly permissions?: FilePermission;
}
export interface IWatchOptions {
@@ -400,7 +413,7 @@ export interface IFileSystemProvider {
readonly capabilities: FileSystemProviderCapabilities;
readonly onDidChangeCapabilities: Event<void>;
readonly onDidErrorOccur?: Event<string>; // TODO@bpasero remove once file watchers are solid
readonly onDidErrorOccur?: Event<string>;
readonly onDidChangeFile: Event<readonly IFileChange[]>;
watch(resource: URI, opts: IWatchOptions): IDisposable;
@@ -475,7 +488,7 @@ export enum FileSystemProviderErrorCode {
export class FileSystemProviderError extends Error {
constructor(message: string, public readonly code: FileSystemProviderErrorCode) {
constructor(message: string, readonly code: FileSystemProviderErrorCode) {
super(message);
}
}
@@ -592,7 +605,7 @@ export class FileOperationEvent {
constructor(resource: URI, operation: FileOperation.DELETE);
constructor(resource: URI, operation: FileOperation.CREATE | FileOperation.MOVE | FileOperation.COPY, target: IFileStatWithMetadata);
constructor(public readonly resource: URI, public readonly operation: FileOperation, public readonly target?: IFileStatWithMetadata) { }
constructor(readonly resource: URI, readonly operation: FileOperation, readonly target?: IFileStatWithMetadata) { }
isOperation(operation: FileOperation.DELETE): boolean;
isOperation(operation: FileOperation.MOVE | FileOperation.COPY | FileOperation.CREATE): this is { readonly target: IFileStatWithMetadata };
@@ -762,16 +775,6 @@ export class FileChangesEvent {
return !!this.deleted;
}
/**
* @deprecated use the `contains()` method to efficiently find out if the event
* relates to a given resource. this method ensures:
* - that there is no expensive lookup needed by using a `TernarySearchTree`
* - correctly handles `FileChangeType.DELETED` events
*/
getUpdated(): IFileChange[] {
return this.getOfType(FileChangeType.UPDATED);
}
/**
* Returns if this event contains updated files.
*/
@@ -791,16 +794,6 @@ export class FileChangesEvent {
return changes;
}
/**
* @deprecated use the `contains()` method to efficiently find out if the event
* relates to a given resource. this method ensures:
* - that there is no expensive lookup needed by using a `TernarySearchTree`
* - correctly handles `FileChangeType.DELETED` events
*/
filter(filterFn: (change: IFileChange) => boolean): FileChangesEvent {
return new FileChangesEvent(this.changes.filter(change => filterFn(change)), this.ignorePathCasing);
}
}
export function isParent(path: string, candidate: string, ignoreCase?: boolean): boolean {
@@ -868,6 +861,11 @@ interface IBaseStat {
* it is optional.
*/
readonly etag?: string;
/**
* The file is read-only.
*/
readonly readonly?: boolean;
}
export interface IBaseStatWithMetadata extends Required<IBaseStat> { }
@@ -906,6 +904,7 @@ export interface IFileStatWithMetadata extends IFileStat, IBaseStatWithMetadata
readonly ctime: number;
readonly etag: string;
readonly size: number;
readonly readonly: boolean;
readonly children?: IFileStatWithMetadata[];
}
@@ -956,7 +955,7 @@ export interface IReadFileOptions extends IBaseReadFileOptions {
*
* 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
* a file event occurred 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.
*
@@ -1019,12 +1018,23 @@ export interface ICreateFileOptions {
}
export class FileOperationError extends Error {
constructor(message: string, public fileOperationResult: FileOperationResult, public options?: IReadFileOptions & IWriteFileOptions & ICreateFileOptions) {
constructor(
message: string,
readonly fileOperationResult: FileOperationResult,
readonly options?: IReadFileOptions & IWriteFileOptions & ICreateFileOptions
) {
super(message);
}
}
static isFileOperationError(obj: unknown): obj is FileOperationError {
return obj instanceof Error && !isUndefinedOrNull((obj as FileOperationError).fileOperationResult);
export class NotModifiedSinceFileOperationError extends FileOperationError {
constructor(
message: string,
readonly stat: IFileStatWithMetadata,
options?: IReadFileOptions
) {
super(message, FileOperationResult.FILE_NOT_MODIFIED_SINCE, options);
}
}

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';

View File

@@ -3,11 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { isWindows } from 'vs/base/common/platform';
import { basename } from 'vs/base/common/path';
import { DiskFileSystemProvider as NodeDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/node/diskFileSystemProvider';
import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { isWindows } from 'vs/base/common/platform';
import { localize } from 'vs/nls';
import { basename } from 'vs/base/common/path';
import { ILogService } from 'vs/platform/log/common/log';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
@@ -34,8 +34,11 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
return super.doDelete(filePath, opts);
}
const result = await this.nativeHostService.moveItemToTrash(filePath);
if (!result) {
try {
await this.nativeHostService.moveItemToTrash(filePath);
} catch (error) {
this.logService.error(error);
throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath)));
}
}

View File

@@ -3,14 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { open, close, read, write, fdatasync, Stats, promises } from 'fs';
import { promisify } from 'util';
import { Stats } from 'fs';
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, 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';
import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir, IDirent } from 'vs/base/node/pfs';
import { SymlinkSupport, RimRafMode, IDirent, Promises } from 'vs/base/node/pfs';
import { normalize, basename, dirname } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { isEqual } from 'vs/base/common/extpath';
@@ -47,7 +46,7 @@ export class DiskFileSystemProvider extends Disposable implements
private readonly BUFFER_SIZE = this.options?.bufferSize || 64 * 1024;
constructor(
private readonly logService: ILogService,
protected readonly logService: ILogService,
private readonly options?: IDiskFileSystemProviderOptions
) {
super();
@@ -96,7 +95,7 @@ export class DiskFileSystemProvider extends Disposable implements
async readdir(resource: URI): Promise<[string, FileType][]> {
try {
const children = await readdir(this.toFilePath(resource), { withFileTypes: true });
const children = await Promises.readdir(this.toFilePath(resource), { withFileTypes: true });
const result: [string, FileType][] = [];
await Promise.all(children.map(async child => {
@@ -152,7 +151,7 @@ export class DiskFileSystemProvider extends Disposable implements
try {
const filePath = this.toFilePath(resource);
return await promises.readFile(filePath);
return await Promises.readFile(filePath);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
@@ -176,7 +175,7 @@ export class DiskFileSystemProvider extends Disposable implements
// Validate target unless { create: true, overwrite: true }
if (!opts.create || !opts.overwrite) {
const fileExists = await exists(filePath);
const fileExists = await Promises.exists(filePath);
if (fileExists) {
if (!opts.overwrite) {
throw createFileSystemProviderError(localize('fileExists', "File already exists"), FileSystemProviderErrorCode.FileExists);
@@ -216,7 +215,7 @@ export class DiskFileSystemProvider extends Disposable implements
try {
const { stat } = await SymlinkSupport.stat(filePath);
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
await promises.chmod(filePath, stat.mode | 0o200);
await Promises.chmod(filePath, stat.mode | 0o200);
}
} catch (error) {
this.logService.trace(error); // ignore any errors here and try to just write
@@ -232,7 +231,7 @@ export class DiskFileSystemProvider extends Disposable implements
// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
// (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams
// (see https://github.com/microsoft/vscode/issues/6363)
await promises.truncate(filePath, 0);
await Promises.truncate(filePath, 0);
// After a successful truncate() the flag can be set to 'r+' which will not truncate.
flags = 'r+';
@@ -256,7 +255,7 @@ export class DiskFileSystemProvider extends Disposable implements
flags = 'r';
}
const handle = await promisify(open)(filePath, flags);
const handle = await Promises.open(filePath, flags);
// remember this handle to track file position of the handle
// we init the position to 0 since the file descriptor was
@@ -290,7 +289,7 @@ export class DiskFileSystemProvider extends Disposable implements
// to flush the contents to disk if possible.
if (this.writeHandles.delete(fd) && this.canFlush) {
try {
await promisify(fdatasync)(fd);
await Promises.fdatasync(fd);
} catch (error) {
// In some exotic setups it is well possible that node fails to sync
// In that case we disable flushing and log the error to our logger
@@ -299,7 +298,7 @@ export class DiskFileSystemProvider extends Disposable implements
}
}
return await promisify(close)(fd);
return await Promises.close(fd);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
@@ -310,7 +309,7 @@ export class DiskFileSystemProvider extends Disposable implements
let bytesRead: number | null = null;
try {
const result = await promisify(read)(fd, data, offset, length, normalizedPos);
const result = await Promises.read(fd, data, offset, length, normalizedPos);
if (typeof result === 'number') {
bytesRead = result; // node.d.ts fail
@@ -396,7 +395,7 @@ export class DiskFileSystemProvider extends Disposable implements
let bytesWritten: number | null = null;
try {
const result = await promisify(write)(fd, data, offset, length, normalizedPos);
const result = await Promises.write(fd, data, offset, length, normalizedPos);
if (typeof result === 'number') {
bytesWritten = result; // node.d.ts fail
@@ -418,7 +417,7 @@ export class DiskFileSystemProvider extends Disposable implements
async mkdir(resource: URI): Promise<void> {
try {
await promises.mkdir(this.toFilePath(resource));
await Promises.mkdir(this.toFilePath(resource));
} catch (error) {
throw this.toFileSystemProviderError(error);
}
@@ -436,9 +435,9 @@ export class DiskFileSystemProvider extends Disposable implements
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
if (opts.recursive) {
await rimraf(filePath, RimRafMode.MOVE);
await Promises.rm(filePath, RimRafMode.MOVE);
} else {
await promises.unlink(filePath);
await Promises.unlink(filePath);
}
}
@@ -456,7 +455,7 @@ export class DiskFileSystemProvider extends Disposable implements
await this.validateTargetDeleted(from, to, 'move', opts.overwrite);
// Move
await move(fromFilePath, toFilePath);
await Promises.move(fromFilePath, toFilePath);
} catch (error) {
// rewrite some typical errors that can happen especially around symlinks
@@ -483,7 +482,7 @@ export class DiskFileSystemProvider extends Disposable implements
await this.validateTargetDeleted(from, to, 'copy', opts.overwrite);
// Copy
await copy(fromFilePath, toFilePath, { preserveSymlinks: true });
await Promises.copy(fromFilePath, toFilePath, { preserveSymlinks: true });
} catch (error) {
// rewrite some typical errors that can happen especially around symlinks
@@ -511,7 +510,7 @@ export class DiskFileSystemProvider extends Disposable implements
}
// handle existing target (unless this is a case change)
if (!isSameResourceWithDifferentPathCase && await exists(toFilePath)) {
if (!isSameResourceWithDifferentPathCase && await Promises.exists(toFilePath)) {
if (!overwrite) {
throw createFileSystemProviderError(localize('fileCopyErrorExists', "File at target already exists"), FileSystemProviderErrorCode.FileExists);
}
@@ -542,7 +541,7 @@ export class DiskFileSystemProvider extends Disposable implements
return this.watchRecursive(resource, opts.excludes);
}
return this.watchNonRecursive(resource); // TODO@bpasero ideally the same watcher can be used in both cases
return this.watchNonRecursive(resource);
}
private watchRecursive(resource: URI, excludes: string[]): IDisposable {

View File

@@ -14,11 +14,9 @@ import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, I
import { assertIsDefined } from 'vs/base/common/types';
import { basename, joinPath } from 'vs/base/common/resources';
import { bufferToReadable, bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
import { flakySuite } from 'vs/base/test/common/testUtils';
suite('IndexedDB File Service', function () {
// IDB sometimes under pressure in build machines.
this.retries(3);
flakySuite('IndexedDB File Service', function () {
const logSchema = 'logs';

View File

@@ -60,7 +60,6 @@ suite('Files', () => {
assert.strictEqual(6, event.changes.length);
assert.strictEqual(1, event.getAdded().length);
assert.strictEqual(true, event.gotAdded());
assert.strictEqual(2, event.getUpdated().length);
assert.strictEqual(true, event.gotUpdated());
assert.strictEqual(ignorePathCasing ? 2 : 3, event.getDeleted().length);
assert.strictEqual(true, event.gotDeleted());
@@ -102,9 +101,6 @@ suite('Files', () => {
case FileChangeType.ADDED:
assert.strictEqual(8, event.getAdded().length);
break;
case FileChangeType.UPDATED:
assert.strictEqual(8, event.getUpdated().length);
break;
case FileChangeType.DELETED:
assert.strictEqual(8, event.getDeleted().length);
break;

View File

@@ -10,10 +10,10 @@ import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { getRandomTestPath, getPathFromAmdModule } from 'vs/base/test/node/testUtils';
import { join, basename, dirname, posix } from 'vs/base/common/path';
import { copy, rimraf, rimrafSync } from 'vs/base/node/pfs';
import { Promises, 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, IReadFileOptions } from 'vs/platform/files/common/files';
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream } from 'fs';
import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata, IReadFileOptions, FilePermission, NotModifiedSinceFileOperationError } 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';
@@ -56,6 +56,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
private invalidStatSize: boolean = false;
private smallStatSize: boolean = false;
private readonly: boolean = false;
private _testCapabilities!: FileSystemProviderCapabilities;
override get capabilities(): FileSystemProviderCapabilities {
@@ -88,13 +89,19 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
this.smallStatSize = enabled;
}
setReadonly(readonly: boolean): void {
this.readonly = readonly;
}
override async stat(resource: URI): Promise<IStat> {
const res = await super.stat(resource);
if (this.invalidStatSize) {
res.size = String(res.size) as any; // for https://github.com/microsoft/vscode/issues/72909
(res as any).size = String(res.size) as any; // for https://github.com/microsoft/vscode/issues/72909
} else if (this.smallStatSize) {
res.size = 1;
(res as any).size = 1;
} else if (this.readonly) {
(res as any).permissions = FilePermission.Readonly;
}
return res;
@@ -147,13 +154,13 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
const sourceDir = getPathFromAmdModule(require, './fixtures/service');
await copy(sourceDir, testDir, { preserveSymlinks: false });
await Promises.copy(sourceDir, testDir, { preserveSymlinks: false });
});
teardown(() => {
disposables.clear();
return rimraf(testDir);
return Promises.rm(testDir);
});
test('createFolder', async () => {
@@ -213,6 +220,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
assert.strictEqual(resolved.name, 'index.html');
assert.strictEqual(resolved.isFile, true);
assert.strictEqual(resolved.isDirectory, false);
assert.strictEqual(resolved.readonly, false);
assert.strictEqual(resolved.isSymbolicLink, false);
assert.strictEqual(resolved.resource.toString(), resource.toString());
assert.strictEqual(resolved.children, undefined);
@@ -233,6 +241,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result!.isDirectory);
assert.strictEqual(result.readonly, false);
assert.ok(result.mtime! > 0);
assert.ok(result.ctime! > 0);
assert.strictEqual(result.children!.length, testsElements.length);
@@ -408,7 +417,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
test('resolve - folder symbolic link', async () => {
const link = URI.file(join(testDir, 'deep-link'));
await promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction');
await Promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction');
const resolved = await service.resolve(link);
assert.strictEqual(resolved.children!.length, 4);
@@ -418,7 +427,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => {
const link = URI.file(join(testDir, 'lorem.txt-linked'));
await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath);
await Promises.symlink(join(testDir, 'lorem.txt'), link.fsPath);
const resolved = await service.resolve(link);
assert.strictEqual(resolved.isDirectory, false);
@@ -426,7 +435,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
});
test('resolve - symbolic link pointing to non-existing file does not break', async () => {
await promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction');
await Promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction');
const resolved = await service.resolve(URI.file(testDir));
assert.strictEqual(resolved.isDirectory, true);
@@ -477,7 +486,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => {
const target = URI.file(join(testDir, 'lorem.txt'));
const link = URI.file(join(testDir, 'lorem.txt-linked'));
await promises.symlink(target.fsPath, link.fsPath);
await Promises.symlink(target.fsPath, link.fsPath);
const source = await service.resolve(link);
@@ -499,7 +508,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => {
const target = URI.file(join(testDir, 'foo'));
const link = URI.file(join(testDir, 'bar'));
await promises.symlink(target.fsPath, link.fsPath);
await Promises.symlink(target.fsPath, link.fsPath);
let event: FileOperationEvent;
disposables.add(service.onDidRunOperation(e => event = e));
@@ -1482,6 +1491,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
assert.ok(error);
assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE);
assert.ok(error instanceof NotModifiedSinceFileOperationError && error.stat);
assert.strictEqual(fileProvider.totalBytesRead, 0);
}
@@ -1592,7 +1602,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
(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);
await Promises.symlink(join(testDir, 'small.js'), link.fsPath);
let error: FileOperationError | undefined = undefined;
try {
@@ -1928,8 +1938,8 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
await service.writeFile(lockedFile, VSBuffer.fromString('Locked File'));
const stats = await promises.stat(lockedFile.fsPath);
await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200);
const stats = await Promises.stat(lockedFile.fsPath);
await Promises.chmod(lockedFile.fsPath, stats.mode & ~0o200);
let error;
const newContent = 'Updates to locked file';
@@ -2091,7 +2101,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
(runWatchTests && !isWindows /* windows: cannot create file symbolic link without elevated context */ ? test : test.skip)('watch - file symbolic link', async () => {
const toWatch = URI.file(join(testDir, 'lorem.txt-linked'));
await promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath);
await Promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath);
const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]);
setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50);
@@ -2219,7 +2229,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
(runWatchTests ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async () => {
const watchDir = URI.file(join(testDir, 'deep-link'));
await promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction');
await Promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction');
const file = URI.file(join(watchDir.fsPath, 'index.html'));
writeFileSync(file.fsPath, 'Init');
@@ -2390,4 +2400,32 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
await fileProvider.close(fdWrite);
await fileProvider.close(fdRead);
});
test('readonly - is handled properly for a single resource', async () => {
fileProvider.setReadonly(true);
const resource = URI.file(join(testDir, 'index.html'));
const resolveResult = await service.resolve(resource);
assert.strictEqual(resolveResult.readonly, true);
const readResult = await service.readFile(resource);
assert.strictEqual(readResult.readonly, true);
let writeFileError: Error | undefined = undefined;
try {
await service.writeFile(resource, VSBuffer.fromString('Hello Test'));
} catch (error) {
writeFileError = error;
}
assert.ok(writeFileError);
let deleteFileError: Error | undefined = undefined;
try {
await service.del(resource);
} catch (error) {
deleteFileError = error;
}
assert.ok(deleteFileError);
});
});

View File

@@ -25,12 +25,12 @@ h1, h2, h3, h4, h5, h6
margin: 0px;
}
textarea
textarea
{
font-family: Consolas
}
#results
#results
{
margin-top: 2em;
margin-left: 2em;

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* 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';

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';

View File

@@ -58,6 +58,7 @@ export interface IssueReporterData extends WindowData {
issueType?: IssueType;
extensionId?: string;
experiments?: string;
restrictedMode: boolean;
githubAccessToken: string;
readonly issueTitle?: string;
readonly issueBody?: string;
@@ -70,8 +71,14 @@ export interface ISettingSearchResult {
}
export interface ProcessExplorerStyles extends WindowStyles {
hoverBackground?: string;
hoverForeground?: string;
listHoverBackground?: string;
listHoverForeground?: string;
listFocusBackground?: string;
listFocusForeground?: string;
listFocusOutline?: string;
listActiveSelectionBackground?: string;
listActiveSelectionForeground?: string;
listHoverOutline?: string;
}
export interface ProcessExplorerData extends WindowData {

View File

@@ -25,6 +25,13 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
export const IIssueMainService = createDecorator<IIssueMainService>('issueMainService');
interface IBrowserWindowOptions {
backgroundColor: string | undefined;
title: string;
zoomLevel: number;
alwaysOnTop: boolean;
}
export interface IIssueMainService extends ICommonIssueService { }
export class IssueMainService implements ICommonIssueService {
@@ -189,7 +196,12 @@ export class IssueMainService implements ICommonIssueService {
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);
this.issueReporterWindow = this.createBrowserWindow(position, issueReporterWindowConfigUrl, {
backgroundColor: data.styles.backgroundColor,
title: localize('issueReporter', "Issue Reporter"),
zoomLevel: data.zoomLevel,
alwaysOnTop: false
});
// Store into config object URL
issueReporterWindowConfigUrl.update({
@@ -239,7 +251,12 @@ export class IssueMainService implements ICommonIssueService {
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);
this.processExplorerWindow = this.createBrowserWindow(position, processExplorerWindowConfigUrl, {
backgroundColor: data.styles.backgroundColor,
title: localize('processExplorer', "Process Explorer"),
zoomLevel: data.zoomLevel,
alwaysOnTop: true
});
// Store into config object URL
processExplorerWindowConfigUrl.update({
@@ -273,7 +290,7 @@ export class IssueMainService implements ICommonIssueService {
this.processExplorerWindow?.focus();
}
private createBrowserWindow<T>(position: IWindowState, ipcObjectUrl: IIPCObjectUrl<T>, backgroundColor: string | undefined, title: string, zoomLevel: number): BrowserWindow {
private createBrowserWindow<T>(position: IWindowState, ipcObjectUrl: IIPCObjectUrl<T>, options: IBrowserWindowOptions): BrowserWindow {
const window = new BrowserWindow({
fullscreen: false,
skipTaskbar: true,
@@ -284,20 +301,20 @@ export class IssueMainService implements ICommonIssueService {
minHeight: 200,
x: position.x,
y: position.y,
title,
backgroundColor: backgroundColor || IssueMainService.DEFAULT_BACKGROUND_COLOR,
title: options.title,
backgroundColor: options.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),
zoomFactor: zoomLevelToZoomFactor(options.zoomLevel),
sandbox: true,
contextIsolation: true
}
contextIsolation: true,
},
alwaysOnTop: options.alwaysOnTop
});
window.setMenuBarVisibility(false);

View File

@@ -77,4 +77,4 @@ class JSONContributionRegistry implements IJSONContributionRegistry {
}
const jsonContributionRegistry = new JSONContributionRegistry();
platform.Registry.add(Extensions.JSONContribution, jsonContributionRegistry);
platform.Registry.add(Extensions.JSONContribution, jsonContributionRegistry);

View File

@@ -24,6 +24,9 @@ interface CurrentChord {
label: string | null;
}
// Skip logging for high-frequency text editing commands
const HIGH_FREQ_COMMANDS = /^(cursor|delete)/;
export abstract class AbstractKeybindingService extends Disposable implements IKeybindingService {
public _serviceBrand: undefined;
@@ -107,8 +110,8 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
);
}
public lookupKeybinding(commandId: string): ResolvedKeybinding | undefined {
const result = this._getResolver().lookupPrimaryKeybinding(commandId);
public lookupKeybinding(commandId: string, context?: IContextKeyService): ResolvedKeybinding | undefined {
const result = this._getResolver().lookupPrimaryKeybinding(commandId, context);
if (!result) {
return undefined;
}
@@ -263,7 +266,9 @@ export abstract class AbstractKeybindingService extends Disposable implements IK
} else {
this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs).then(undefined, err => this._notificationService.warn(err));
}
this._telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: resolveResult.commandId, from: 'keybinding' });
if (!HIGH_FREQ_COMMANDS.test(resolveResult.commandId)) {
this._telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: resolveResult.commandId, from: 'keybinding' });
}
}
return shouldPreventDefault;

View File

@@ -6,7 +6,7 @@
import { Event } from 'vs/base/common/event';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Keybinding, KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
@@ -85,7 +85,7 @@ export interface IKeybindingService {
* Look up the preferred (last defined) keybinding for a command.
* @returns The preferred keybinding or null if the command is not bound.
*/
lookupKeybinding(commandId: string): ResolvedKeybinding | undefined;
lookupKeybinding(commandId: string, context?: IContextKeyService): ResolvedKeybinding | undefined;
getDefaultKeybindingsContent(): string;

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IContext, ContextKeyExpression, ContextKeyExprType } from 'vs/platform/contextkey/common/contextkey';
import { ContextKeyExpression, ContextKeyExprType, IContext, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
export interface IResolveResult {
@@ -183,10 +183,10 @@ export class KeybindingResolver {
* Returns true if it is provable `a` implies `b`.
*/
public static whenIsEntirelyIncluded(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean {
if (!b) {
if (!b || b.type === ContextKeyExprType.True) {
return true;
}
if (!a) {
if (!a || a.type === ContextKeyExprType.True) {
return false;
}
@@ -247,13 +247,15 @@ export class KeybindingResolver {
return result;
}
public lookupPrimaryKeybinding(commandId: string): ResolvedKeybindingItem | null {
public lookupPrimaryKeybinding(commandId: string, context?: IContextKeyService): ResolvedKeybindingItem | null {
let items = this._lookupMap.get(commandId);
if (typeof items === 'undefined' || items.length === 0) {
return null;
}
return items[items.length - 1];
const itemMatchingContext = context &&
Array.from(items).reverse().find(item => context.contextMatchesRules(item.when));
return itemMatchingContext ?? items[items.length - 1];
}
public resolve(context: IContext, currentChord: string | null, keypress: string): IResolveResult | null {

View File

@@ -193,16 +193,27 @@ suite('KeybindingResolver', () => {
});
test('contextIsEntirelyIncluded', () => {
const assertIsIncluded = (a: string | null, b: string | null) => {
assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true);
const toContextKeyExpression = (expr: ContextKeyExpression | string | null) => {
if (typeof expr === 'string' || !expr) {
return ContextKeyExpr.deserialize(expr as string); // {{SQL CARBON EDIT}} Cast to string
}
return expr;
};
const assertIsNotIncluded = (a: string | null, b: string | null) => {
assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false);
const assertIsIncluded = (a: ContextKeyExpression | string | null, b: ContextKeyExpression | string | null) => {
assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(toContextKeyExpression(a), toContextKeyExpression(b)), true);
};
const assertIsNotIncluded = (a: ContextKeyExpression | string | null, b: ContextKeyExpression | string | null) => {
assert.strictEqual(KeybindingResolver.whenIsEntirelyIncluded(toContextKeyExpression(a), toContextKeyExpression(b)), false);
};
assertIsIncluded(null, null);
assertIsIncluded(null, ContextKeyExpr.true());
assertIsIncluded(ContextKeyExpr.true(), null);
assertIsIncluded(ContextKeyExpr.true(), ContextKeyExpr.true());
assertIsIncluded('key1', null);
assertIsIncluded('key1', '');
assertIsIncluded('key1', 'key1');
assertIsIncluded('key1', ContextKeyExpr.true());
assertIsIncluded('!key1', '');
assertIsIncluded('!key1', '!key1');
assertIsIncluded('key2', '');

View File

@@ -14,4 +14,4 @@ export function getDispatchConfig(configurationService: IConfigurationService):
const keyboard = configurationService.getValue('keyboard');
const r = (keyboard ? (<any>keyboard).dispatch : null);
return (r === 'keyCode' ? DispatchConfig.KeyCode : DispatchConfig.Code);
}
}

View File

@@ -5,7 +5,7 @@
import { ipcMain, app, BrowserWindow } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
import { IStateService } from 'vs/platform/state/node/state';
import { IStateMainService } from 'vs/platform/state/electron-main/state';
import { Event, Emitter } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ICodeWindow } from 'vs/platform/windows/electron-main/windows';
@@ -115,12 +115,12 @@ export interface ILifecycleMainService {
/**
* Restart the application with optional arguments (CLI). All lifecycle event handlers are triggered.
*/
relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void;
relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise<void>;
/**
* Shutdown the application normally. All lifecycle event handlers are triggered.
*/
quit(fromUpdate?: boolean): Promise<boolean /* veto */>;
quit(willRestart?: boolean): Promise<boolean /* veto */>;
/**
* Forcefully shutdown the application. No livecycle event handlers are triggered.
@@ -158,7 +158,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
declare readonly _serviceBrand: undefined;
private static readonly QUIT_FROM_RESTART_MARKER = 'quit.from.restart'; // use a marker to find out if the session was restarted
private static readonly QUIT_AND_RESTART_KEY = 'lifecycle.quitAndRestart';
private readonly _onBeforeShutdown = this._register(new Emitter<void>());
readonly onBeforeShutdown = this._onBeforeShutdown.event;
@@ -188,28 +188,29 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
private oneTimeListenerTokenGenerator = 0;
private windowCounter = 0;
private pendingQuitPromise: Promise<boolean> | null = null;
private pendingQuitPromiseResolve: { (veto: boolean): void } | null = null;
private pendingQuitPromise: Promise<boolean> | undefined = undefined;
private pendingQuitPromiseResolve: { (veto: boolean): void } | undefined = undefined;
private pendingWillShutdownPromise: Promise<void> | null = null;
private pendingWillShutdownPromise: Promise<void> | undefined = undefined;
private readonly phaseWhen = new Map<LifecycleMainPhase, Barrier>();
constructor(
@ILogService private readonly logService: ILogService,
@IStateService private readonly stateService: IStateService
@IStateMainService private readonly stateMainService: IStateMainService
) {
super();
this.handleRestarted();
this.resolveRestarted();
this.when(LifecycleMainPhase.Ready).then(() => this.registerListeners());
}
private handleRestarted(): void {
this._wasRestarted = !!this.stateService.getItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER);
private resolveRestarted(): void {
this._wasRestarted = !!this.stateMainService.getItem(LifecycleMainService.QUIT_AND_RESTART_KEY);
if (this._wasRestarted) {
this.stateService.removeItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER); // remove the marker right after if found
// remove the marker right after if found
this.stateMainService.removeItem(LifecycleMainService.QUIT_AND_RESTART_KEY);
}
}
@@ -294,7 +295,23 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
}
});
this.pendingWillShutdownPromise = Promises.settled(joiners).then(() => undefined, err => this.logService.error(err));
this.pendingWillShutdownPromise = (async () => {
// Settle all shutdown event joiners
try {
await Promises.settled(joiners);
} catch (error) {
this.logService.error(error);
}
// Then, always make sure at the end
// the state service is flushed.
try {
await this.stateMainService.close();
} catch (error) {
this.logService.error(error);
}
})();
return this.pendingWillShutdownPromise;
}
@@ -454,8 +471,8 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
private resolvePendingQuitPromise(veto: boolean): void {
if (this.pendingQuitPromiseResolve) {
this.pendingQuitPromiseResolve(veto);
this.pendingQuitPromiseResolve = null;
this.pendingQuitPromise = null;
this.pendingQuitPromiseResolve = undefined;
this.pendingQuitPromise = undefined;
}
}
@@ -502,16 +519,16 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
});
}
quit(fromUpdate?: boolean): Promise<boolean /* veto */> {
quit(willRestart?: boolean): Promise<boolean /* veto */> {
if (this.pendingQuitPromise) {
return this.pendingQuitPromise;
}
this.logService.trace(`Lifecycle#quit() - from update: ${fromUpdate}`);
this.logService.trace(`Lifecycle#quit() - will restart: ${willRestart}`);
// Remember the reason for quit was to restart
if (fromUpdate) {
this.stateService.setItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER, true);
// Remember if we are about to restart
if (willRestart) {
this.stateMainService.setItem(LifecycleMainService.QUIT_AND_RESTART_KEY, true);
}
this.pendingQuitPromise = new Promise(resolve => {
@@ -528,7 +545,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
return this.pendingQuitPromise;
}
relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void {
async relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise<void> {
this.logService.trace('Lifecycle#relaunch()');
const args = process.argv.slice(1);
@@ -545,37 +562,34 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
}
}
let quitVetoed = false;
app.once('quit', () => {
if (!quitVetoed) {
// Remember the reason for quit was to restart
this.stateService.setItem(LifecycleMainService.QUIT_FROM_RESTART_MARKER, true);
// Windows: we are about to restart and as such we need to restore the original
// current working directory we had on startup to get the exact same startup
// behaviour. As such, we briefly change back to that directory and then when
// Code starts it will set it back to the installation directory again.
try {
if (isWindows) {
const currentWorkingDir = cwd();
if (currentWorkingDir !== process.cwd()) {
process.chdir(currentWorkingDir);
}
const quitListener = () => {
// Windows: we are about to restart and as such we need to restore the original
// current working directory we had on startup to get the exact same startup
// behaviour. As such, we briefly change back to that directory and then when
// Code starts it will set it back to the installation directory again.
try {
if (isWindows) {
const currentWorkingDir = cwd();
if (currentWorkingDir !== process.cwd()) {
process.chdir(currentWorkingDir);
}
} catch (err) {
this.logService.error(err);
}
// relaunch after we are sure there is no veto
this.logService.trace('Lifecycle#relaunch() - calling app.relaunch()');
app.relaunch({ args });
} catch (err) {
this.logService.error(err);
}
});
// relaunch after we are sure there is no veto
this.logService.trace('Lifecycle#relaunch() - calling app.relaunch()');
app.relaunch({ args });
};
app.once('quit', quitListener);
// app.relaunch() does not quit automatically, so we quit first,
// check for vetoes and then relaunch from the app.on('quit') event
this.quit().then(veto => quitVetoed = veto);
const veto = await this.quit(true /* will restart */);
if (veto) {
app.removeListener('quit', quitListener);
}
}
async kill(code?: number): Promise<void> {

View File

@@ -183,7 +183,7 @@ function toWorkbenchListOptions<T>(options: IListOptions<T>, configurationServic
}
};
result.smoothScrolling = configurationService.getValue<boolean>(listSmoothScrolling);
result.smoothScrolling = Boolean(configurationService.getValue<boolean>(listSmoothScrolling));
return [result, disposables];
}
@@ -221,7 +221,7 @@ export class WorkbenchList<T> extends List<T> {
@IConfigurationService configurationService: IConfigurationService,
@IKeybindingService keybindingService: IKeybindingService
) {
const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue<boolean>(horizontalScrollingKey);
const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue<boolean>(horizontalScrollingKey));
const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService);
super(user, container, delegate, renderers,
@@ -282,11 +282,11 @@ export class WorkbenchList<T> extends List<T> {
let options: IListOptionsUpdate = {};
if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) {
const horizontalScrolling = configurationService.getValue<boolean>(horizontalScrollingKey);
const horizontalScrolling = Boolean(configurationService.getValue<boolean>(horizontalScrollingKey));
options = { ...options, horizontalScrolling };
}
if (e.affectsConfiguration(listSmoothScrolling)) {
const smoothScrolling = configurationService.getValue<boolean>(listSmoothScrolling);
const smoothScrolling = Boolean(configurationService.getValue<boolean>(listSmoothScrolling));
options = { ...options, smoothScrolling };
}
if (Object.keys(options).length > 0) {
@@ -348,7 +348,7 @@ export class WorkbenchPagedList<T> extends PagedList<T> {
@IConfigurationService configurationService: IConfigurationService,
@IKeybindingService keybindingService: IKeybindingService
) {
const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue<boolean>(horizontalScrollingKey);
const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue<boolean>(horizontalScrollingKey));
const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService);
super(user, container, delegate, renderers,
{
@@ -394,11 +394,11 @@ export class WorkbenchPagedList<T> extends PagedList<T> {
let options: IListOptionsUpdate = {};
if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) {
const horizontalScrolling = configurationService.getValue<boolean>(horizontalScrollingKey);
const horizontalScrolling = Boolean(configurationService.getValue<boolean>(horizontalScrollingKey));
options = { ...options, horizontalScrolling };
}
if (e.affectsConfiguration(listSmoothScrolling)) {
const smoothScrolling = configurationService.getValue<boolean>(listSmoothScrolling);
const smoothScrolling = Boolean(configurationService.getValue<boolean>(listSmoothScrolling));
options = { ...options, smoothScrolling };
}
if (Object.keys(options).length > 0) {
@@ -469,7 +469,7 @@ export class WorkbenchTable<TRow> extends Table<TRow> {
@IConfigurationService configurationService: IConfigurationService,
@IKeybindingService keybindingService: IKeybindingService
) {
const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : configurationService.getValue<boolean>(horizontalScrollingKey);
const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue<boolean>(horizontalScrollingKey));
const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService);
super(user, container, delegate, columns, renderers,
@@ -531,11 +531,11 @@ export class WorkbenchTable<TRow> extends Table<TRow> {
let options: IListOptionsUpdate = {};
if (e.affectsConfiguration(horizontalScrollingKey) && this.horizontalScrolling === undefined) {
const horizontalScrolling = configurationService.getValue<boolean>(horizontalScrollingKey);
const horizontalScrolling = Boolean(configurationService.getValue<boolean>(horizontalScrollingKey));
options = { ...options, horizontalScrolling };
}
if (e.affectsConfiguration(listSmoothScrolling)) {
const smoothScrolling = configurationService.getValue<boolean>(listSmoothScrolling);
const smoothScrolling = Boolean(configurationService.getValue<boolean>(listSmoothScrolling));
options = { ...options, smoothScrolling };
}
if (Object.keys(options).length > 0) {
@@ -999,10 +999,10 @@ function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTre
const getAutomaticKeyboardNavigation = () => {
// give priority to the context key value to disable this completely
let automaticKeyboardNavigation = contextKeyService.getContextKeyValue<boolean>(WorkbenchListAutomaticKeyboardNavigationKey);
let automaticKeyboardNavigation = Boolean(contextKeyService.getContextKeyValue<boolean>(WorkbenchListAutomaticKeyboardNavigationKey));
if (automaticKeyboardNavigation) {
automaticKeyboardNavigation = configurationService.getValue<boolean>(automaticKeyboardNavigationSettingKey);
automaticKeyboardNavigation = Boolean(configurationService.getValue<boolean>(automaticKeyboardNavigationSettingKey));
}
return automaticKeyboardNavigation;
@@ -1010,7 +1010,7 @@ function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTre
const accessibilityOn = accessibilityService.isScreenReaderOptimized();
const keyboardNavigation = options.simpleKeyboardNavigation || accessibilityOn ? 'simple' : configurationService.getValue<string>(keyboardNavigationSettingKey);
const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : configurationService.getValue<boolean>(horizontalScrollingKey);
const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : Boolean(configurationService.getValue<boolean>(horizontalScrollingKey));
const [workbenchListOptions, disposable] = toWorkbenchListOptions(options, configurationService, keybindingService);
const additionalScrollHeight = options.additionalScrollHeight;
@@ -1023,7 +1023,7 @@ function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTre
...workbenchListOptions,
indent: configurationService.getValue<number>(treeIndentKey),
renderIndentGuides: configurationService.getValue<RenderIndentGuides>(treeRenderIndentGuidesKey),
smoothScrolling: configurationService.getValue<boolean>(listSmoothScrolling),
smoothScrolling: Boolean(configurationService.getValue<boolean>(listSmoothScrolling)),
automaticKeyboardNavigation: getAutomaticKeyboardNavigation(),
simpleKeyboardNavigation: keyboardNavigation === 'simple',
filterOnType: keyboardNavigation === 'filter',
@@ -1120,7 +1120,7 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
newOptions = { ...newOptions, renderIndentGuides };
}
if (e.affectsConfiguration(listSmoothScrolling)) {
const smoothScrolling = configurationService.getValue<boolean>(listSmoothScrolling);
const smoothScrolling = Boolean(configurationService.getValue<boolean>(listSmoothScrolling));
newOptions = { ...newOptions, smoothScrolling };
}
if (e.affectsConfiguration(keyboardNavigationSettingKey)) {
@@ -1130,7 +1130,7 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
newOptions = { ...newOptions, automaticKeyboardNavigation: getAutomaticKeyboardNavigation() };
}
if (e.affectsConfiguration(horizontalScrollingKey) && options.horizontalScrolling === undefined) {
const horizontalScrolling = configurationService.getValue<boolean>(horizontalScrollingKey);
const horizontalScrolling = Boolean(configurationService.getValue<boolean>(horizontalScrollingKey));
newOptions = { ...newOptions, horizontalScrolling };
}
if (e.affectsConfiguration(treeExpandMode) && options.expandOnlyOnTwistieClick === undefined) {
@@ -1171,20 +1171,20 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': 'workbench',
'order': 7,
'title': localize('workbenchConfigurationTitle', "Workbench"),
'type': 'object',
'properties': {
id: 'workbench',
order: 7,
title: localize('workbenchConfigurationTitle', "Workbench"),
type: 'object',
properties: {
[multiSelectModifierSettingKey]: {
'type': 'string',
'enum': ['ctrlCmd', 'alt'],
'enumDescriptions': [
type: 'string',
enum: ['ctrlCmd', 'alt'],
enumDescriptions: [
localize('multiSelectModifier.ctrlCmd', "Maps to `Control` on Windows and Linux and to `Command` on macOS."),
localize('multiSelectModifier.alt', "Maps to `Alt` on Windows and Linux and to `Option` on macOS.")
],
'default': 'ctrlCmd',
'description': localize({
default: 'ctrlCmd',
description: localize({
key: 'multiSelectModifier',
comment: [
'- `ctrlCmd` refers to a value the setting can take and should not be localized.',
@@ -1193,25 +1193,25 @@ configurationRegistry.registerConfiguration({
}, "The modifier to be used to add an item in trees and lists to a multi-selection with the mouse (for example in the explorer, open editors and scm view). The 'Open to Side' mouse gestures - if supported - will adapt such that they do not conflict with the multiselect modifier.")
},
[openModeSettingKey]: {
'type': 'string',
'enum': ['singleClick', 'doubleClick'],
'default': 'singleClick',
'description': localize({
type: 'string',
enum: ['singleClick', 'doubleClick'],
default: 'singleClick',
description: localize({
key: 'openModeModifier',
comment: ['`singleClick` and `doubleClick` refers to a value the setting can take and should not be localized.']
}, "Controls how to open items in trees and lists using the mouse (if supported). Note that some trees and lists might choose to ignore this setting if it is not applicable.")
},
[horizontalScrollingKey]: {
'type': 'boolean',
'default': false,
'description': localize('horizontalScrolling setting', "Controls whether lists and trees support horizontal scrolling in the workbench. Warning: turning on this setting has a performance implication.")
type: 'boolean',
default: false,
description: localize('horizontalScrolling setting', "Controls whether lists and trees support horizontal scrolling in the workbench. Warning: turning on this setting has a performance implication.")
},
[treeIndentKey]: {
'type': 'number',
'default': 8,
type: 'number',
default: 8,
minimum: 0,
maximum: 40,
'description': localize('tree indent setting', "Controls tree indentation in pixels.")
description: localize('tree indent setting', "Controls tree indentation in pixels.")
},
[treeRenderIndentGuidesKey]: {
type: 'string',
@@ -1225,19 +1225,19 @@ configurationRegistry.registerConfiguration({
description: localize('list smoothScrolling setting', "Controls whether lists and trees have smooth scrolling."),
},
[keyboardNavigationSettingKey]: {
'type': 'string',
'enum': ['simple', 'highlight', 'filter'],
'enumDescriptions': [
type: 'string',
enum: ['simple', 'highlight', 'filter'],
enumDescriptions: [
localize('keyboardNavigationSettingKey.simple', "Simple keyboard navigation focuses elements which match the keyboard input. Matching is done only on prefixes."),
localize('keyboardNavigationSettingKey.highlight', "Highlight keyboard navigation highlights elements which match the keyboard input. Further up and down navigation will traverse only the highlighted elements."),
localize('keyboardNavigationSettingKey.filter', "Filter keyboard navigation will filter out and hide all the elements which do not match the keyboard input.")
],
'default': 'highlight',
'description': localize('keyboardNavigationSettingKey', "Controls the keyboard navigation style for lists and trees in the workbench. Can be simple, highlight and filter.")
default: 'highlight',
description: localize('keyboardNavigationSettingKey', "Controls the keyboard navigation style for lists and trees in the workbench. Can be simple, highlight and filter.")
},
[automaticKeyboardNavigationSettingKey]: {
'type': 'boolean',
'default': true,
type: 'boolean',
default: true,
markdownDescription: localize('automatic keyboard navigation setting', "Controls whether keyboard navigation in lists and trees is automatically triggered simply by typing. If set to `false`, keyboard navigation is only triggered when executing the `list.toggleKeyboardNavigation` command, for which you can assign a keyboard shortcut.")
},
[treeExpandMode]: {

View File

@@ -3,8 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { writeFile } from 'vs/base/node/pfs';
import { promises } from 'fs';
import { Promises } from 'vs/base/node/pfs';
import { createHash } from 'crypto';
import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Disposable } from 'vs/base/common/lifecycle';
@@ -158,7 +157,7 @@ class LanguagePacksCache extends Disposable {
private withLanguagePacks<T>(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise<T> {
return this.languagePacksFileLimiter.queue(() => {
let result: T | null = null;
return promises.readFile(this.languagePacksFilePath, 'utf8')
return Promises.readFile(this.languagePacksFilePath, 'utf8')
.then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err))
.then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } })
.then(languagePacks => { result = fn(languagePacks); return languagePacks; })
@@ -172,7 +171,7 @@ class LanguagePacksCache extends Disposable {
this.initializedCache = true;
const raw = JSON.stringify(this.languagePacks);
this.logService.debug('Writing language packs', raw);
return writeFile(this.languagePacksFilePath, raw);
return Promises.writeFile(this.languagePacksFilePath, raw);
})
.then(() => result, error => this.logService.error(error));
});

View File

@@ -7,20 +7,21 @@ import { LogLevel, ILogger, AbstractMessageLogger } from 'vs/platform/log/common
import * as spdlog from 'spdlog';
import { ByteSize } from 'vs/platform/files/common/files';
async function createSpdLogLogger(name: string, logfilePath: string, filesize: number, filecount: number): Promise<spdlog.RotatingLogger | null> {
async function createSpdLogLogger(name: string, logfilePath: string, filesize: number, filecount: number): Promise<spdlog.Logger | null> {
// Do not crash if spdlog cannot be loaded
try {
const _spdlog = await import('spdlog');
_spdlog.setAsyncMode(8192, 500);
return _spdlog.createRotatingLoggerAsync(name, logfilePath, filesize, filecount);
_spdlog.setFlushOn(LogLevel.Trace);
return _spdlog.createAsyncRotatingLogger(name, logfilePath, filesize, filecount);
} catch (e) {
console.error(e);
}
return null;
}
export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): spdlog.RotatingLogger {
export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): Promise<spdlog.Logger> {
const _spdlog: typeof spdlog = require.__$__nodeRequire('spdlog');
_spdlog.setFlushOn(LogLevel.Trace);
return _spdlog.createRotatingLogger(name, filename, filesize, filecount);
}
@@ -29,7 +30,7 @@ interface ILog {
message: string;
}
function log(logger: spdlog.RotatingLogger, level: LogLevel, message: string): void {
function log(logger: spdlog.Logger, level: LogLevel, message: string): void {
switch (level) {
case LogLevel.Trace: logger.trace(message); break;
case LogLevel.Debug: logger.debug(message); break;
@@ -45,7 +46,7 @@ export class SpdLogLogger extends AbstractMessageLogger implements ILogger {
private buffer: ILog[] = [];
private readonly _loggerCreationPromise: Promise<void>;
private _logger: spdlog.RotatingLogger | undefined;
private _logger: spdlog.Logger | undefined;
constructor(
private readonly name: string,

View File

@@ -19,7 +19,7 @@ import { IWindowsMainService, IWindowsCountChangedEvent, OpenContext } from 'vs/
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemRecentAction, IMenubarMenuRecentItemAction } from 'vs/platform/menubar/common/menubar';
import { URI } from 'vs/base/common/uri';
import { IStateService } from 'vs/platform/state/node/state';
import { IStateMainService } from 'vs/platform/state/electron-main/state';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
@@ -70,7 +70,7 @@ export class Menubar {
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
@IStateService private readonly stateService: IStateService,
@IStateMainService private readonly stateMainService: IStateMainService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@ILogService private readonly logService: ILogService,
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@@ -100,7 +100,7 @@ export class Menubar {
}
private restoreCachedMenubarData() {
const menubarData = this.stateService.getItem<IMenubarData>(Menubar.lastKnownMenubarStorageKey);
const menubarData = this.stateMainService.getItem<IMenubarData>(Menubar.lastKnownMenubarStorageKey);
if (menubarData) {
if (menubarData.menus) {
this.menubarMenus = menubarData.menus;
@@ -200,7 +200,7 @@ export class Menubar {
this.keybindings = menubarData.keybindings;
// Save off new menu and keybindings
this.stateService.setItem(Menubar.lastKnownMenubarStorageKey, menubarData);
this.stateMainService.setItem(Menubar.lastKnownMenubarStorageKey, menubarData);
this.scheduleUpdateMenu();
}
@@ -286,53 +286,60 @@ export class Menubar {
}
// File
const fileMenu = new Menu();
const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu });
this.setMenuById(fileMenu, 'File');
menubar.append(fileMenuItem);
if (this.shouldDrawMenu('File')) {
const fileMenu = new Menu();
const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu });
this.setMenuById(fileMenu, 'File');
menubar.append(fileMenuItem);
}
// Edit
const editMenu = new Menu();
const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu });
this.setMenuById(editMenu, 'Edit');
menubar.append(editMenuItem);
if (this.shouldDrawMenu('Edit')) {
const editMenu = new Menu();
const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu });
this.setMenuById(editMenu, 'Edit');
menubar.append(editMenuItem);
}
// Selection
/*const selectionMenu = new Menu();
const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu });
this.setMenuById(selectionMenu, 'Selection');
menubar.append(selectionMenuItem); {{SQL CARBON EDIT}} - Disable unused menus */
/*if (this.shouldDrawMenu('Selection')) {
const selectionMenu = new Menu();
const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu });
this.setMenuById(selectionMenu, 'Selection');
menubar.append(selectionMenuItem);
} {{SQL CARBON EDIT}} - Disable unused menus */
// View
const viewMenu = new Menu();
const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu });
this.setMenuById(viewMenu, 'View');
menubar.append(viewMenuItem);
if (this.shouldDrawMenu('View')) {
const viewMenu = new Menu();
const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu });
this.setMenuById(viewMenu, 'View');
menubar.append(viewMenuItem);
}
// Go
/* const gotoMenu = new Menu();
const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu });
this.setMenuById(gotoMenu, 'Go');
menubar.append(gotoMenuItem); {{SQL CARBON EDIT}} - Disable unused menus */
/* if (this.shouldDrawMenu('Go')) {
const gotoMenu = new Menu();
const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu });
this.setMenuById(gotoMenu, 'Go');
menubar.append(gotoMenuItem);
} {{SQL CARBON EDIT}} - Disable unused menus */
// Debug
/*const debugMenu = new Menu();
const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mRun', comment: ['&& denotes a mnemonic'] }, "&&Run")), submenu: debugMenu });
this.setMenuById(debugMenu, 'Run');
menubar.append(debugMenuItem); {{SQL CARBON EDIT}} - Disable unused menus */
/* if (this.shouldDrawMenu('Run')) {
const debugMenu = new Menu();
const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mRun', comment: ['&& denotes a mnemonic'] }, "&&Run")), submenu: debugMenu });
this.setMenuById(debugMenu, 'Run');
menubar.append(debugMenuItem);
} {{SQL CARBON EDIT}} - Disable unused menus */
// Terminal
/*const terminalMenu = new Menu();
const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu });
this.setMenuById(terminalMenu, 'Terminal');
menubar.append(terminalMenuItem); {{SQL CARBON EDIT}} - Disable unused menus */
/* if (this.shouldDrawMenu('Terminal')) {
const terminalMenu = new Menu();
const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu });
this.setMenuById(terminalMenu, 'Terminal');
menubar.append(terminalMenuItem);
} {{SQL CARBON EDIT}} - Disable unused menus */
// Mac: Window
let macWindowMenuItem: MenuItem | undefined;
@@ -347,11 +354,12 @@ export class Menubar {
}
// Help
const helpMenu = new Menu();
const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' });
this.setMenuById(helpMenu, 'Help');
menubar.append(helpMenuItem);
if (this.shouldDrawMenu('Help')) {
const helpMenu = new Menu();
const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' });
this.setMenuById(helpMenu, 'Help');
menubar.append(helpMenuItem);
}
if (menubar.items && menubar.items.length > 0) {
Menu.setApplicationMenu(menubar);

View File

@@ -5,7 +5,7 @@
import { Event } from 'vs/base/common/event';
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows';
import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions, IColorScheme, IPartsSplash } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { URI } from 'vs/base/common/uri';
@@ -72,6 +72,8 @@ export interface ICommonNativeHostService {
setMinimumSize(width: number | undefined, height: number | undefined): Promise<void>;
saveWindowSplash(splash: IPartsSplash): Promise<void>;
/**
* Make the window focused.
*
@@ -97,7 +99,7 @@ export interface ICommonNativeHostService {
setRepresentedFilename(path: string): Promise<void>;
setDocumentEdited(edited: boolean): Promise<void>;
openExternal(url: string): Promise<boolean>;
moveItemToTrash(fullPath: string, deleteOnFail?: boolean): Promise<boolean>;
moveItemToTrash(fullPath: string): Promise<void>;
isAdmin(): Promise<boolean>;
writeElevated(source: URI, target: URI, options?: { unlock?: boolean }): Promise<void>;
@@ -128,6 +130,10 @@ export interface ICommonNativeHostService {
toggleWindowTabsBar(): Promise<void>;
updateTouchBar(items: ISerializableCommandAction[][]): Promise<void>;
// macOS Shell command
installShellCommand(): Promise<void>;
uninstallShellCommand(): Promise<void>;
// Lifecycle
notifyReady(): Promise<void>
relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise<void>;

View File

@@ -3,11 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { exec } from 'child_process';
import { promisify } from 'util';
import { localize } from 'vs/nls';
import { realpath } from 'vs/base/node/extpath';
import { Emitter, Event } from 'vs/base/common/event';
import { IWindowsMainService, ICodeWindow, OpenContext } from 'vs/platform/windows/electron-main/windows';
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme, screen, Display } from 'electron';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows';
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme, IPartsSplash } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { isMacintosh, isWindows, isLinux, isLinuxSnap } from 'vs/base/common/platform';
import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native';
@@ -15,7 +19,7 @@ import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { AddFirstParameterToFunctions } from 'vs/base/common/types';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService';
import { SymlinkSupport } from 'vs/base/node/pfs';
import { Promises, SymlinkSupport } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@@ -23,11 +27,12 @@ import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
import { arch, totalmem, release, platform, type, loadavg, freemem, cpus } from 'os';
import { virtualMachineHint } from 'vs/base/node/id';
import { ILogService } from 'vs/platform/log/common/log';
import { dirname, join } from 'vs/base/common/path';
import { dirname, join, resolve } from 'vs/base/common/path';
import { IProductService } from 'vs/platform/product/common/productService';
import { memoize } from 'vs/base/common/decorators';
import { Disposable } from 'vs/base/common/lifecycle';
import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess';
import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
export interface INativeHostMainService extends AddFirstParameterToFunctions<ICommonNativeHostService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
@@ -50,7 +55,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ILogService private readonly logService: ILogService,
@IProductService private readonly productService: IProductService
@IProductService private readonly productService: IProductService,
@IThemeMainService private readonly themeMainService: IThemeMainService
) {
super();
@@ -247,9 +253,106 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
}
async saveWindowSplash(windowId: number | undefined, splash: IPartsSplash): Promise<void> {
this.themeMainService.saveWindowSplash(windowId, splash);
}
//#endregion
//#region macOS Shell Command
async installShellCommand(windowId: number | undefined): Promise<void> {
const { source, target } = await this.getShellCommandLink();
// Only install unless already existing
try {
const { symbolicLink } = await SymlinkSupport.stat(source);
if (symbolicLink && !symbolicLink.dangling) {
const linkTargetRealPath = await realpath(source);
if (target === linkTargetRealPath) {
return;
}
}
// Different source, delete it first
await Promises.unlink(source);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error; // throw on any error but file not found
}
}
try {
await Promises.symlink(target, source);
} catch (error) {
if (error.code !== 'EACCES' && error.code !== 'ENOENT') {
throw error;
}
const { response } = await this.showMessageBox(windowId, {
type: 'info',
message: localize('warnEscalation', "{0} will now prompt with 'osascript' for Administrator privileges to install the shell command.", this.productService.nameShort),
buttons: [localize('ok', "OK"), localize('cancel', "Cancel")],
cancelId: 1
});
if (response === 0 /* OK */) {
try {
const command = `osascript -e "do shell script \\"mkdir -p /usr/local/bin && ln -sf \'${target}\' \'${source}\'\\" with administrator privileges"`;
await promisify(exec)(command);
} catch (error) {
throw new Error(localize('cantCreateBinFolder', "Unable to install the shell command '{0}'.", source));
}
}
}
}
async uninstallShellCommand(windowId: number | undefined): Promise<void> {
const { source } = await this.getShellCommandLink();
try {
await Promises.unlink(source);
} catch (error) {
switch (error.code) {
case 'EACCES':
const { response } = await this.showMessageBox(windowId, {
type: 'info',
message: localize('warnEscalationUninstall', "{0} will now prompt with 'osascript' for Administrator privileges to uninstall the shell command.", this.productService.nameShort),
buttons: [localize('ok', "OK"), localize('cancel', "Cancel")],
cancelId: 1
});
if (response === 0 /* OK */) {
try {
const command = `osascript -e "do shell script \\"rm \'${source}\'\\" with administrator privileges"`;
await promisify(exec)(command);
} catch (error) {
throw new Error(localize('cantUninstall', "Unable to uninstall the shell command '{0}'.", source));
}
}
break;
case 'ENOENT':
break; // ignore file not found
default:
throw error;
}
}
}
private async getShellCommandLink(): Promise<{ readonly source: string, readonly target: string }> {
const target = resolve(this.environmentMainService.appRoot, 'bin', 'code');
const source = `/usr/local/bin/${this.productService.applicationName}`;
// Ensure source exists
const sourceExists = await Promises.exists(target);
if (!sourceExists) {
throw new Error(localize('sourceMissing', "Unable to find shell script in '{0}'", target));
}
return { source, target };
}
//#region Dialog
async showMessageBox(windowId: number | undefined, options: MessageBoxOptions): Promise<MessageBoxReturnValue> {
@@ -376,8 +479,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
process.env['GDK_PIXBUF_MODULEDIR'] = gdkPixbufModuleDir;
}
async moveItemToTrash(windowId: number | undefined, fullPath: string): Promise<boolean> {
return shell.moveItemToTrash(fullPath);
moveItemToTrash(windowId: number | undefined, fullPath: string): Promise<void> {
return shell.trashItem(fullPath);
}
async isAdmin(): Promise<boolean> {
@@ -604,9 +707,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
// Otherwise: normal quit
else {
setTimeout(() => {
this.lifecycleMainService.quit();
}, 10 /* delay to unwind callback stack (IPC) */);
this.lifecycleMainService.quit();
}
}
@@ -716,6 +817,27 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
async setPassword(windowId: number | undefined, service: string, account: string, password: string): Promise<void> {
const keytar = await this.withKeytar();
const MAX_SET_ATTEMPTS = 3;
// Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times.
const setPasswordWithRetry = async (service: string, account: string, password: string) => {
let attempts = 0;
let error: any;
while (attempts < MAX_SET_ATTEMPTS) {
try {
await keytar.setPassword(service, account, password);
return;
} catch (e) {
error = e;
this.logService.warn('Error attempting to set a password: ', e);
attempts++;
await new Promise(resolve => setTimeout(resolve, 200));
}
}
// throw last error
throw error;
};
if (isWindows && password.length > NativeHostMainService.MAX_PASSWORD_LENGTH) {
let index = 0;
@@ -731,12 +853,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
hasNextChunk: hasNextChunk
};
await keytar.setPassword(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content));
await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content));
chunk++;
}
} else {
await keytar.setPassword(service, account, password);
await setPasswordWithRetry(service, account, password);
}
this._onDidChangePassword.fire({ service, account });

View File

@@ -12,7 +12,7 @@ export const INativeHostService = createDecorator<INativeHostService>('nativeHos
* A set of methods specific to a native host, i.e. unsupported in web
* environments.
*
* @see `IHostService` for methods that can be used in native and web
* @see {@link IHostService} for methods that can be used in native and web
* hosts.
*/
export interface INativeHostService extends ICommonNativeHostService { }

View File

@@ -6,11 +6,12 @@
import { Event } from 'vs/base/common/event';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { $, EventHelper, EventLike } from 'vs/base/browser/dom';
import { domEvent } from 'vs/base/browser/event';
import { DomEmitter, domEvent } from 'vs/base/browser/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable } from 'vs/base/common/lifecycle';
import { Color } from 'vs/base/common/color';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
export interface ILinkDescriptor {
readonly label: string;
@@ -18,75 +19,86 @@ export interface ILinkDescriptor {
readonly title?: string;
}
export interface ILinkStyles {
readonly textLinkForeground?: Color;
readonly disabled?: boolean;
export interface ILinkOptions {
readonly opener?: (href: string) => void;
readonly textLinkForeground?: string;
}
export class Link extends Disposable {
readonly el: HTMLAnchorElement;
private disabled: boolean;
private styles: ILinkStyles = {
textLinkForeground: Color.fromHex('#006AB1')
};
private _enabled: boolean = true;
get enabled(): boolean {
return this._enabled;
}
set enabled(enabled: boolean) {
if (enabled) {
this.el.setAttribute('aria-disabled', 'false');
this.el.tabIndex = 0;
this.el.style.pointerEvents = 'auto';
this.el.style.opacity = '1';
this.el.style.cursor = 'pointer';
this._enabled = false;
} else {
this.el.setAttribute('aria-disabled', 'true');
this.el.tabIndex = -1;
this.el.style.pointerEvents = 'none';
this.el.style.opacity = '0.4';
this.el.style.cursor = 'default';
this._enabled = true;
}
this._enabled = enabled;
}
constructor(
link: ILinkDescriptor,
options: ILinkOptions | undefined = undefined,
@IOpenerService openerService: IOpenerService
) {
super();
this.el = $<HTMLAnchorElement>('a', {
this.el = $<HTMLAnchorElement>('a.monaco-link', {
tabIndex: 0,
href: link.href,
title: link.title
}, link.label);
const onClick = domEvent(this.el, 'click');
const onClickEmitter = this._register(new DomEmitter(this.el, 'click'));
const onEnterPress = Event.chain(domEvent(this.el, 'keypress'))
.map(e => new StandardKeyboardEvent(e))
.filter(e => e.keyCode === KeyCode.Enter)
.event;
const onOpen = Event.any<EventLike>(onClick, onEnterPress);
const onOpen = Event.any<EventLike>(onClickEmitter.event, onEnterPress);
this._register(onOpen(e => {
if (!this.enabled) {
return;
}
EventHelper.stop(e, true);
if (!this.disabled) {
if (options?.opener) {
options.opener(link.href);
} else {
openerService.open(link.href, { allowCommands: true });
}
}));
this.disabled = false;
this.applyStyles();
}
style(styles: ILinkStyles): void {
this.styles = styles;
this.applyStyles();
}
private applyStyles(): void {
const color = this.styles.textLinkForeground?.toString();
if (color) {
this.el.style.color = color;
}
if (typeof this.styles.disabled === 'boolean' && this.styles.disabled !== this.disabled) {
if (this.styles.disabled) {
this.el.setAttribute('aria-disabled', 'true');
this.el.tabIndex = -1;
this.el.style.pointerEvents = 'none';
this.el.style.opacity = '0.4';
this.el.style.cursor = 'default';
this.disabled = true;
} else {
this.el.setAttribute('aria-disabled', 'false');
this.el.tabIndex = 0;
this.el.style.pointerEvents = 'auto';
this.el.style.opacity = '1';
this.el.style.cursor = 'pointer';
this.disabled = false;
}
}
this.enabled = true;
}
}
registerThemingParticipant((theme, collector) => {
const textLinkForegroundColor = theme.getColor(textLinkForeground);
if (textLinkForegroundColor) {
collector.addRule(`.monaco-link { color: ${textLinkForegroundColor}; }`);
}
const textLinkActiveForegroundColor = theme.getColor(textLinkActiveForeground);
if (textLinkActiveForegroundColor) {
collector.addRule(`.monaco-link:hover { color: ${textLinkActiveForegroundColor}; }`);
}
});

View File

@@ -109,6 +109,7 @@ export interface IOpenerService {
/**
* Resolve a resource to its external form.
* @throws whenever resolvers couldn't resolve this resource externally.
*/
resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise<IResolvedExternalUri>;
}

View File

@@ -13,7 +13,7 @@ import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes
let product: IProductConfiguration;
// Native sandbox environment
if (typeof globals.vscode !== 'undefined') {
if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.context !== 'undefined') {
const configuration: ISandboxConfiguration | undefined = globals.vscode.context.configuration();
if (configuration) {
product = configuration.product;
@@ -67,10 +67,10 @@ else {
extensionAllowedProposedApi: [
'ms-vscode.vscode-js-profile-flame',
'ms-vscode.vscode-js-profile-table',
'ms-vscode.github-browser',
'ms-vscode.github-richnav',
'ms-vscode.remotehub',
'ms-vscode.remotehub-insiders'
'ms-vscode.remotehub-insiders',
'GitHub.remotehub',
'GitHub.remotehub-insiders'
],
});
}

View File

@@ -19,7 +19,7 @@ export interface IProgressService {
readonly _serviceBrand: undefined;
withProgress<R>(
options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions,
options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions,
task: (progress: IProgress<IProgressStep>) => Promise<R>,
onDidCancel?: (choice?: number) => void
): Promise<R>;
@@ -66,6 +66,11 @@ export interface IProgressNotificationOptions extends IProgressOptions {
readonly silent?: boolean;
}
export interface IProgressDialogOptions extends IProgressOptions {
readonly delay?: number;
readonly detail?: string;
}
export interface IProgressWindowOptions extends IProgressOptions {
readonly location: ProgressLocation.Window;
readonly command?: string;
@@ -134,7 +139,7 @@ export class UnmanagedProgress extends Disposable {
private lastStep?: IProgressStep;
constructor(
options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions,
options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions,
@IProgressService progressService: IProgressService,
) {
super();
@@ -159,7 +164,6 @@ export class UnmanagedProgress extends Disposable {
}
}
export class LongRunningOperation extends Disposable {
private currentOperationId = 0;
private readonly currentOperationDisposables = this._register(new DisposableStore());

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
@@ -34,12 +34,8 @@ export interface IProtocolMainService {
/**
* Allows to make an object accessible to a renderer
* via `ipcRenderer.invoke(resource.toString())`.
*
* @param obj the (optional) object to make accessible to the
* renderer. Can be updated later via the `IObjectUrl#update`
* method too.
*/
createIPCObjectUrl<T>(obj?: T): IIPCObjectUrl<T>;
createIPCObjectUrl<T>(): IIPCObjectUrl<T>;
/**
* Adds a `URI` as root to the list of allowed

View File

@@ -22,7 +22,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ
declare readonly _serviceBrand: undefined;
private readonly validRoots = TernarySearchTree.forUris<boolean>(() => !isLinux);
private readonly validExtensions = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp']); // https://github.com/microsoft/vscode/issues/119384
private readonly validExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg', '.gif', '.bmp']); // https://github.com/microsoft/vscode/issues/119384
constructor(
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@@ -47,10 +47,10 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ
const { defaultSession } = session;
// Register vscode-file:// handler
defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback as unknown as ProtocolCallback));
defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback));
// Intercept any file:// access
defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback as unknown as ProtocolCallback));
defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback));
// Cleanup
this._register(toDisposable(() => {
@@ -142,7 +142,8 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ
//#region IPC Object URLs
createIPCObjectUrl<T>(obj: T): IIPCObjectUrl<T> {
createIPCObjectUrl<T>(): IIPCObjectUrl<T> {
let obj: T | undefined = undefined;
// Create unique URI
const resource = URI.from({
@@ -152,7 +153,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ
// Install IPC handler
const channel = resource.toString();
const handler = async (): Promise<T> => obj;
const handler = async (): Promise<T | undefined> => obj;
ipcMain.handle(channel, handler);
this.logService.trace(`IPC Object URL: Registered new channel ${channel}.`);

View File

@@ -19,7 +19,8 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import Severity from 'vs/base/common/severity';
import { toErrorMessage } from 'vs/base/common/errorMessage';
export interface ICommandQuickPick extends IPickerQuickAccessItem {
@@ -47,14 +48,14 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
@IKeybindingService private readonly keybindingService: IKeybindingService,
@ICommandService private readonly commandService: ICommandService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@INotificationService private readonly notificationService: INotificationService
@IDialogService private readonly dialogService: IDialogService
) {
super(AbstractCommandsQuickAccessProvider.PREFIX, options);
this.options = options;
}
protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<ICommandQuickPick | IQuickPickSeparator>> {
protected async _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<ICommandQuickPick | IQuickPickSeparator>> {
// Ask subclass for all command picks
const allCommandPicks = await this.getCommandPicks(disposables, token);
@@ -162,7 +163,7 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
await this.commandService.executeCommand(commandPick.commandId);
} catch (error) {
if (!isPromiseCanceledError(error)) {
this.notificationService.error(localize('canNotRun', "Command '{0}' resulted in an error ({1})", commandPick.label, toErrorMessage(error)));
this.dialogService.show(Severity.Error, localize('canNotRun', "Command '{0}' resulted in an error ({1})", commandPick.label, toErrorMessage(error)));
}
}
}

View File

@@ -5,7 +5,7 @@
import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput';
import { IQuickPickSeparator, IKeyMods, IQuickPickDidAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput';
import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess';
import { IDisposable, DisposableStore, Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { timeout, isThenable } from 'vs/base/common/async';
@@ -42,7 +42,7 @@ export interface IPickerQuickAccessItem extends IQuickPickItem {
* @param keyMods the state of modifier keys when the item was accepted.
* @param event the underlying event that caused the accept to trigger.
*/
accept?(keyMods: IKeyMods, event: IQuickPickAcceptEvent): void;
accept?(keyMods: IKeyMods, event: IQuickPickDidAcceptEvent): void;
/**
* A method that will be executed when a button of the pick item was
@@ -122,7 +122,7 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
// Collect picks and support both long running and short or combined
const picksToken = picksCts.token;
const picksFilter = picker.value.substr(this.prefix.length).trim();
const providedPicks = this.getPicks(picksFilter, picksDisposables, picksToken);
const providedPicks = this._getPicks(picksFilter, picksDisposables, picksToken);
const applyPicks = (picks: Picks<T>, skipEmpty?: boolean): boolean => {
let items: readonly Pick<T>[];
@@ -330,5 +330,5 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
* @returns the picks either directly, as promise or combined fast and slow results.
* Pickers can return `null` to signal that no change in picks is needed.
*/
protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Picks<T> | Promise<Picks<T>> | FastAndSlowPicks<T> | null;
protected abstract _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Picks<T> | Promise<Picks<T>> | FastAndSlowPicks<T> | null;
}

View File

@@ -31,7 +31,17 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
super();
}
pick(value = '', options?: IQuickAccessOptions): Promise<IQuickPickItem[] | undefined> {
return this.doShowOrPick(value, true, options);
}
show(value = '', options?: IQuickAccessOptions): void {
this.doShowOrPick(value, false, options);
}
private doShowOrPick(value: string, pick: true, options?: IQuickAccessOptions): Promise<IQuickPickItem[] | undefined>;
private doShowOrPick(value: string, pick: false, options?: IQuickAccessOptions): void;
private doShowOrPick(value: string, pick: boolean, options?: IQuickAccessOptions): Promise<IQuickPickItem[] | undefined> | void {
// Find provider for the value to show
const [provider, descriptor] = this.getOrInstantiateProvider(value);
@@ -99,6 +109,18 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
picker.ariaLabel = descriptor?.placeholder;
}
// Pick mode: setup a promise that can be resolved
// with the selected items and prevent execution
let pickPromise: Promise<IQuickPickItem[]> | undefined = undefined;
let pickResolve: Function | undefined = undefined;
if (pick) {
pickPromise = new Promise<IQuickPickItem[]>(resolve => pickResolve = resolve);
disposables.add(once(picker.onWillAccept)(e => {
e.veto();
picker.hide();
}));
}
// Register listeners
disposables.add(this.registerPickerListeners(picker, provider, descriptor, value));
@@ -119,12 +141,20 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
// Start to dispose once picker hides
disposables.dispose();
// Resolve pick promise with selected items
pickResolve?.(picker.selectedItems);
});
// Finally, show the picker. This is important because a provider
// may not call this and then our disposables would leak that rely
// on the onDidHide event.
picker.show();
// Pick mode: return with promise
if (pick) {
return pickPromise;
}
}
private adjustValueSelection(picker: IQuickPick<IQuickPickItem>, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void {

View File

@@ -7,7 +7,7 @@ import { IQuickInputService, IQuickPickItem, IPickOptions, IInputOptions, IQuick
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService, Themable } from 'vs/platform/theme/common/themeService';
import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, listFocusForeground, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground, quickInputListFocusBackground, keybindingLabelBackground, keybindingLabelForeground, keybindingLabelBorder, keybindingLabelBottomBorder } from 'vs/platform/theme/common/colorRegistry';
import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground, quickInputListFocusBackground, keybindingLabelBackground, keybindingLabelForeground, keybindingLabelBorder, keybindingLabelBottomBorder, quickInputListFocusForeground } from 'vs/platform/theme/common/colorRegistry';
import { CancellationToken } from 'vs/base/common/cancellation';
import { computeStyles } from 'vs/platform/theme/common/styler';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
@@ -219,7 +219,7 @@ export class QuickInputService extends Themable implements IQuickInputService {
list: computeStyles(this.theme, {
listBackground: quickInputBackground,
// Look like focused when inactive.
listInactiveFocusForeground: listFocusForeground,
listInactiveFocusForeground: quickInputListFocusForeground,
listInactiveFocusBackground: quickInputListFocusBackground,
listFocusOutline: activeContrastBorder,
listInactiveFocusOutline: activeContrastBorder,

View File

@@ -36,6 +36,13 @@ export interface IQuickAccessController {
* Open the quick access picker with the optional value prefilled.
*/
show(value?: string, options?: IQuickAccessOptions): void;
/**
* Same as `show()` but instead of executing the selected pick item,
* it will be returned. May return `undefined` in case no item was
* picked by the user.
*/
pick(value?: string, options?: IQuickAccessOptions): Promise<IQuickPickItem[] | undefined>;
}
export enum DefaultQuickAccessFilterValue {

View File

@@ -40,6 +40,10 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
return this._cache.get(authority)!;
}
async getCanonicalURI(uri: URI): Promise<URI> {
return uri;
}
getConnectionData(authority: string): IRemoteConnectionData | null {
if (!this._cache.has(authority)) {
return null;
@@ -76,4 +80,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
RemoteAuthorities.setConnectionToken(authority, connectionToken);
this._onDidChangeConnectionData.fire();
}
_setCanonicalURIProvider(provider: (uri: URI) => Promise<URI>): void {
}
}

View File

@@ -5,6 +5,7 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
export const IRemoteAuthorityResolverService = createDecorator<IRemoteAuthorityResolverService>('remoteAuthorityResolverService');
@@ -15,15 +16,9 @@ export interface ResolvedAuthority {
readonly connectionToken: string | undefined;
}
export enum RemoteTrustOption {
Unknown = 0,
DisableTrust = 1,
MachineTrusted = 2
}
export interface ResolvedOptions {
readonly extensionHostEnv?: { [key: string]: string | null };
readonly trust?: RemoteTrustOption;
readonly isTrusted?: boolean;
}
export interface TunnelDescription {
@@ -98,9 +93,18 @@ export interface IRemoteAuthorityResolverService {
resolveAuthority(authority: string): Promise<ResolverResult>;
getConnectionData(authority: string): IRemoteConnectionData | null;
/**
* Get the canonical URI for a `vscode-remote://` URI.
*
* **NOTE**: This can throw e.g. in cases where there is no resolver installed for the specific remote authority.
*
* @param uri The `vscode-remote://` URI
*/
getCanonicalURI(uri: URI): Promise<URI>;
_clearResolvedAuthority(authority: string): void;
_setResolvedAuthority(resolvedAuthority: ResolvedAuthority, resolvedOptions?: ResolvedOptions): void;
_setResolvedAuthorityError(authority: string, err: any): void;
_setAuthorityConnectionToken(authority: string, connectionToken: string): void;
_setCanonicalURIProvider(provider: (uri: URI) => Promise<URI>): void;
}

View File

@@ -26,7 +26,7 @@ export function getRemoteName(authority: string | undefined): string | undefined
return authority.substr(0, pos);
}
function isVirtualResource(resource: URI) {
export function isVirtualResource(resource: URI) {
return resource.scheme !== Schemas.file && resource.scheme !== Schemas.vscodeRemote;
}
@@ -42,3 +42,7 @@ export function getVirtualWorkspaceLocation(workspace: IWorkspace): { scheme: st
export function getVirtualWorkspaceScheme(workspace: IWorkspace): string | undefined {
return getVirtualWorkspaceLocation(workspace)?.scheme;
}
export function isVirtualWorkspace(workspace: IWorkspace): boolean {
return getVirtualWorkspaceLocation(workspace) !== undefined;
}

View File

@@ -86,6 +86,7 @@ export interface ITunnelService {
readonly onTunnelOpened: Event<RemoteTunnel>;
readonly onTunnelClosed: Event<{ host: string, port: number; }>;
readonly canElevate: boolean;
readonly hasTunnelProvider: boolean;
canTunnel(uri: URI): boolean;
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, isPublic?: boolean): Promise<RemoteTunnel | undefined> | undefined;
@@ -141,6 +142,10 @@ export abstract class AbstractTunnelService implements ITunnelService {
@ILogService protected readonly logService: ILogService
) { }
get hasTunnelProvider(): boolean {
return !!this._tunnelProvider;
}
setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable {
this._tunnelProvider = provider;
if (!provider) {

View File

@@ -8,22 +8,27 @@ import * as errors from 'vs/base/common/errors';
import { RemoteAuthorities } from 'vs/base/common/network';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
class PendingResolveAuthorityRequest {
class PendingPromise<I, R> {
public readonly promise: Promise<R>;
public readonly input: I;
public result: R | null;
private _resolve!: (value: R) => void;
private _reject!: (err: any) => void;
public value: ResolverResult | null;
constructor(
private readonly _resolve: (value: ResolverResult) => void,
private readonly _reject: (err: any) => void,
public readonly promise: Promise<ResolverResult>,
) {
this.value = null;
constructor(request: I) {
this.input = request;
this.promise = new Promise<R>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
this.result = null;
}
resolve(value: ResolverResult): void {
this.value = value;
this._resolve(this.value);
resolve(result: R): void {
this.result = result;
this._resolve(this.result);
}
reject(err: any): void {
@@ -38,40 +43,50 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
private readonly _onDidChangeConnectionData = this._register(new Emitter<void>());
public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event;
private readonly _resolveAuthorityRequests: Map<string, PendingResolveAuthorityRequest>;
private readonly _resolveAuthorityRequests: Map<string, PendingPromise<string, ResolverResult>>;
private readonly _connectionTokens: Map<string, string>;
private readonly _canonicalURIRequests: Map<string, PendingPromise<URI, URI>>;
private _canonicalURIProvider: ((uri: URI) => Promise<URI>) | null;
constructor() {
super();
this._resolveAuthorityRequests = new Map<string, PendingResolveAuthorityRequest>();
this._resolveAuthorityRequests = new Map<string, PendingPromise<string, ResolverResult>>();
this._connectionTokens = new Map<string, string>();
this._canonicalURIRequests = new Map<string, PendingPromise<URI, URI>>();
this._canonicalURIProvider = null;
}
resolveAuthority(authority: string): Promise<ResolverResult> {
if (!this._resolveAuthorityRequests.has(authority)) {
let resolve: (value: ResolverResult) => void;
let reject: (err: any) => void;
const promise = new Promise<ResolverResult>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
this._resolveAuthorityRequests.set(authority, new PendingResolveAuthorityRequest(resolve!, reject!, promise));
this._resolveAuthorityRequests.set(authority, new PendingPromise<string, ResolverResult>(authority));
}
return this._resolveAuthorityRequests.get(authority)!.promise;
}
async getCanonicalURI(uri: URI): Promise<URI> {
const key = uri.toString();
if (!this._canonicalURIRequests.has(key)) {
const request = new PendingPromise<URI, URI>(uri);
if (this._canonicalURIProvider) {
this._canonicalURIProvider(request.input).then((uri) => request.resolve(uri), (err) => request.reject(err));
}
this._canonicalURIRequests.set(key, request);
}
return this._canonicalURIRequests.get(key)!.promise;
}
getConnectionData(authority: string): IRemoteConnectionData | null {
if (!this._resolveAuthorityRequests.has(authority)) {
return null;
}
const request = this._resolveAuthorityRequests.get(authority)!;
if (!request.value) {
if (!request.result) {
return null;
}
const connectionToken = this._connectionTokens.get(authority);
return {
host: request.value.authority.host,
port: request.value.authority.port,
host: request.result.authority.host,
port: request.result.authority.port,
connectionToken: connectionToken
};
}
@@ -107,4 +122,11 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
RemoteAuthorities.setConnectionToken(authority, connectionToken);
this._onDidChangeConnectionData.fire();
}
_setCanonicalURIProvider(provider: (uri: URI) => Promise<URI>): void {
this._canonicalURIProvider = provider;
this._canonicalURIRequests.forEach((value) => {
this._canonicalURIProvider!(value.input).then((uri) => value.resolve(uri), (err) => value.reject(err));
});
}
}

View File

@@ -8,6 +8,7 @@ import { Barrier } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { findFreePortFaster } from 'vs/base/node/ports';
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
@@ -15,8 +16,8 @@ import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/t
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
import { ISignService } from 'vs/platform/sign/common/sign';
async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise<RemoteTunnel> {
const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort);
async function createRemoteTunnel(options: IConnectionOptions, defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise<RemoteTunnel> {
const tunnel = new NodeRemoteTunnel(options, defaultTunnelHost, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort);
return tunnel.waitForReady();
}
@@ -38,7 +39,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
private readonly _socketsDispose: Map<string, () => void> = new Map();
constructor(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) {
constructor(options: IConnectionOptions, private readonly defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) {
super();
this._options = options;
this._server = net.createServer();
@@ -76,17 +77,19 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
// if that fails, the method above returns 0, which works out fine below...
let address: string | net.AddressInfo | null = null;
address = (<net.AddressInfo>this._server.listen(localPort).address());
this._server.listen(localPort, this.defaultTunnelHost);
await this._barrier.wait();
address = <net.AddressInfo>this._server.address();
// It is possible for findFreePortFaster to return a port that there is already a server listening on. This causes the previous listen call to error out.
if (!address) {
localPort = 0;
address = (<net.AddressInfo>this._server.listen(localPort).address());
this._server.listen(localPort, this.defaultTunnelHost);
await this._barrier.wait();
address = <net.AddressInfo>this._server.address();
}
this.tunnelLocalPort = address.port;
await this._barrier.wait();
this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`;
return this;
}
@@ -135,11 +138,16 @@ export class BaseTunnelService extends AbstractTunnelService {
private readonly socketFactory: ISocketFactory,
@ILogService logService: ILogService,
@ISignService private readonly signService: ISignService,
@IProductService private readonly productService: IProductService
@IProductService private readonly productService: IProductService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super(logService);
}
private get defaultTunnelHost(): string {
return (this.configurationService.getValue('remote.localPortHost') === 'localhost') ? '127.0.0.1' : '0.0.0.0';
}
protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise<RemoteTunnel | undefined> | undefined {
const existing = this.getTunnelFromMap(remoteHost, remotePort);
if (existing) {
@@ -160,7 +168,7 @@ export class BaseTunnelService extends AbstractTunnelService {
ipcLogger: null
};
const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort);
const tunnel = createRemoteTunnel(options, this.defaultTunnelHost, remoteHost, remotePort, localPort);
this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created without provider.');
this.addTunnelToMap(remoteHost, remotePort, tunnel);
return tunnel;
@@ -172,8 +180,9 @@ export class TunnelService extends BaseTunnelService {
public constructor(
@ILogService logService: ILogService,
@ISignService signService: ISignService,
@IProductService productService: IProductService
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService
) {
super(nodeSocketFactory, logService, signService, productService);
super(nodeSocketFactory, logService, signService, productService, configurationService);
}
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import product from 'vs/platform/product/common/product';
import { BrowserWindow, ipcMain, Event as ElectronEvent, MessagePortMain, IpcMainEvent, RenderProcessGoneDetails } from 'electron';
import { BrowserWindow, ipcMain, Event as ElectronEvent, MessagePortMain, IpcMainEvent } from 'electron';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { Barrier } from 'vs/base/common/async';
import { ILogService } from 'vs/platform/log/common/log';
@@ -18,7 +18,6 @@ import { connect as connectMessagePort } from 'vs/base/parts/ipc/electron-main/i
import { assertIsDefined } from 'vs/base/common/types';
import { Emitter, Event } from 'vs/base/common/event';
import { WindowError } from 'vs/platform/windows/electron-main/windows';
import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv';
import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol';
export class SharedProcess extends Disposable implements ISharedProcess {
@@ -28,7 +27,7 @@ export class SharedProcess extends Disposable implements ISharedProcess {
private window: BrowserWindow | undefined = undefined;
private windowCloseListener: ((event: ElectronEvent) => void) | undefined = undefined;
private readonly _onDidError = this._register(new Emitter<{ type: WindowError, details: string | RenderProcessGoneDetails }>());
private readonly _onDidError = this._register(new Emitter<{ type: WindowError, details?: { reason: string, exitCode: number } }>());
readonly onDidError = Event.buffer(this._onDidError.event); // buffer until we have a listener!
constructor(
@@ -139,9 +138,6 @@ export class SharedProcess extends Disposable implements ISharedProcess {
// Always wait for first window asking for connection
await this.firstWindowConnectionBarrier.wait();
// Resolve shell environment
this.userEnv = { ...this.userEnv, ...(await resolveShellEnv(this.logService, this.environmentMainService.args, process.env)) };
// Create window for shared process
this.createWindow();
@@ -174,7 +170,6 @@ export class SharedProcess extends Disposable implements ISharedProcess {
nodeIntegration: true,
contextIsolation: false,
enableWebSQL: false,
enableRemoteModule: false,
spellcheck: false,
nativeWindowOpen: true,
images: false,
@@ -188,7 +183,7 @@ export class SharedProcess extends Disposable implements ISharedProcess {
machineId: this.machineId,
windowId: this.window.id,
appRoot: this.environmentMainService.appRoot,
nodeCachedDataDir: this.environmentMainService.nodeCachedDataDir,
codeCachePath: this.environmentMainService.codeCachePath,
backupWorkspacesPath: this.environmentMainService.backupWorkspacesPath,
userEnv: this.userEnv,
args: this.environmentMainService.args,
@@ -224,8 +219,8 @@ export class SharedProcess extends Disposable implements ISharedProcess {
// We use `onUnexpectedError` explicitly because the error handler
// will send the error to the active window to log in devtools too
this.window.webContents.on('render-process-gone', (event, details) => this._onDidError.fire({ type: WindowError.CRASHED, details }));
this.window.on('unresponsive', () => this._onDidError.fire({ type: WindowError.UNRESPONSIVE, details: 'SharedProcess: detected unresponsive window' }));
this.window.webContents.on('did-fail-load', (event, errorCode, errorDescription) => this._onDidError.fire({ type: WindowError.LOAD, details: `SharedProcess: failed to load: ${errorDescription}` }));
this.window.on('unresponsive', () => this._onDidError.fire({ type: WindowError.UNRESPONSIVE }));
this.window.webContents.on('did-fail-load', (event, exitCode, reason) => this._onDidError.fire({ type: WindowError.LOAD, details: { reason, exitCode } }));
}
async connect(): Promise<MessagePortMain> {

View File

@@ -5,15 +5,19 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IStateService = createDecorator<IStateService>('stateService');
export const IStateMainService = createDecorator<IStateMainService>('stateMainService');
export interface IStateMainService {
export interface IStateService {
readonly _serviceBrand: undefined;
getItem<T>(key: string, defaultValue: T): T;
getItem<T>(key: string, defaultValue?: T): T | undefined;
setItem(key: string, data?: object | string | number | boolean | undefined | null): void;
setItems(items: readonly { key: string, data?: object | string | number | boolean | undefined | null }[]): void;
removeItem(key: string): void;
close(): Promise<void>;
}

View File

@@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { join } from 'vs/base/common/path';
import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types';
import { IStateMainService } from 'vs/platform/state/electron-main/state';
import { ILogService } from 'vs/platform/log/common/log';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { ThrottledDelayer } from 'vs/base/common/async';
import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
type StorageDatabase = { [key: string]: unknown; };
export class FileStorage {
private storage: StorageDatabase = Object.create(null);
private lastSavedStorageContents = '';
private readonly flushDelayer = new ThrottledDelayer<void>(100 /* buffer saves over a short time */);
private initializing: Promise<void> | undefined = undefined;
private closing: Promise<void> | undefined = undefined;
constructor(
private readonly storagePath: URI,
private readonly logService: ILogService,
private readonly fileService: IFileService
) {
}
init(): Promise<void> {
if (!this.initializing) {
this.initializing = this.doInit();
}
return this.initializing;
}
private async doInit(): Promise<void> {
try {
this.lastSavedStorageContents = (await this.fileService.readFile(this.storagePath)).value.toString();
this.storage = JSON.parse(this.lastSavedStorageContents);
} catch (error) {
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {
this.logService.error(error);
}
}
}
getItem<T>(key: string, defaultValue: T): T;
getItem<T>(key: string, defaultValue?: T): T | undefined;
getItem<T>(key: string, defaultValue?: T): T | undefined {
const res = this.storage[key];
if (isUndefinedOrNull(res)) {
return defaultValue;
}
return res as T;
}
setItem(key: string, data?: object | string | number | boolean | undefined | null): void {
this.setItems([{ key, data }]);
}
setItems(items: readonly { key: string, data?: object | string | number | boolean | undefined | null }[]): void {
let save = false;
for (const { key, data } of items) {
// Shortcut for data that did not change
if (this.storage[key] === data) {
continue;
}
// Remove items when they are undefined or null
if (isUndefinedOrNull(data)) {
if (!isUndefined(this.storage[key])) {
this.storage[key] = undefined;
save = true;
}
}
// Otherwise add an item
else {
this.storage[key] = data;
save = true;
}
}
if (save) {
this.save();
}
}
removeItem(key: string): void {
// Only update if the key is actually present (not undefined)
if (!isUndefined(this.storage[key])) {
this.storage[key] = undefined;
this.save();
}
}
private async save(delay?: number): Promise<void> {
if (this.closing) {
return; // already about to close
}
return this.flushDelayer.trigger(() => this.doSave(), delay);
}
private async doSave(): Promise<void> {
if (!this.initializing) {
return; // if we never initialized, we should not save our state
}
// Make sure to wait for init to finish first
await this.initializing;
// Return early if the database has not changed
const serializedDatabase = JSON.stringify(this.storage, null, 4);
if (serializedDatabase === this.lastSavedStorageContents) {
return;
}
// Write to disk
try {
await this.fileService.writeFile(this.storagePath, VSBuffer.fromString(serializedDatabase));
this.lastSavedStorageContents = serializedDatabase;
} catch (error) {
this.logService.error(error);
}
}
async close(): Promise<void> {
if (!this.closing) {
this.closing = this.flushDelayer.trigger(() => this.doSave(), 0 /* as soon as possible */);
}
return this.closing;
}
}
export class StateMainService implements IStateMainService {
declare readonly _serviceBrand: undefined;
private static readonly STATE_FILE = 'storage.json';
private readonly fileStorage: FileStorage;
constructor(
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
@ILogService logService: ILogService,
@IFileService fileService: IFileService
) {
this.fileStorage = new FileStorage(URI.file(join(environmentMainService.userDataPath, StateMainService.STATE_FILE)), logService, fileService);
}
async init(): Promise<void> {
return this.fileStorage.init();
}
getItem<T>(key: string, defaultValue: T): T;
getItem<T>(key: string, defaultValue?: T): T | undefined;
getItem<T>(key: string, defaultValue?: T): T | undefined {
return this.fileStorage.getItem(key, defaultValue);
}
setItem(key: string, data?: object | string | number | boolean | undefined | null): void {
this.fileStorage.setItem(key, data);
}
setItems(items: readonly { key: string, data?: object | string | number | boolean | undefined | null }[]): void {
this.fileStorage.setItems(items);
}
removeItem(key: string): void {
this.fileStorage.removeItem(key);
}
close(): Promise<void> {
return this.fileStorage.close();
}
}

View File

@@ -1,158 +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 * as path from 'vs/base/common/path';
import * as fs from 'fs';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { writeFileSync } from 'vs/base/node/pfs';
import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types';
import { IStateService } from 'vs/platform/state/node/state';
import { ILogService } from 'vs/platform/log/common/log';
type StorageDatabase = { [key: string]: any; };
export class FileStorage {
private _database: StorageDatabase | null = null;
private lastFlushedSerializedDatabase: string | null = null;
constructor(private dbPath: string, private onError: (error: Error) => void) { }
private get database(): StorageDatabase {
if (!this._database) {
this._database = this.loadSync();
}
return this._database;
}
async init(): Promise<void> {
if (this._database) {
return; // return if database was already loaded
}
const database = await this.loadAsync();
if (this._database) {
return; // return if database was already loaded
}
this._database = database;
}
private loadSync(): StorageDatabase {
try {
this.lastFlushedSerializedDatabase = fs.readFileSync(this.dbPath).toString();
return JSON.parse(this.lastFlushedSerializedDatabase);
} catch (error) {
if (error.code !== 'ENOENT') {
this.onError(error);
}
return {};
}
}
private async loadAsync(): Promise<StorageDatabase> {
try {
this.lastFlushedSerializedDatabase = (await fs.promises.readFile(this.dbPath)).toString();
return JSON.parse(this.lastFlushedSerializedDatabase);
} catch (error) {
if (error.code !== 'ENOENT') {
this.onError(error);
}
return {};
}
}
getItem<T>(key: string, defaultValue: T): T;
getItem<T>(key: string, defaultValue?: T): T | undefined;
getItem<T>(key: string, defaultValue?: T): T | undefined {
const res = this.database[key];
if (isUndefinedOrNull(res)) {
return defaultValue;
}
return res;
}
setItem(key: string, data?: object | string | number | boolean | undefined | null): void {
// Remove an item when it is undefined or null
if (isUndefinedOrNull(data)) {
return this.removeItem(key);
}
// Shortcut for primitives that did not change
if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') {
if (this.database[key] === data) {
return;
}
}
this.database[key] = data;
this.saveSync();
}
removeItem(key: string): void {
// Only update if the key is actually present (not undefined)
if (!isUndefined(this.database[key])) {
this.database[key] = undefined;
this.saveSync();
}
}
private saveSync(): void {
const serializedDatabase = JSON.stringify(this.database, null, 4);
if (serializedDatabase === this.lastFlushedSerializedDatabase) {
return; // return early if the database has not changed
}
try {
writeFileSync(this.dbPath, serializedDatabase); // permission issue can happen here
this.lastFlushedSerializedDatabase = serializedDatabase;
} catch (error) {
this.onError(error);
}
}
}
export class StateService implements IStateService {
declare readonly _serviceBrand: undefined;
private static readonly STATE_FILE = 'storage.json';
private fileStorage: FileStorage;
constructor(
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@ILogService logService: ILogService
) {
this.fileStorage = new FileStorage(path.join(environmentService.userDataPath, StateService.STATE_FILE), error => logService.error(error));
}
init(): Promise<void> {
return this.fileStorage.init();
}
getItem<T>(key: string, defaultValue: T): T;
getItem<T>(key: string, defaultValue?: T): T | undefined;
getItem<T>(key: string, defaultValue?: T): T | undefined {
return this.fileStorage.getItem(key, defaultValue);
}
setItem(key: string, data?: object | string | number | boolean | undefined | null): void {
this.fileStorage.setItem(key, data);
}
removeItem(key: string): void {
this.fileStorage.removeItem(key);
}
}

View File

@@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { tmpdir } from 'os';
import { readFileSync } from 'fs';
import { join } from 'vs/base/common/path';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { FileStorage } from 'vs/platform/state/electron-main/stateMainService';
import { Promises, writeFileSync } from 'vs/base/node/pfs';
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
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 { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
flakySuite('StateMainService', () => {
let testDir: string;
let fileService: IFileService;
let logService: ILogService;
let diskFileSystemProvider: DiskFileSystemProvider;
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'statemainservice');
logService = new NullLogService();
fileService = new FileService(logService);
diskFileSystemProvider = new DiskFileSystemProvider(logService);
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
return Promises.mkdir(testDir, { recursive: true });
});
teardown(() => {
fileService.dispose();
diskFileSystemProvider.dispose();
return Promises.rm(testDir);
});
test('Basics', async function () {
const storageFile = join(testDir, 'storage.json');
writeFileSync(storageFile, '');
let service = new FileStorage(URI.file(storageFile), logService, fileService);
await service.init();
service.setItem('some.key', 'some.value');
assert.strictEqual(service.getItem('some.key'), 'some.value');
service.removeItem('some.key');
assert.strictEqual(service.getItem('some.key', 'some.default'), 'some.default');
assert.ok(!service.getItem('some.unknonw.key'));
service.setItem('some.other.key', 'some.other.value');
await service.close();
service = new FileStorage(URI.file(storageFile), logService, fileService);
await service.init();
assert.strictEqual(service.getItem('some.other.key'), 'some.other.value');
service.setItem('some.other.key', 'some.other.value');
assert.strictEqual(service.getItem('some.other.key'), 'some.other.value');
service.setItem('some.undefined.key', undefined);
assert.strictEqual(service.getItem('some.undefined.key', 'some.default'), 'some.default');
service.setItem('some.null.key', null);
assert.strictEqual(service.getItem('some.null.key', 'some.default'), 'some.default');
service.setItems([
{ key: 'some.setItems.key1', data: 'some.value' },
{ key: 'some.setItems.key2', data: 0 },
{ key: 'some.setItems.key3', data: true },
{ key: 'some.setItems.key4', data: null },
{ key: 'some.setItems.key5', data: undefined }
]);
assert.strictEqual(service.getItem('some.setItems.key1'), 'some.value');
assert.strictEqual(service.getItem('some.setItems.key2'), 0);
assert.strictEqual(service.getItem('some.setItems.key3'), true);
assert.strictEqual(service.getItem('some.setItems.key4'), undefined);
assert.strictEqual(service.getItem('some.setItems.key5'), undefined);
service.setItems([
{ key: 'some.setItems.key1', data: undefined },
{ key: 'some.setItems.key2', data: undefined },
{ key: 'some.setItems.key3', data: undefined },
{ key: 'some.setItems.key4', data: null },
{ key: 'some.setItems.key5', data: undefined }
]);
assert.strictEqual(service.getItem('some.setItems.key1'), undefined);
assert.strictEqual(service.getItem('some.setItems.key2'), undefined);
assert.strictEqual(service.getItem('some.setItems.key3'), undefined);
assert.strictEqual(service.getItem('some.setItems.key4'), undefined);
assert.strictEqual(service.getItem('some.setItems.key5'), undefined);
});
test('Multiple ops are buffered and applied', async function () {
const storageFile = join(testDir, 'storage.json');
writeFileSync(storageFile, '');
let service = new FileStorage(URI.file(storageFile), logService, fileService);
await service.init();
service.setItem('some.key1', 'some.value1');
service.setItem('some.key2', 'some.value2');
service.setItem('some.key3', 'some.value3');
service.setItem('some.key4', 'some.value4');
service.removeItem('some.key4');
assert.strictEqual(service.getItem('some.key1'), 'some.value1');
assert.strictEqual(service.getItem('some.key2'), 'some.value2');
assert.strictEqual(service.getItem('some.key3'), 'some.value3');
assert.strictEqual(service.getItem('some.key4'), undefined);
await service.close();
service = new FileStorage(URI.file(storageFile), logService, fileService);
await service.init();
assert.strictEqual(service.getItem('some.key1'), 'some.value1');
assert.strictEqual(service.getItem('some.key2'), 'some.value2');
assert.strictEqual(service.getItem('some.key3'), 'some.value3');
assert.strictEqual(service.getItem('some.key4'), undefined);
});
test('Used before init', async function () {
const storageFile = join(testDir, 'storage.json');
writeFileSync(storageFile, '');
let service = new FileStorage(URI.file(storageFile), logService, fileService);
service.setItem('some.key1', 'some.value1');
service.setItem('some.key2', 'some.value2');
service.setItem('some.key3', 'some.value3');
service.setItem('some.key4', 'some.value4');
service.removeItem('some.key4');
assert.strictEqual(service.getItem('some.key1'), 'some.value1');
assert.strictEqual(service.getItem('some.key2'), 'some.value2');
assert.strictEqual(service.getItem('some.key3'), 'some.value3');
assert.strictEqual(service.getItem('some.key4'), undefined);
await service.init();
assert.strictEqual(service.getItem('some.key1'), 'some.value1');
assert.strictEqual(service.getItem('some.key2'), 'some.value2');
assert.strictEqual(service.getItem('some.key3'), 'some.value3');
assert.strictEqual(service.getItem('some.key4'), undefined);
});
test('Used after close', async function () {
const storageFile = join(testDir, 'storage.json');
writeFileSync(storageFile, '');
const service = new FileStorage(URI.file(storageFile), logService, fileService);
await service.init();
service.setItem('some.key1', 'some.value1');
service.setItem('some.key2', 'some.value2');
service.setItem('some.key3', 'some.value3');
service.setItem('some.key4', 'some.value4');
await service.close();
service.setItem('some.key5', 'some.marker');
const contents = readFileSync(storageFile).toString();
assert.ok(contents.includes('some.value1'));
assert.ok(!contents.includes('some.marker'));
await service.close();
});
test('Closed before init', async function () {
const storageFile = join(testDir, 'storage.json');
writeFileSync(storageFile, '');
const service = new FileStorage(URI.file(storageFile), logService, fileService);
service.setItem('some.key1', 'some.value1');
service.setItem('some.key2', 'some.value2');
service.setItem('some.key3', 'some.value3');
service.setItem('some.key4', 'some.value4');
await service.close();
const contents = readFileSync(storageFile).toString();
assert.strictEqual(contents.length, 0);
});
});

View File

@@ -1,57 +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 * as assert from 'assert';
import { tmpdir } from 'os';
import { promises } from 'fs';
import { join } from 'vs/base/common/path';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { FileStorage } from 'vs/platform/state/node/stateService';
import { rimraf, writeFileSync } from 'vs/base/node/pfs';
flakySuite('StateService', () => {
let testDir: string;
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'stateservice');
return promises.mkdir(testDir, { recursive: true });
});
teardown(() => {
return rimraf(testDir);
});
test('Basics', async function () {
const storageFile = join(testDir, 'storage.json');
writeFileSync(storageFile, '');
let service = new FileStorage(storageFile, () => null);
service.setItem('some.key', 'some.value');
assert.strictEqual(service.getItem('some.key'), 'some.value');
service.removeItem('some.key');
assert.strictEqual(service.getItem('some.key', 'some.default'), 'some.default');
assert.ok(!service.getItem('some.unknonw.key'));
service.setItem('some.other.key', 'some.other.value');
service = new FileStorage(storageFile, () => null);
assert.strictEqual(service.getItem('some.other.key'), 'some.other.value');
service.setItem('some.other.key', 'some.other.value');
assert.strictEqual(service.getItem('some.other.key'), 'some.other.value');
service.setItem('some.undefined.key', undefined);
assert.strictEqual(service.getItem('some.undefined.key', 'some.default'), 'some.default');
service.setItem('some.null.key', null);
assert.strictEqual(service.getItem('some.null.key', 'some.default'), 'some.default');
});
});

View File

@@ -3,17 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { StorageScope, IS_NEW_KEY, AbstractStorageService } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { StorageScope, IS_NEW_KEY, AbstractStorageService, StorageTarget } from 'vs/platform/storage/common/storage';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { IFileService, FileChangeType } from 'vs/platform/files/common/files';
import { IStorage, Storage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { IStorage, Storage, IStorageDatabase, IUpdateRequest, InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
import { Promises } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { ILogService } from 'vs/platform/log/common/log';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { joinPath } from 'vs/base/common/resources';
export class BrowserStorageService extends AbstractStorageService {
@@ -22,40 +22,41 @@ export class BrowserStorageService extends AbstractStorageService {
private globalStorage: IStorage | undefined;
private workspaceStorage: IStorage | undefined;
private globalStorageDatabase: FileStorageDatabase | undefined;
private workspaceStorageDatabase: FileStorageDatabase | undefined;
private globalStorageFile: URI | undefined;
private workspaceStorageFile: URI | undefined;
private globalStorageDatabase: IIndexedDBStorageDatabase | undefined;
private workspaceStorageDatabase: IIndexedDBStorageDatabase | undefined;
get hasPendingUpdate(): boolean {
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
return Boolean(this.globalStorageDatabase?.hasPendingUpdate || this.workspaceStorageDatabase?.hasPendingUpdate);
}
constructor(
private readonly payload: IWorkspaceInitializationPayload,
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IFileService private readonly fileService: IFileService
) {
super({ flushInterval: BrowserStorageService.BROWSER_DEFAULT_FLUSH_INTERVAL });
}
private getId(scope: StorageScope): string {
return scope === StorageScope.GLOBAL ? 'global' : this.payload.id;
}
protected async doInitialize(): Promise<void> {
// Ensure state folder exists
const stateRoot = joinPath(this.environmentService.userRoamingDataHome, 'state');
await this.fileService.createFolder(stateRoot);
// Create Storage in Parallel
const [workspaceStorageDatabase, globalStorageDatabase] = await Promises.settled([
IndexedDBStorageDatabase.create(this.getId(StorageScope.WORKSPACE), this.logService),
IndexedDBStorageDatabase.create(this.getId(StorageScope.GLOBAL), this.logService)
]);
// Workspace Storage
this.workspaceStorageFile = joinPath(stateRoot, `${this.payload.id}.json`);
this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, false /* do not watch for external changes */, this.fileService));
this.workspaceStorageDatabase = this._register(workspaceStorageDatabase);
this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase));
this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key)));
// Global Storage
this.globalStorageFile = joinPath(stateRoot, 'global.json');
this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, true /* watch for external changes */, this.fileService));
this.globalStorageDatabase = this._register(globalStorageDatabase);
this.globalStorage = this._register(new Storage(this.globalStorageDatabase));
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
@@ -68,6 +69,7 @@ export class BrowserStorageService extends AbstractStorageService {
// Check to see if this is the first time we are "opening" the application
const firstOpen = this.globalStorage.getBoolean(IS_NEW_KEY);
if (firstOpen === undefined) {
await this.migrateOldStorage(StorageScope.GLOBAL); // TODO@bpasero remove browser storage migration
this.globalStorage.set(IS_NEW_KEY, true);
} else if (firstOpen) {
this.globalStorage.set(IS_NEW_KEY, false);
@@ -76,18 +78,49 @@ export class BrowserStorageService extends AbstractStorageService {
// Check to see if this is the first time we are "opening" this workspace
const firstWorkspaceOpen = this.workspaceStorage.getBoolean(IS_NEW_KEY);
if (firstWorkspaceOpen === undefined) {
await this.migrateOldStorage(StorageScope.WORKSPACE); // TODO@bpasero remove browser storage migration
this.workspaceStorage.set(IS_NEW_KEY, true);
} else if (firstWorkspaceOpen) {
this.workspaceStorage.set(IS_NEW_KEY, false);
}
}
private async migrateOldStorage(scope: StorageScope): Promise<void> {
try {
const stateRoot = joinPath(this.environmentService.userRoamingDataHome, 'state');
if (scope === StorageScope.GLOBAL) {
const globalStorageFile = joinPath(stateRoot, 'global.json');
const globalItemsRaw = await this.fileService.readFile(globalStorageFile);
const globalItems = new Map<string, string>(JSON.parse(globalItemsRaw.value.toString()));
for (const [key, value] of globalItems) {
this.globalStorage?.set(key, value);
}
await this.fileService.del(globalStorageFile);
} else if (scope === StorageScope.WORKSPACE) {
const workspaceStorageFile = joinPath(stateRoot, `${this.payload.id}.json`);
const workspaceItemsRaw = await this.fileService.readFile(workspaceStorageFile);
const workspaceItems = new Map<string, string>(JSON.parse(workspaceItemsRaw.value.toString()));
for (const [key, value] of workspaceItems) {
this.workspaceStorage?.set(key, value);
}
await this.fileService.del(workspaceStorageFile);
}
} catch (error) {
// ignore
}
}
protected getStorage(scope: StorageScope): IStorage | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage;
}
protected getLogDetails(scope: StorageScope): string | undefined {
return scope === StorageScope.GLOBAL ? this.globalStorageFile?.toString() : this.workspaceStorageFile?.toString();
return this.getId(scope);
}
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
@@ -118,144 +151,214 @@ export class BrowserStorageService extends AbstractStorageService {
// get triggered in this phase.
this.dispose();
}
async clear(): Promise<void> {
// Clear key/values
for (const scope of [StorageScope.GLOBAL, StorageScope.WORKSPACE]) {
for (const target of [StorageTarget.USER, StorageTarget.MACHINE]) {
for (const key of this.keys(scope, target)) {
this.remove(key, scope);
}
}
await this.getStorage(scope)?.whenFlushed();
}
// Clear databases
await Promises.settled([
this.globalStorageDatabase?.clear() ?? Promise.resolve(),
this.workspaceStorageDatabase?.clear() ?? Promise.resolve()
]);
}
}
export class FileStorageDatabase extends Disposable implements IStorageDatabase {
interface IIndexedDBStorageDatabase extends IStorageDatabase, IDisposable {
private readonly _onDidChangeItemsExternal = this._register(new Emitter<IStorageItemsChangeEvent>());
readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event;
/**
* Whether an update in the DB is currently pending
* (either update or delete operation).
*/
readonly hasPendingUpdate: boolean;
private cache: Map<string, string> | undefined;
/**
* For testing only.
*/
clear(): Promise<void>;
}
private pendingUpdate: Promise<void> = Promise.resolve();
class InMemoryIndexedDBStorageDatabase extends InMemoryStorageDatabase implements IIndexedDBStorageDatabase {
private _hasPendingUpdate = false;
get hasPendingUpdate(): boolean {
return this._hasPendingUpdate;
readonly hasPendingUpdate = false;
async clear(): Promise<void> {
(await this.getItems()).clear();
}
private isWatching = false;
dispose(): void {
// No-op
}
}
constructor(
private readonly file: URI,
private readonly watchForExternalChanges: boolean,
@IFileService private readonly fileService: IFileService
export class IndexedDBStorageDatabase extends Disposable implements IIndexedDBStorageDatabase {
static async create(id: string, logService: ILogService): Promise<IIndexedDBStorageDatabase> {
try {
const database = new IndexedDBStorageDatabase(id, logService);
await database.whenConnected;
return database;
} catch (error) {
logService.error(`[IndexedDB Storage ${id}] create(): ${toErrorMessage(error, true)}`);
return new InMemoryIndexedDBStorageDatabase();
}
}
private static readonly STORAGE_DATABASE_PREFIX = 'vscode-web-state-db-';
private static readonly STORAGE_OBJECT_STORE = 'ItemTable';
readonly onDidChangeItemsExternal = Event.None; // IndexedDB currently does not support observers (https://github.com/w3c/IndexedDB/issues/51)
private pendingUpdate: Promise<void> | undefined = undefined;
get hasPendingUpdate(): boolean { return !!this.pendingUpdate; }
private readonly name: string;
private readonly whenConnected: Promise<IDBDatabase>;
private constructor(
id: string,
private readonly logService: ILogService
) {
super();
this.name = `${IndexedDBStorageDatabase.STORAGE_DATABASE_PREFIX}${id}`;
this.whenConnected = this.connect();
}
private async ensureWatching(): Promise<void> {
if (this.isWatching || !this.watchForExternalChanges) {
return;
}
private connect(): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
const request = window.indexedDB.open(this.name);
const exists = await this.fileService.exists(this.file);
if (this.isWatching || !exists) {
return; // file must exist to be watched
}
// Create `ItemTable` object-store when this DB is new
request.onupgradeneeded = () => {
request.result.createObjectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE);
};
this.isWatching = true;
// IndexedDB opened successfully
request.onsuccess = () => resolve(request.result);
this._register(this.fileService.watch(this.file));
this._register(this.fileService.onDidFilesChange(e => {
if (document.hasFocus()) {
return; // optimization: ignore changes from ourselves by checking for focus
}
if (!e.contains(this.file, FileChangeType.UPDATED)) {
return; // not our file
}
this.onDidStorageChangeExternal();
}));
// Fail on error (we will then fallback to in-memory DB)
request.onerror = () => reject(request.error);
});
}
private async onDidStorageChangeExternal(): Promise<void> {
const items = await this.doGetItemsFromFile();
getItems(): Promise<Map<string, string>> {
return new Promise<Map<string, string>>(async resolve => {
const items = new Map<string, string>();
// pervious cache, diff for changes
let changed = new Map<string, string>();
let deleted = new Set<string>();
if (this.cache) {
items.forEach((value, key) => {
const existingValue = this.cache?.get(key);
if (existingValue !== value) {
changed.set(key, value);
// Open a IndexedDB Cursor to iterate over key/values
const db = await this.whenConnected;
const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readonly');
const objectStore = transaction.objectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE);
const cursor = objectStore.openCursor();
if (!cursor) {
return resolve(items); // this means the `ItemTable` was empty
}
// Iterate over rows of `ItemTable` until the end
cursor.onsuccess = () => {
if (cursor.result) {
// Keep cursor key/value in our map
if (typeof cursor.result.value === 'string') {
items.set(cursor.result.key.toString(), cursor.result.value);
}
// Advance cursor to next row
cursor.result.continue();
} else {
resolve(items); // reached end of table
}
});
};
this.cache.forEach((_, key) => {
if (!items.has(key)) {
deleted.add(key);
}
});
}
const onError = (error: Error | null) => {
this.logService.error(`[IndexedDB Storage ${this.name}] getItems(): ${toErrorMessage(error, true)}`);
// no previous cache, consider all as changed
else {
changed = items;
}
resolve(items);
};
// Update cache
this.cache = items;
// Emit as event as needed
if (changed.size > 0 || deleted.size > 0) {
this._onDidChangeItemsExternal.fire({ changed, deleted });
}
}
async getItems(): Promise<Map<string, string>> {
if (!this.cache) {
try {
this.cache = await this.doGetItemsFromFile();
} catch (error) {
this.cache = new Map();
}
}
return this.cache;
}
private async doGetItemsFromFile(): Promise<Map<string, string>> {
await this.pendingUpdate;
const itemsRaw = await this.fileService.readFile(this.file);
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
return new Map(JSON.parse(itemsRaw.value.toString()));
// Error handlers
cursor.onerror = () => onError(cursor.error);
transaction.onerror = () => onError(transaction.error);
});
}
async updateItems(request: IUpdateRequest): Promise<void> {
const items = await this.getItems();
this.pendingUpdate = this.doUpdateItems(request);
try {
await this.pendingUpdate;
} finally {
this.pendingUpdate = undefined;
}
}
if (request.insert) {
request.insert.forEach((value, key) => items.set(key, value));
private async doUpdateItems(request: IUpdateRequest): Promise<void> {
// Return early if the request is empty
const toInsert = request.insert;
const toDelete = request.delete;
if ((!toInsert && !toDelete) || (toInsert?.size === 0 && toDelete?.size === 0)) {
return;
}
if (request.delete) {
request.delete.forEach(key => items.delete(key));
}
// Update `ItemTable` with inserts and/or deletes
return new Promise<void>(async (resolve, reject) => {
const db = await this.whenConnected;
const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite');
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
const objectStore = transaction.objectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE);
// Inserts
if (toInsert) {
for (const [key, value] of toInsert) {
objectStore.put(value, key);
}
}
// Deletes
if (toDelete) {
for (const key of toDelete) {
objectStore.delete(key);
}
}
});
}
async close(): Promise<void> {
const db = await this.whenConnected;
// Wait for pending updates to having finished
await this.pendingUpdate;
this.pendingUpdate = (async () => {
try {
this._hasPendingUpdate = true;
await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(Array.from(items.entries()))));
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
} finally {
this._hasPendingUpdate = false;
}
})();
return this.pendingUpdate;
// Finally, close IndexedDB
return db.close();
}
close(): Promise<void> {
return this.pendingUpdate;
clear(): Promise<void> {
return new Promise<void>(async (resolve, reject) => {
const db = await this.whenConnected;
const transaction = db.transaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite');
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
// Clear every row in the `ItemTable`
const objectStore = transaction.objectStore(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE);
objectStore.clear();
});
}
}

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