Merge VS Code 1.26.1 (#2394)

* Squash merge commits for 1.26 (#1) (#2323)

* Polish tag search as per feedback (#55269)

* Polish tag search as per feedback

* Updated regex

* Allow users to opt-out of features that send online requests in the background (#55097)

* settings sweep #54690

* Minor css tweaks to enable eoverflow elipsis in more places (#55277)

* fix an issue with titlebarheight when not scaling with zoom

* Settings descriptions update #54690

* fixes #55209

* Settings editor - many padding fixes

* More space above level 2 label

* Fixing Cannot debug npm script using Yarn #55103

* Settings editor - show ellipsis when description overflows

* Settings editor - ... fix measuring around links, relayout

* Setting descriptions

* Settings editor - fix ... for some short lines, fix select container width

* Settings editor - overlay trees so scrollable shadow is full width

* Fix #54133 - missing extension settings after reload

* Settings color token description tweak

* Settings editor - disable overflow indicator temporarily, needs to be faster

* Added command to Run the selected npm script

* fixes #54452

* fixes #54929

* fixes #55248

* prefix command with extension name

* Contribute run selected to the context menu

* node-debug@1.26.6

* Allow terminal rendererType to be swapped out at runtime

Part of #53274
Fixes #55344

* Settings editor - fix not focusing search when restoring editor
setInput must be actually async. Will be fixed naturally when we aren't using winJS promises...

* Settings editor - TOC should only expand the section with a selected item

* Bump node-debug2

* Settings editor - Tree focus outlines

* Settings editor - don't blink the scrollbar when toc selection changes
And hide TOC correctly when the editor is narrow

* Settings editor - header rows should not be selectable

* fixes #54877

* change debug assignee to isi

* Settings sweep (#54690)

* workaround for #55051

* Settings sweep (#54690)

* settings sweep

#54690

* Don't try closing tags when you type > after another >

* Describe what implementation code lens does

Fixes #55370

* fix javadoc formatter setting description

* fixes #55325

* update to officical TS version

* Settings editor - Even more padding, use semibold instead of bold

* Fix #55357 - fix TOC twistie

* fixes #55288

* explorer: refresh on di change file system provider registration

fixes #53256

* Disable push to Linux repo to test standalone publisher

* New env var to notify log level to extensions #54001

* Disable snippets in extension search (when not in suggest dropdown) (#55281)

* Disable snippits in extension search (when not in suggest dropdown)

* Add monaco input contributions

* Fix bug preventing snippetSuggestions from taking effect in sub-editors

* Latest emmet helper to fix #52366

* Fix comment updates for threads within same file

* Allow extensions to log telemetry to log files #54001

* Pull latest css grammar

* files.exclude control - use same style for "add" vs "edit"

* files.exclude control - focus/keyboard behavior

* don't show menubar too early

* files.exclude - better styling

* Place cursor at end of extensions search box on autofill (#55254)

* Place cursor at end of extensions search box on autofill

* Use position instead of selection

* fix linux build issue (empty if block)

* Settings editor - fix extension category prefixes

* Settings editor - add simple ellipsis for first line that overflows, doesn't cover case when first line does not overflow but there is more text, TODO

* File/Text search provider docs

* Fixes #52655

* Include epoch (#55008)

* Fixes #53385

* Fixes #49480

*  VS Code Insiders (Users) not opening Fixes #55353

* Better handling of the case when the extension host fails to start

* Fixes #53966

*  Remove confusing Start from wordPartLeft commands ID

* vscode-xterm@3.6.0-beta12

Fixes #55488

* Initial size is set to infinity!! Fixes #55461

* Polish embeddedEditorBackground

* configuration service misses event

* Fix #55224 - fix duplicate results in multiroot workspace from splitting the diskseach query

* Select all not working in issue reporter on mac, fixes #55424

* Disable fuzzy matching for extensions autosuggest (#55498)

* Fix clipping of extensions search border in some third party themes (#55504)

* fixes #55538

* Fix bug causing an aria alert to not be shown the third time
 (and odd numbers thereafter)

* Settings editor - work around rendering glitch with webkit-line-clamp

* Settings editor - revert earlier '...' changes

* Settings editor - move enumDescription to its own div, because it disturbs -webkit-line-clamp for some reason

* Settings editor - better overflow indicator

* Don't show existing filters in autocomplete (#55495)

* Dont show existing filters in autocomplete

* Simplify

* Settings Editor: Add aria labels for input elements Fixes: #54836 (#55543)

* fixes #55223

* Update vscode-css-languageservice to 3.0.10-next.1

* Fix #55509 - settings navigation

* Fix #55519

* Fix #55520

* FIx #55524

* Fix #55556 - include wordSeparators in all search queries, so findTextInFiles can respect isWordMatch correctly

* oss updates for endgame

* Fix unit tests

* fixes #55522

* Avoid missing manifest error from bubbling up #54757

* Settings format crawl

* Search provider - Fix FileSearchProvider to return array, not progress

* Fix #55598

* Settings editor - fix NPE rendering settings with no description

* dont render inden guides in search box (#55600)

* fixes #55454

* More settings crawl

* Another change for #55598 - maxResults applies to FileSearch and TextSearch but not FileIndex

* Fix FileSearchProvider unit tests for progress change

* fixes #55561

* Settings description update for #54690

* Update setting descriptions for online services

* Minor edits

* fixes #55513

* fixes #55451

* Fix #55612 - fix findTextInFiles cancellation

* fixes #55539

* More setting description tweaks

* Setting to disable online experiments #54354

* fixes #55507

* fixes #55515

* Show online services action only in Insiders for now

* Settings editor - change toc behavior default to 'filter'

* Settings editor - nicer filter count style during search

* Fix #55617 - search viewlet icons

* Settings editor - better styling for element count indicator

* SearchProvider - fix NPE when searching extraFileResources

* Allow extends to work without json suffix

Fixes #16905

* Remove accessability options logic entirely

Follow up on #55451

* use latest version of DAP

* fixes #55490

* fixes #55122

* fixes #52332

* Avoid assumptions about git: URIs (fixes #36236)

* relative path for descriptions

* resourece: get rid of isFile context key

fixes #48275

* Register previous ids for compatibility (#53497)

* more tuning for #48275

* no need to always re-read "files explorer"

fixes #52003

* read out active composites properly

fixes #51967

* Update link colors for hc theme to meet color contrast ratio, fixes #55651

Also updated link color for `textLinkActiveForeground` to be the same as `textLinkForeground` as it wasn't properly updated

* detect 'winpty-agent.exe'; fixes #55672

* node-debug@1.26.7

* reset counter on new label

* Settings editor - fix multiple setting links in one description

* Settings editor - color code blocks in setting descriptions, fix #55532

* Settings editor - hover color in TOC

* Settings editor - fix navigation NPE

* Settings editor - fix text control width

* Settings editor - maybe fix #55684

* Fix bug causing cursor to not move on paste

* fixes #53582

* Use ctrlCmd instead of ctrl for go down from search box

* fixes #55264

* fixes #55456

* filter for spcaes before triggering search (#55611)

* Fix #55698 - don't lose filtered TOC counts when refreshing TOC

* fixes #55421

* fixes #28979

* fixes #55576

* only add check for updates to windows/linux help

* readonly files: append decoration to label

fixes #53022

* debug: do not show toolbar while initialising

fixes #55026

* Opening launch.json should not activate debug extensions

fixes #55029

* fixes #55435

* fixes #55434

* fixes #55439

* trigger menu only on altkey up

* Fix #50555 - fix settings editor memory leak

* Fix #55712 - no need to focus 'a' anymore when restoring control focus after tree render

* fixes #55335

* proper fix for readonly model

fixes #53022

* improve FoldingRangeKind spec (for #55686)

* Use class with static fields (fixes #55494)

* Fixes #53671

* fixes #54630

* [html] should disable ionic suggestions by default. Currently forces deprecated Ionic v1 suggestions in .html files while typing. Fixes #53324

* cleanup deps

* debug issues back to andre

* update electron for smoketest

* Fix #55757 - prevent settings tabs from overflowing

* Fix #53897 - revert setting menu defaults to old editor

* Add enum descriptions to `typescript.preferences.importModuleSpecifier`

* Fix #55767 - leaking style elements from settings editor

* Fix #55521 - prevent flashing when clicking in exclude control

* Update Git modified color for contrast ratio, fixes #53140

* Revert "Merge branch 'master' of github.com:Microsoft/vscode"

This reverts commit bf46b6bfbae0cab99c2863e1244a916181fa9fbc, reversing
changes made to e275a424483dfb4ed33b428c97d5e2c441d6b917.

* Revert "Revert "Merge branch 'master' of github.com:Microsoft/vscode""

This reverts commit 53949d963f39e40757557c6526332354a31d9154.

* don't ask to install an incomplete menu

* Fix NPE in terminal AccessibilityManager

Fixes #55744

* don't display fallback menu unless we've closed the last window

* fixes #55547

* Fix smoke tests for extension search box

* Update OSSREADME.json for Electron 2.0.5

* Update distro

Includes Chromium license changes

* fix #55455

* fix #55865

* fixes #55893

* Fix bug causing workspace recommendations to go away upon ignoring a recommendation (#55805)

* Fix bug causing workspace recommendations to go away upon ignoring a recommendation

* ONly show on @recommended or @recommended:workspace

* Make more consistant

* Fix #55911

* Understand json activity (#55926)

* Understand json file activity

* Refactoring

* adding composer.json

* Distro update for experiments

* use terminal.processId for auto-attach; fixes #55918

* Reject invalid URI with vscode.openFolder (for #55891)

* improve win32 setup system vs user detection

fixes #55840

fixes #55840

delay winreg import

related to #55840

show notification earlier

related to #55840

fix #55840

update inno setup message

related to #55840

* Fix #55593 - this code only operates on local paths, so use fsPath and Uri.file instead

* Bring back the old menu due to electron 2.0 issues (#55913)

* add the old menu back for native menus

* make menu labels match

* `vscode.openFolder`: treat missing URI schema gracefully (for #55891)

* delay EH reattach; fixes #55955

* Mark all json files under appSettingsHome as settings

* Use localized strings for telemetry opt-out

* Exception when saving file editor opened from remote file provider (fixes #55051)

* Remove terminal menu from stable

Fixes 56003

* VSCode Insiders crashes on open with TypeError: Cannot read property 'lastIndexOf' of undefined. Fixes #54933

* improve fix for #55891

* fix #55916

* Improve #55891

* increase EH debugging restart delay; fixes #55955

* Revert "Don't include non-resource entries in history quick pick"

This reverts commit 37209a838e9f7e9abe6dc53ed73cdf1e03b72060.

* Diff editor: horizontal scrollbar height is smaller (fixes #56062)

* improve openFolder uri fix (correctly treat backslashes)

* fixes #56116
repair ipc for native menubar keybindings

* Fix #56240 - Open the JSON settings editor instead of the UI editor

* Fix #55536

* uriDisplay: if no formatter is registered fall back to getPathlabel

fixes #56104

* VSCode hangs when opening python file. Fixes #56377

* VS Code Hangs When Opening Specific PowerShell File. Fixes #56430

* Fix #56433 - search extraFileResources even when no folders open

* Workaround #55649

* Fix in master #56371

* Fix tests #56371

* Fix in master #56317

* increase version to 1.26.1

* Fixes #56387: Handle SIGPIPE in extension host

* fixes #56185

* Fix merge issues (part 1)

* Fix build breaks (part 1)

* Build breaks (part 2)

* Build breaks (part 3)

* More build breaks (part 4)

* Fix build breaks (part 5)

* WIP

* Fix menus

* Render query result and message panels (#2363)

* Put back query editor hot exit changes

* Fix grid changes that broke profiler (#2365)

* Update APIs for saving query editor state

* Fix restore view state for profiler and edit data

* Updating custom default themes to support 4.5:1 contrast ratio

* Test updates

* Fix Extension Manager and Windows Setup

* Update license headers

* Add appveyor and travis files back

* Fix hidden modal dropdown issue
This commit is contained in:
Karl Burtram
2018-09-04 14:55:00 -07:00
committed by GitHub
parent 3763278366
commit 81329fa7fa
2638 changed files with 118456 additions and 64012 deletions

View File

@@ -7,7 +7,7 @@
import { localize } from 'vs/nls';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IMenu, MenuItemAction, IMenuActionOptions, ICommandAction } from 'vs/platform/actions/common/actions';
import { IMenu, MenuItemAction, IMenuActionOptions, ICommandAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { IAction } from 'vs/base/common/actions';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
@@ -16,7 +16,6 @@ import { Emitter } from 'vs/base/common/event';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { createCSSRule } from 'vs/base/browser/dom';
import URI from 'vs/base/common/uri';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { isWindows, isLinux } from 'vs/base/common/platform';
@@ -26,6 +25,7 @@ class AlternativeKeyEmitter extends Emitter<boolean> {
private _subscriptions: IDisposable[] = [];
private _isPressed: boolean;
private static instance: AlternativeKeyEmitter;
private _suppressAltKeyUp: boolean = false;
private constructor(contextMenuService: IContextMenuService) {
super();
@@ -33,7 +33,16 @@ class AlternativeKeyEmitter extends Emitter<boolean> {
this._subscriptions.push(domEvent(document.body, 'keydown')(e => {
this.isPressed = e.altKey || ((isWindows || isLinux) && e.shiftKey);
}));
this._subscriptions.push(domEvent(document.body, 'keyup')(e => this.isPressed = false));
this._subscriptions.push(domEvent(document.body, 'keyup')(e => {
if (this.isPressed) {
if (this._suppressAltKeyUp) {
e.preventDefault();
}
}
this._suppressAltKeyUp = false;
this.isPressed = false;
}));
this._subscriptions.push(domEvent(document.body, 'mouseleave')(e => this.isPressed = false));
this._subscriptions.push(domEvent(document.body, 'blur')(e => this.isPressed = false));
// Workaround since we do not get any events while a context menu is shown
@@ -49,6 +58,12 @@ class AlternativeKeyEmitter extends Emitter<boolean> {
this.fire(this._isPressed);
}
suppressAltKeyUp() {
// Sometimes the native alt behavior needs to be suppresed since the alt was already used as an alternative key
// Example: windows behavior to toggle tha top level menu #44396
this._suppressAltKeyUp = true;
}
static getInstance(contextMenuService: IContextMenuService) {
if (!AlternativeKeyEmitter.instance) {
AlternativeKeyEmitter.instance = new AlternativeKeyEmitter(contextMenuService);
@@ -63,17 +78,25 @@ class AlternativeKeyEmitter extends Emitter<boolean> {
}
}
export function fillInActions(menu: IMenu, options: IMenuActionOptions, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, contextMenuService: IContextMenuService, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {
export function fillInContextMenuActions(menu: IMenu, options: IMenuActionOptions, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, contextMenuService: IContextMenuService, isPrimaryGroup?: (group: string) => boolean): void {
const groups = menu.getActions(options);
if (groups.length === 0) {
return;
}
const getAlternativeActions = AlternativeKeyEmitter.getInstance(contextMenuService).isPressed;
fillInActions(groups, target, getAlternativeActions, isPrimaryGroup);
}
export function fillInActionBarActions(menu: IMenu, options: IMenuActionOptions, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): void {
const groups = menu.getActions(options);
// Action bars handle alternative actions on their own so the alternative actions should be ignored
fillInActions(groups, target, false, isPrimaryGroup);
}
// {{SQL CARBON EDIT}} add export modifier
export function fillInActions(groups: [string, (MenuItemAction | SubmenuItemAction)[]][], target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, getAlternativeActions, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {
for (let tuple of groups) {
let [group, actions] = tuple;
if (getAlternativeActions) {
actions = actions.map(a => !!a.alt ? a.alt : a);
actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a);
}
if (isPrimaryGroup(group)) {
@@ -129,7 +152,7 @@ export class MenuItemActionItem extends ActionItem {
static readonly ICON_PATH_TO_CSS_RULES: Map<string /* path*/, string /* CSS rule */> = new Map<string, string>();
private _wantsAltCommand: boolean = false;
private _wantsAltCommand: boolean;
private _itemClassDispose: IDisposable;
constructor(
@@ -138,7 +161,7 @@ export class MenuItemActionItem extends ActionItem {
@INotificationService protected _notificationService: INotificationService,
@IContextMenuService private readonly _contextMenuService: IContextMenuService
) {
super(undefined, _action, { icon: !!(_action.class || _action.item.iconPath), label: !_action.class && !_action.item.iconPath });
super(undefined, _action, { icon: !!(_action.class || _action.item.iconLocation), label: !_action.class && !_action.item.iconLocation });
}
protected get _commandAction(): IAction {
@@ -149,6 +172,11 @@ export class MenuItemActionItem extends ActionItem {
event.preventDefault();
event.stopPropagation();
const altKey = AlternativeKeyEmitter.getInstance(this._contextMenuService);
if (altKey.isPressed) {
altKey.suppressAltKeyUp();
}
this.actionRunner.run(this._commandAction)
.done(undefined, err => this._notificationService.error(err));
}
@@ -159,7 +187,8 @@ export class MenuItemActionItem extends ActionItem {
this._updateItemClass(this._action.item);
let mouseOver = false;
let alternativeKeyDown = false;
const alternativeKeyEmitter = AlternativeKeyEmitter.getInstance(this._contextMenuService);
let alternativeKeyDown = alternativeKeyEmitter.isPressed;
const updateAltState = () => {
const wantsAltCommand = mouseOver && alternativeKeyDown;
@@ -171,7 +200,7 @@ export class MenuItemActionItem extends ActionItem {
}
};
this._callOnDispose.push(AlternativeKeyEmitter.getInstance(this._contextMenuService).event(value => {
this._callOnDispose.push(alternativeKeyEmitter.event(value => {
alternativeKeyDown = value;
updateAltState();
}));
@@ -217,16 +246,18 @@ export class MenuItemActionItem extends ActionItem {
dispose(this._itemClassDispose);
this._itemClassDispose = undefined;
if (item.iconPath) {
if (item.iconLocation) {
let iconClass: string;
if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(item.iconPath.dark)) {
iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(item.iconPath.dark);
const iconPathMapKey = item.iconLocation.dark.toString();
if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) {
iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(iconPathMapKey);
} else {
iconClass = ids.nextId();
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath.light || item.iconPath.dark).toString()}")`);
createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath.dark).toString()}")`);
MenuItemActionItem.ICON_PATH_TO_CSS_RULES.set(item.iconPath.dark, iconClass);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${(item.iconLocation.light || item.iconLocation.dark).toString()}")`);
createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${item.iconLocation.dark.toString()}")`);
MenuItemActionItem.ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass);
}
this.$e.getHTMLElement().classList.add('icon', iconClass);

View File

@@ -13,20 +13,28 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import URI, { UriComponents } from 'vs/base/common/uri';
export interface ILocalizedString {
value: string;
original: string;
}
export interface ICommandAction {
export interface IBaseCommandAction {
id: string;
title: string | ILocalizedString;
category?: string | ILocalizedString;
iconPath?: { dark: string; light?: string; };
}
export interface ICommandAction extends IBaseCommandAction {
iconLocation?: { dark: URI; light?: URI; };
precondition?: ContextKeyExpr;
}
export interface ISerializableCommandAction extends IBaseCommandAction {
iconLocation?: { dark: UriComponents; light?: UriComponents; };
}
export interface IMenuItem {
command: ICommandAction;
alt?: ICommandAction;
@@ -35,6 +43,22 @@ export interface IMenuItem {
order?: number;
}
export interface ISubmenuItem {
title: string | ILocalizedString;
submenu: MenuId;
when?: ContextKeyExpr;
group?: 'navigation' | string;
order?: number;
}
export function isIMenuItem(item: IMenuItem | ISubmenuItem): item is IMenuItem {
return (item as IMenuItem).command !== undefined;
}
export function isISubmenuItem(item: IMenuItem | ISubmenuItem): item is ISubmenuItem {
return (item as ISubmenuItem).submenu !== undefined;
}
export class MenuId {
private static ID = 1;
@@ -42,6 +66,7 @@ export class MenuId {
static readonly EditorTitle = new MenuId();
static readonly EditorTitleContext = new MenuId();
static readonly EditorContext = new MenuId();
static readonly EmptyEditorGroupContext = new MenuId();
static readonly ExplorerContext = new MenuId();
static readonly OpenEditorsContext = new MenuId();
static readonly ProblemsPanelContext = new MenuId();
@@ -60,9 +85,25 @@ export class MenuId {
static readonly ViewItemContext = new MenuId();
static readonly TouchBarContext = new MenuId();
static readonly SearchContext = new MenuId();
static readonly MenubarFileMenu = new MenuId();
static readonly MenubarEditMenu = new MenuId();
static readonly MenubarRecentMenu = new MenuId();
static readonly MenubarSelectionMenu = new MenuId();
static readonly MenubarViewMenu = new MenuId();
static readonly MenubarAppearanceMenu = new MenuId();
static readonly MenubarLayoutMenu = new MenuId();
static readonly MenubarGoMenu = new MenuId();
static readonly MenubarSwitchEditorMenu = new MenuId();
static readonly MenubarSwitchGroupMenu = new MenuId();
static readonly MenubarDebugMenu = new MenuId();
static readonly MenubarNewBreakpointMenu = new MenuId();
static readonly MenubarTasksMenu = new MenuId();
static readonly MenubarPreferencesMenu = new MenuId();
static readonly MenubarHelpMenu = new MenuId();
// {{SQL CARBON EDIT}}
static readonly ObjectExplorerItemContext = new MenuId();
readonly id: string = String(MenuId.ID++);
}
@@ -73,7 +114,7 @@ export interface IMenuActionOptions {
export interface IMenu extends IDisposable {
onDidChange: Event<IMenu>;
getActions(options?: IMenuActionOptions): [string, MenuItemAction[]][];
getActions(options?: IMenuActionOptions): [string, (MenuItemAction | SubmenuItemAction)[]][];
}
export const IMenuService = createDecorator<IMenuService>('menuService');
@@ -88,15 +129,15 @@ export interface IMenuService {
export interface IMenuRegistry {
addCommand(userCommand: ICommandAction): boolean;
getCommand(id: string): ICommandAction;
appendMenuItem(menu: MenuId, item: IMenuItem): IDisposable;
getMenuItems(loc: MenuId): IMenuItem[];
appendMenuItem(menu: MenuId, item: IMenuItem | ISubmenuItem): IDisposable;
getMenuItems(loc: MenuId): (IMenuItem | ISubmenuItem)[];
}
export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
private _commands: { [id: string]: ICommandAction } = Object.create(null);
private _menuItems: { [loc: string]: IMenuItem[] } = Object.create(null);
private _menuItems: { [loc: string]: (IMenuItem | ISubmenuItem)[] } = Object.create(null);
addCommand(command: ICommandAction): boolean {
const old = this._commands[command.id];
@@ -108,7 +149,7 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
return this._commands[id];
}
appendMenuItem({ id }: MenuId, item: IMenuItem): IDisposable {
appendMenuItem({ id }: MenuId, item: IMenuItem | ISubmenuItem): IDisposable {
let array = this._menuItems[id];
if (!array) {
this._menuItems[id] = array = [item];
@@ -125,7 +166,7 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
};
}
getMenuItems({ id }: MenuId): IMenuItem[] {
getMenuItems({ id }: MenuId): (IMenuItem | ISubmenuItem)[] {
const result = this._menuItems[id] || [];
if (id === MenuId.CommandPalette.id) {
@@ -136,9 +177,12 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
return result;
}
private _appendImplicitItems(result: IMenuItem[]) {
private _appendImplicitItems(result: (IMenuItem | ISubmenuItem)[]) {
const set = new Set<string>();
for (const { command, alt } of result) {
const temp = result.filter(item => { return isIMenuItem(item); }) as IMenuItem[];
for (const { command, alt } of temp) {
set.add(command.id);
if (alt) {
set.add(alt.id);
@@ -167,6 +211,16 @@ export class ExecuteCommandAction extends Action {
}
}
export class SubmenuItemAction extends Action {
// private _options: IMenuActionOptions;
readonly item: ISubmenuItem;
constructor(item: ISubmenuItem) {
typeof item.title === 'string' ? super('', item.title, 'submenu') : super('', item.title.value, 'submenu');
this.item = item;
}
}
export class MenuItemAction extends ExecuteCommandAction {
private _options: IMenuActionOptions;

View File

@@ -9,10 +9,10 @@ import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { TPromise } from 'vs/base/common/winjs.base';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MenuId, MenuRegistry, MenuItemAction, IMenu, IMenuItem, IMenuActionOptions } from 'vs/platform/actions/common/actions';
import { MenuId, MenuRegistry, MenuItemAction, IMenu, IMenuItem, IMenuActionOptions, ISubmenuItem, SubmenuItemAction, isIMenuItem } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
type MenuItemGroup = [string, IMenuItem[]];
type MenuItemGroup = [string, (IMenuItem | ISubmenuItem)[]];
export class Menu implements IMenu {
@@ -66,14 +66,14 @@ export class Menu implements IMenu {
return this._onDidChange.event;
}
getActions(options: IMenuActionOptions): [string, MenuItemAction[]][] {
const result: [string, MenuItemAction[]][] = [];
getActions(options: IMenuActionOptions): [string, (MenuItemAction | SubmenuItemAction)[]][] {
const result: [string, (MenuItemAction | SubmenuItemAction)[]][] = [];
for (let group of this._menuGroups) {
const [id, items] = group;
const activeActions: MenuItemAction[] = [];
const activeActions: (MenuItemAction | SubmenuItemAction)[] = [];
for (const item of items) {
if (this._contextKeyService.contextMatchesRules(item.when)) {
const action = new MenuItemAction(item.command, item.alt, options, this._contextKeyService, this._commandService);
const action = isIMenuItem(item) ? new MenuItemAction(item.command, item.alt, options, this._contextKeyService, this._commandService) : new SubmenuItemAction(item);
action.order = item.order; //TODO@Ben order is menu item property, not an action property
activeActions.push(action);
}

View File

@@ -5,11 +5,15 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import URI from 'vs/base/common/uri';
export interface IBackupWorkspacesFormat {
rootWorkspaces: IWorkspaceIdentifier[];
folderWorkspaces: string[];
folderURIWorkspaces: string[];
emptyWorkspaces: string[];
// deprecated
folderWorkspaces?: string[]; // use folderURIWorkspaces instead
}
export const IBackupMainService = createDecorator<IBackupMainService>('backupMainService');
@@ -20,10 +24,14 @@ export interface IBackupMainService {
isHotExitEnabled(): boolean;
getWorkspaceBackups(): IWorkspaceIdentifier[];
getFolderBackupPaths(): string[];
getFolderBackupPaths(): URI[];
getEmptyWindowBackupPaths(): string[];
registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier, migrateFrom?: string): string;
registerFolderBackupSync(folderPath: string): string;
registerFolderBackupSync(folderUri: URI): string;
registerEmptyWindowBackupSync(backupFolder?: string): string;
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void;
unregisterFolderBackupSync(folderUri: URI): void;
unregisterEmptyWindowBackupSync(backupFolder: string): void;
}

View File

@@ -3,18 +3,23 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as arrays from 'vs/base/common/arrays';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import * as platform from 'vs/base/common/platform';
import * as extfs from 'vs/base/node/extfs';
import { IBackupWorkspacesFormat, IBackupMainService } from 'vs/platform/backup/common/backup';
import * as arrays from 'vs/base/common/arrays';
import { IBackupMainService, IBackupWorkspacesFormat } from 'vs/platform/backup/common/backup';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import URI from 'vs/base/common/uri';
import { isEqual as areResourcesEquals, getComparisonKey, hasToIgnoreCase } from 'vs/base/common/resources';
import { isEqual } from 'vs/base/common/paths';
import { Schemas } from 'vs/base/common/network';
export class BackupMainService implements IBackupMainService {
@@ -23,7 +28,9 @@ export class BackupMainService implements IBackupMainService {
protected backupHome: string;
protected workspacesJsonPath: string;
protected backups: IBackupWorkspacesFormat;
protected rootWorkspaces: IWorkspaceIdentifier[];
protected folderWorkspaces: URI[];
protected emptyWorkspaces: string[];
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@@ -43,17 +50,16 @@ export class BackupMainService implements IBackupMainService {
return [];
}
return this.backups.rootWorkspaces.slice(0); // return a copy
return this.rootWorkspaces.slice(0); // return a copy
}
public getFolderBackupPaths(): string[] {
public getFolderBackupPaths(): URI[] {
if (this.isHotExitOnExitAndWindowClose()) {
// Only non-folder windows are restored on main process launch when
// hot exit is configured as onExitAndWindowClose.
return [];
}
return this.backups.folderWorkspaces.slice(0); // return a copy
return this.folderWorkspaces.slice(0); // return a copy
}
public isHotExitEnabled(): boolean {
@@ -71,13 +77,16 @@ export class BackupMainService implements IBackupMainService {
}
public getEmptyWindowBackupPaths(): string[] {
return this.backups.emptyWorkspaces.slice(0); // return a copy
return this.emptyWorkspaces.slice(0); // return a copy
}
public registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier, migrateFrom?: string): string {
this.pushBackupPathsSync(workspace, this.backups.rootWorkspaces);
if (!this.rootWorkspaces.some(w => w.id === workspace.id)) {
this.rootWorkspaces.push(workspace);
this.saveSync();
}
const backupPath = path.join(this.backupHome, workspace.id);
const backupPath = this.getBackupPath(workspace.id);
if (migrateFrom) {
this.moveBackupFolderSync(backupPath, migrateFrom);
@@ -103,10 +112,28 @@ export class BackupMainService implements IBackupMainService {
}
}
public registerFolderBackupSync(folderPath: string): string {
this.pushBackupPathsSync(folderPath, this.backups.folderWorkspaces);
public unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void {
let index = arrays.firstIndex(this.rootWorkspaces, w => w.id === workspace.id);
if (index !== -1) {
this.rootWorkspaces.splice(index, 1);
this.saveSync();
}
}
return path.join(this.backupHome, this.getFolderHash(folderPath));
public registerFolderBackupSync(folderUri: URI): string {
if (!this.folderWorkspaces.some(uri => areResourcesEquals(folderUri, uri, hasToIgnoreCase(folderUri)))) {
this.folderWorkspaces.push(folderUri);
this.saveSync();
}
return this.getBackupPath(this.getFolderHash(folderUri));
}
public unregisterFolderBackupSync(folderUri: URI): void {
let index = arrays.firstIndex(this.folderWorkspaces, uri => areResourcesEquals(folderUri, uri, hasToIgnoreCase(folderUri)));
if (index !== -1) {
this.folderWorkspaces.splice(index, 1);
this.saveSync();
}
}
public registerEmptyWindowBackupSync(backupFolder?: string): string {
@@ -115,52 +142,23 @@ export class BackupMainService implements IBackupMainService {
if (!backupFolder) {
backupFolder = this.getRandomEmptyWindowId();
}
this.pushBackupPathsSync(backupFolder, this.backups.emptyWorkspaces);
return path.join(this.backupHome, backupFolder);
if (!this.emptyWorkspaces.some(w => isEqual(w, backupFolder, !platform.isLinux))) {
this.emptyWorkspaces.push(backupFolder);
this.saveSync();
}
return this.getBackupPath(backupFolder);
}
private pushBackupPathsSync(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void {
if (this.indexOf(workspaceIdentifier, target) === -1) {
target.push(workspaceIdentifier);
public unregisterEmptyWindowBackupSync(backupFolder: string): void {
let index = arrays.firstIndex(this.emptyWorkspaces, w => isEqual(w, backupFolder, !platform.isLinux));
if (index !== -1) {
this.emptyWorkspaces.splice(index, 1);
this.saveSync();
}
}
protected removeBackupPathSync(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void {
if (!target) {
return;
}
const index = this.indexOf(workspaceIdentifier, target);
if (index === -1) {
return;
}
target.splice(index, 1);
this.saveSync();
}
private indexOf(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): number {
if (!target) {
return -1;
}
const sanitizedWorkspaceIdentifier = this.sanitizeId(workspaceIdentifier);
return arrays.firstIndex(target, id => this.sanitizeId(id) === sanitizedWorkspaceIdentifier);
}
private sanitizeId(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string {
if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {
return this.sanitizePath(workspaceIdentifier);
}
return workspaceIdentifier.id;
}
protected loadSync(): void {
let backups: IBackupWorkspacesFormat;
try {
backups = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here
@@ -168,117 +166,168 @@ export class BackupMainService implements IBackupMainService {
backups = Object.create(null);
}
// Ensure rootWorkspaces is a object[]
if (backups.rootWorkspaces) {
const rws = backups.rootWorkspaces;
if (!Array.isArray(rws) || rws.some(r => typeof r !== 'object')) {
backups.rootWorkspaces = [];
}
} else {
backups.rootWorkspaces = [];
}
// read empty worrkspace backs first
this.emptyWorkspaces = this.validateEmptyWorkspaces(backups.emptyWorkspaces);
// Ensure folderWorkspaces is a string[]
if (backups.folderWorkspaces) {
const fws = backups.folderWorkspaces;
if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) {
backups.folderWorkspaces = [];
}
} else {
backups.folderWorkspaces = [];
}
// read workspace backups
this.rootWorkspaces = this.validateWorkspaces(backups.rootWorkspaces);
// Ensure emptyWorkspaces is a string[]
if (backups.emptyWorkspaces) {
const fws = backups.emptyWorkspaces;
if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) {
backups.emptyWorkspaces = [];
}
} else {
backups.emptyWorkspaces = [];
}
this.backups = this.dedupeBackups(backups);
// Validate backup workspaces
this.validateBackupWorkspaces(backups);
}
protected dedupeBackups(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat {
// De-duplicate folder/workspace backups. don't worry about cleaning them up any duplicates as
// they will be removed when there are no backups.
backups.folderWorkspaces = arrays.distinct(backups.folderWorkspaces, ws => this.sanitizePath(ws));
backups.rootWorkspaces = arrays.distinct(backups.rootWorkspaces, ws => this.sanitizePath(ws.id));
return backups;
}
private validateBackupWorkspaces(backups: IBackupWorkspacesFormat): void {
const staleBackupWorkspaces: { workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; backupPath: string; target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] }[] = [];
const workspaceAndFolders: { workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] }[] = [];
workspaceAndFolders.push(...backups.rootWorkspaces.map(r => ({ workspaceIdentifier: r, target: backups.rootWorkspaces })));
workspaceAndFolders.push(...backups.folderWorkspaces.map(f => ({ workspaceIdentifier: f, target: backups.folderWorkspaces })));
// Validate Workspace and Folder Backups
workspaceAndFolders.forEach(workspaceOrFolder => {
const workspaceId = workspaceOrFolder.workspaceIdentifier;
const workspacePath = isSingleFolderWorkspaceIdentifier(workspaceId) ? workspaceId : workspaceId.configPath;
const backupPath = path.join(this.backupHome, isSingleFolderWorkspaceIdentifier(workspaceId) ? this.getFolderHash(workspaceId) : workspaceId.id);
const hasBackups = this.hasBackupsSync(backupPath);
const missingWorkspace = hasBackups && !fs.existsSync(workspacePath);
// If the workspace/folder has no backups, make sure to delete it
// If the workspace/folder has backups, but the target workspace is missing, convert backups to empty ones
if (!hasBackups || missingWorkspace) {
staleBackupWorkspaces.push({ workspaceIdentifier: workspaceId, backupPath, target: workspaceOrFolder.target });
if (missingWorkspace) {
this.convertToEmptyWindowBackup(backupPath);
// read folder backups
let workspaceFolders: URI[];
try {
if (Array.isArray(backups.folderURIWorkspaces)) {
workspaceFolders = backups.folderURIWorkspaces.map(f => URI.parse(f));
} else if (Array.isArray(backups.folderWorkspaces)) {
// migrate legacy folder paths
workspaceFolders = [];
for (const folderPath of backups.folderWorkspaces) {
const oldFolderHash = this.getLegacyFolderHash(folderPath);
const folderUri = URI.file(folderPath);
const newFolderHash = this.getFolderHash(folderUri);
if (newFolderHash !== oldFolderHash) {
this.moveBackupFolderSync(this.getBackupPath(newFolderHash), this.getBackupPath(oldFolderHash));
}
workspaceFolders.push(folderUri);
}
}
});
} catch (e) {
// ignore URI parsing exceptions
}
this.folderWorkspaces = this.validateFolders(workspaceFolders);
// save again in case some workspaces or folders have been removed
this.saveSync();
}
private getBackupPath(oldFolderHash: string): string {
return path.join(this.backupHome, oldFolderHash);
}
private validateWorkspaces(rootWorkspaces: IWorkspaceIdentifier[]): IWorkspaceIdentifier[] {
if (!Array.isArray(rootWorkspaces)) {
return [];
}
const seenIds: { [id: string]: boolean } = Object.create(null);
const result: IWorkspaceIdentifier[] = [];
// Validate Workspaces
for (let workspace of rootWorkspaces) {
if (!isWorkspaceIdentifier(workspace)) {
return []; // wrong format, skip all entries
}
if (!seenIds[workspace.id]) {
seenIds[workspace.id] = true;
const backupPath = this.getBackupPath(workspace.id);
const hasBackups = this.hasBackupsSync(backupPath);
// If the workspace has no backups, ignore it
if (hasBackups) {
if (fs.existsSync(workspace.configPath)) {
result.push(workspace);
} else {
// If the workspace has backups, but the target workspace is missing, convert backups to empty ones
this.convertToEmptyWindowBackup(backupPath);
}
} else {
this.deleteStaleBackup(backupPath);
}
}
}
return result;
}
private validateFolders(folderWorkspaces: URI[]): URI[] {
if (!Array.isArray(folderWorkspaces)) {
return [];
}
const result: URI[] = [];
const seen: { [id: string]: boolean } = Object.create(null);
for (let folderURI of folderWorkspaces) {
const key = getComparisonKey(folderURI);
if (!seen[key]) {
seen[key] = true;
const backupPath = this.getBackupPath(this.getFolderHash(folderURI));
const hasBackups = this.hasBackupsSync(backupPath);
// If the folder has no backups, ignore it
if (hasBackups) {
if (folderURI.scheme !== Schemas.file || fs.existsSync(folderURI.fsPath)) {
result.push(folderURI);
} else {
// If the folder has backups, but the target workspace is missing, convert backups to empty ones
this.convertToEmptyWindowBackup(backupPath);
}
} else {
this.deleteStaleBackup(backupPath);
}
}
}
return result;
}
private validateEmptyWorkspaces(emptyWorkspaces: string[]): string[] {
if (!Array.isArray(emptyWorkspaces)) {
return [];
}
const result: string[] = [];
const seen: { [id: string]: boolean } = Object.create(null);
// Validate Empty Windows
backups.emptyWorkspaces.forEach(backupFolder => {
const backupPath = path.join(this.backupHome, backupFolder);
if (!this.hasBackupsSync(backupPath)) {
staleBackupWorkspaces.push({ workspaceIdentifier: backupFolder, backupPath, target: backups.emptyWorkspaces });
for (let backupFolder of emptyWorkspaces) {
if (typeof backupFolder !== 'string') {
return [];
}
});
// Clean up stale backups
staleBackupWorkspaces.forEach(staleBackupWorkspace => {
const { backupPath, workspaceIdentifier, target } = staleBackupWorkspace;
if (!seen[backupFolder]) {
seen[backupFolder] = true;
try {
const backupPath = this.getBackupPath(backupFolder);
if (this.hasBackupsSync(backupPath)) {
result.push(backupFolder);
} else {
this.deleteStaleBackup(backupPath);
}
}
}
return result;
}
private deleteStaleBackup(backupPath: string) {
try {
if (fs.existsSync(backupPath)) {
extfs.delSync(backupPath);
} catch (ex) {
this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`);
}
this.removeBackupPathSync(workspaceIdentifier, target);
});
} catch (ex) {
this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`);
}
}
private convertToEmptyWindowBackup(backupPath: string): boolean {
// New empty window backup
const identifier = this.getRandomEmptyWindowId();
this.pushBackupPathsSync(identifier, this.backups.emptyWorkspaces);
let newBackupFolder = this.getRandomEmptyWindowId();
while (this.emptyWorkspaces.some(w => isEqual(w, newBackupFolder, platform.isLinux))) {
newBackupFolder = this.getRandomEmptyWindowId();
}
// Rename backupPath to new empty window backup path
const newEmptyWindowBackupPath = path.join(this.backupHome, identifier);
const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder);
try {
fs.renameSync(backupPath, newEmptyWindowBackupPath);
} catch (ex) {
this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);
this.removeBackupPathSync(identifier, this.backups.emptyWorkspaces);
return false;
}
this.emptyWorkspaces.push(newBackupFolder);
return true;
}
@@ -308,8 +357,12 @@ export class BackupMainService implements IBackupMainService {
if (!fs.existsSync(this.backupHome)) {
fs.mkdirSync(this.backupHome);
}
extfs.writeFileAndFlushSync(this.workspacesJsonPath, JSON.stringify(this.backups));
const backups: IBackupWorkspacesFormat = {
rootWorkspaces: this.rootWorkspaces,
folderURIWorkspaces: this.folderWorkspaces.map(f => f.toString()),
emptyWorkspaces: this.emptyWorkspaces
};
extfs.writeFileAndFlushSync(this.workspacesJsonPath, JSON.stringify(backups));
} catch (ex) {
this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
}
@@ -319,11 +372,19 @@ export class BackupMainService implements IBackupMainService {
return (Date.now() + Math.round(Math.random() * 1000)).toString();
}
private sanitizePath(p: string): string {
return platform.isLinux ? p : p.toLowerCase();
protected getFolderHash(folderUri: URI): string {
let key;
if (folderUri.scheme === Schemas.file) {
// for backward compatibility, use the fspath as key
key = platform.isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase();
} else {
key = hasToIgnoreCase(folderUri) ? folderUri.toString().toLowerCase() : folderUri.toString();
}
return crypto.createHash('md5').update(key).digest('hex');
}
protected getFolderHash(folderPath: string): string {
return crypto.createHash('md5').update(this.sanitizePath(folderPath)).digest('hex');
protected getLegacyFolderHash(folderPath: string): string {
return crypto.createHash('md5').update(platform.isLinux ? folderPath : folderPath.toLowerCase()).digest('hex');
}
}
}

View File

@@ -25,6 +25,11 @@ import { getRandomTestPath } from 'vs/workbench/test/workbenchTestServices';
import { Schemas } from 'vs/base/common/network';
suite('BackupMainService', () => {
function assertEqualUris(actual: Uri[], expected: Uri[]) {
assert.deepEqual(actual.map(a => a.toString()), expected.map(a => a.toString()));
}
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupservice');
const backupHome = path.join(parentDir, 'Backups');
const backupWorkspacesPath = path.join(backupHome, 'workspaces.json');
@@ -43,28 +48,21 @@ suite('BackupMainService', () => {
this.loadSync();
}
public get backupsData(): IBackupWorkspacesFormat {
return this.backups;
}
public removeBackupPathSync(workspaceIdentifier: string | IWorkspaceIdentifier, target: (string | IWorkspaceIdentifier)[]): void {
return super.removeBackupPathSync(workspaceIdentifier, target);
}
public loadSync(): void {
super.loadSync();
}
public dedupeBackups(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat {
return super.dedupeBackups(backups);
public toBackupPath(arg: Uri | string): string {
const id = arg instanceof Uri ? super.getFolderHash(arg) : arg;
return path.join(this.backupHome, id);
}
public toBackupPath(workspacePath: string): string {
return path.join(this.backupHome, super.getFolderHash(workspacePath));
public getFolderHash(folderUri: Uri): string {
return super.getFolderHash(folderUri);
}
public getFolderHash(folderPath: string): string {
return super.getFolderHash(folderPath);
public toLegacyBackupPath(folderPath: string): string {
return path.join(this.backupHome, super.getLegacyFolderHash(folderPath));
}
}
@@ -75,6 +73,31 @@ suite('BackupMainService', () => {
};
}
async function ensureFolderExists(uri: Uri): Promise<void> {
if (!fs.existsSync(uri.fsPath)) {
fs.mkdirSync(uri.fsPath);
}
const backupFolder = service.toBackupPath(uri);
await createBackupFolder(backupFolder);
}
async function ensureWorkspaceExists(workspace: IWorkspaceIdentifier): Promise<IWorkspaceIdentifier> {
if (!fs.existsSync(workspace.configPath)) {
await pfs.writeFile(workspace.configPath, 'Hello');
}
const backupFolder = service.toBackupPath(workspace.id);
await createBackupFolder(backupFolder);
return workspace;
}
async function createBackupFolder(backupFolder: string): Promise<void> {
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');
}
}
function sanitizePath(p: string): string {
return platform.isLinux ? p : p.toLowerCase();
}
@@ -82,6 +105,8 @@ suite('BackupMainService', () => {
const fooFile = Uri.file(platform.isWindows ? 'C:\\foo' : '/foo');
const barFile = Uri.file(platform.isWindows ? 'C:\\bar' : '/bar');
const existingTestFolder1 = Uri.file(path.join(parentDir, 'folder1'));
let service: TestBackupMainService;
let configService: TestConfigurationService;
@@ -103,40 +128,40 @@ suite('BackupMainService', () => {
this.timeout(1000 * 10); // increase timeout for this test
// 1) backup workspace path does not exist
service.registerFolderBackupSync(fooFile.fsPath);
service.registerFolderBackupSync(barFile.fsPath);
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
// 2) backup workspace path exists with empty contents within
fs.mkdirSync(service.toBackupPath(fooFile.fsPath));
fs.mkdirSync(service.toBackupPath(barFile.fsPath));
service.registerFolderBackupSync(fooFile.fsPath);
service.registerFolderBackupSync(barFile.fsPath);
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath)));
assertEqualUris(service.getFolderBackupPaths(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
// 3) backup workspace path exists with empty folders within
fs.mkdirSync(service.toBackupPath(fooFile.fsPath));
fs.mkdirSync(service.toBackupPath(barFile.fsPath));
fs.mkdirSync(path.join(service.toBackupPath(fooFile.fsPath), Schemas.file));
fs.mkdirSync(path.join(service.toBackupPath(barFile.fsPath), Schemas.untitled));
service.registerFolderBackupSync(fooFile.fsPath);
service.registerFolderBackupSync(barFile.fsPath);
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
fs.mkdirSync(path.join(service.toBackupPath(fooFile), Schemas.file));
fs.mkdirSync(path.join(service.toBackupPath(barFile), Schemas.untitled));
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath)));
assertEqualUris(service.getFolderBackupPaths(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
// 4) backup workspace path points to a workspace that no longer exists
// so it should convert the backup worspace to an empty workspace backup
const fileBackups = path.join(service.toBackupPath(fooFile.fsPath), Schemas.file);
fs.mkdirSync(service.toBackupPath(fooFile.fsPath));
fs.mkdirSync(service.toBackupPath(barFile.fsPath));
const fileBackups = path.join(service.toBackupPath(fooFile), Schemas.file);
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
fs.mkdirSync(fileBackups);
service.registerFolderBackupSync(fooFile.fsPath);
service.registerFolderBackupSync(fooFile);
assert.equal(service.getFolderBackupPaths().length, 1);
assert.equal(service.getEmptyWindowBackupPaths().length, 0);
fs.writeFileSync(path.join(fileBackups, 'backup.txt'), '');
@@ -155,32 +180,32 @@ suite('BackupMainService', () => {
assert.deepEqual(service.getWorkspaceBackups(), []);
// 2) backup workspace path exists with empty contents within
fs.mkdirSync(service.toBackupPath(fooFile.fsPath));
fs.mkdirSync(service.toBackupPath(barFile.fsPath));
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath));
service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath));
service.loadSync();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath)));
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
// 3) backup workspace path exists with empty folders within
fs.mkdirSync(service.toBackupPath(fooFile.fsPath));
fs.mkdirSync(service.toBackupPath(barFile.fsPath));
fs.mkdirSync(path.join(service.toBackupPath(fooFile.fsPath), Schemas.file));
fs.mkdirSync(path.join(service.toBackupPath(barFile.fsPath), Schemas.untitled));
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
fs.mkdirSync(path.join(service.toBackupPath(fooFile), Schemas.file));
fs.mkdirSync(path.join(service.toBackupPath(barFile), Schemas.untitled));
service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath));
service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath));
service.loadSync();
assert.deepEqual(service.getWorkspaceBackups(), []);
assert.ok(!fs.existsSync(service.toBackupPath(fooFile.fsPath)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile.fsPath)));
assert.ok(!fs.existsSync(service.toBackupPath(fooFile)));
assert.ok(!fs.existsSync(service.toBackupPath(barFile)));
// 4) backup workspace path points to a workspace that no longer exists
// so it should convert the backup worspace to an empty workspace backup
const fileBackups = path.join(service.toBackupPath(fooFile.fsPath), Schemas.file);
fs.mkdirSync(service.toBackupPath(fooFile.fsPath));
fs.mkdirSync(service.toBackupPath(barFile.fsPath));
const fileBackups = path.join(service.toBackupPath(fooFile), Schemas.file);
fs.mkdirSync(service.toBackupPath(fooFile));
fs.mkdirSync(service.toBackupPath(barFile));
fs.mkdirSync(fileBackups);
service.registerWorkspaceBackupSync(toWorkspace(fooFile.fsPath));
assert.equal(service.getWorkspaceBackups().length, 1);
@@ -192,10 +217,10 @@ suite('BackupMainService', () => {
});
test('service supports to migrate backup data from another location', () => {
const backupPathToMigrate = service.toBackupPath(fooFile.fsPath);
const backupPathToMigrate = service.toBackupPath(fooFile);
fs.mkdirSync(backupPathToMigrate);
fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data');
service.registerFolderBackupSync(backupPathToMigrate);
service.registerFolderBackupSync(Uri.file(backupPathToMigrate));
const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath), backupPathToMigrate);
@@ -208,15 +233,15 @@ suite('BackupMainService', () => {
});
test('service backup migration makes sure to preserve existing backups', () => {
const backupPathToMigrate = service.toBackupPath(fooFile.fsPath);
const backupPathToMigrate = service.toBackupPath(fooFile);
fs.mkdirSync(backupPathToMigrate);
fs.writeFileSync(path.join(backupPathToMigrate, 'backup.txt'), 'Some Data');
service.registerFolderBackupSync(backupPathToMigrate);
service.registerFolderBackupSync(Uri.file(backupPathToMigrate));
const backupPathToPreserve = service.toBackupPath(barFile.fsPath);
const backupPathToPreserve = service.toBackupPath(barFile);
fs.mkdirSync(backupPathToPreserve);
fs.writeFileSync(path.join(backupPathToPreserve, 'backup.txt'), 'Some Data');
service.registerFolderBackupSync(backupPathToPreserve);
service.registerFolderBackupSync(Uri.file(backupPathToPreserve));
const workspaceBackupPath = service.registerWorkspaceBackupSync(toWorkspace(barFile.fsPath), backupPathToMigrate);
@@ -229,56 +254,102 @@ suite('BackupMainService', () => {
assert.equal(1, fs.readdirSync(path.join(backupHome, emptyBackups[0])).length);
});
suite('migrate folderPath to folderURI', () => {
test('migration makes sure to preserve existing backups', async () => {
if (platform.isLinux) {
return; // TODO:Martin #54483 fix tests
}
let path1 = path.join(parentDir, 'folder1').toLowerCase();
let path2 = path.join(parentDir, 'folder2').toUpperCase();
let uri1 = Uri.file(path1);
let uri2 = Uri.file(path2);
if (!fs.existsSync(path1)) {
fs.mkdirSync(path1);
}
if (!fs.existsSync(path2)) {
fs.mkdirSync(path2);
}
const backupFolder1 = service.toLegacyBackupPath(path1);
if (!fs.existsSync(backupFolder1)) {
fs.mkdirSync(backupFolder1);
fs.mkdirSync(path.join(backupFolder1, Schemas.file));
await pfs.writeFile(path.join(backupFolder1, Schemas.file, 'unsaved1.txt'), 'Legacy');
}
const backupFolder2 = service.toLegacyBackupPath(path2);
if (!fs.existsSync(backupFolder2)) {
fs.mkdirSync(backupFolder2);
fs.mkdirSync(path.join(backupFolder2, Schemas.file));
await pfs.writeFile(path.join(backupFolder2, Schemas.file, 'unsaved2.txt'), 'Legacy');
}
const workspacesJson = { rootWorkspaces: [], folderWorkspaces: [path1, path2], emptyWorkspaces: [] };
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => {
service.loadSync();
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => {
const json = <IBackupWorkspacesFormat>JSON.parse(content);
assert.deepEqual(json.folderURIWorkspaces, [uri1.toString(), uri2.toString()]);
const newBackupFolder1 = service.toBackupPath(uri1);
assert.ok(fs.existsSync(path.join(newBackupFolder1, Schemas.file, 'unsaved1.txt')));
const newBackupFolder2 = service.toBackupPath(uri2);
assert.ok(fs.existsSync(path.join(newBackupFolder2, Schemas.file, 'unsaved2.txt')));
});
});
});
});
suite('loadSync', () => {
test('getFolderBackupPaths() should return [] when workspaces.json doesn\'t exist', () => {
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
});
test('getFolderBackupPaths() should return [] when workspaces.json is not properly formed JSON', () => {
fs.writeFileSync(backupWorkspacesPath, '');
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{]');
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, 'foo');
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
});
test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is absent', () => {
fs.writeFileSync(backupWorkspacesPath, '{}');
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
});
test('getFolderBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', () => {
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{}}');
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": ["bar"]}}');
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": []}}');
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":{"foo": "bar"}}');
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":"foo"}');
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaces":1}');
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
});
test('getFolderBackupPaths() should return [] when files.hotExit = "onExitAndWindowClose"', () => {
service.registerFolderBackupSync(fooFile.fsPath.toUpperCase());
assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath.toUpperCase()]);
service.registerFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase()));
assertEqualUris(service.getFolderBackupPaths(), [Uri.file(fooFile.fsPath.toUpperCase())]);
configService.setUserConfiguration('files.hotExit', HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE);
service.loadSync();
assert.deepEqual(service.getFolderBackupPaths(), []);
assertEqualUris(service.getFolderBackupPaths(), []);
});
test('getWorkspaceBackups() should return [] when workspaces.json doesn\'t exist', () => {
@@ -379,59 +450,82 @@ suite('BackupMainService', () => {
});
suite('dedupeFolderWorkspaces', () => {
test('should ignore duplicates on Windows and Mac (folder workspace)', () => {
// Skip test on Linux
if (platform.isLinux) {
return;
}
test('should ignore duplicates (folder workspace)', async () => {
const backups: IBackupWorkspacesFormat = {
await ensureFolderExists(existingTestFolder1);
const workspacesJson: IBackupWorkspacesFormat = {
rootWorkspaces: [],
folderWorkspaces: platform.isWindows ? ['c:\\FOO', 'C:\\FOO', 'c:\\foo'] : ['/FOO', '/foo'],
folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString()],
emptyWorkspaces: []
};
service.dedupeBackups(backups);
assert.equal(backups.folderWorkspaces.length, 1);
if (platform.isWindows) {
assert.deepEqual(backups.folderWorkspaces, ['c:\\FOO'], 'should return the first duplicated entry');
} else {
assert.deepEqual(backups.folderWorkspaces, ['/FOO'], 'should return the first duplicated entry');
}
return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => {
service.loadSync();
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
});
});
test('should ignore duplicates on Windows and Mac (root workspace)', () => {
// Skip test on Linux
if (platform.isLinux) {
return;
}
test('should ignore duplicates on Windows and Mac (folder workspace)', async () => {
const backups: IBackupWorkspacesFormat = {
rootWorkspaces: platform.isWindows ? [toWorkspace('c:\\FOO'), toWorkspace('C:\\FOO'), toWorkspace('c:\\foo')] : [toWorkspace('/FOO'), toWorkspace('/foo')],
folderWorkspaces: [],
await ensureFolderExists(existingTestFolder1);
const workspacesJson: IBackupWorkspacesFormat = {
rootWorkspaces: [],
folderURIWorkspaces: [existingTestFolder1.toString(), existingTestFolder1.toString().toLowerCase()],
emptyWorkspaces: []
};
return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => {
service.loadSync();
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
});
});
service.dedupeBackups(backups);
assert.equal(backups.rootWorkspaces.length, 1);
if (platform.isWindows) {
assert.deepEqual(backups.rootWorkspaces.map(r => r.configPath), ['c:\\FOO'], 'should return the first duplicated entry');
} else {
assert.deepEqual(backups.rootWorkspaces.map(r => r.configPath), ['/FOO'], 'should return the first duplicated entry');
test('should ignore duplicates on Windows and Mac (root workspace)', async () => {
if (platform.isLinux) {
return; // TODO:Martin #54483 fix tests
}
const workspacePath = path.join(parentDir, 'Foo.code-workspace');
const workspace1 = await ensureWorkspaceExists(toWorkspace(workspacePath));
const workspace2 = await ensureWorkspaceExists(toWorkspace(workspacePath.toUpperCase()));
const workspace3 = await ensureWorkspaceExists(toWorkspace(workspacePath.toLowerCase()));
const workspacesJson: IBackupWorkspacesFormat = {
rootWorkspaces: [workspace1, workspace2, workspace3],
folderURIWorkspaces: [],
emptyWorkspaces: []
};
return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => {
service.loadSync();
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.equal(json.rootWorkspaces.length, platform.isLinux ? 3 : 1);
if (platform.isLinux) {
assert.deepEqual(json.rootWorkspaces.map(r => r.configPath), [workspacePath, workspacePath.toUpperCase(), workspacePath.toLowerCase()]);
} else {
assert.deepEqual(json.rootWorkspaces.map(r => r.configPath), [workspacePath], 'should return the first duplicated entry');
}
});
});
});
});
suite('registerWindowForBackups', () => {
test('should persist paths to workspaces.json (folder workspace)', () => {
service.registerFolderBackupSync(fooFile.fsPath);
service.registerFolderBackupSync(barFile.fsPath);
assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath, barFile.fsPath]);
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath, barFile.fsPath]);
assert.deepEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]);
});
});
@@ -455,11 +549,11 @@ suite('BackupMainService', () => {
});
test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (folder workspace)', () => {
service.registerFolderBackupSync(fooFile.fsPath.toUpperCase());
assert.deepEqual(service.getFolderBackupPaths(), [fooFile.fsPath.toUpperCase()]);
service.registerFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase()));
assertEqualUris(service.getFolderBackupPaths(), [Uri.file(fooFile.fsPath.toUpperCase())]);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath.toUpperCase()]);
assert.deepEqual(json.folderURIWorkspaces, [Uri.file(fooFile.fsPath.toUpperCase()).toString()]);
});
});
@@ -475,16 +569,16 @@ suite('BackupMainService', () => {
suite('removeBackupPathSync', () => {
test('should remove folder workspaces from workspaces.json (folder workspace)', () => {
service.registerFolderBackupSync(fooFile.fsPath);
service.registerFolderBackupSync(barFile.fsPath);
service.removeBackupPathSync(fooFile.fsPath, service.backupsData.folderWorkspaces);
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(barFile);
service.unregisterFolderBackupSync(fooFile);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.folderWorkspaces, [barFile.fsPath]);
service.removeBackupPathSync(barFile.fsPath, service.backupsData.folderWorkspaces);
assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]);
service.unregisterFolderBackupSync(barFile);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => {
const json2 = <IBackupWorkspacesFormat>JSON.parse(content);
assert.deepEqual(json2.folderWorkspaces, []);
assert.deepEqual(json2.folderURIWorkspaces, []);
});
});
});
@@ -494,11 +588,11 @@ suite('BackupMainService', () => {
service.registerWorkspaceBackupSync(ws1);
const ws2 = toWorkspace(barFile.fsPath);
service.registerWorkspaceBackupSync(ws2);
service.removeBackupPathSync(ws1, service.backupsData.rootWorkspaces);
service.unregisterWorkspaceBackupSync(ws1);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.rootWorkspaces.map(r => r.configPath), [barFile.fsPath]);
service.removeBackupPathSync(ws2, service.backupsData.rootWorkspaces);
service.unregisterWorkspaceBackupSync(ws2);
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => {
const json2 = <IBackupWorkspacesFormat>JSON.parse(content);
assert.deepEqual(json2.rootWorkspaces, []);
@@ -509,11 +603,11 @@ suite('BackupMainService', () => {
test('should remove empty workspaces from workspaces.json', () => {
service.registerEmptyWindowBackupSync('foo');
service.registerEmptyWindowBackupSync('bar');
service.removeBackupPathSync('foo', service.backupsData.emptyWorkspaces);
service.unregisterEmptyWindowBackupSync('foo');
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => {
const json = <IBackupWorkspacesFormat>JSON.parse(buffer);
assert.deepEqual(json.emptyWorkspaces, ['bar']);
service.removeBackupPathSync('bar', service.backupsData.emptyWorkspaces);
service.unregisterEmptyWindowBackupSync('bar');
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => {
const json2 = <IBackupWorkspacesFormat>JSON.parse(content);
assert.deepEqual(json2.emptyWorkspaces, []);
@@ -521,23 +615,24 @@ suite('BackupMainService', () => {
});
});
test('should fail gracefully when removing a path that doesn\'t exist', () => {
const workspacesJson: IBackupWorkspacesFormat = { rootWorkspaces: [], folderWorkspaces: [fooFile.fsPath], emptyWorkspaces: [] };
test('should fail gracefully when removing a path that doesn\'t exist', async () => {
await ensureFolderExists(existingTestFolder1); // make sure backup folder exists, so the folder is not removed on loadSync
const workspacesJson: IBackupWorkspacesFormat = { rootWorkspaces: [], folderURIWorkspaces: [existingTestFolder1.toString()], emptyWorkspaces: [] };
return pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)).then(() => {
service.removeBackupPathSync(barFile.fsPath, service.backupsData.folderWorkspaces);
service.removeBackupPathSync('test', service.backupsData.emptyWorkspaces);
service.loadSync();
service.unregisterFolderBackupSync(barFile);
service.unregisterEmptyWindowBackupSync('test');
return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => {
const json = <IBackupWorkspacesFormat>JSON.parse(content);
assert.deepEqual(json.folderWorkspaces, [fooFile.fsPath]);
assert.deepEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]);
});
});
});
});
suite('getWorkspaceHash', () => {
test('should perform an md5 hash on the path', () => {
assert.equal(service.getFolderHash('/foo'), '1effb2475fcfba4f9e8b8a1dbc8f3caf');
});
test('should ignore case on Windows and Mac', () => {
// Skip test on Linux
@@ -546,19 +641,19 @@ suite('BackupMainService', () => {
}
if (platform.isMacintosh) {
assert.equal(service.getFolderHash('/foo'), service.getFolderHash('/FOO'));
assert.equal(service.getFolderHash(Uri.file('/foo')), service.getFolderHash(Uri.file('/FOO')));
}
if (platform.isWindows) {
assert.equal(service.getFolderHash('c:\\foo'), service.getFolderHash('C:\\FOO'));
assert.equal(service.getFolderHash(Uri.file('c:\\foo')), service.getFolderHash(Uri.file('C:\\FOO')));
}
});
});
suite('mixed path casing', () => {
test('should handle case insensitive paths properly (registerWindowForBackupsSync) (folder workspace)', () => {
service.registerFolderBackupSync(fooFile.fsPath);
service.registerFolderBackupSync(fooFile.fsPath.toUpperCase());
service.registerFolderBackupSync(fooFile);
service.registerFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase()));
if (platform.isLinux) {
assert.equal(service.getFolderBackupPaths().length, 2);
@@ -581,13 +676,13 @@ suite('BackupMainService', () => {
test('should handle case insensitive paths properly (removeBackupPathSync) (folder workspace)', () => {
// same case
service.registerFolderBackupSync(fooFile.fsPath);
service.removeBackupPathSync(fooFile.fsPath, service.backupsData.folderWorkspaces);
service.registerFolderBackupSync(fooFile);
service.unregisterFolderBackupSync(fooFile);
assert.equal(service.getFolderBackupPaths().length, 0);
// mixed case
service.registerFolderBackupSync(fooFile.fsPath);
service.removeBackupPathSync(fooFile.fsPath.toUpperCase(), service.backupsData.folderWorkspaces);
service.registerFolderBackupSync(fooFile);
service.unregisterFolderBackupSync(Uri.file(fooFile.fsPath.toUpperCase()));
if (platform.isLinux) {
assert.equal(service.getFolderBackupPaths().length, 1);

View File

@@ -35,17 +35,17 @@ export interface IClipboardService {
writeFindText(text: string): void;
/**
* Writes files to the system clipboard.
* Writes resources to the system clipboard.
*/
writeFiles(files: URI[]): void;
writeResources(resources: URI[]): void;
/**
* Reads files from the system clipboard.
* Reads resources from the system clipboard.
*/
readFiles(): URI[];
readResources(): URI[];
/**
* Find out if files are copied to the clipboard.
* Find out if resources are copied to the clipboard.
*/
hasFiles(): boolean;
hasResources(): boolean;
}

View File

@@ -9,7 +9,6 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService
import { clipboard } from 'electron';
import URI from 'vs/base/common/uri';
import { isMacintosh } from 'vs/base/common/platform';
import { Schemas } from 'vs/base/common/network';
export class ClipboardService implements IClipboardService {
@@ -40,27 +39,25 @@ export class ClipboardService implements IClipboardService {
}
}
public writeFiles(resources: URI[]): void {
const files = resources.filter(f => f.scheme === Schemas.file);
if (files.length) {
clipboard.writeBuffer(ClipboardService.FILE_FORMAT, this.filesToBuffer(files));
public writeResources(resources: URI[]): void {
if (resources.length) {
clipboard.writeBuffer(ClipboardService.FILE_FORMAT, this.resourcesToBuffer(resources));
}
}
public readFiles(): URI[] {
return this.bufferToFiles(clipboard.readBuffer(ClipboardService.FILE_FORMAT));
public readResources(): URI[] {
return this.bufferToResources(clipboard.readBuffer(ClipboardService.FILE_FORMAT));
}
public hasFiles(): boolean {
public hasResources(): boolean {
return clipboard.has(ClipboardService.FILE_FORMAT);
}
private filesToBuffer(resources: URI[]): Buffer {
return Buffer.from(resources.map(r => r.fsPath).join('\n'));
private resourcesToBuffer(resources: URI[]): Buffer {
return Buffer.from(resources.map(r => r.toString()).join('\n'));
}
private bufferToFiles(buffer: Buffer): URI[] {
private bufferToResources(buffer: Buffer): URI[] {
if (!buffer) {
return [];
}
@@ -71,9 +68,9 @@ export class ClipboardService implements IClipboardService {
}
try {
return bufferValue.split('\n').map(f => URI.file(f));
return bufferValue.split('\n').map(f => URI.parse(f));
} catch (error) {
return []; // do not trust clipboard data
}
}
}
}

View File

@@ -5,7 +5,7 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { TypeConstraint, validateConstraints } from 'vs/base/common/types';
import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
@@ -46,6 +46,7 @@ export interface ICommandHandlerDescription {
export interface ICommandRegistry {
registerCommand(id: string, command: ICommandHandler): IDisposable;
registerCommand(command: ICommand): IDisposable;
registerCommandAlias(oldId: string, newId: string): IDisposable;
getCommand(id: string): ICommand;
getCommands(): ICommandsMap;
}
@@ -91,14 +92,18 @@ export const CommandsRegistry: ICommandRegistry = new class implements ICommandR
let removeFn = commands.unshift(idOrCommand);
return {
dispose: () => {
removeFn();
if (this._commands.get(id).isEmpty()) {
this._commands.delete(id);
}
return toDisposable(() => {
removeFn();
if (this._commands.get(id).isEmpty()) {
this._commands.delete(id);
}
};
});
}
registerCommandAlias(oldId: string, newId: string): IDisposable {
return CommandsRegistry.registerCommand(oldId, (accessor, ...args) => {
accessor.get(ICommandService).executeCommand(newId, ...args);
});
}
getCommand(id: string): ICommand {

View File

@@ -34,6 +34,12 @@ export interface IConfigurationRegistry {
*/
notifyConfigurationSchemaUpdated(configuration: IConfigurationNode): void;
/**
* Event that fires whenver a configuration has been
* registered.
*/
onDidSchemaChange: Event<void>;
/**
* Event that fires whenver a configuration has been
* registered.
@@ -72,6 +78,7 @@ export interface IConfigurationPropertySchema extends IJSONSchema {
scope?: ConfigurationScope;
notMultiRootAdopted?: boolean;
included?: boolean;
tags?: string[];
}
export interface IConfigurationNode {
@@ -84,6 +91,7 @@ export interface IConfigurationNode {
allOf?: IConfigurationNode[];
overridable?: boolean;
scope?: ConfigurationScope;
contributedByExtension?: boolean;
}
export interface IDefaultConfigurationExtension {
@@ -109,6 +117,9 @@ class ConfigurationRegistry implements IConfigurationRegistry {
private overrideIdentifiers: string[] = [];
private overridePropertyPattern: string;
private readonly _onDidSchemaChange: Emitter<void> = new Emitter<void>();
readonly onDidSchemaChange: Event<void> = this._onDidSchemaChange.event;
private readonly _onDidRegisterConfiguration: Emitter<string[]> = new Emitter<string[]>();
readonly onDidRegisterConfiguration: Event<string[]> = this._onDidRegisterConfiguration.event;
@@ -175,7 +186,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
}
private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, scope: ConfigurationScope = ConfigurationScope.WINDOW, overridable: boolean = false): string[] {
scope = configuration.scope !== void 0 && configuration.scope !== null ? configuration.scope : scope;
scope = types.isUndefinedOrNull(configuration.scope) ? scope : configuration.scope;
overridable = configuration.overridable || overridable;
let propertyKeys = [];
let properties = configuration.properties;
@@ -197,8 +208,11 @@ class ConfigurationRegistry implements IConfigurationRegistry {
if (overridable) {
property.overridable = true;
}
if (property.scope === void 0) {
property.scope = scope;
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
property.scope = void 0; // No scope for overridable properties `[${identifier}]`
} else {
property.scope = types.isUndefinedOrNull(property.scope) ? scope : property.scope;
}
// Add to properties maps
@@ -239,7 +253,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
function register(configuration: IConfigurationNode) {
let properties = configuration.properties;
if (properties) {
for (let key in properties) {
for (const key in properties) {
allSettings.properties[key] = properties[key];
switch (properties[key].scope) {
case ConfigurationScope.APPLICATION:
@@ -260,6 +274,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
}
}
register(configuration);
this._onDidSchemaChange.fire();
}
private updateSchemaForOverrideSettingsConfiguration(configuration: IConfigurationNode): void {
@@ -291,6 +306,8 @@ class ConfigurationRegistry implements IConfigurationRegistry {
applicationSettings.patternProperties[this.overridePropertyPattern] = patternProperties;
windowSettings.patternProperties[this.overridePropertyPattern] = patternProperties;
resourceSettings.patternProperties[this.overridePropertyPattern] = patternProperties;
this._onDidSchemaChange.fire();
}
private update(configuration: IConfigurationNode): void {

View File

@@ -89,7 +89,7 @@ class ConfigAwareContextValuesContainer extends Context {
const contextKey = `config.${configKey}`;
if (contextKey in this._value) {
this._value[contextKey] = this._configurationService.getValue(configKey);
this._emitter.fire(configKey);
this._emitter.fire(contextKey);
}
}
}
@@ -331,6 +331,7 @@ class ScopedContextKeyService extends AbstractContextKeyService {
this._parent.disposeContext(this._myContextId);
if (this._domNode) {
this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);
this._domNode = undefined;
}
}

View File

@@ -431,7 +431,10 @@ export class ContextKeyRegexExpr implements ContextKeyExpr {
}
public serialize(): string {
return `${this.key} =~ /${this.regexp ? this.regexp.source : '<invalid>'}/${this.regexp.ignoreCase ? 'i' : ''}`;
const value = this.regexp
? `/${this.regexp.source}/${this.regexp.ignoreCase ? 'i' : ''}`
: '/invalid/';
return `${this.key} =~ ${value}`;
}
public keys(): string[] {

View File

@@ -9,7 +9,7 @@ import 'vs/css!./contextMenuHandler';
import { $, Builder } from 'vs/base/browser/builder';
import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IActionRunner, ActionRunner, IAction, IRunEvent } from 'vs/base/common/actions';
import { ActionRunner, IAction, IRunEvent } from 'vs/base/common/actions';
import { Menu } from 'vs/base/browser/ui/menu/menu';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
@@ -23,10 +23,8 @@ export class ContextMenuHandler {
private notificationService: INotificationService;
private telemetryService: ITelemetryService;
private actionRunner: IActionRunner;
private $el: Builder;
private menuContainerElement: HTMLElement;
private toDispose: IDisposable[];
constructor(element: HTMLElement, contextViewService: IContextViewService, telemetryService: ITelemetryService, notificationService: INotificationService) {
this.setContainer(element);
@@ -35,41 +33,7 @@ export class ContextMenuHandler {
this.telemetryService = telemetryService;
this.notificationService = notificationService;
this.actionRunner = new ActionRunner();
this.menuContainerElement = null;
this.toDispose = [];
let hideViewOnRun = false;
this.toDispose.push(this.actionRunner.onDidBeforeRun((e: IRunEvent) => {
if (this.telemetryService) {
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' });
}
hideViewOnRun = !!(<any>e).retainActionItem;
if (!hideViewOnRun) {
this.contextViewService.hideContextView(false);
}
}));
this.toDispose.push(this.actionRunner.onDidRun((e: IRunEvent) => {
if (hideViewOnRun) {
this.contextViewService.hideContextView(false);
}
hideViewOnRun = false;
if (e.error && this.notificationService) {
this.notificationService.error(e.error);
}
}));
}
public setContainer(container: HTMLElement): void {
@@ -85,6 +49,10 @@ export class ContextMenuHandler {
public showContextMenu(delegate: IContextMenuDelegate): void {
delegate.getActions().done((actions: IAction[]) => {
if (!actions.length) {
return; // Don't render an empty context menu
}
this.contextViewService.showContextView({
getAnchor: () => delegate.getAnchor(),
canRelayout: false,
@@ -98,23 +66,25 @@ export class ContextMenuHandler {
container.className += ' ' + className;
}
let menu = new Menu(container, actions, {
const menuDisposables: IDisposable[] = [];
const actionRunner = delegate.actionRunner || new ActionRunner();
actionRunner.onDidBeforeRun(this.onActionRun, this, menuDisposables);
actionRunner.onDidRun(this.onDidActionRun, this, menuDisposables);
const menu = new Menu(container, actions, {
actionItemProvider: delegate.getActionItem,
context: delegate.getActionsContext ? delegate.getActionsContext() : null,
actionRunner: this.actionRunner
actionRunner,
getKeyBinding: delegate.getKeyBinding
});
let listener1 = menu.onDidCancel(() => {
this.contextViewService.hideContextView(true);
});
menu.onDidCancel(() => this.contextViewService.hideContextView(true), null, menuDisposables);
menu.onDidBlur(() => this.contextViewService.hideContextView(true), null, menuDisposables);
let listener2 = menu.onDidBlur(() => {
this.contextViewService.hideContextView(true);
});
menu.focus(!!delegate.autoSelectFirstItem);
menu.focus();
return combinedDisposable([listener1, listener2, menu]);
return combinedDisposable([...menuDisposables, menu]);
},
onHide: (didCancel?: boolean) => {
@@ -128,6 +98,26 @@ export class ContextMenuHandler {
});
}
private onActionRun(e: IRunEvent): void {
if (this.telemetryService) {
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' });
}
this.contextViewService.hideContextView(false);
}
private onDidActionRun(e: IRunEvent): void {
if (e.error && this.notificationService) {
this.notificationService.error(e.error);
}
}
private onMouseDown(e: MouseEvent): void {
if (!this.menuContainerElement) {
return;

View File

@@ -9,10 +9,10 @@ import * as platform from 'vs/base/common/platform';
suite('Keytar', () => {
test('loads and is functional', done => {
test('loads and is functional', function (done) {
if (platform.isLinux) {
// Skip test due to set up issue with Travis.
done();
this.skip();
return;
}
(async () => {

View File

@@ -9,6 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs';
import Severity from 'vs/base/common/severity';
import { Event } from 'vs/base/common/event';
export interface IDialogChannel extends IChannel {
call(command: 'show'): TPromise<number>;
@@ -18,7 +19,10 @@ export interface IDialogChannel extends IChannel {
export class DialogChannel implements IDialogChannel {
constructor(@IDialogService private dialogService: IDialogService) {
constructor(@IDialogService private dialogService: IDialogService) { }
listen<T>(event: string): Event<T> {
throw new Error('No event found');
}
call(command: string, args?: any[]): TPromise<any> {

View File

@@ -8,6 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs';
import Severity from 'vs/base/common/severity';
import { localize } from 'vs/nls';
import { canceled } from 'vs/base/common/errors';
export class CommandLineDialogService implements IDialogService {
@@ -31,7 +32,7 @@ export class CommandLineDialogService implements IDialogService {
});
rl.once('SIGINT', () => {
rl.close();
promise.cancel();
e(canceled());
});
});
return promise;
@@ -64,4 +65,4 @@ export class CommandLineDialogService implements IDialogService {
} as IConfirmationResult;
});
}
}
}

View File

@@ -8,6 +8,7 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
export const ID = 'driverService';
export const IDriver = createDecorator<IDriver>(ID);
@@ -21,6 +22,8 @@ export interface IElement {
textContent: string;
attributes: { [name: string]: string; };
children: IElement[];
top: number;
left: number;
}
export interface IDriver {
@@ -32,14 +35,13 @@ export interface IDriver {
dispatchKeybinding(windowId: number, keybinding: string): TPromise<void>;
click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): TPromise<void>;
doubleClick(windowId: number, selector: string): TPromise<void>;
move(windowId: number, selector: string): TPromise<void>;
setValue(windowId: number, selector: string, text: string): TPromise<void>;
paste(windowId: number, selector: string, text: string): TPromise<void>;
getTitle(windowId: number): TPromise<string>;
isActiveElement(windowId: number, selector: string): TPromise<boolean>;
getElements(windowId: number, selector: string, recursive?: boolean): TPromise<IElement[]>;
typeInEditor(windowId: number, selector: string, text: string): TPromise<void>;
getTerminalBuffer(windowId: number, selector: string): TPromise<string[]>;
writeInTerminal(windowId: number, selector: string, text: string): TPromise<void>;
}
//*END
@@ -50,14 +52,13 @@ export interface IDriverChannel extends IChannel {
call(command: 'dispatchKeybinding', arg: [number, string]): TPromise<void>;
call(command: 'click', arg: [number, string, number | undefined, number | undefined]): TPromise<void>;
call(command: 'doubleClick', arg: [number, string]): TPromise<void>;
call(command: 'move', arg: [number, string]): TPromise<void>;
call(command: 'setValue', arg: [number, string, string]): TPromise<void>;
call(command: 'paste', arg: [number, string, string]): TPromise<void>;
call(command: 'getTitle', arg: [number]): TPromise<string>;
call(command: 'isActiveElement', arg: [number, string]): TPromise<boolean>;
call(command: 'getElements', arg: [number, string, boolean]): TPromise<IElement[]>;
call(command: 'typeInEditor', arg: [number, string, string]): TPromise<void>;
call(command: 'getTerminalBuffer', arg: [number, string]): TPromise<string[]>;
call(command: 'writeInTerminal', arg: [number, string, string]): TPromise<void>;
call(command: string, arg: any): TPromise<any>;
}
@@ -65,6 +66,10 @@ export class DriverChannel implements IDriverChannel {
constructor(private driver: IDriver) { }
listen<T>(event: string): Event<T> {
throw new Error('No event found');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'getWindowIds': return this.driver.getWindowIds();
@@ -73,14 +78,13 @@ export class DriverChannel implements IDriverChannel {
case 'dispatchKeybinding': return this.driver.dispatchKeybinding(arg[0], arg[1]);
case 'click': return this.driver.click(arg[0], arg[1], arg[2], arg[3]);
case 'doubleClick': return this.driver.doubleClick(arg[0], arg[1]);
case 'move': return this.driver.move(arg[0], arg[1]);
case 'setValue': return this.driver.setValue(arg[0], arg[1], arg[2]);
case 'paste': return this.driver.paste(arg[0], arg[1], arg[2]);
case 'getTitle': return this.driver.getTitle(arg[0]);
case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]);
case 'getElements': return this.driver.getElements(arg[0], arg[1], arg[2]);
case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1], arg[2]);
case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg[0], arg[1]);
case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1], arg[2]);
}
return undefined;
@@ -117,18 +121,10 @@ export class DriverChannelClient implements IDriver {
return this.channel.call('doubleClick', [windowId, selector]);
}
move(windowId: number, selector: string): TPromise<void> {
return this.channel.call('move', [windowId, selector]);
}
setValue(windowId: number, selector: string, text: string): TPromise<void> {
return this.channel.call('setValue', [windowId, selector, text]);
}
paste(windowId: number, selector: string, text: string): TPromise<void> {
return this.channel.call('paste', [windowId, selector, text]);
}
getTitle(windowId: number): TPromise<string> {
return this.channel.call('getTitle', [windowId]);
}
@@ -148,15 +144,23 @@ export class DriverChannelClient implements IDriver {
getTerminalBuffer(windowId: number, selector: string): TPromise<string[]> {
return this.channel.call('getTerminalBuffer', [windowId, selector]);
}
writeInTerminal(windowId: number, selector: string, text: string): TPromise<void> {
return this.channel.call('writeInTerminal', [windowId, selector, text]);
}
}
export interface IDriverOptions {
verbose: boolean;
}
export interface IWindowDriverRegistry {
registerWindowDriver(windowId: number): TPromise<void>;
registerWindowDriver(windowId: number): TPromise<IDriverOptions>;
reloadWindowDriver(windowId: number): TPromise<void>;
}
export interface IWindowDriverRegistryChannel extends IChannel {
call(command: 'registerWindowDriver', arg: number): TPromise<void>;
call(command: 'registerWindowDriver', arg: number): TPromise<IDriverOptions>;
call(command: 'reloadWindowDriver', arg: number): TPromise<void>;
call(command: string, arg: any): TPromise<any>;
}
@@ -165,6 +169,10 @@ export class WindowDriverRegistryChannel implements IWindowDriverRegistryChannel
constructor(private registry: IWindowDriverRegistry) { }
listen<T>(event: string): Event<T> {
throw new Error('No event found');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'registerWindowDriver': return this.registry.registerWindowDriver(arg);
@@ -181,7 +189,7 @@ export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry
constructor(private channel: IWindowDriverRegistryChannel) { }
registerWindowDriver(windowId: number): TPromise<void> {
registerWindowDriver(windowId: number): TPromise<IDriverOptions> {
return this.channel.call('registerWindowDriver', windowId);
}
@@ -193,27 +201,25 @@ export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry
export interface IWindowDriver {
click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): TPromise<void>;
doubleClick(selector: string): TPromise<void>;
move(selector: string): TPromise<void>;
setValue(selector: string, text: string): TPromise<void>;
paste(selector: string, text: string): TPromise<void>;
getTitle(): TPromise<string>;
isActiveElement(selector: string): TPromise<boolean>;
getElements(selector: string, recursive: boolean): TPromise<IElement[]>;
typeInEditor(selector: string, text: string): TPromise<void>;
getTerminalBuffer(selector: string): TPromise<string[]>;
writeInTerminal(selector: string, text: string): TPromise<void>;
}
export interface IWindowDriverChannel extends IChannel {
call(command: 'click', arg: [string, number | undefined, number | undefined]): TPromise<void>;
call(command: 'doubleClick', arg: string): TPromise<void>;
call(command: 'move', arg: string): TPromise<void>;
call(command: 'setValue', arg: [string, string]): TPromise<void>;
call(command: 'paste', arg: [string, string]): TPromise<void>;
call(command: 'getTitle'): TPromise<string>;
call(command: 'isActiveElement', arg: string): TPromise<boolean>;
call(command: 'getElements', arg: [string, boolean]): TPromise<IElement[]>;
call(command: 'typeInEditor', arg: [string, string]): TPromise<void>;
call(command: 'getTerminalBuffer', arg: string): TPromise<string[]>;
call(command: 'writeInTerminal', arg: [string, string]): TPromise<void>;
call(command: string, arg: any): TPromise<any>;
}
@@ -221,18 +227,21 @@ export class WindowDriverChannel implements IWindowDriverChannel {
constructor(private driver: IWindowDriver) { }
listen<T>(event: string): Event<T> {
throw new Error('No event found');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'click': return this.driver.click(arg[0], arg[1], arg[2]);
case 'doubleClick': return this.driver.doubleClick(arg);
case 'move': return this.driver.move(arg);
case 'setValue': return this.driver.setValue(arg[0], arg[1]);
case 'paste': return this.driver.paste(arg[0], arg[1]);
case 'getTitle': return this.driver.getTitle();
case 'isActiveElement': return this.driver.isActiveElement(arg);
case 'getElements': return this.driver.getElements(arg[0], arg[1]);
case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]);
case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg);
case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1]);
}
return undefined;
@@ -253,18 +262,10 @@ export class WindowDriverChannelClient implements IWindowDriver {
return this.channel.call('doubleClick', selector);
}
move(selector: string): TPromise<void> {
return this.channel.call('move', selector);
}
setValue(selector: string, text: string): TPromise<void> {
return this.channel.call('setValue', [selector, text]);
}
paste(selector: string, text: string): TPromise<void> {
return this.channel.call('paste', [selector, text]);
}
getTitle(): TPromise<string> {
return this.channel.call('getTitle');
}
@@ -284,4 +285,8 @@ export class WindowDriverChannelClient implements IWindowDriver {
getTerminalBuffer(selector: string): TPromise<string[]> {
return this.channel.call('getTerminalBuffer', selector);
}
writeInTerminal(selector: string, text: string): TPromise<void> {
return this.channel.call('writeInTerminal', [selector, text]);
}
}

View File

@@ -12,6 +12,8 @@ import { IPCClient } from 'vs/base/parts/ipc/common/ipc';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { getTopLeftOffset, getClientArea } from 'vs/base/browser/dom';
import * as electron from 'electron';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { Terminal } from 'vscode-xterm';
function serializeElement(element: Element, recursive: boolean): IElement {
const attributes = Object.create(null);
@@ -29,20 +31,26 @@ function serializeElement(element: Element, recursive: boolean): IElement {
}
}
const { left, top } = getTopLeftOffset(element as HTMLElement);
return {
tagName: element.tagName,
className: element.className,
textContent: element.textContent || '',
attributes,
children
children,
left,
top
};
}
class WindowDriver implements IWindowDriver {
constructor() { }
constructor(
@IWindowService private windowService: IWindowService
) { }
async click(selector: string, xoffset?: number, yoffset?: number): TPromise<void> {
click(selector: string, xoffset?: number, yoffset?: number): TPromise<void> {
return this._click(selector, 1, xoffset, yoffset);
}
@@ -50,11 +58,11 @@ class WindowDriver implements IWindowDriver {
return this._click(selector, 2);
}
private async _getElementXY(selector: string, xoffset?: number, yoffset?: number): TPromise<{ x: number; y: number; }> {
private _getElementXY(selector: string, xoffset?: number, yoffset?: number): TPromise<{ x: number; y: number; }> {
const element = document.querySelector(selector);
if (!element) {
throw new Error('Element not found');
return TPromise.wrapError(new Error('Element not found'));
}
const { left, top } = getTopLeftOffset(element as HTMLElement);
@@ -72,31 +80,27 @@ class WindowDriver implements IWindowDriver {
x = Math.round(x);
y = Math.round(y);
return { x, y };
return TPromise.as({ x, y });
}
private async _click(selector: string, clickCount: number, xoffset?: number, yoffset?: number): TPromise<void> {
const { x, y } = await this._getElementXY(selector, xoffset, yoffset);
const webContents = electron.remote.getCurrentWebContents();
webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any);
webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any);
private _click(selector: string, clickCount: number, xoffset?: number, yoffset?: number): TPromise<void> {
return this._getElementXY(selector, xoffset, yoffset).then(({ x, y }) => {
await TPromise.timeout(100);
const webContents = electron.remote.getCurrentWebContents();
webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any);
return TPromise.timeout(10).then(() => {
webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any);
return TPromise.timeout(100);
});
});
}
async move(selector: string): TPromise<void> {
const { x, y } = await this._getElementXY(selector);
const webContents = electron.remote.getCurrentWebContents();
webContents.sendInputEvent({ type: 'mouseMove', x, y } as any);
await TPromise.timeout(100);
}
async setValue(selector: string, text: string): TPromise<void> {
setValue(selector: string, text: string): TPromise<void> {
const element = document.querySelector(selector);
if (!element) {
throw new Error('Element not found');
return TPromise.wrapError(new Error('Element not found'));
}
const inputElement = element as HTMLInputElement;
@@ -104,33 +108,37 @@ class WindowDriver implements IWindowDriver {
const event = new Event('input', { bubbles: true, cancelable: true });
inputElement.dispatchEvent(event);
return TPromise.as(null);
}
async paste(selector: string, text: string): TPromise<void> {
getTitle(): TPromise<string> {
return TPromise.as(document.title);
}
isActiveElement(selector: string): TPromise<boolean> {
const element = document.querySelector(selector);
if (!element) {
throw new Error('Element not found');
if (element !== document.activeElement) {
const chain = [];
let el = document.activeElement;
while (el) {
const tagName = el.tagName;
const id = el.id ? `#${el.id}` : '';
const classes = el.className.split(/\s+/g).map(c => c.trim()).filter(c => !!c).map(c => `.${c}`).join('');
chain.unshift(`${tagName}${id}${classes}`);
el = el.parentElement;
}
return TPromise.wrapError(new Error(`Active element not found. Current active element is '${chain.join(' > ')}'`));
}
const inputElement = element as HTMLInputElement;
const clipboardData = new DataTransfer();
clipboardData.setData('text/plain', text);
const event = new ClipboardEvent('paste', { clipboardData } as any);
inputElement.dispatchEvent(event);
return TPromise.as(true);
}
async getTitle(): TPromise<string> {
return document.title;
}
async isActiveElement(selector: string): TPromise<boolean> {
const element = document.querySelector(selector);
return element === document.activeElement;
}
async getElements(selector: string, recursive: boolean): TPromise<IElement[]> {
getElements(selector: string, recursive: boolean): TPromise<IElement[]> {
const query = document.querySelectorAll(selector);
const result: IElement[] = [];
@@ -139,14 +147,14 @@ class WindowDriver implements IWindowDriver {
result.push(serializeElement(element, recursive));
}
return result;
return TPromise.as(result);
}
async typeInEditor(selector: string, text: string): TPromise<void> {
typeInEditor(selector: string, text: string): TPromise<void> {
const element = document.querySelector(selector);
if (!element) {
throw new Error('Editor not found: ' + selector);
return TPromise.wrapError(new Error('Editor not found: ' + selector));
}
const textarea = element as HTMLTextAreaElement;
@@ -160,28 +168,52 @@ class WindowDriver implements IWindowDriver {
const event = new Event('input', { 'bubbles': true, 'cancelable': true });
textarea.dispatchEvent(event);
return TPromise.as(null);
}
async getTerminalBuffer(selector: string): TPromise<string[]> {
getTerminalBuffer(selector: string): TPromise<string[]> {
const element = document.querySelector(selector);
if (!element) {
throw new Error('Terminal not found: ' + selector);
return TPromise.wrapError(new Error('Terminal not found: ' + selector));
}
const xterm = (element as any).xterm;
const xterm: Terminal = (element as any).xterm;
if (!xterm) {
throw new Error('Xterm not found: ' + selector);
return TPromise.wrapError(new Error('Xterm not found: ' + selector));
}
const lines: string[] = [];
for (let i = 0; i < xterm.buffer.lines.length; i++) {
lines.push(xterm.buffer.translateBufferLineToString(i, true));
for (let i = 0; i < xterm._core.buffer.lines.length; i++) {
lines.push(xterm._core.buffer.translateBufferLineToString(i, true));
}
return lines;
return TPromise.as(lines);
}
writeInTerminal(selector: string, text: string): TPromise<void> {
const element = document.querySelector(selector);
if (!element) {
return TPromise.wrapError(new Error('Element not found'));
}
const xterm: Terminal = (element as any).xterm;
if (!xterm) {
return TPromise.wrapError(new Error('Xterm not found'));
}
xterm._core.send(text);
return TPromise.as(null);
}
openDevTools(): TPromise<void> {
return this.windowService.openDevTools({ mode: 'detach' });
}
}
@@ -189,7 +221,7 @@ export async function registerWindowDriver(
client: IPCClient,
windowId: number,
instantiationService: IInstantiationService
): TPromise<IDisposable> {
): Promise<IDisposable> {
const windowDriver = instantiationService.createInstance(WindowDriver);
const windowDriverChannel = new WindowDriverChannel(windowDriver);
client.registerChannel('windowDriver', windowDriverChannel);
@@ -197,8 +229,12 @@ export async function registerWindowDriver(
const windowDriverRegistryChannel = client.getChannel('windowDriverRegistry');
const windowDriverRegistry = new WindowDriverRegistryChannelClient(windowDriverRegistryChannel);
await windowDriverRegistry.registerWindowDriver(windowId);
const options = await windowDriverRegistry.registerWindowDriver(windowId);
if (options.verbose) {
windowDriver.openDevTools();
}
const disposable = toDisposable(() => windowDriverRegistry.reloadWindowDriver(windowId));
return combinedDisposable([disposable, client]);
}
}

View File

@@ -6,7 +6,7 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IDriver, DriverChannel, IElement, IWindowDriverChannel, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IWindowDriver } from 'vs/platform/driver/common/driver';
import { IDriver, DriverChannel, IElement, IWindowDriverChannel, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IWindowDriver, IDriverOptions } from 'vs/platform/driver/common/driver';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net';
import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle';
@@ -16,18 +16,23 @@ import { SimpleKeybinding, KeyCode } from 'vs/base/common/keyCodes';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { OS } from 'vs/base/common/platform';
import { Emitter, toPromise } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
// TODO@joao: bad layering!
import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO';
import { ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode';
import { NativeImage } from 'electron';
import { toWinJsPromise } from 'vs/base/common/async';
class WindowRouter implements IClientRouter {
constructor(private windowId: number) { }
route(command: string, arg: any): string {
return `window:${this.windowId}`;
routeCall(): TPromise<string> {
return TPromise.as(`window:${this.windowId}`);
}
routeEvent(): TPromise<string> {
return TPromise.as(`window:${this.windowId}`);
}
}
@@ -45,59 +50,61 @@ export class Driver implements IDriver, IWindowDriverRegistry {
constructor(
private windowServer: IPCServer,
private options: IDriverOptions,
@IWindowsMainService private windowsService: IWindowsMainService
) { }
async registerWindowDriver(windowId: number): TPromise<void> {
registerWindowDriver(windowId: number): TPromise<IDriverOptions> {
this.registeredWindowIds.add(windowId);
this.reloadingWindowIds.delete(windowId);
this.onDidReloadingChange.fire();
return TPromise.as(this.options);
}
async reloadWindowDriver(windowId: number): TPromise<void> {
reloadWindowDriver(windowId: number): TPromise<void> {
this.reloadingWindowIds.add(windowId);
return TPromise.as(null);
}
async getWindowIds(): TPromise<number[]> {
return this.windowsService.getWindows()
getWindowIds(): TPromise<number[]> {
return TPromise.as(this.windowsService.getWindows()
.map(w => w.id)
.filter(id => this.registeredWindowIds.has(id) && !this.reloadingWindowIds.has(id));
.filter(id => this.registeredWindowIds.has(id) && !this.reloadingWindowIds.has(id)));
}
async capturePage(windowId: number): TPromise<string> {
await this.whenUnfrozen(windowId);
const window = this.windowsService.getWindowById(windowId);
const webContents = window.win.webContents;
const image = await new Promise<NativeImage>(c => webContents.capturePage(c));
const buffer = image.toPNG();
return buffer.toString('base64');
capturePage(windowId: number): TPromise<string> {
return this.whenUnfrozen(windowId).then(() => {
const window = this.windowsService.getWindowById(windowId);
const webContents = window.win.webContents;
return new TPromise(c => webContents.capturePage(image => c(image.toPNG().toString('base64'))));
});
}
async reloadWindow(windowId: number): TPromise<void> {
await this.whenUnfrozen(windowId);
const window = this.windowsService.getWindowById(windowId);
this.reloadingWindowIds.add(windowId);
this.windowsService.reload(window);
reloadWindow(windowId: number): TPromise<void> {
return this.whenUnfrozen(windowId).then(() => {
const window = this.windowsService.getWindowById(windowId);
this.reloadingWindowIds.add(windowId);
this.windowsService.reload(window);
});
}
async dispatchKeybinding(windowId: number, keybinding: string): TPromise<void> {
await this.whenUnfrozen(windowId);
dispatchKeybinding(windowId: number, keybinding: string): TPromise<void> {
return this.whenUnfrozen(windowId).then(() => {
const [first, second] = KeybindingIO._readUserBinding(keybinding);
const [first, second] = KeybindingIO._readUserBinding(keybinding);
await this._dispatchKeybinding(windowId, first);
if (second) {
await this._dispatchKeybinding(windowId, second);
}
return this._dispatchKeybinding(windowId, first).then(() => {
if (second) {
return this._dispatchKeybinding(windowId, second);
} else {
return TPromise.as(null);
}
});
});
}
private async _dispatchKeybinding(windowId: number, keybinding: SimpleKeybinding | ScanCodeBinding): TPromise<void> {
private _dispatchKeybinding(windowId: number, keybinding: SimpleKeybinding | ScanCodeBinding): TPromise<void> {
if (keybinding instanceof ScanCodeBinding) {
throw new Error('ScanCodeBindings not supported');
return TPromise.wrapError(new Error('ScanCodeBindings not supported'));
}
const window = this.windowsService.getWindowById(windowId);
@@ -132,68 +139,76 @@ export class Driver implements IDriver, IWindowDriverRegistry {
webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers } as any);
await TPromise.timeout(100);
return TPromise.timeout(100);
}
async click(windowId: number, selector: string, xoffset?: number, yoffset?: number): TPromise<void> {
const windowDriver = await this.getWindowDriver(windowId);
return windowDriver.click(selector, xoffset, yoffset);
click(windowId: number, selector: string, xoffset?: number, yoffset?: number): TPromise<void> {
return this.getWindowDriver(windowId).then(windowDriver => {
return windowDriver.click(selector, xoffset, yoffset);
});
}
async doubleClick(windowId: number, selector: string): TPromise<void> {
const windowDriver = await this.getWindowDriver(windowId);
return windowDriver.doubleClick(selector);
doubleClick(windowId: number, selector: string): TPromise<void> {
return this.getWindowDriver(windowId).then(windowDriver => {
return windowDriver.doubleClick(selector);
});
}
async move(windowId: number, selector: string): TPromise<void> {
const windowDriver = await this.getWindowDriver(windowId);
return windowDriver.move(selector);
setValue(windowId: number, selector: string, text: string): TPromise<void> {
return this.getWindowDriver(windowId).then(windowDriver => {
return windowDriver.setValue(selector, text);
});
}
async setValue(windowId: number, selector: string, text: string): TPromise<void> {
const windowDriver = await this.getWindowDriver(windowId);
return windowDriver.setValue(selector, text);
getTitle(windowId: number): TPromise<string> {
return this.getWindowDriver(windowId).then(windowDriver => {
return windowDriver.getTitle();
});
}
async paste(windowId: number, selector: string, text: string): TPromise<void> {
const windowDriver = await this.getWindowDriver(windowId);
return windowDriver.paste(selector, text);
isActiveElement(windowId: number, selector: string): TPromise<boolean> {
return this.getWindowDriver(windowId).then(windowDriver => {
return windowDriver.isActiveElement(selector);
});
}
async getTitle(windowId: number): TPromise<string> {
const windowDriver = await this.getWindowDriver(windowId);
return windowDriver.getTitle();
getElements(windowId: number, selector: string, recursive: boolean): TPromise<IElement[]> {
return this.getWindowDriver(windowId).then(windowDriver => {
return windowDriver.getElements(selector, recursive);
});
}
async isActiveElement(windowId: number, selector: string): TPromise<boolean> {
const windowDriver = await this.getWindowDriver(windowId);
return windowDriver.isActiveElement(selector);
typeInEditor(windowId: number, selector: string, text: string): TPromise<void> {
return this.getWindowDriver(windowId).then(windowDriver => {
return windowDriver.typeInEditor(selector, text);
});
}
async getElements(windowId: number, selector: string, recursive: boolean): TPromise<IElement[]> {
const windowDriver = await this.getWindowDriver(windowId);
return windowDriver.getElements(selector, recursive);
getTerminalBuffer(windowId: number, selector: string): TPromise<string[]> {
return this.getWindowDriver(windowId).then(windowDriver => {
return windowDriver.getTerminalBuffer(selector);
});
}
async typeInEditor(windowId: number, selector: string, text: string): TPromise<void> {
const windowDriver = await this.getWindowDriver(windowId);
return windowDriver.typeInEditor(selector, text);
writeInTerminal(windowId: number, selector: string, text: string): TPromise<void> {
return this.getWindowDriver(windowId).then(windowDriver => {
return windowDriver.writeInTerminal(selector, text);
});
}
async getTerminalBuffer(windowId: number, selector: string): TPromise<string[]> {
const windowDriver = await this.getWindowDriver(windowId);
return windowDriver.getTerminalBuffer(selector);
private getWindowDriver(windowId: number): TPromise<IWindowDriver> {
return this.whenUnfrozen(windowId).then(() => {
const router = new WindowRouter(windowId);
const windowDriverChannel = this.windowServer.getChannel<IWindowDriverChannel>('windowDriver', router);
return new WindowDriverChannelClient(windowDriverChannel);
});
}
private async getWindowDriver(windowId: number): TPromise<IWindowDriver> {
await this.whenUnfrozen(windowId);
const router = new WindowRouter(windowId);
const windowDriverChannel = this.windowServer.getChannel<IWindowDriverChannel>('windowDriver', router);
return new WindowDriverChannelClient(windowDriverChannel);
private whenUnfrozen(windowId: number): TPromise<void> {
return toWinJsPromise(this._whenUnfrozen(windowId));
}
private async whenUnfrozen(windowId: number): TPromise<void> {
private async _whenUnfrozen(windowId: number): Promise<void> {
while (this.reloadingWindowIds.has(windowId)) {
await toPromise(this.onDidReloadingChange.event);
}
@@ -203,9 +218,11 @@ export class Driver implements IDriver, IWindowDriverRegistry {
export async function serve(
windowServer: IPCServer,
handle: string,
environmentService: IEnvironmentService,
instantiationService: IInstantiationService
): TPromise<IDisposable> {
const driver = instantiationService.createInstance(Driver, windowServer);
): Promise<IDisposable> {
const verbose = environmentService.driverVerbose;
const driver = instantiationService.createInstance(Driver, windowServer, { verbose });
const windowDriverRegistryChannel = new WindowDriverRegistryChannel(driver);
windowServer.registerChannel('windowDriverRegistry', windowDriverRegistryChannel);
@@ -215,4 +232,4 @@ export async function serve(
server.registerChannel('driver', channel);
return combinedDisposable([server, windowServer]);
}
}

View File

@@ -5,11 +5,10 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IDriver, DriverChannelClient } from 'vs/platform/driver/common/driver';
import { connect as connectNet, Client } from 'vs/base/parts/ipc/node/ipc.net';
export async function connect(handle: string): TPromise<{ client: Client, driver: IDriver }> {
export async function connect(handle: string): Promise<{ client: Client, driver: IDriver }> {
const client = await connectNet(handle, 'driverClient');
const channel = client.getChannel('driver');
const driver = new DriverChannelClient(channel);

View File

@@ -6,24 +6,13 @@
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
export const IEditorService = createDecorator<IEditorService>('editorService');
export interface IEditorService {
_serviceBrand: any;
/**
* Specific overload to open an instance of IResourceInput.
*/
openEditor(input: IResourceInput, sideBySide?: boolean): TPromise<IEditor>;
}
export interface IEditorModel {
/**
* Emitted when the model is disposed.
*/
onDispose: Event<void>;
/**
@@ -68,191 +57,6 @@ export interface IResourceInput extends IBaseResourceInput {
encoding?: string;
}
export interface IUntitledResourceInput extends IBaseResourceInput {
/**
* Optional resource. If the resource is not provided a new untitled file is created.
*/
resource?: URI;
/**
* Optional file path. Using the file resource will associate the file to the untitled resource.
*/
filePath?: string;
/**
* Optional language of the untitled resource.
*/
language?: string;
/**
* Optional contents of the untitled resource.
*/
contents?: string;
/**
* Optional encoding of the untitled resource.
*/
encoding?: string;
}
export interface IResourceDiffInput extends IBaseResourceInput {
/**
* The left hand side URI to open inside a diff editor.
*/
leftResource: URI;
/**
* The right hand side URI to open inside a diff editor.
*/
rightResource: URI;
}
export interface IResourceSideBySideInput extends IBaseResourceInput {
/**
* The right hand side URI to open inside a side by side editor.
*/
masterResource: URI;
/**
* The left hand side URI to open inside a side by side editor.
*/
detailResource: URI;
}
export interface IEditorControl {
}
export interface IEditor {
/**
* The assigned input of this editor.
*/
input: IEditorInput;
/**
* The assigned options of this editor.
*/
options: IEditorOptions;
/**
* The assigned position of this editor.
*/
position: Position;
/**
* Returns the unique identifier of this editor.
*/
getId(): string;
/**
* Returns the underlying control of this editor.
*/
getControl(): IEditorControl;
/**
* Asks the underlying control to focus.
*/
focus(): void;
/**
* Finds out if this editor is visible or not.
*/
isVisible(): boolean;
}
/**
* Possible locations for opening an editor.
*/
export enum Position {
/** Opens the editor in the first position replacing the input currently showing */
ONE = 0,
/** Opens the editor in the second position replacing the input currently showing */
TWO = 1,
/** Opens the editor in the third most position replacing the input currently showing */
THREE = 2
}
export const POSITIONS = [Position.ONE, Position.TWO, Position.THREE];
export enum Direction {
LEFT,
RIGHT
}
export enum Verbosity {
SHORT,
MEDIUM,
LONG
}
export interface IRevertOptions {
/**
* Forces to load the contents of the editor again even if the editor is not dirty.
*/
force?: boolean;
/**
* A soft revert will clear dirty state of an editor but will not attempt to load it.
*/
soft?: boolean;
}
export interface IEditorInput extends IDisposable {
/**
* Triggered when this input is disposed.
*/
onDispose: Event<void>;
/**
* Returns the associated resource of this input.
*/
getResource(): URI;
/**
* Returns the display name of this input.
*/
getName(): string;
/**
* Returns the display description of this input.
*/
getDescription(verbosity?: Verbosity): string;
/**
* Returns the display title of this input.
*/
getTitle(verbosity?: Verbosity): string;
/**
* Resolves the input.
*/
resolve(): TPromise<IEditorModel>;
/**
* Returns if this input is dirty or not.
*/
isDirty(): boolean;
/**
* Reverts this input.
*/
revert(options?: IRevertOptions): TPromise<boolean>;
/**
* Returns if the other object matches this input.
*/
matches(other: any): boolean;
}
export interface IEditorOptions {
/**
@@ -262,23 +66,23 @@ export interface IEditorOptions {
readonly preserveFocus?: boolean;
/**
* Tells the editor to replace the editor input in the editor even if it is identical to the one
* already showing. By default, the editor will not replace the input if it is identical to the
* 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 forceOpen?: boolean;
readonly forceReload?: boolean;
/**
* Will reveal the editor if it is already opened and visible in any of the opened editor groups. 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.
* to the side of another one or into a specific editor group.
*/
readonly revealIfVisible?: boolean;
/**
* Will reveal the editor if it is already opened (even when not visible) in any of the opened editor groups. 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.
* to the side of another one or into a specific editor group.
*/
readonly revealIfOpened?: boolean;
@@ -286,7 +90,7 @@ export interface IEditorOptions {
* 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.
*/
pinned?: boolean;
readonly pinned?: boolean;
/**
* The index in the document stack where to insert the editor into when opening.
@@ -323,4 +127,4 @@ export interface ITextEditorOptions extends IEditorOptions {
* Option to scroll vertically or horizontally as necessary and reveal a range centered vertically only if it lies outside the viewport.
*/
revealInCenterIfOutsideViewport?: boolean;
}
}

View File

@@ -8,6 +8,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
export interface ParsedArgs {
[arg: string]: any;
_: string[];
'folder-uri'?: string | string[];
_urls?: string[];
help?: boolean;
version?: boolean;
@@ -25,10 +26,10 @@ export interface ParsedArgs {
performance?: boolean;
'prof-startup'?: string;
'prof-startup-prefix'?: string;
'prof-append-timers'?: string;
verbose?: boolean;
log?: string;
logExtensionHostCommunication?: boolean;
'disable-extensions'?: boolean;
'extensions-dir'?: string;
extensionDevelopmentPath?: string;
extensionTestsPath?: string;
@@ -37,6 +38,8 @@ export interface ParsedArgs {
debugId?: string;
debugSearch?: string;
debugBrkSearch?: string;
'disable-extensions'?: boolean;
'disable-extension'?: string | string[];
'list-extensions'?: boolean;
'show-versions'?: boolean;
'install-extension'?: string | string[];
@@ -58,6 +61,7 @@ export interface ParsedArgs {
'file-chmod'?: boolean;
'upload-logs'?: string;
'driver'?: string;
'driver-verbose'?: boolean;
}
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
@@ -98,7 +102,7 @@ export interface IEnvironmentService {
workspacesHome: string;
isExtensionDevelopment: boolean;
disableExtensions: boolean;
disableExtensions: boolean | string[];
extensionsPath: string;
extensionDevelopmentPath: string;
extensionTestsPath: string;
@@ -115,6 +119,7 @@ export interface IEnvironmentService {
performance: boolean;
// logging
log: string;
logsPath: string;
verbose: boolean;
@@ -133,4 +138,5 @@ export interface IEnvironmentService {
disableCrashReporter: boolean;
driverHandle: string;
driverVerbose: boolean;
}

View File

@@ -11,15 +11,18 @@ import { localize } from 'vs/nls';
import { ParsedArgs } from '../common/environment';
import { isWindows } from 'vs/base/common/platform';
import product from 'vs/platform/node/product';
import { MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files';
const options: minimist.Opts = {
string: [
'locale',
'user-data-dir',
'extensions-dir',
'folder-uri',
'extensionDevelopmentPath',
'extensionTestsPath',
'install-extension',
'disable-extension',
'uninstall-extension',
'debugId',
'debugPluginHost',
@@ -62,7 +65,8 @@ const options: minimist.Opts = {
'skip-add-to-recently-opened',
'status',
'file-write',
'file-chmod'
'file-chmod',
'driver-verbose'
],
alias: {
add: 'a',
@@ -89,6 +93,10 @@ function validate(args: ParsedArgs): ParsedArgs {
args._.forEach(arg => assert(/^(\w:)?[^:]+(:\d*){0,2}$/.test(arg), localize('gotoValidation', "Arguments in `--goto` mode should be in the format of `FILE(:LINE(:CHARACTER))`.")));
}
if (args['max-memory']) {
assert(args['max-memory'] >= MIN_MAX_MEMORY_SIZE_MB, `The max-memory argument cannot be specified lower than ${MIN_MAX_MEMORY_SIZE_MB} MB.`);
}
return args;
}
@@ -137,10 +145,11 @@ export function parseArgs(args: string[]): ParsedArgs {
const optionsHelp: { [name: string]: string; } = {
'-d, --diff <file> <file>': localize('diff', "Compare two files with each other."),
'--folder-uri <uri>': localize('folder uri', "Opens a window with given folder uri(s)"),
'-a, --add <dir>': localize('add', "Add folder(s) to the last active window."),
'-g, --goto <file:line[:character]>': localize('goto', "Open a file at the path on the specified line and character position."),
'-n, --new-window': localize('newWindow', "Force to open a new window."),
'-r, --reuse-window': localize('reuseWindow', "Force to open a file or folder in the last active window."),
'-r, --reuse-window': localize('reuseWindow', "Force to open a file or folder in an already opened window."),
'-w, --wait': localize('wait', "Wait for the files to be closed before returning."),
'--locale <locale>': localize('locale', "The locale to use (e.g. en-US or zh-TW)."),
'--user-data-dir <dir>': localize('userDataDir', "Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code."),
@@ -154,7 +163,7 @@ const extensionsHelp: { [name: string]: string; } = {
'--show-versions': localize('showVersions', "Show versions of installed extensions, when using --list-extension."),
'--install-extension (<extension-id> | <extension-vsix-path>)': localize('installExtension', "Installs an extension."),
'--uninstall-extension (<extension-id> | <extension-vsix-path>)': localize('uninstallExtension', "Uninstalls an extension."),
'--enable-proposed-api <extension-id>': localize('experimentalApis', "Enables proposed API features for an extension.")
'--enable-proposed-api (<extension-id>)': localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.")
};
const troubleshootingHelp: { [name: string]: string; } = {
@@ -164,6 +173,7 @@ const troubleshootingHelp: { [name: string]: string; } = {
'-p, --performance': localize('performance', "Start with the 'Developer: Startup Performance' command enabled."),
'--prof-startup': localize('prof-startup', "Run CPU profiler during startup"),
'--disable-extensions': localize('disableExtensions', "Disable all installed extensions."),
'--disable-extension <extension-id>': localize('disableExtension', "Disable an extension."),
'--inspect-extensions': localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI."),
'--inspect-brk-extensions': 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': localize('disableGPU', "Disable GPU hardware acceleration."),

View File

@@ -90,7 +90,13 @@ export class EnvironmentService implements IEnvironmentService {
get userHome(): string { return os.homedir(); }
@memoize
get userDataPath(): string { return parseUserDataDir(this._args, process); }
get userDataPath(): string {
if (process.env['VSCODE_PORTABLE']) {
return path.join(process.env['VSCODE_PORTABLE'], 'user-data');
}
return parseUserDataDir(this._args, process);
}
get appNameLong(): string { return product.nameLong; }
@@ -127,7 +133,19 @@ export class EnvironmentService implements IEnvironmentService {
get installSourcePath(): string { return path.join(this.userDataPath, 'installSource'); }
@memoize
get extensionsPath(): string { return parsePathArg(this._args['extensions-dir'], process) || process.env['VSCODE_EXTENSIONS'] || path.join(this.userHome, product.dataFolderName, 'extensions'); }
get extensionsPath(): string {
const fromArgs = parsePathArg(this._args['extensions-dir'], process);
if (fromArgs) {
return fromArgs;
} else if (process.env['VSCODE_EXTENSIONS']) {
return process.env['VSCODE_EXTENSIONS'];
} else if (process.env['VSCODE_PORTABLE']) {
return path.join(process.env['VSCODE_PORTABLE'], 'extensions');
} else {
return path.join(this.userHome, product.dataFolderName, 'extensions');
}
}
@memoize
get extensionDevelopmentPath(): string { return this._args.extensionDevelopmentPath ? path.normalize(this._args.extensionDevelopmentPath) : this._args.extensionDevelopmentPath; }
@@ -135,7 +153,21 @@ export class EnvironmentService implements IEnvironmentService {
@memoize
get extensionTestsPath(): string { return this._args.extensionTestsPath ? path.normalize(this._args.extensionTestsPath) : this._args.extensionTestsPath; }
get disableExtensions(): boolean { return this._args['disable-extensions']; }
get disableExtensions(): boolean | string[] {
if (this._args['disable-extensions']) {
return true;
}
const disableExtensions: string | string[] = this._args['disable-extension'];
if (disableExtensions) {
if (typeof disableExtensions === 'string') {
return [disableExtensions];
}
if (Array.isArray(disableExtensions) && disableExtensions.length > 0) {
return disableExtensions;
}
}
return false;
}
get skipGettingStarted(): boolean { return this._args['skip-getting-started']; }
@@ -151,6 +183,7 @@ export class EnvironmentService implements IEnvironmentService {
get isBuilt(): boolean { return !process.env['VSCODE_DEV']; }
get verbose(): boolean { return this._args.verbose; }
get log(): string { return this._args.log; }
get wait(): boolean { return this._args.wait; }
get logExtensionHostCommunication(): boolean { return this._args.logExtensionHostCommunication; }
@@ -171,6 +204,7 @@ export class EnvironmentService implements IEnvironmentService {
get disableCrashReporter(): boolean { return !!this._args['disable-crash-reporter']; }
get driverHandle(): string { return this._args['driver']; }
get driverVerbose(): boolean { return this._args['driver-verbose']; }
constructor(private _args: ParsedArgs, private _execPath: string) {
if (!process.env['VSCODE_LOGS']) {

View File

@@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IExtensionManagementService, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, ILocalExtension, isIExtensionIdentifier, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement';
import { getIdFromLocalExtensionId, areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { getIdFromLocalExtensionId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -29,7 +29,7 @@ export class ExtensionEnablementService implements IExtensionEnablementService {
@IStorageService private storageService: IStorageService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IExtensionManagementService extensionManagementService: IExtensionManagementService
@IExtensionManagementService private extensionManagementService: IExtensionManagementService
) {
extensionManagementService.onDidUninstallExtension(this._onDidUninstallExtension, this, this.disposables);
}
@@ -38,7 +38,11 @@ export class ExtensionEnablementService implements IExtensionEnablementService {
return this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY;
}
getDisabledExtensions(): TPromise<IExtensionIdentifier[]> {
get allUserExtensionsDisabled(): boolean {
return this.environmentService.disableExtensions === true;
}
async getDisabledExtensions(): Promise<IExtensionIdentifier[]> {
let result = this._getDisabledExtensions(StorageScope.GLOBAL);
@@ -54,14 +58,25 @@ export class ExtensionEnablementService implements IExtensionEnablementService {
}
}
return TPromise.as(result);
if (this.environmentService.disableExtensions) {
const allInstalledExtensions = await this.extensionManagementService.getInstalled();
for (const installedExtension of allInstalledExtensions) {
if (this._isExtensionDisabledInEnvironment(installedExtension)) {
if (!result.some(r => areSameExtensions(r, installedExtension.galleryIdentifier))) {
result.push(installedExtension.galleryIdentifier);
}
}
}
}
return result;
}
getEnablementState(extension: ILocalExtension): EnablementState {
if (this.environmentService.disableExtensions && extension.type === LocalExtensionType.User) {
if (this._isExtensionDisabledInEnvironment(extension)) {
return EnablementState.Disabled;
}
const identifier = this._getIdentifier(extension);
const identifier = extension.galleryIdentifier;
if (this.hasWorkspace) {
if (this._getEnabledExtensions(StorageScope.WORKSPACE).filter(e => areSameExtensions(e, identifier))[0]) {
return EnablementState.WorkspaceEnabled;
@@ -95,7 +110,7 @@ export class ExtensionEnablementService implements IExtensionEnablementService {
if (!this.canChangeEnablement(arg)) {
return TPromise.wrap(false);
}
identifier = this._getIdentifier(arg);
identifier = arg.galleryIdentifier;
}
const workspace = newState === EnablementState.WorkspaceDisabled || newState === EnablementState.WorkspaceEnabled;
@@ -134,6 +149,17 @@ export class ExtensionEnablementService implements IExtensionEnablementService {
return enablementState === EnablementState.WorkspaceEnabled || enablementState === EnablementState.Enabled;
}
private _isExtensionDisabledInEnvironment(extension: ILocalExtension): boolean {
if (this.allUserExtensionsDisabled) {
return extension.type === LocalExtensionType.User;
}
const disabledExtensions = this.environmentService.disableExtensions;
if (Array.isArray(disabledExtensions)) {
return disabledExtensions.some(id => areSameExtensions({ id }, extension.galleryIdentifier));
}
return false;
}
private _getEnablementState(identifier: IExtensionIdentifier): EnablementState {
if (this.hasWorkspace) {
if (this._getEnabledExtensions(StorageScope.WORKSPACE).filter(e => areSameExtensions(e, identifier))[0]) {
@@ -150,10 +176,6 @@ export class ExtensionEnablementService implements IExtensionEnablementService {
return EnablementState.Enabled;
}
private _getIdentifier(extension: ILocalExtension): IExtensionIdentifier {
return { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid };
}
private _enableExtension(identifier: IExtensionIdentifier): void {
this._removeFromDisabledExtensions(identifier, StorageScope.WORKSPACE);
this._removeFromEnabledExtensions(identifier, StorageScope.WORKSPACE);

View File

@@ -11,6 +11,8 @@ import { Event } from 'vs/base/common/event';
import { IPager } from 'vs/base/common/paging';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILocalization } from 'vs/platform/localizations/common/localizations';
import URI from 'vs/base/common/uri';
import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace';
export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$';
export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN);
@@ -76,6 +78,11 @@ export interface ITheme {
label: string;
}
export interface IViewContainer {
id: string;
title: string;
}
export interface IView {
id: string;
name: string;
@@ -89,7 +96,7 @@ export interface IColor {
export interface IExtensionContributions {
commands?: ICommand[];
configuration?: IConfiguration;
configuration?: IConfiguration | IConfiguration[];
debuggers?: IDebugger[];
grammars?: IGrammar[];
jsonValidation?: IJSONValidation[];
@@ -99,6 +106,7 @@ export interface IExtensionContributions {
snippets?: ISnippet[];
themes?: ITheme[];
iconThemes?: ITheme[];
viewsContainers?: { [location: string]: IViewContainer[] };
views?: { [location: string]: IView[] };
colors?: IColor[];
localizations?: ILocalization[];
@@ -117,6 +125,7 @@ export interface IExtensionManifest {
categories?: string[];
activationEvents?: string[];
extensionDependencies?: string[];
extensionPack?: string[];
contributes?: IExtensionContributions;
repository?: {
url: string;
@@ -128,6 +137,7 @@ export interface IExtensionManifest {
export interface IGalleryExtensionProperties {
dependencies?: string[];
extensionPack?: string[];
engine?: string;
}
@@ -146,6 +156,7 @@ export interface IGalleryExtensionAssets {
icon: IGalleryExtensionAsset;
license: IGalleryExtensionAsset;
repository: IGalleryExtensionAsset;
coreTranslations: { [languageId: string]: IGalleryExtensionAsset };
}
export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier {
@@ -199,9 +210,10 @@ export enum LocalExtensionType {
export interface ILocalExtension {
type: LocalExtensionType;
identifier: IExtensionIdentifier;
galleryIdentifier: IExtensionIdentifier;
manifest: IExtensionManifest;
metadata: IGalleryMetadata;
path: string;
location: URI;
readmeUrl: string;
changelogUrl: string;
}
@@ -245,15 +257,25 @@ export interface IReportedExtension {
malicious: boolean;
}
export enum InstallOperation {
Install = 1,
Update
}
export interface ITranslation {
contents: { [key: string]: {} };
}
export interface IExtensionGalleryService {
_serviceBrand: any;
isEnabled(): boolean;
query(options?: IQueryOptions): TPromise<IPager<IGalleryExtension>>;
download(extension: IGalleryExtension): TPromise<string>;
download(extension: IGalleryExtension, operation: InstallOperation): TPromise<string>;
reportStatistic(publisher: string, name: string, version: string, type: StatisticType): TPromise<void>;
getReadme(extension: IGalleryExtension): TPromise<string>;
getManifest(extension: IGalleryExtension): TPromise<IExtensionManifest>;
getChangelog(extension: IGalleryExtension): TPromise<string>;
getCoreTranslation(extension: IGalleryExtension, languageId: string): TPromise<ITranslation>;
loadCompatibleVersion(extension: IGalleryExtension): TPromise<IGalleryExtension>;
loadAllDependencies(dependencies: IExtensionIdentifier[]): TPromise<IGalleryExtension[]>;
getExtensionsReport(): TPromise<IReportedExtension[]>;
@@ -267,6 +289,7 @@ export interface InstallExtensionEvent {
export interface DidInstallExtensionEvent {
identifier: IExtensionIdentifier;
operation: InstallOperation;
zipPath?: string;
gallery?: IGalleryExtension;
local?: ILocalExtension;
@@ -286,16 +309,30 @@ export interface IExtensionManagementService {
onUninstallExtension: Event<IExtensionIdentifier>;
onDidUninstallExtension: Event<DidUninstallExtensionEvent>;
install(zipPath: string): TPromise<ILocalExtension>;
installFromGallery(extension: IGalleryExtension): TPromise<ILocalExtension>;
install(zipPath: string): TPromise<void>;
installFromGallery(extension: IGalleryExtension): TPromise<void>;
uninstall(extension: ILocalExtension, force?: boolean): TPromise<void>;
reinstallFromGallery(extension: ILocalExtension): TPromise<ILocalExtension>;
reinstallFromGallery(extension: ILocalExtension): TPromise<void>;
getInstalled(type?: LocalExtensionType): TPromise<ILocalExtension[]>;
getExtensionsReport(): TPromise<IReportedExtension[]>;
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): TPromise<ILocalExtension>;
}
export const IExtensionManagementServerService = createDecorator<IExtensionManagementServerService>('extensionManagementServerService');
export interface IExtensionManagementServer {
extensionManagementService: IExtensionManagementService;
location: URI;
}
export interface IExtensionManagementServerService {
_serviceBrand: any;
readonly extensionManagementServers: IExtensionManagementServer[];
getDefaultExtensionManagementServer(): IExtensionManagementServer;
getExtensionManagementServer(location: URI): IExtensionManagementServer;
}
export enum EnablementState {
Disabled,
WorkspaceDisabled,
@@ -309,6 +346,8 @@ export const IExtensionEnablementService = createDecorator<IExtensionEnablementS
export interface IExtensionEnablementService {
_serviceBrand: any;
readonly allUserExtensionsDisabled: boolean;
/**
* Event to listen on for extension enablement changes
*/
@@ -318,7 +357,7 @@ export interface IExtensionEnablementService {
* Returns all disabled extension identifiers for current workspace
* Returns an empty array if none exist
*/
getDisabledExtensions(): TPromise<IExtensionIdentifier[]>;
getDisabledExtensions(): Promise<IExtensionIdentifier[]>;
/**
* Returns the enablement state for the given extension
@@ -347,24 +386,49 @@ export interface IExtensionEnablementService {
setEnablement(extension: ILocalExtension, state: EnablementState): TPromise<boolean>;
}
export interface IExtensionsConfigContent {
recommendations: string[];
unwantedRecommendations: string[];
}
export type RecommendationChangeNotification = {
extensionId: string,
isRecommended: boolean
};
export type DynamicRecommendation = 'dynamic';
export type ExecutableRecommendation = 'executable';
export type CachedRecommendation = 'cached';
export type ApplicationRecommendation = 'application';
export type ExtensionRecommendationSource = IWorkspace | IWorkspaceFolder | URI | DynamicRecommendation | ExecutableRecommendation | CachedRecommendation | ApplicationRecommendation;
export interface IExtensionRecommendation {
extensionId: string;
sources: ExtensionRecommendationSource[];
}
export const IExtensionTipsService = createDecorator<IExtensionTipsService>('extensionTipsService');
export interface IExtensionTipsService {
_serviceBrand: any;
getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason, reasonText: string }; };
getFileBasedRecommendations(): string[];
getOtherRecommendations(): TPromise<string[]>;
getWorkspaceRecommendations(): TPromise<string[]>;
getKeymapRecommendations(): string[];
getFileBasedRecommendations(): IExtensionRecommendation[];
getOtherRecommendations(): TPromise<IExtensionRecommendation[]>;
getWorkspaceRecommendations(): TPromise<IExtensionRecommendation[]>;
getKeymapRecommendations(): IExtensionRecommendation[];
getAllRecommendations(): TPromise<IExtensionRecommendation[]>;
getKeywordsForExtension(extension: string): string[];
getRecommendationsForExtension(extension: string): string[];
toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void;
getAllIgnoredRecommendations(): { global: string[], workspace: string[] };
onRecommendationChange: Event<RecommendationChangeNotification>;
}
export enum ExtensionRecommendationReason {
Workspace,
File,
Executable,
DynamicWorkspace
DynamicWorkspace,
Experimental
}
export const ExtensionsLabel = localize('extensions', "Extensions");

View File

@@ -6,22 +6,24 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, LocalExtensionType, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension } from './extensionManagement';
import { Event, buffer } from 'vs/base/common/event';
import { Event, buffer, mapEvent } from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
import { IURITransformer } from 'vs/base/common/uriIpc';
export interface IExtensionManagementChannel extends IChannel {
call(command: 'event:onInstallExtension'): TPromise<void>;
call(command: 'event:onDidInstallExtension'): TPromise<void>;
call(command: 'event:onUninstallExtension'): TPromise<void>;
call(command: 'event:onDidUninstallExtension'): TPromise<void>;
call(command: 'install', path: string): TPromise<ILocalExtension>;
call(command: 'installFromGallery', extension: IGalleryExtension): TPromise<ILocalExtension>;
listen(event: 'onInstallExtension'): Event<InstallExtensionEvent>;
listen(event: 'onDidInstallExtension'): Event<DidInstallExtensionEvent>;
listen(event: 'onUninstallExtension'): Event<IExtensionIdentifier>;
listen(event: 'onDidUninstallExtension'): Event<DidUninstallExtensionEvent>;
call(command: 'install', args: [string]): TPromise<void>;
call(command: 'installFromGallery', args: [IGalleryExtension]): TPromise<void>;
call(command: 'uninstall', args: [ILocalExtension, boolean]): TPromise<void>;
call(command: 'reinstallFromGallery', args: [ILocalExtension]): TPromise<ILocalExtension>;
call(command: 'getInstalled'): TPromise<ILocalExtension[]>;
call(command: 'reinstallFromGallery', args: [ILocalExtension]): TPromise<void>;
call(command: 'getInstalled', args: [LocalExtensionType]): TPromise<ILocalExtension[]>;
call(command: 'getExtensionsReport'): TPromise<IReportedExtension[]>;
call(command: string, arg?: any): TPromise<any>;
call(command: 'updateMetadata', args: [ILocalExtension, IGalleryMetadata]): TPromise<ILocalExtension>;
}
export class ExtensionManagementChannel implements IExtensionManagementChannel {
@@ -38,21 +40,33 @@ export class ExtensionManagementChannel implements IExtensionManagementChannel {
this.onDidUninstallExtension = buffer(service.onDidUninstallExtension, true);
}
call(command: string, arg?: any): TPromise<any> {
listen(event: string): Event<any> {
switch (event) {
case 'onInstallExtension': return this.onInstallExtension;
case 'onDidInstallExtension': return this.onDidInstallExtension;
case 'onUninstallExtension': return this.onUninstallExtension;
case 'onDidUninstallExtension': return this.onDidUninstallExtension;
}
throw new Error('Invalid listen');
}
call(command: string, args?: any): TPromise<any> {
switch (command) {
case 'event:onInstallExtension': return eventToCall(this.onInstallExtension);
case 'event:onDidInstallExtension': return eventToCall(this.onDidInstallExtension);
case 'event:onUninstallExtension': return eventToCall(this.onUninstallExtension);
case 'event:onDidUninstallExtension': return eventToCall(this.onDidUninstallExtension);
case 'install': return this.service.install(arg);
case 'installFromGallery': return this.service.installFromGallery(arg[0]);
case 'uninstall': return this.service.uninstall(arg[0], arg[1]);
case 'reinstallFromGallery': return this.service.reinstallFromGallery(arg[0]);
case 'getInstalled': return this.service.getInstalled(arg);
case 'updateMetadata': return this.service.updateMetadata(arg[0], arg[1]);
case 'install': return this.service.install(args[0]);
case 'installFromGallery': return this.service.installFromGallery(args[0]);
case 'uninstall': return this.service.uninstall(this._transform(args[0]), args[1]);
case 'reinstallFromGallery': return this.service.reinstallFromGallery(this._transform(args[0]));
case 'getInstalled': return this.service.getInstalled(args[0]);
case 'updateMetadata': return this.service.updateMetadata(this._transform(args[0]), args[1]);
case 'getExtensionsReport': return this.service.getExtensionsReport();
}
return undefined;
throw new Error('Invalid call');
}
private _transform(extension: ILocalExtension): ILocalExtension {
return extension ? { ...extension, ...{ location: URI.revive(extension.location) } } : extension;
}
}
@@ -60,45 +74,49 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer
_serviceBrand: any;
constructor(private channel: IExtensionManagementChannel) { }
constructor(private channel: IExtensionManagementChannel, private uriTransformer: IURITransformer) { }
private _onInstallExtension = eventFromCall<InstallExtensionEvent>(this.channel, 'event:onInstallExtension');
get onInstallExtension(): Event<InstallExtensionEvent> { return this._onInstallExtension; }
get onInstallExtension(): Event<InstallExtensionEvent> { return this.channel.listen('onInstallExtension'); }
get onDidInstallExtension(): Event<DidInstallExtensionEvent> { return mapEvent(this.channel.listen('onDidInstallExtension'), i => ({ ...i, local: this._transformIncoming(i.local) })); }
get onUninstallExtension(): Event<IExtensionIdentifier> { return this.channel.listen('onUninstallExtension'); }
get onDidUninstallExtension(): Event<DidUninstallExtensionEvent> { return this.channel.listen('onDidUninstallExtension'); }
private _onDidInstallExtension = eventFromCall<DidInstallExtensionEvent>(this.channel, 'event:onDidInstallExtension');
get onDidInstallExtension(): Event<DidInstallExtensionEvent> { return this._onDidInstallExtension; }
private _onUninstallExtension = eventFromCall<IExtensionIdentifier>(this.channel, 'event:onUninstallExtension');
get onUninstallExtension(): Event<IExtensionIdentifier> { return this._onUninstallExtension; }
private _onDidUninstallExtension = eventFromCall<DidUninstallExtensionEvent>(this.channel, 'event:onDidUninstallExtension');
get onDidUninstallExtension(): Event<DidUninstallExtensionEvent> { return this._onDidUninstallExtension; }
install(zipPath: string): TPromise<ILocalExtension> {
return this.channel.call('install', zipPath);
install(zipPath: string): TPromise<void> {
return this.channel.call('install', [zipPath]);
}
installFromGallery(extension: IGalleryExtension): TPromise<ILocalExtension> {
installFromGallery(extension: IGalleryExtension): TPromise<void> {
return this.channel.call('installFromGallery', [extension]);
}
uninstall(extension: ILocalExtension, force = false): TPromise<void> {
return this.channel.call('uninstall', [extension, force]);
return this.channel.call('uninstall', [this._transformOutgoing(extension), force]);
}
reinstallFromGallery(extension: ILocalExtension): TPromise<ILocalExtension> {
return this.channel.call('reinstallFromGallery', [extension]);
reinstallFromGallery(extension: ILocalExtension): TPromise<void> {
return this.channel.call('reinstallFromGallery', [this._transformOutgoing(extension)]);
}
getInstalled(type: LocalExtensionType = null): TPromise<ILocalExtension[]> {
return this.channel.call('getInstalled', type);
return this.channel.call('getInstalled', [type])
.then(extensions => extensions.map(extension => this._transformIncoming(extension)));
}
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): TPromise<ILocalExtension> {
return this.channel.call('updateMetadata', [local, metadata]);
return this.channel.call('updateMetadata', [this._transformOutgoing(local), metadata])
.then(extension => this._transformIncoming(extension));
}
getExtensionsReport(): TPromise<IReportedExtension[]> {
return this.channel.call('getExtensionsReport');
}
private _transformIncoming(extension: ILocalExtension): ILocalExtension {
return extension ? { ...extension, ...{ location: URI.revive(this.uriTransformer.transformIncoming(extension.location)) } } : extension;
}
private _transformOutgoing(extension: ILocalExtension): ILocalExtension {
return extension ? { ...extension, ...{ location: this.uriTransformer.transformOutgoing(extension.location) } } : extension;
}
}

View File

@@ -18,7 +18,7 @@ export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifi
}
export function getGalleryExtensionId(publisher: string, name: string): string {
return `${publisher}.${name.toLocaleLowerCase()}`;
return `${publisher.toLocaleLowerCase()}.${name.toLocaleLowerCase()}`;
}
export function getGalleryExtensionIdFromLocal(local: ILocalExtension): string {

View File

@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TPromise } from 'vs/base/common/winjs.base';
import { Event, EventMultiplexer } from 'vs/base/common/event';
import {
IExtensionManagementService, ILocalExtension, IGalleryExtension, LocalExtensionType, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata,
IExtensionManagementServerService, IExtensionManagementServer
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { flatten } from 'vs/base/common/arrays';
export class MulitExtensionManagementService implements IExtensionManagementService {
_serviceBrand: any;
onInstallExtension: Event<InstallExtensionEvent>;
onDidInstallExtension: Event<DidInstallExtensionEvent>;
onUninstallExtension: Event<IExtensionIdentifier>;
onDidUninstallExtension: Event<DidUninstallExtensionEvent>;
private readonly servers: IExtensionManagementServer[];
constructor(
@IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService
) {
this.servers = this.extensionManagementServerService.extensionManagementServers;
this.onInstallExtension = this.servers.reduce((emitter: EventMultiplexer<InstallExtensionEvent>, server) => { emitter.add(server.extensionManagementService.onInstallExtension); return emitter; }, new EventMultiplexer<InstallExtensionEvent>()).event;
this.onDidInstallExtension = this.servers.reduce((emitter: EventMultiplexer<DidInstallExtensionEvent>, server) => { emitter.add(server.extensionManagementService.onDidInstallExtension); return emitter; }, new EventMultiplexer<DidInstallExtensionEvent>()).event;
this.onUninstallExtension = this.servers.reduce((emitter: EventMultiplexer<IExtensionIdentifier>, server) => { emitter.add(server.extensionManagementService.onUninstallExtension); return emitter; }, new EventMultiplexer<IExtensionIdentifier>()).event;
this.onDidUninstallExtension = this.servers.reduce((emitter: EventMultiplexer<DidUninstallExtensionEvent>, server) => { emitter.add(server.extensionManagementService.onDidUninstallExtension); return emitter; }, new EventMultiplexer<DidUninstallExtensionEvent>()).event;
}
getInstalled(type?: LocalExtensionType): TPromise<ILocalExtension[]> {
return TPromise.join(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type)))
.then(result => flatten(result));
}
uninstall(extension: ILocalExtension, force?: boolean): TPromise<void> {
return this.getServer(extension).extensionManagementService.uninstall(extension, force);
}
reinstallFromGallery(extension: ILocalExtension): TPromise<void> {
return this.getServer(extension).extensionManagementService.reinstallFromGallery(extension);
}
updateMetadata(extension: ILocalExtension, metadata: IGalleryMetadata): TPromise<ILocalExtension> {
return this.getServer(extension).extensionManagementService.updateMetadata(extension, metadata);
}
install(zipPath: string): TPromise<void> {
return this.servers[0].extensionManagementService.install(zipPath);
}
installFromGallery(extension: IGalleryExtension): TPromise<void> {
return TPromise.join(this.servers.map(server => server.extensionManagementService.installFromGallery(extension))).then(() => null);
}
getExtensionsReport(): TPromise<IReportedExtension[]> {
return this.servers[0].extensionManagementService.getExtensionsReport();
}
private getServer(extension: ILocalExtension): IExtensionManagementServer {
return this.extensionManagementServerService.getExtensionManagementServer(extension.location);
}
}

View File

@@ -9,7 +9,7 @@ import * as path from 'path';
import { TPromise } from 'vs/base/common/winjs.base';
import { distinct } from 'vs/base/common/arrays';
import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
import { StatisticType, IGalleryExtension, IExtensionGalleryService, IGalleryExtensionAsset, IQueryOptions, SortBy, SortOrder, IExtensionManifest, IExtensionIdentifier, IReportedExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { StatisticType, IGalleryExtension, IExtensionGalleryService, IGalleryExtensionAsset, IQueryOptions, SortBy, SortOrder, IExtensionManifest, IExtensionIdentifier, IReportedExtension, InstallOperation, ITranslation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { assign, getOrDefault } from 'vs/base/common/objects';
import { IRequestService } from 'vs/platform/request/node/request';
@@ -117,6 +117,7 @@ const AssetType = {
const PropertyType = {
Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies',
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
Engine: 'Microsoft.VisualStudio.Code.Engine'
};
@@ -205,6 +206,15 @@ function getStatistic(statistics: IRawGalleryExtensionStatistics[], name: string
return result ? result.value : 0;
}
function getCoreTranslationAssets(version: IRawGalleryExtensionVersion): { [languageId: string]: IGalleryExtensionAsset } {
const coreTranslationAssetPrefix = 'Microsoft.VisualStudio.Code.Translation.';
const result = version.files.filter(f => f.assetType.indexOf(coreTranslationAssetPrefix) === 0);
return result.reduce((result, file) => {
result[file.assetType.substring(coreTranslationAssetPrefix.length)] = getVersionAsset(version, file.assetType);
return result;
}, {});
}
function getVersionAsset(version: IRawGalleryExtensionVersion, type: string): IGalleryExtensionAsset {
const result = version.files.filter(f => f.assetType === type)[0];
@@ -253,8 +263,8 @@ function getVersionAsset(version: IRawGalleryExtensionVersion, type: string): IG
if (type === AssetType.VSIX) {
return {
// {{SQL CARBON EDIT}}
uri: uriFromSource || `${version.fallbackAssetUri}/${type}?redirect=true&install=true`,
fallbackUri: `${version.fallbackAssetUri}/${type}?install=true`
uri: uriFromSource || `${version.fallbackAssetUri}/${type}?redirect=true`,
fallbackUri: `${version.fallbackAssetUri}/${type}`
};
}
@@ -272,8 +282,8 @@ function getVersionAsset(version: IRawGalleryExtensionVersion, type: string): IG
}
}
function getDependencies(version: IRawGalleryExtensionVersion): string[] {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Dependency) : [];
function getExtensions(version: IRawGalleryExtensionVersion, property: string): string[] {
const values = version.properties ? version.properties.filter(p => p.key === property) : [];
const value = values.length > 0 && values[0].value;
return value ? value.split(',').map(v => adoptToGalleryExtensionId(v)) : [];
}
@@ -299,6 +309,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, extensionsGalleryUr
icon: getVersionAsset(version, AssetType.Icon),
license: getVersionAsset(version, AssetType.License),
repository: getVersionAsset(version, AssetType.Repository),
coreTranslations: getCoreTranslationAssets(version)
};
return {
@@ -314,18 +325,19 @@ function toExtension(galleryExtension: IRawGalleryExtension, extensionsGalleryUr
publisher: galleryExtension.publisher.publisherName,
publisherDisplayName: galleryExtension.publisher.displayName,
description: galleryExtension.shortDescription || '',
installCount: getStatistic(galleryExtension.statistics, 'install'),
installCount: getStatistic(galleryExtension.statistics, 'install') + getStatistic(galleryExtension.statistics, 'updateCount'),
rating: getStatistic(galleryExtension.statistics, 'averagerating'),
ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'),
assets,
properties: {
dependencies: getDependencies(version),
dependencies: getExtensions(version, PropertyType.Dependency),
extensionPack: getExtensions(version, PropertyType.ExtensionPack),
engine: getEngine(version)
},
/* __GDPR__FRAGMENT__
"GalleryExtensionTelemetryData2" : {
"index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"searchText": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"searchText": { "classification": "CustomerContent", "purpose": "FeatureInsight" },
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
@@ -384,7 +396,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
/* __GDPR__
"galleryService:query" : {
"type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"text": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"text": { "classification": "CustomerContent", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('galleryService:query', { type, text });
@@ -393,8 +405,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
.withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, pageSize)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
.withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished))
.withAssetTypes(AssetType.Icon, AssetType.License, AssetType.Details, AssetType.Manifest, AssetType.VSIX, AssetType.Changelog);
.withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished));
if (text) {
// Use category filter instead of "category:themes"
@@ -575,7 +586,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
});
}
download(extension: IGalleryExtension): TPromise<string> {
download(extension: IGalleryExtension, operation: InstallOperation): TPromise<string> {
return this.loadCompatibleVersion(extension)
.then(extension => {
if (!extension) {
@@ -594,7 +605,13 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
*/
const log = (duration: number) => this.telemetryService.publicLog('galleryService:downloadVSIX', assign(data, { duration }));
return this.getAsset(extension.assets.download)
const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : '';
const downloadAsset = operationParam ? {
uri: `${extension.assets.download.uri}&${operationParam}=true`,
fallbackUri: `${extension.assets.download.fallbackUri}?${operationParam}=true`
} : extension.assets.download;
return this.getAsset(downloadAsset)
.then(context => download(zipPath, context))
.then(() => log(new Date().getTime() - startTime))
.then(() => zipPath);
@@ -612,6 +629,16 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
.then(JSON.parse);
}
getCoreTranslation(extension: IGalleryExtension, languageId: string): TPromise<ITranslation> {
const asset = extension.assets.coreTranslations[languageId.toUpperCase()];
if (asset) {
return this.getAsset(asset)
.then(asText)
.then(JSON.parse);
}
return TPromise.as(null);
}
getChangelog(extension: IGalleryExtension): TPromise<string> {
return this.getAsset(extension.assets.changelog)
.then(asText);
@@ -645,7 +672,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
return this.getLastValidExtensionVersion(rawExtension, rawExtension.versions)
.then(rawVersion => {
if (rawVersion) {
extension.properties.dependencies = getDependencies(rawVersion);
extension.properties.dependencies = getExtensions(rawVersion, PropertyType.Dependency);
extension.properties.engine = getEngine(rawVersion);
// {{SQL CARBON EDIT}}
extension.assets.download = getVersionAsset(rawVersion, AssetType.VSIX);

View File

@@ -10,9 +10,10 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { ILogService } from 'vs/platform/log/common/log';
import { fork, ChildProcess } from 'child_process';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { join } from 'vs/base/common/paths';
import { posix } from 'path';
import { Limiter } from 'vs/base/common/async';
import { fromNodeEventEmitter, anyEvent, mapEvent, debounceEvent } from 'vs/base/common/event';
import { Schemas } from 'vs/base/common/network';
export class ExtensionsLifecycle extends Disposable {
@@ -36,13 +37,13 @@ export class ExtensionsLifecycle extends Disposable {
}
private parseUninstallScript(extension: ILocalExtension): { uninstallHook: string, args: string[] } {
if (extension.manifest && extension.manifest['scripts'] && typeof extension.manifest['scripts']['vscode:uninstall'] === 'string') {
if (extension.location.scheme === Schemas.file && extension.manifest && extension.manifest['scripts'] && typeof extension.manifest['scripts']['vscode:uninstall'] === 'string') {
const uninstallScript = (<string>extension.manifest['scripts']['vscode:uninstall']).split(' ');
if (uninstallScript.length < 2 || uninstallScript[0] !== 'node' || !uninstallScript[1]) {
this.logService.warn(extension.identifier.id, 'Uninstall script should be a node script');
return null;
}
return { uninstallHook: join(extension.path, uninstallScript[1]), args: uninstallScript.slice(2) || [] };
return { uninstallHook: posix.join(extension.location.fsPath, uninstallScript[1]), args: uninstallScript.slice(2) || [] };
}
return null;
}

View File

@@ -11,18 +11,19 @@ import * as pfs from 'vs/base/node/pfs';
import * as errors from 'vs/base/common/errors';
import { assign } from 'vs/base/common/objects';
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { flatten, distinct } from 'vs/base/common/arrays';
import { flatten } from 'vs/base/common/arrays';
import { extract, buffer, ExtractError } from 'vs/base/node/zip';
import { TPromise } from 'vs/base/common/winjs.base';
import { TPromise, ValueCallback, ErrorCallback } from 'vs/base/common/winjs.base';
import {
IExtensionManagementService, IExtensionGalleryService, ILocalExtension,
IGalleryExtension, IExtensionManifest, IGalleryMetadata,
InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, LocalExtensionType,
StatisticType,
IExtensionIdentifier,
IReportedExtension
IReportedExtension,
InstallOperation
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { getGalleryExtensionIdFromLocal, adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getLocalExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { getGalleryExtensionIdFromLocal, adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getLocalExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getIdFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { localizeManifest } from '../common/extensionNls';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { Limiter, always } from 'vs/base/common/async';
@@ -52,6 +53,7 @@ const INSTALL_ERROR_LOCAL = 'local';
const INSTALL_ERROR_EXTRACTING = 'extracting';
const INSTALL_ERROR_RENAMING = 'renaming';
const INSTALL_ERROR_DELETING = 'deleting';
const INSTALL_ERROR_MALICIOUS = 'malicious';
const ERROR_UNKNOWN = 'unknown';
export class ExtensionManagementError extends Error {
@@ -102,11 +104,6 @@ interface InstallableExtension {
metadata?: IGalleryMetadata;
}
enum Operation {
Install = 1,
Update
}
export class ExtensionManagementService extends Disposable implements IExtensionManagementService {
_serviceBrand: any;
@@ -116,8 +113,8 @@ export class ExtensionManagementService extends Disposable implements IExtension
private uninstalledFileLimiter: Limiter<void>;
private reportedExtensions: TPromise<IReportedExtension[]> | undefined;
private lastReportTimestamp = 0;
private readonly installationStartTime: Map<string, number> = new Map<string, number>();
private readonly installingExtensions: Map<string, TPromise<ILocalExtension>> = new Map<string, TPromise<ILocalExtension>>();
private readonly installingExtensions: Map<string, TPromise<void>> = new Map<string, TPromise<void>>();
private readonly uninstallingExtensions: Map<string, TPromise<void>> = new Map<string, TPromise<void>>();
private readonly manifestCache: ExtensionsManifestCache;
private readonly extensionLifecycle: ExtensionsLifecycle;
@@ -144,12 +141,18 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.extensionsPath = environmentService.extensionsPath;
this.uninstalledPath = path.join(this.extensionsPath, '.obsolete');
this.uninstalledFileLimiter = new Limiter(1);
this._register(toDisposable(() => this.installingExtensions.clear()));
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
this.extensionLifecycle = this._register(new ExtensionsLifecycle(this.logService));
this._register(toDisposable(() => {
this.installingExtensions.forEach(promise => promise.cancel());
this.uninstallingExtensions.forEach(promise => promise.cancel());
this.installingExtensions.clear();
this.uninstallingExtensions.clear();
}));
}
install(zipPath: string): TPromise<ILocalExtension> {
install(zipPath: string): TPromise<void> {
zipPath = path.resolve(zipPath);
return validateLocalExtension(zipPath)
@@ -171,14 +174,15 @@ export class ExtensionManagementService extends Disposable implements IExtension
// Until there's a gallery for SQL Ops Studio, skip retrieving the metadata from the gallery
return this.installExtension({ zipPath, id: identifier.id, metadata: null })
.then(
local => this._onDidInstallExtension.fire({ identifier, zipPath, local }),
error => { this._onDidInstallExtension.fire({ identifier, zipPath, error }); return TPromise.wrapError(error); }
local => this._onDidInstallExtension.fire({ identifier, zipPath, local, operation: InstallOperation.Install }),
error => { this._onDidInstallExtension.fire({ identifier, zipPath, error, operation: InstallOperation.Install }); return TPromise.wrapError(error); }
);
/*
return this.getMetadata(getGalleryExtensionId(manifest.publisher, manifest.name))
.then(
metadata => this.installFromZipPath(identifier, zipPath, metadata, manifest),
error => this.installFromZipPath(identifier, zipPath, null, manifest));
local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; },
*/
}
return null;
@@ -216,45 +220,73 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private installFromZipPath(identifier: IExtensionIdentifier, zipPath: string, metadata: IGalleryMetadata, manifest: IExtensionManifest): TPromise<ILocalExtension> {
return this.installExtension({ zipPath, id: identifier.id, metadata })
.then(local => {
if (this.galleryService.isEnabled() && local.manifest.extensionDependencies && local.manifest.extensionDependencies.length) {
return this.getDependenciesToInstall(local.manifest.extensionDependencies)
.then(dependenciesToInstall => this.downloadAndInstallExtensions(metadata ? dependenciesToInstall.filter(d => d.identifier.uuid !== metadata.id) : dependenciesToInstall))
.then(() => local, error => {
this.setUninstalled(local);
return TPromise.wrapError(new Error(nls.localize('errorInstallingDependencies', "Error while installing dependencies. {0}", error instanceof Error ? error.message : error)));
// private installFromZipPath(identifier: IExtensionIdentifier, zipPath: string, metadata: IGalleryMetadata, manifest: IExtensionManifest): TPromise<ILocalExtension> {
// return this.toNonCancellablePromise(this.getInstalled()
// .then(installed => {
// const operation = this.getOperation({ id: getIdFromLocalExtensionId(identifier.id), uuid: identifier.uuid }, installed);
// return this.installExtension({ zipPath, id: identifier.id, metadata })
// .then(local => this.installDependenciesAndPackExtensions(local, null).then(() => local, error => this.uninstall(local, true).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error))))
// .then(
// local => { this._onDidInstallExtension.fire({ identifier, zipPath, local, operation }); return local; },
// error => { this._onDidInstallExtension.fire({ identifier, zipPath, operation, error }); return TPromise.wrapError(error); }
// );
// }));
// }
installFromGallery(extension: IGalleryExtension): TPromise<void> {
let installingExtension = this.installingExtensions.get(extension.identifier.id);
if (!installingExtension) {
let successCallback: ValueCallback<void>, errorCallback: ErrorCallback;
installingExtension = new TPromise((c, e) => { successCallback = c; errorCallback = e; });
this.installingExtensions.set(extension.identifier.id, installingExtension);
try {
const startTime = new Date().getTime();
const identifier = { id: getLocalExtensionIdFromGallery(extension, extension.version), uuid: extension.identifier.uuid };
const telemetryData = getGalleryExtensionTelemetryData(extension);
let operation: InstallOperation = InstallOperation.Install;
this.logService.info('Installing extension:', extension.name);
this._onInstallExtension.fire({ identifier, gallery: extension });
this.checkMalicious(extension)
.then(() => this.getInstalled(LocalExtensionType.User))
.then(installed => {
const existingExtension = installed.filter(i => areSameExtensions(i.galleryIdentifier, extension.identifier))[0];
operation = existingExtension ? InstallOperation.Update : InstallOperation.Install;
return this.downloadInstallableExtension(extension, operation)
.then(installableExtension => this.installExtension(installableExtension).then(local => always(pfs.rimraf(installableExtension.zipPath), () => null).then(() => local)))
.then(local => this.installDependenciesAndPackExtensions(local, existingExtension)
.then(() => local, error => this.uninstall(local, true).then(() => TPromise.wrapError(error), () => TPromise.wrapError(error))));
})
.then(
local => {
this.installingExtensions.delete(extension.identifier.id);
this.logService.info(`Extensions installed successfully:`, extension.identifier.id);
this._onDidInstallExtension.fire({ identifier, gallery: extension, local, operation });
this.reportTelemetry(this.getTelemetryEvent(operation), telemetryData, new Date().getTime() - startTime, void 0);
successCallback(null);
},
error => {
this.installingExtensions.delete(extension.identifier.id);
const errorCode = error && (<ExtensionManagementError>error).code ? (<ExtensionManagementError>error).code : ERROR_UNKNOWN;
this.logService.error(`Failed to install extension:`, extension.identifier.id, error ? error.message : errorCode);
this._onDidInstallExtension.fire({ identifier, gallery: extension, operation, error: errorCode });
this.reportTelemetry(this.getTelemetryEvent(operation), telemetryData, new Date().getTime() - startTime, error);
errorCallback(error);
});
}
return local;
})
.then(
local => { this._onDidInstallExtension.fire({ identifier, zipPath, local }); return local; },
error => { this._onDidInstallExtension.fire({ identifier, zipPath, error }); return TPromise.wrapError(error); }
);
} catch (error) {
this.installingExtensions.delete(extension.identifier.id);
errorCallback(error);
}
}
return installingExtension;
}
installFromGallery(extension: IGalleryExtension): TPromise<ILocalExtension> {
this.onInstallExtensions([extension]);
return this.getInstalled(LocalExtensionType.User)
.then(installed => this.collectExtensionsToInstall(extension)
.then(
extensionsToInstall => {
if (extensionsToInstall.length > 1) {
this.onInstallExtensions(extensionsToInstall.slice(1));
}
const operataions: Operation[] = extensionsToInstall.map(e => this.getOperation(e, installed));
return this.downloadAndInstallExtensions(extensionsToInstall)
.then(
locals => this.onDidInstallExtensions(extensionsToInstall, locals, operataions, [])
.then(() => locals.filter(l => areSameExtensions({ id: getGalleryExtensionIdFromLocal(l), uuid: l.identifier.uuid }, extension.identifier)[0])),
errors => this.onDidInstallExtensions(extensionsToInstall, [], operataions, errors));
},
error => this.onDidInstallExtensions([extension], [], [this.getOperation(extension, installed)], [error])));
}
reinstallFromGallery(extension: ILocalExtension): TPromise<ILocalExtension> {
reinstallFromGallery(extension: ILocalExtension): TPromise<void> {
if (!this.galleryService.isEnabled()) {
return TPromise.wrapError(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled")));
}
@@ -271,54 +303,26 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private getOperation(extensionToInstall: IGalleryExtension, installed: ILocalExtension[]): Operation {
return installed.some(i => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i), uuid: i.identifier.uuid }, extensionToInstall.identifier)) ? Operation.Update : Operation.Install;
// private getOperation(extensionToInstall: IGalleryExtension, installed: ILocalExtension[]): Operation {
// return installed.some(i => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i), uuid: i.identifier.uuid }, extensionToInstall.identifier)) ? Operation.Update : Operation.Install;
// }
private getTelemetryEvent(operation: InstallOperation): string {
return operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install';
}
private collectExtensionsToInstall(extension: IGalleryExtension): TPromise<IGalleryExtension[]> {
return this.galleryService.loadCompatibleVersion(extension)
.then(compatible => {
if (!compatible) {
// {{SQL CARBON EDIT}}
return TPromise.wrapError<IGalleryExtension[]>(new ExtensionManagementError(nls.localize('notFoundCompatible', "Unable to install because, the extension '{0}' compatible with current version '{1}' of SQL Operations Studio is not found.", extension.identifier.id, pkg.version), INSTALL_ERROR_INCOMPATIBLE));
private checkMalicious(extension: IGalleryExtension): TPromise<void> {
return this.getExtensionsReport()
.then(report => {
if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) {
throw new ExtensionManagementError(INSTALL_ERROR_MALICIOUS, nls.localize('malicious extension', "Can't install extension since it was reported to be problematic."));
} else {
return null;
}
return this.getDependenciesToInstall(compatible.properties.dependencies)
.then(
dependenciesToInstall => ([compatible, ...dependenciesToInstall.filter(d => d.identifier.uuid !== compatible.identifier.uuid)]),
error => TPromise.wrapError<IGalleryExtension[]>(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY)));
},
error => TPromise.wrapError<IGalleryExtension[]>(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY)));
});
}
private downloadAndInstallExtensions(extensions: IGalleryExtension[]): TPromise<ILocalExtension[]> {
return TPromise.join(extensions.map(extensionToInstall => this.downloadAndInstallExtension(extensionToInstall)))
.then(null, errors => this.rollback(extensions).then(() => TPromise.wrapError(errors), () => TPromise.wrapError(errors)));
}
private downloadAndInstallExtension(extension: IGalleryExtension): TPromise<ILocalExtension> {
let installingExtension = this.installingExtensions.get(extension.identifier.id);
if (!installingExtension) {
installingExtension = this.getExtensionsReport()
.then(report => {
if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) {
throw new Error(nls.localize('malicious extension', "Can't install extension since it was reported to be problematic."));
} else {
return extension;
}
})
.then(extension => this.downloadInstallableExtension(extension))
.then(installableExtension => this.installExtension(installableExtension))
.then(
local => { this.installingExtensions.delete(extension.identifier.id); return local; },
e => { this.installingExtensions.delete(extension.identifier.id); return TPromise.wrapError(e); }
);
this.installingExtensions.set(extension.identifier.id, installingExtension);
}
return installingExtension;
}
private downloadInstallableExtension(extension: IGalleryExtension): TPromise<InstallableExtension> {
private downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): TPromise<InstallableExtension> {
const metadata = <IGalleryMetadata>{
id: extension.identifier.uuid,
publisherId: extension.publisherId,
@@ -330,10 +334,10 @@ export class ExtensionManagementService extends Disposable implements IExtension
compatible => {
if (compatible) {
this.logService.trace('Started downloading extension:', extension.name);
return this.galleryService.download(extension)
return this.galleryService.download(extension, operation)
.then(
zipPath => {
this.logService.info('Downloaded extension:', extension.name);
this.logService.info('Downloaded extension:', extension.name, zipPath);
return validateLocalExtension(zipPath)
.then(
manifest => (<InstallableExtension>{ zipPath, id: getLocalExtensionIdFromManifest(manifest), metadata }),
@@ -346,54 +350,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
},
error => TPromise.wrapError<InstallableExtension>(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY)));
}
private onInstallExtensions(extensions: IGalleryExtension[]): void {
for (const extension of extensions) {
this.logService.info('Installing extension:', extension.name);
this.installationStartTime.set(extension.identifier.id, new Date().getTime());
const id = getLocalExtensionIdFromGallery(extension, extension.version);
this._onInstallExtension.fire({ identifier: { id, uuid: extension.identifier.uuid }, gallery: extension });
}
}
private onDidInstallExtensions(extensions: IGalleryExtension[], locals: ILocalExtension[], operations: Operation[], errors: Error[]): TPromise<any> {
extensions.forEach((gallery, index) => {
const identifier = { id: getLocalExtensionIdFromGallery(gallery, gallery.version), uuid: gallery.identifier.uuid };
const local = locals[index];
const error = errors[index];
if (local) {
this.logService.info(`Extensions installed successfully:`, gallery.identifier.id);
this._onDidInstallExtension.fire({ identifier, gallery, local });
} else {
const errorCode = error && (<ExtensionManagementError>error).code ? (<ExtensionManagementError>error).code : ERROR_UNKNOWN;
this.logService.error(`Failed to install extension:`, gallery.identifier.id, error ? error.message : errorCode);
this._onDidInstallExtension.fire({ identifier, gallery, error: errorCode });
}
const startTime = this.installationStartTime.get(gallery.identifier.id);
this.reportTelemetry(operations[index] === Operation.Install ? 'extensionGallery:install' : 'extensionGallery:update', getGalleryExtensionTelemetryData(gallery), startTime ? new Date().getTime() - startTime : void 0, error);
this.installationStartTime.delete(gallery.identifier.id);
});
return errors.length ? TPromise.wrapError(this.joinErrors(errors)) : TPromise.as(null);
}
private getDependenciesToInstall(dependencies: string[]): TPromise<IGalleryExtension[]> {
if (dependencies.length) {
return this.getInstalled()
.then(installed => {
const uninstalledDeps = dependencies.filter(d => installed.every(i => getGalleryExtensionId(i.manifest.publisher, i.manifest.name) !== d));
if (uninstalledDeps.length) {
return this.galleryService.loadAllDependencies(uninstalledDeps.map(id => (<IExtensionIdentifier>{ id })))
.then(allDependencies => allDependencies.filter(d => {
const extensionId = getLocalExtensionIdFromGallery(d, d.version);
return installed.every(({ identifier }) => identifier.id !== extensionId);
}));
}
return [];
});
}
return TPromise.as([]);
}
private installExtension(installableExtension: InstallableExtension): TPromise<ILocalExtension> {
return this.unsetUninstalledAndGetLocal(installableExtension.id)
@@ -432,7 +391,8 @@ export class ExtensionManagementService extends Disposable implements IExtension
private extractAndInstall({ zipPath, id, metadata }: InstallableExtension): TPromise<ILocalExtension> {
const tempPath = path.join(this.extensionsPath, `.${id}`);
const extensionPath = path.join(this.extensionsPath, id);
return this.extractAndRename(id, zipPath, tempPath, extensionPath)
return pfs.rimraf(extensionPath)
.then(() => this.extractAndRename(id, zipPath, tempPath, extensionPath), e => TPromise.wrapError(new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, id), INSTALL_ERROR_DELETING)))
.then(() => {
this.logService.info('Installation completed.', id);
return this.scanExtension(id, this.extensionsPath, LocalExtensionType.User);
@@ -448,7 +408,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
private extractAndRename(id: string, zipPath: string, extractPath: string, renamePath: string): TPromise<void> {
return this.extract(id, zipPath, extractPath)
.then(() => this.rename(id, extractPath, renamePath, Date.now() + (30 * 1000) /* Retry for 30 seconds */)
.then(() => this.rename(id, extractPath, renamePath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */)
.then(
() => this.logService.info('Renamed to', renamePath),
e => {
@@ -480,22 +440,55 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension): TPromise<void> {
if (this.galleryService.isEnabled()) {
const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || [];
if (installed.manifest.extensionPack) {
for (const extension of installed.manifest.extensionPack) {
// add only those extensions which are new in currently installed extension
if (!(existing && existing.manifest.extensionPack && existing.manifest.extensionPack.some(old => areSameExtensions({ id: old }, { id: extension })))) {
if (dependenciesAndPackExtensions.every(e => !areSameExtensions({ id: e }, { id: extension }))) {
dependenciesAndPackExtensions.push(extension);
}
}
}
}
if (dependenciesAndPackExtensions.length) {
return this.getInstalled()
.then(installed => {
// filter out installing and installed extensions
const names = dependenciesAndPackExtensions.filter(id => !this.installingExtensions.has(adoptToGalleryExtensionId(id)) && installed.every(({ galleryIdentifier }) => !areSameExtensions(galleryIdentifier, { id })));
if (names.length) {
return this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length })
.then(galleryResult => {
const extensionsToInstall = galleryResult.firstPage;
return TPromise.join(extensionsToInstall.map(e => this.installFromGallery(e)))
.then(() => null, errors => this.rollback(extensionsToInstall).then(() => TPromise.wrapError(errors), () => TPromise.wrapError(errors)));
});
}
return null;
});
}
}
return TPromise.as(null);
}
private rollback(extensions: IGalleryExtension[]): TPromise<void> {
return this.getInstalled(LocalExtensionType.User)
.then(installed =>
TPromise.join(installed.filter(local => extensions.some(galleryExtension => local.identifier.id === getLocalExtensionIdFromGallery(galleryExtension, galleryExtension.version))) // Only check id (pub.name-version) because we want to rollback the exact version
.map(local => this.setUninstalled(local))))
.map(local => this.uninstall(local, true))))
.then(() => null, () => null);
}
uninstall(extension: ILocalExtension, force = false): TPromise<void> {
return this.getInstalled(LocalExtensionType.User)
return this.toNonCancellablePromise(this.getInstalled(LocalExtensionType.User)
.then(installed => {
const promises = installed
.filter(e => e.manifest.publisher === extension.manifest.publisher && e.manifest.name === extension.manifest.name)
.map(e => this.checkForDependenciesAndUninstall(e, installed, force));
return TPromise.join(promises).then(() => null, error => TPromise.wrapError(this.joinErrors(error)));
});
}));
}
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): TPromise<ILocalExtension> {
@@ -552,7 +545,22 @@ export class ExtensionManagementService extends Disposable implements IExtension
private checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise<void> {
return this.preUninstallExtension(extension)
.then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed, force) : this.promptAndUninstall(extension, installed, force))
.then(() => {
const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed);
if (packedExtensions.length) {
return this.uninstallExtensions(extension, packedExtensions, installed);
}
const dependencies = this.getDependenciesToUninstall(extension, installed);
if (dependencies.length) {
if (force) {
return this.uninstallExtensions(extension, dependencies, installed);
} else {
return this.promptForDependenciesAndUninstall(extension, dependencies, installed);
}
} else {
return this.uninstallExtensions(extension, [], installed);
}
})
.then(() => this.postUninstallExtension(extension),
error => {
this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL));
@@ -560,68 +568,35 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private hasDependencies(extension: ILocalExtension, installed: ILocalExtension[]): boolean {
if (extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length) {
return installed.some(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1);
}
return false;
}
private promptForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise<void> {
if (force) {
const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension);
return this.uninstallWithDependencies(extension, dependencies, installed);
}
const message = nls.localize('uninstallDependeciesConfirmation', "Would you like to uninstall '{0}' only or its dependencies also?", extension.manifest.displayName || extension.manifest.name);
private promptForDependenciesAndUninstall(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): TPromise<void> {
const message = nls.localize('uninstallDependeciesConfirmation', "Also uninstall the dependencies of the extension '{0}'?", extension.manifest.displayName || extension.manifest.name);
const buttons = [
nls.localize('uninstallOnly', "Extension Only"),
nls.localize('uninstallAll', "Uninstall All"),
nls.localize('yes', "Yes"),
nls.localize('no', "No"),
nls.localize('cancel', "Cancel")
];
this.logService.info('Requesting for confirmation to uninstall extension with dependencies', extension.identifier.id);
return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 2 })
.then<void>(value => {
if (value === 0) {
return this.uninstallWithDependencies(extension, [], installed);
return this.uninstallExtensions(extension, dependencies, installed);
}
if (value === 1) {
const dependencies = distinct(this.getDependenciesToUninstallRecursively(extension, installed, [])).filter(e => e !== extension);
return this.uninstallWithDependencies(extension, dependencies, installed);
return this.uninstallExtensions(extension, [], installed);
}
this.logService.info('Cancelled uninstalling extension:', extension.identifier.id);
return TPromise.wrapError(errors.canceled());
}, error => TPromise.wrapError(errors.canceled()));
}
private promptAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], force: boolean): TPromise<void> {
if (force) {
return this.uninstallWithDependencies(extension, [], installed);
}
const message = nls.localize('uninstallConfirmation', "Are you sure you want to uninstall '{0}'?", extension.manifest.displayName || extension.manifest.name);
const buttons = [
nls.localize('ok', "OK"),
nls.localize('cancel', "Cancel")
];
this.logService.info('Requesting for confirmation to uninstall extension', extension.identifier.id);
return this.dialogService.show(Severity.Info, message, buttons, { cancelId: 1 })
.then<void>(value => {
if (value === 0) {
return this.uninstallWithDependencies(extension, [], installed);
}
this.logService.info('Cancelled uninstalling extension:', extension.identifier.id);
return TPromise.wrapError(errors.canceled());
}, error => TPromise.wrapError(errors.canceled()));
}
private uninstallWithDependencies(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): TPromise<void> {
const dependenciesToUninstall = this.filterDependents(extension, dependencies, installed);
let dependents = this.getDependents(extension, installed).filter(dependent => extension !== dependent && dependenciesToUninstall.indexOf(dependent) === -1);
private uninstallExtensions(extension: ILocalExtension, otherExtensionsToUninstall: ILocalExtension[], installed: ILocalExtension[]): TPromise<void> {
const dependents = this.getDependents(extension, installed);
if (dependents.length) {
return TPromise.wrapError<void>(new Error(this.getDependentsErrorMessage(extension, dependents)));
const remainingDependents = dependents.filter(dependent => extension !== dependent && otherExtensionsToUninstall.indexOf(dependent) === -1);
if (remainingDependents.length) {
return TPromise.wrapError<void>(new Error(this.getDependentsErrorMessage(extension, remainingDependents)));
}
}
return TPromise.join([this.uninstallExtension(extension), ...dependenciesToUninstall.map(d => this.doUninstall(d))]).then(() => null);
return TPromise.join([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]).then(() => null);
}
private getDependentsErrorMessage(extension: ILocalExtension, dependents: ILocalExtension[]): string {
@@ -637,7 +612,23 @@ export class ExtensionManagementService extends Disposable implements IExtension
extension.manifest.displayName || extension.manifest.name, dependents[0].manifest.displayName || dependents[0].manifest.name, dependents[1].manifest.displayName || dependents[1].manifest.name);
}
private getDependenciesToUninstallRecursively(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[]): ILocalExtension[] {
private getDependenciesToUninstall(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] {
const dependencies = this.getAllDependenciesToUninstall(extension, installed).filter(e => e !== extension);
const dependenciesToUninstall = dependencies.slice(0);
for (let index = 0; index < dependencies.length; index++) {
const dep = dependencies[index];
const dependents = this.getDependents(dep, installed);
// Remove the dependency from the uninstall list if there is a dependent which will not be uninstalled.
if (dependents.some(e => e !== extension && dependencies.indexOf(e) === -1)) {
dependenciesToUninstall.splice(index - (dependencies.length - dependenciesToUninstall.length), 1);
}
}
return dependenciesToUninstall;
}
private getAllDependenciesToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] {
if (checked.indexOf(extension) !== -1) {
return [];
}
@@ -645,29 +636,32 @@ export class ExtensionManagementService extends Disposable implements IExtension
if (!extension.manifest.extensionDependencies || extension.manifest.extensionDependencies.length === 0) {
return [];
}
const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(i)) !== -1);
const dependenciesToUninstall = installed.filter(i => extension.manifest.extensionDependencies.some(id => areSameExtensions({ id }, i.galleryIdentifier)));
const depsOfDeps = [];
for (const dep of dependenciesToUninstall) {
depsOfDeps.push(...this.getDependenciesToUninstallRecursively(dep, installed, checked));
depsOfDeps.push(...this.getAllDependenciesToUninstall(dep, installed, checked));
}
return [...dependenciesToUninstall, ...depsOfDeps];
}
private filterDependents(extension: ILocalExtension, dependencies: ILocalExtension[], installed: ILocalExtension[]): ILocalExtension[] {
installed = installed.filter(i => i !== extension && i.manifest.extensionDependencies && i.manifest.extensionDependencies.length > 0);
let result = dependencies.slice(0);
for (let i = 0; i < dependencies.length; i++) {
const dep = dependencies[i];
const dependents = this.getDependents(dep, installed).filter(e => dependencies.indexOf(e) === -1);
if (dependents.length) {
result.splice(i - (dependencies.length - result.length), 1);
}
private getAllPackExtensionsToUninstall(extension: ILocalExtension, installed: ILocalExtension[], checked: ILocalExtension[] = []): ILocalExtension[] {
if (checked.indexOf(extension) !== -1) {
return [];
}
return result;
checked.push(extension);
if (!extension.manifest.extensionPack || extension.manifest.extensionPack.length === 0) {
return [];
}
const packedExtensions = installed.filter(i => extension.manifest.extensionPack.some(id => areSameExtensions({ id }, i.galleryIdentifier)));
const packOfPackedExtensions = [];
for (const packedExtension of packedExtensions) {
packOfPackedExtensions.push(...this.getAllPackExtensionsToUninstall(packedExtension, installed, checked));
}
return [...packedExtensions, ...packOfPackedExtensions];
}
private getDependents(extension: ILocalExtension, installed: ILocalExtension[]): ILocalExtension[] {
return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.indexOf(getGalleryExtensionIdFromLocal(extension)) !== -1);
return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.galleryIdentifier)));
}
private doUninstall(extension: ILocalExtension): TPromise<void> {
@@ -681,7 +675,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
private preUninstallExtension(extension: ILocalExtension): TPromise<void> {
return pfs.exists(extension.path)
return pfs.exists(extension.location.fsPath)
.then(exists => exists ? null : TPromise.wrapError(new Error(nls.localize('notExists', "Could not find extension"))))
.then(() => {
this.logService.info('Uninstalling extension:', extension.identifier.id);
@@ -690,12 +684,19 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
private uninstallExtension(local: ILocalExtension): TPromise<void> {
// Set all versions of the extension as uninstalled
return this.scanUserExtensions(false)
.then(userExtensions => this.setUninstalled(...userExtensions.filter(u => areSameExtensions({ id: getGalleryExtensionIdFromLocal(u), uuid: u.identifier.uuid }, { id: getGalleryExtensionIdFromLocal(local), uuid: local.identifier.uuid }))));
const id = getGalleryExtensionIdFromLocal(local);
let promise = this.uninstallingExtensions.get(id);
if (!promise) {
// Set all versions of the extension as uninstalled
promise = this.scanUserExtensions(false)
.then(userExtensions => this.setUninstalled(...userExtensions.filter(u => areSameExtensions({ id: getGalleryExtensionIdFromLocal(u), uuid: u.identifier.uuid }, { id, uuid: local.identifier.uuid }))))
.then(() => { this.uninstallingExtensions.delete(id); });
this.uninstallingExtensions.set(id, promise);
}
return promise;
}
private async postUninstallExtension(extension: ILocalExtension, error?: Error): TPromise<void> {
private async postUninstallExtension(extension: ILocalExtension, error?: Error): Promise<void> {
if (error) {
this.logService.error('Failed to uninstall extension:', extension.identifier.id, error.message);
} else {
@@ -769,8 +770,12 @@ export class ExtensionManagementService extends Disposable implements IExtension
if (manifest.extensionDependencies) {
manifest.extensionDependencies = manifest.extensionDependencies.map(id => adoptToGalleryExtensionId(id));
}
if (manifest.extensionPack) {
manifest.extensionPack = manifest.extensionPack.map(id => adoptToGalleryExtensionId(id));
}
const identifier = { id: type === LocalExtensionType.System ? folderName : getLocalExtensionIdFromManifest(manifest), uuid: metadata ? metadata.id : null };
return { type, identifier, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl };
const galleryIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: identifier.uuid };
return { type, identifier, galleryIdentifier, manifest, metadata, location: URI.file(extensionPath), readmeUrl, changelogUrl };
}))
.then(null, () => null);
}
@@ -811,7 +816,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
private removeExtension(extension: ILocalExtension, type: string): TPromise<void> {
this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id);
return pfs.rimraf(extension.path).then(() => this.logService.info('Deleted from disk', extension.identifier.id));
return pfs.rimraf(extension.location.fsPath).then(() => this.logService.info('Deleted from disk', extension.identifier.id));
}
private isUninstalled(id: string): TPromise<boolean> {
@@ -886,10 +891,25 @@ export class ExtensionManagementService extends Disposable implements IExtension
});
}
private toNonCancellablePromise<T>(promise: TPromise<T>): TPromise<T> {
return new TPromise((c, e) => promise.then(result => c(result), error => e(error)), () => this.logService.debug('Request Cancelled'));
}
private reportTelemetry(eventName: string, extensionData: any, duration: number, error?: Error): void {
const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ERROR_UNKNOWN : void 0;
/* __GDPR__
"extensionGallery:install" : {
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
"recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
/* __GDPR__
"extensionGallery:uninstall" : {
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
@@ -899,7 +919,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
*/
/* __GDPR__
"extensionGallery:uninstall" : {
"extensionGallery:update" : {
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },

View File

@@ -35,10 +35,11 @@ export class TestExtensionEnablementService extends ExtensionEnablementService {
constructor(instantiationService: TestInstantiationService) {
super(storageService(instantiationService), instantiationService.get(IWorkspaceContextService),
instantiationService.get(IEnvironmentService) || instantiationService.stub(IEnvironmentService, {} as IEnvironmentService),
instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: new Emitter() }));
instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService,
{ onDidUninstallExtension: new Emitter<DidUninstallExtensionEvent>().event } as IExtensionManagementService));
}
public reset(): TPromise<void> {
public async reset(): Promise<void> {
return this.getDisabledExtensions().then(extensions => extensions.forEach(d => this.setEnablement(aLocalExtension(d.id), EnablementState.Enabled)));
}
}
@@ -52,7 +53,7 @@ suite('ExtensionEnablementService Test', () => {
setup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event });
instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, getInstalled: () => TPromise.as([]) } as IExtensionManagementService);
testObject = new TestExtensionEnablementService(instantiationService);
});
@@ -331,6 +332,12 @@ suite('ExtensionEnablementService Test', () => {
assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a')), false);
});
test('test canChangeEnablement return false when the extension is disabled in environment', () => {
instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService);
testObject = new TestExtensionEnablementService(instantiationService);
assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a')), false);
});
test('test canChangeEnablement return true for system extensions when extensions are disabled in environment', () => {
instantiationService.stub(IEnvironmentService, { disableExtensions: true } as IEnvironmentService);
testObject = new TestExtensionEnablementService(instantiationService);
@@ -338,12 +345,32 @@ suite('ExtensionEnablementService Test', () => {
extension.type = LocalExtensionType.System;
assert.equal(testObject.canChangeEnablement(extension), true);
});
test('test canChangeEnablement return false for system extensions when extension is disabled in environment', () => {
instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService);
testObject = new TestExtensionEnablementService(instantiationService);
const extension = aLocalExtension('pub.a');
extension.type = LocalExtensionType.System;
assert.equal(testObject.canChangeEnablement(extension), true);
});
test('test getDisabledExtensions include extensions disabled in enviroment', () => {
instantiationService.stub(IEnvironmentService, { disableExtensions: ['pub.a'] } as IEnvironmentService);
instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, getInstalled: () => TPromise.as([aLocalExtension('pub.a'), aLocalExtension('pub.b')]) } as IExtensionManagementService);
testObject = new TestExtensionEnablementService(instantiationService);
return testObject.getDisabledExtensions()
.then(actual => {
assert.equal(actual.length, 1);
assert.equal(actual[0].id, 'pub.a');
});
});
});
function aLocalExtension(id: string, contributes?: IExtensionContributions): ILocalExtension {
const [publisher, name] = id.split('.');
return <ILocalExtension>Object.create({
identifier: { id },
galleryIdentifier: { id, uuid: void 0 },
manifest: {
name,
publisher,

View File

@@ -128,16 +128,11 @@ export interface IFileService {
createFolder(resource: URI): TPromise<IFileStat>;
/**
* Renames the provided file to use the new name. The returned promise
* will have the stat model object as a result.
* Deletes the provided file. The optional useTrash parameter allows to
* move the file to trash. The optional recursive parameter allows to delete
* non-empty folders recursively.
*/
rename(resource: URI, newName: string): TPromise<IFileStat>;
/**
* Deletes the provided file. The optional useTrash parameter allows to
* move the file to trash.
*/
del(resource: URI, useTrash?: boolean): TPromise<void>;
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise<void>;
/**
* Allows to start a watcher that reports file change events on the provided resource.
@@ -164,6 +159,10 @@ export interface FileWriteOptions {
create: boolean;
}
export interface FileDeleteOptions {
recursive: boolean;
}
export enum FileType {
Unknown = 0,
File = 1,
@@ -188,7 +187,8 @@ export enum FileSystemProviderCapabilities {
FileOpenReadWriteClose = 1 << 2,
FileFolderCopy = 1 << 3,
PathCaseSensitive = 1 << 10
PathCaseSensitive = 1 << 10,
Readonly = 1 << 11
}
export interface IFileSystemProvider {
@@ -201,7 +201,7 @@ export interface IFileSystemProvider {
stat(resource: URI): TPromise<IStat>;
mkdir(resource: URI): TPromise<void>;
readdir(resource: URI): TPromise<[string, FileType][]>;
delete(resource: URI): TPromise<void>;
delete(resource: URI, opts: FileDeleteOptions): TPromise<void>;
rename(from: URI, to: URI, opts: FileOverwriteOptions): TPromise<void>;
copy?(from: URI, to: URI, opts: FileOverwriteOptions): TPromise<void>;
@@ -233,15 +233,15 @@ export class FileOperationEvent {
constructor(private _resource: URI, private _operation: FileOperation, private _target?: IFileStat) {
}
public get resource(): URI {
get resource(): URI {
return this._resource;
}
public get target(): IFileStat {
get target(): IFileStat {
return this._target;
}
public get operation(): FileOperation {
get operation(): FileOperation {
return this._operation;
}
}
@@ -279,7 +279,7 @@ export class FileChangesEvent {
this._changes = changes;
}
public get changes() {
get changes() {
return this._changes;
}
@@ -288,7 +288,7 @@ export class FileChangesEvent {
* type DELETED, this method will also return true if a folder got deleted that is the parent of the
* provided file path.
*/
public contains(resource: URI, type: FileChangeType): boolean {
contains(resource: URI, type: FileChangeType): boolean {
if (!resource) {
return false;
}
@@ -310,42 +310,42 @@ export class FileChangesEvent {
/**
* Returns the changes that describe added files.
*/
public getAdded(): IFileChange[] {
getAdded(): IFileChange[] {
return this.getOfType(FileChangeType.ADDED);
}
/**
* Returns if this event contains added files.
*/
public gotAdded(): boolean {
gotAdded(): boolean {
return this.hasType(FileChangeType.ADDED);
}
/**
* Returns the changes that describe deleted files.
*/
public getDeleted(): IFileChange[] {
getDeleted(): IFileChange[] {
return this.getOfType(FileChangeType.DELETED);
}
/**
* Returns if this event contains deleted files.
*/
public gotDeleted(): boolean {
gotDeleted(): boolean {
return this.hasType(FileChangeType.DELETED);
}
/**
* Returns the changes that describe updated files.
*/
public getUpdated(): IFileChange[] {
getUpdated(): IFileChange[] {
return this.getOfType(FileChangeType.UPDATED);
}
/**
* Returns if this event contains updated files.
*/
public gotUpdated(): boolean {
gotUpdated(): boolean {
return this.hasType(FileChangeType.UPDATED);
}
@@ -404,6 +404,11 @@ export interface IBaseStat {
* current state of the file or directory.
*/
etag: string;
/**
* The resource is readonly.
*/
isReadonly?: boolean;
}
/**
@@ -834,12 +839,12 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab
order: 33
},
gbk: {
labelLong: 'Chinese (GBK)',
labelLong: 'Simplified Chinese (GBK)',
labelShort: 'GBK',
order: 34
},
gb18030: {
labelLong: 'Chinese (GB18030)',
labelLong: 'Simplified Chinese (GB18030)',
labelShort: 'GB18030',
order: 35
},
@@ -910,3 +915,6 @@ export enum FileKind {
FOLDER,
ROOT_FOLDER
}
export const MIN_MAX_MEMORY_SIZE_MB = 2048;
export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096;

View File

@@ -24,7 +24,7 @@ export interface IHistoryMainService {
addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], files: string[]): void;
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened;
removeFromRecentlyOpened(paths: string[]): void;
removeFromRecentlyOpened(paths: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string)[]): void;
clearRecentlyOpened(): void;
updateWindowsJumpList(): void;

View File

@@ -5,22 +5,34 @@
'use strict';
import * as path from 'path';
import * as nls from 'vs/nls';
import * as arrays from 'vs/base/common/arrays';
import { trim } from 'vs/base/common/strings';
import { IStateService } from 'vs/platform/state/common/state';
import { app } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
import { getPathLabel, getBaseLabel } from 'vs/base/common/labels';
import { getBaseLabel } from 'vs/base/common/labels';
import { IPath } from 'vs/platform/windows/common/windows';
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
import { isWindows, isMacintosh, isLinux } from 'vs/base/common/platform';
import { IWorkspaceIdentifier, IWorkspacesMainService, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceSavedEvent } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceIdentifier, IWorkspacesMainService, getWorkspaceLabel, IWorkspaceSavedEvent, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { isEqual } from 'vs/base/common/paths';
import { RunOnceScheduler } from 'vs/base/common/async';
import { getComparisonKey, isEqual as areResourcesEqual, hasToIgnoreCase, dirname } from 'vs/base/common/resources';
import URI, { UriComponents } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
interface ISerializedRecentlyOpened {
workspaces2: (IWorkspaceIdentifier | string)[]; // IWorkspaceIdentifier or URI.toString()
files: string[];
}
interface ILegacySerializedRecentlyOpened {
workspaces: (IWorkspaceIdentifier | string | UriComponents)[]; // legacy (UriComponents was also supported for a few insider builds)
}
export class HistoryMainService implements IHistoryMainService {
@@ -41,6 +53,7 @@ export class HistoryMainService implements IHistoryMainService {
@ILogService private logService: ILogService,
@IWorkspacesMainService private workspacesMainService: IWorkspacesMainService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IUriDisplayService private uriDisplayService: IUriDisplayService
) {
this.macOSRecentDocumentsUpdater = new RunOnceScheduler(() => this.updateMacOSRecentDocuments(), 800);
@@ -57,34 +70,38 @@ export class HistoryMainService implements IHistoryMainService {
this.addRecentlyOpened([e.workspace], []);
}
public addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], files: string[]): void {
addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[], files: string[]): void {
if ((workspaces && workspaces.length > 0) || (files && files.length > 0)) {
const mru = this.getRecentlyOpened();
// Workspaces
workspaces.forEach(workspace => {
const isUntitledWorkspace = !isSingleFolderWorkspaceIdentifier(workspace) && this.workspacesMainService.isUntitledWorkspace(workspace);
if (isUntitledWorkspace) {
return; // only store saved workspaces
}
if (Array.isArray(workspaces)) {
workspaces.forEach(workspace => {
const isUntitledWorkspace = !isSingleFolderWorkspaceIdentifier(workspace) && this.workspacesMainService.isUntitledWorkspace(workspace);
if (isUntitledWorkspace) {
return; // only store saved workspaces
}
mru.workspaces.unshift(workspace);
mru.workspaces = arrays.distinct(mru.workspaces, workspace => this.distinctFn(workspace));
mru.workspaces.unshift(workspace);
mru.workspaces = arrays.distinct(mru.workspaces, workspace => this.distinctFn(workspace));
// We do not add to recent documents here because on Windows we do this from a custom
// JumpList and on macOS we fill the recent documents in one go from all our data later.
});
// We do not add to recent documents here because on Windows we do this from a custom
// JumpList and on macOS we fill the recent documents in one go from all our data later.
});
}
// Files
files.forEach((path) => {
mru.files.unshift(path);
mru.files = arrays.distinct(mru.files, file => this.distinctFn(file));
if (Array.isArray(files)) {
files.forEach((path) => {
mru.files.unshift(path);
mru.files = arrays.distinct(mru.files, file => this.distinctFn(file));
// Add to recent documents (Windows only, macOS later)
if (isWindows) {
app.addRecentDocument(path);
}
});
// Add to recent documents (Windows only, macOS later)
if (isWindows) {
app.addRecentDocument(path);
}
});
}
// Make sure its bounded
mru.workspaces = mru.workspaces.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES);
@@ -100,21 +117,37 @@ export class HistoryMainService implements IHistoryMainService {
}
}
public removeFromRecentlyOpened(pathsToRemove: string[]): void {
removeFromRecentlyOpened(pathsToRemove: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string)[]): void {
const mru = this.getRecentlyOpened();
let update = false;
pathsToRemove.forEach((pathToRemove => {
// Remove workspace
let index = arrays.firstIndex(mru.workspaces, workspace => isEqual(isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath, pathToRemove, !isLinux /* ignorecase */));
let index = arrays.firstIndex(mru.workspaces, workspace => {
if (isWorkspaceIdentifier(pathToRemove)) {
return isWorkspaceIdentifier(workspace) && isEqual(pathToRemove.configPath, workspace.configPath, !isLinux /* ignorecase */);
}
if (isSingleFolderWorkspaceIdentifier(pathToRemove)) {
return isSingleFolderWorkspaceIdentifier(workspace) && areResourcesEqual(pathToRemove, workspace, hasToIgnoreCase(pathToRemove));
}
if (typeof pathToRemove === 'string') {
if (isSingleFolderWorkspaceIdentifier(workspace)) {
return workspace.scheme === Schemas.file && areResourcesEqual(URI.file(pathToRemove), workspace, hasToIgnoreCase(workspace));
}
if (isWorkspaceIdentifier(workspace)) {
return isEqual(pathToRemove, workspace.configPath, !isLinux /* ignorecase */);
}
}
return false;
});
if (index >= 0) {
mru.workspaces.splice(index, 1);
update = true;
}
// Remove file
index = arrays.firstIndex(mru.files, file => isEqual(file, pathToRemove, !isLinux /* ignorecase */));
index = arrays.firstIndex(mru.files, file => typeof pathToRemove === 'string' && isEqual(file, pathToRemove, !isLinux /* ignorecase */));
if (index >= 0) {
mru.files.splice(index, 1);
update = true;
@@ -149,9 +182,10 @@ export class HistoryMainService implements IHistoryMainService {
let maxEntries = HistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES;
// Take up to maxEntries/2 workspaces
for (let i = 0; i < mru.workspaces.length && i < HistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES / 2; i++) {
const workspace = mru.workspaces[i];
app.addRecentDocument(isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath);
const workspaces = mru.workspaces.filter(w => !(isSingleFolderWorkspaceIdentifier(w) && w.scheme !== Schemas.file));
for (let i = 0; i < workspaces.length && i < HistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES / 2; i++) {
const workspace = workspaces[i];
app.addRecentDocument(isSingleFolderWorkspaceIdentifier(workspace) ? workspace.scheme === Schemas.file ? workspace.fsPath : workspace.toString() : workspace.configPath);
maxEntries--;
}
@@ -162,7 +196,7 @@ export class HistoryMainService implements IHistoryMainService {
}
}
public clearRecentlyOpened(): void {
clearRecentlyOpened(): void {
this.saveRecentlyOpened({ workspaces: [], files: [] });
app.clearRecentDocuments();
@@ -170,12 +204,12 @@ export class HistoryMainService implements IHistoryMainService {
this._onRecentlyOpenedChange.fire();
}
public getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened {
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened {
let workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[];
let files: string[];
// Get from storage
const storedRecents = this.stateService.getItem<IRecentlyOpened>(HistoryMainService.recentlyOpenedStorageKey);
const storedRecents = this.getRecentlyOpenedFromStorage();
if (storedRecents) {
workspaces = storedRecents.workspaces || [];
files = storedRecents.files || [];
@@ -206,17 +240,64 @@ export class HistoryMainService implements IHistoryMainService {
private distinctFn(workspaceOrFile: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string): string {
if (isSingleFolderWorkspaceIdentifier(workspaceOrFile)) {
return getComparisonKey(workspaceOrFile);
}
if (typeof workspaceOrFile === 'string') {
return isLinux ? workspaceOrFile : workspaceOrFile.toLowerCase();
}
return workspaceOrFile.id;
}
private saveRecentlyOpened(recent: IRecentlyOpened): void {
this.stateService.setItem(HistoryMainService.recentlyOpenedStorageKey, recent);
private getRecentlyOpenedFromStorage(): IRecentlyOpened {
const storedRecents = this.stateService.getItem<ISerializedRecentlyOpened & ILegacySerializedRecentlyOpened>(HistoryMainService.recentlyOpenedStorageKey);
const result: IRecentlyOpened = { workspaces: [], files: [] };
if (storedRecents) {
if (Array.isArray(storedRecents.workspaces2)) {
for (const workspace of storedRecents.workspaces2) {
if (isWorkspaceIdentifier(workspace)) {
result.workspaces.push(workspace);
} else if (typeof workspace === 'string') {
result.workspaces.push(URI.parse(workspace));
}
}
} else if (Array.isArray(storedRecents.workspaces)) {
// format of 1.25 and before
for (const workspace of storedRecents.workspaces) {
if (typeof workspace === 'string') {
result.workspaces.push(URI.file(workspace));
} else if (isWorkspaceIdentifier(workspace)) {
result.workspaces.push(workspace);
} else if (workspace && typeof workspace.path === 'string' && typeof workspace.scheme === 'string') {
// added by 1.26-insiders
result.workspaces.push(URI.revive(workspace));
}
}
}
if (Array.isArray(storedRecents.files)) {
for (const file of storedRecents.files) {
if (typeof file === 'string') {
result.files.push(file);
}
}
}
}
return result;
}
public updateWindowsJumpList(): void {
private saveRecentlyOpened(recent: IRecentlyOpened): void {
const serialized: ISerializedRecentlyOpened = { workspaces2: [], files: recent.files };
for (const workspace of recent.workspaces) {
if (isSingleFolderWorkspaceIdentifier(workspace)) {
serialized.workspaces2.push(workspace.toString());
} else {
serialized.workspaces2.push(workspace);
}
}
this.stateService.setItem(HistoryMainService.recentlyOpenedStorageKey, serialized);
}
updateWindowsJumpList(): void {
if (!isWindows) {
return; // only on windows
}
@@ -253,15 +334,26 @@ export class HistoryMainService implements IHistoryMainService {
type: 'custom',
name: nls.localize('recentFolders', "Recent Workspaces"),
items: this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(workspace => {
const title = isSingleFolderWorkspaceIdentifier(workspace) ? getBaseLabel(workspace) : getWorkspaceLabel(workspace, this.environmentService);
const description = isSingleFolderWorkspaceIdentifier(workspace) ? nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(path.dirname(workspace))) : nls.localize('codeWorkspace', "Code Workspace");
const title = getWorkspaceLabel(workspace, this.environmentService, this.uriDisplayService);
const description = isSingleFolderWorkspaceIdentifier(workspace) ? nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), this.uriDisplayService.getLabel(dirname(workspace))) : nls.localize('codeWorkspace', "Code Workspace");
let args;
// use quotes to support paths with whitespaces
if (isSingleFolderWorkspaceIdentifier(workspace)) {
if (workspace.scheme === Schemas.file) {
args = `"${workspace.fsPath}"`;
} else {
args = `--folderUri "${workspace.path}"`;
}
} else {
args = `"${workspace.configPath}"`;
}
return <Electron.JumpListItem>{
type: 'task',
title,
description,
program: process.execPath,
args: `"${isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath}"`, // open folder (use quotes to support paths with whitespaces)
args,
iconPath: 'explorer.exe', // simulate folder icon
iconIndex: 0
};
@@ -280,4 +372,4 @@ export class HistoryMainService implements IHistoryMainService {
this.logService.warn('#setJumpList', error); // since setJumpList is relatively new API, make sure to guard for errors
}
}
}
}

View File

@@ -49,7 +49,7 @@ export class InstantiationService implements IInstantiationService {
get: <T>(id: ServiceIdentifier<T>, isOptional?: typeof optional) => {
const result = this._getOrCreateServiceInstance(id);
if (!result && isOptional !== optional) {
throw new Error(`[invokeFunction] unkown service '${id}'`);
throw new Error(`[invokeFunction] unknown service '${id}'`);
}
return result;
}

View File

@@ -133,7 +133,7 @@ export class IntegrityServiceImpl implements IIntegrityService {
private _resolve(filename: string, expected: string): TPromise<ChecksumPair> {
let fileUri = URI.parse(require.toUrl(filename));
return new TPromise<ChecksumPair>((c, e, p) => {
return new TPromise<ChecksumPair>((c, e) => {
fs.readFile(fileUri.fsPath, (err, buff) => {
if (err) {
return e(err);

View File

@@ -11,6 +11,8 @@ import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensio
export const IIssueService = createDecorator<IIssueService>('issueService');
// Since data sent through the service is serialized to JSON, functions will be lost, so Color objects
// should not be sent as their 'toString' method will be stripped. Instead convert to strings before sending.
export interface WindowStyles {
backgroundColor: string;
color: string;
@@ -29,6 +31,7 @@ export enum IssueType {
export interface IssueReporterStyles extends WindowStyles {
textLinkColor: string;
textLinkActiveForeground: string;
inputBackground: string;
inputForeground: string;
inputBorder: string;

View File

@@ -8,6 +8,7 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IIssueService, IssueReporterData, ProcessExplorerData } from './issue';
import { Event } from 'vs/base/common/event';
export interface IIssueChannel extends IChannel {
call(command: 'openIssueReporter', arg: IssueReporterData): TPromise<void>;
@@ -19,6 +20,10 @@ export class IssueChannel implements IIssueChannel {
constructor(private service: IIssueService) { }
listen<T>(event: string): Event<T> {
throw new Error('No event found');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'openIssueReporter':

View File

@@ -51,33 +51,56 @@ export class IssueService implements IIssueService {
});
this._issueParentWindow = BrowserWindow.getFocusedWindow();
const position = this.getWindowPosition(this._issueParentWindow, 800, 900);
this._issueWindow = new BrowserWindow({
width: position.width,
height: position.height,
minWidth: 300,
minHeight: 200,
x: position.x,
y: position.y,
title: localize('issueReporter', "Issue Reporter"),
backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR
});
const position = this.getWindowPosition(this._issueParentWindow, 700, 800);
if (!this._issueWindow) {
this._issueWindow = new BrowserWindow({
width: position.width,
height: position.height,
minWidth: 300,
minHeight: 200,
x: position.x,
y: position.y,
title: localize('issueReporter', "Issue Reporter"),
backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR,
webPreferences: {
disableBlinkFeatures: 'Auxclick'
}
});
this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented
this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented
// Modified when testing UI
const features: IssueReporterFeatures = {};
// Modified when testing UI
const features: IssueReporterFeatures = {};
this.logService.trace('issueService#openReporter: opening issue reporter');
this._issueWindow.loadURL(this.getIssueReporterPath(data, features));
this.logService.trace('issueService#openReporter: opening issue reporter');
this._issueWindow.loadURL(this.getIssueReporterPath(data, features));
this._issueWindow.on('close', () => this._issueWindow = null);
this._issueParentWindow.on('closed', () => {
if (this._issueWindow) {
this._issueWindow.close();
this._issueWindow = null;
}
});
}
this._issueWindow.focus();
return TPromise.as(null);
}
openProcessExplorer(data: ProcessExplorerData): TPromise<void> {
ipcMain.on('windowsInfoRequest', event => {
this.launchService.getMainProcessInfo().then(info => {
event.sender.send('windowsInfoResponse', info.windows);
});
});
// Create as singleton
if (!this._processExplorerWindow) {
const position = this.getWindowPosition(BrowserWindow.getFocusedWindow(), 800, 300);
const parentWindow = BrowserWindow.getFocusedWindow();
const position = this.getWindowPosition(parentWindow, 800, 300);
this._processExplorerWindow = new BrowserWindow({
skipTaskbar: true,
resizable: true,
@@ -88,7 +111,10 @@ export class IssueService implements IIssueService {
x: position.x,
y: position.y,
backgroundColor: data.styles.backgroundColor,
title: localize('processExplorer', "Process Explorer")
title: localize('processExplorer', "Process Explorer"),
webPreferences: {
disableBlinkFeatures: 'Auxclick'
}
});
this._processExplorerWindow.setMenuBarVisibility(false);
@@ -113,6 +139,13 @@ export class IssueService implements IIssueService {
this._processExplorerWindow.loadURL(`${require.toUrl('vs/code/electron-browser/processExplorer/processExplorer.html')}?config=${encodeURIComponent(JSON.stringify(config))}`);
this._processExplorerWindow.on('close', () => this._processExplorerWindow = void 0);
parentWindow.on('close', () => {
if (this._processExplorerWindow) {
this._processExplorerWindow.close();
this._processExplorerWindow = null;
}
});
}
// Focus

View File

@@ -38,9 +38,7 @@ export interface IJSONContributionRegistry {
getSchemaContributions(): ISchemaContributions;
}
export interface IJSONContributionRegistryEvent {
}
function normalizeId(id: string) {
if (id.length > 0 && id.charAt(id.length - 1) === '#') {

View File

@@ -6,7 +6,7 @@
import * as nls from 'vs/nls';
import { ResolvedKeybinding, Keybinding } from 'vs/base/common/keyCodes';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { KeybindingResolver, IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver';
import { IKeybindingEvent, IKeybindingService, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
@@ -16,18 +16,18 @@ import { Event, Emitter } from 'vs/base/common/event';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IntervalTimer } from 'vs/base/common/async';
interface CurrentChord {
keypress: string;
label: string;
}
export abstract class AbstractKeybindingService implements IKeybindingService {
export abstract class AbstractKeybindingService extends Disposable implements IKeybindingService {
public _serviceBrand: any;
protected toDispose: IDisposable[] = [];
private _currentChord: CurrentChord;
private _currentChordChecker: IntervalTimer;
private _currentChordStatusMessage: IDisposable;
protected _onDidUpdateKeybindings: Emitter<IKeybindingEvent>;
@@ -44,6 +44,7 @@ export abstract class AbstractKeybindingService implements IKeybindingService {
notificationService: INotificationService,
statusService?: IStatusbarService
) {
super();
this._contextKeyService = contextKeyService;
this._commandService = commandService;
this._telemetryService = telemetryService;
@@ -51,13 +52,13 @@ export abstract class AbstractKeybindingService implements IKeybindingService {
this._notificationService = notificationService;
this._currentChord = null;
this._currentChordChecker = new IntervalTimer();
this._currentChordStatusMessage = null;
this._onDidUpdateKeybindings = new Emitter<IKeybindingEvent>();
this.toDispose.push(this._onDidUpdateKeybindings);
this._onDidUpdateKeybindings = this._register(new Emitter<IKeybindingEvent>());
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
super.dispose();
}
get onDidUpdateKeybindings(): Event<IKeybindingEvent> {
@@ -65,6 +66,7 @@ export abstract class AbstractKeybindingService implements IKeybindingService {
}
protected abstract _getResolver(): KeybindingResolver;
protected abstract _documentHasFocus(): boolean;
public abstract resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
public abstract resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding;
public abstract resolveUserBinding(userBinding: string): ResolvedKeybinding[];
@@ -114,6 +116,40 @@ export abstract class AbstractKeybindingService implements IKeybindingService {
return this._getResolver().resolve(contextValue, currentChord, firstPart);
}
private _enterChordMode(firstPart: string, keypressLabel: string): void {
this._currentChord = {
keypress: firstPart,
label: keypressLabel
};
if (this._statusService) {
this._currentChordStatusMessage = this._statusService.setStatusMessage(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
}
const chordEnterTime = Date.now();
this._currentChordChecker.cancelAndSet(() => {
if (!this._documentHasFocus()) {
// Focus has been lost => leave chord mode
this._leaveChordMode();
return;
}
if (Date.now() - chordEnterTime > 5000) {
// 5 seconds elapsed => leave chord mode
this._leaveChordMode();
}
}, 500);
}
private _leaveChordMode(): void {
if (this._currentChordStatusMessage) {
this._currentChordStatusMessage.dispose();
this._currentChordStatusMessage = null;
}
this._currentChordChecker.cancel();
this._currentChord = null;
}
protected _dispatch(e: IKeyboardEvent, target: IContextKeyServiceTarget): boolean {
let shouldPreventDefault = false;
@@ -135,13 +171,7 @@ export abstract class AbstractKeybindingService implements IKeybindingService {
if (resolveResult && resolveResult.enterChord) {
shouldPreventDefault = true;
this._currentChord = {
keypress: firstPart,
label: keypressLabel
};
if (this._statusService) {
this._currentChordStatusMessage = this._statusService.setStatusMessage(nls.localize('first.chord', "({0}) was pressed. Waiting for second key of chord...", keypressLabel));
}
this._enterChordMode(firstPart, keypressLabel);
return shouldPreventDefault;
}
@@ -151,19 +181,18 @@ export abstract class AbstractKeybindingService implements IKeybindingService {
shouldPreventDefault = true;
}
}
if (this._currentChordStatusMessage) {
this._currentChordStatusMessage.dispose();
this._currentChordStatusMessage = null;
}
this._currentChord = null;
this._leaveChordMode();
if (resolveResult && resolveResult.commandId) {
if (!resolveResult.bubble) {
shouldPreventDefault = true;
}
this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs || {}).done(undefined, err => {
this._notificationService.warn(err);
});
if (typeof resolveResult.commandArgs === 'undefined') {
this._commandService.executeCommand(resolveResult.commandId).done(undefined, err => this._notificationService.warn(err));
} else {
this._commandService.executeCommand(resolveResult.commandId, resolveResult.commandArgs).done(undefined, err => this._notificationService.warn(err));
}
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },

View File

@@ -57,6 +57,14 @@ export const enum KeybindingRuleSource {
Extension = 1
}
export const enum KeybindingWeight {
EditorCore = 0,
EditorContrib = 100,
WorkbenchContrib = 200,
BuiltinExtension = 300,
ExternalExtension = 400
}
export interface ICommandAndKeybindingRule extends IKeybindingRule {
handler: ICommandHandler;
description?: ICommandHandlerDescription;
@@ -67,14 +75,6 @@ export interface IKeybindingsRegistry {
registerKeybindingRule2(rule: IKeybindingRule2, source?: KeybindingRuleSource): void;
registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule, source?: KeybindingRuleSource): void;
getDefaultKeybindings(): IKeybindingItem[];
WEIGHT: {
editorCore(importance?: number): number;
editorContrib(importance?: number): number;
workbenchContrib(importance?: number): number;
builtinExtension(importance?: number): number;
externalExtension(importance?: number): number;
};
}
class KeybindingsRegistryImpl implements IKeybindingsRegistry {
@@ -82,24 +82,6 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry {
private _keybindings: IKeybindingItem[];
private _keybindingsSorted: boolean;
public WEIGHT = {
editorCore: (importance: number = 0): number => {
return 0 + importance;
},
editorContrib: (importance: number = 0): number => {
return 100 + importance;
},
workbenchContrib: (importance: number = 0): number => {
return 200 + importance;
},
builtinExtension: (importance: number = 0): number => {
return 300 + importance;
},
externalExtension: (importance: number = 0): number => {
return 400 + importance;
}
};
constructor() {
this._keybindings = [];
this._keybindingsSorted = true;
@@ -220,7 +202,7 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry {
this._keybindings.push({
keybinding: keybinding,
command: commandId,
commandArgs: null,
commandArgs: undefined,
when: when,
weight1: weight1,
weight2: weight2

View File

@@ -49,6 +49,10 @@ suite('AbstractKeybindingService', () => {
return this._resolver;
}
protected _documentHasFocus(): boolean {
return true;
}
public resolveKeybinding(kb: Keybinding): ResolvedKeybinding[] {
return [new USLayoutResolvedKeybinding(kb, OS)];
}
@@ -230,7 +234,7 @@ suite('AbstractKeybindingService', () => {
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, [{
commandId: 'simpleCommand',
args: [{}]
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
@@ -299,7 +303,7 @@ suite('AbstractKeybindingService', () => {
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, [{
commandId: 'simpleCommand',
args: [{}]
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
@@ -330,7 +334,7 @@ suite('AbstractKeybindingService', () => {
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, [{
commandId: 'chordCommand',
args: [{}]
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
@@ -359,7 +363,7 @@ suite('AbstractKeybindingService', () => {
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, [{
commandId: 'simpleCommand',
args: [{}]
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
@@ -377,7 +381,7 @@ suite('AbstractKeybindingService', () => {
assert.equal(shouldPreventDefault, true);
assert.deepEqual(executeCommandCalls, [{
commandId: 'simpleCommand',
args: [{}]
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);
@@ -417,7 +421,7 @@ suite('AbstractKeybindingService', () => {
assert.equal(shouldPreventDefault, false);
assert.deepEqual(executeCommandCalls, [{
commandId: 'simpleCommand',
args: [{}]
args: [null]
}]);
assert.deepEqual(showMessageCalls, []);
assert.deepEqual(statusMessageCalls, []);

View File

@@ -55,24 +55,24 @@ suite('KeybindingLabels', () => {
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyCode.KEY_A, 'Ctrl+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyCode.KEY_A, 'Shift+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyCode.KEY_A, 'Alt+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.WinCtrl | KeyCode.KEY_A, 'Windows+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.WinCtrl | KeyCode.KEY_A, 'Super+A');
// two modifiers
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A, 'Ctrl+Shift+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Alt+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Windows+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Super+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Shift+Alt+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Windows+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Alt+Windows+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Super+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Alt+Super+A');
// three modifiers
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A, 'Ctrl+Shift+Alt+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Windows+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Alt+Windows+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Alt+Windows+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Super+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Alt+Super+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Shift+Alt+Super+A');
// four modifiers
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Windows+A');
assertUSLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Ctrl+Shift+Alt+Super+A');
// chord
assertUSLabel(OperatingSystem.Linux, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'Ctrl+A Ctrl+B');
@@ -122,7 +122,7 @@ suite('KeybindingLabels', () => {
}
assertAriaLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Windows+A');
assertAriaLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Windows+A');
assertAriaLabel(OperatingSystem.Linux, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Super+A');
assertAriaLabel(OperatingSystem.Macintosh, KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_A, 'Control+Shift+Alt+Command+A');
});

View File

@@ -20,7 +20,7 @@ export class LifecycleService implements ILifecycleService {
private static readonly _lastShutdownReasonKey = 'lifecyle.lastShutdownReason';
public _serviceBrand: any;
_serviceBrand: any;
private readonly _onWillShutdown = new Emitter<ShutdownEvent>();
private readonly _onShutdown = new Emitter<ShutdownReason>();
@@ -95,11 +95,11 @@ export class LifecycleService implements ILifecycleService {
return handleVetos(vetos, err => this._notificationService.error(toErrorMessage(err)));
}
public get phase(): LifecyclePhase {
get phase(): LifecyclePhase {
return this._phase;
}
public set phase(value: LifecyclePhase) {
set phase(value: LifecyclePhase) {
if (value < this.phase) {
throw new Error('Lifecycle cannot go backwards');
}
@@ -119,7 +119,7 @@ export class LifecycleService implements ILifecycleService {
}
}
public when(phase: LifecyclePhase): Thenable<any> {
when(phase: LifecyclePhase): Thenable<any> {
if (phase <= this._phase) {
return Promise.resolve();
}
@@ -133,15 +133,15 @@ export class LifecycleService implements ILifecycleService {
return barrier.wait();
}
public get startupKind(): StartupKind {
get startupKind(): StartupKind {
return this._startupKind;
}
public get onWillShutdown(): Event<ShutdownEvent> {
get onWillShutdown(): Event<ShutdownEvent> {
return this._onWillShutdown.event;
}
public get onShutdown(): Event<ShutdownReason> {
get onShutdown(): Event<ShutdownReason> {
return this._onShutdown.event;
}
}

View File

@@ -129,15 +129,15 @@ export class LifecycleService implements ILifecycleService {
}
}
public get wasRestarted(): boolean {
get wasRestarted(): boolean {
return this._wasRestarted;
}
public get isQuitRequested(): boolean {
get isQuitRequested(): boolean {
return !!this.quitRequested;
}
public ready(): void {
ready(): void {
this.registerListeners();
}
@@ -178,7 +178,7 @@ export class LifecycleService implements ILifecycleService {
});
}
public registerWindow(window: ICodeWindow): void {
registerWindow(window: ICodeWindow): void {
// track window count
this.windowCounter++;
@@ -232,7 +232,7 @@ export class LifecycleService implements ILifecycleService {
});
}
public unload(window: ICodeWindow, reason: UnloadReason): TPromise<boolean /* veto */> {
unload(window: ICodeWindow, reason: UnloadReason): TPromise<boolean /* veto */> {
// Always allow to unload a window that is not yet ready
if (window.readyState !== ReadyState.READY) {
@@ -326,7 +326,7 @@ export class LifecycleService implements ILifecycleService {
* A promise that completes to indicate if the quit request has been veto'd
* by the user or not.
*/
public quit(fromUpdate?: boolean): TPromise<boolean /* veto */> {
quit(fromUpdate?: boolean): TPromise<boolean /* veto */> {
this.logService.trace('Lifecycle#quit()');
if (!this.pendingQuitPromise) {
@@ -362,13 +362,13 @@ export class LifecycleService implements ILifecycleService {
return this.pendingQuitPromise;
}
public kill(code?: number): void {
kill(code?: number): void {
this.logService.trace('Lifecycle#kill()');
app.exit(code);
}
public relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void {
relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void {
this.logService.trace('Lifecycle#relaunch()');
const args = process.argv.slice(1);

View File

@@ -4,15 +4,15 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ITree, ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree';
import { ITree, ITreeConfiguration, ITreeOptions, IRenderer as ITreeRenderer } from 'vs/base/parts/tree/browser/tree';
import { List, IListOptions, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent, IMultipleSelectionController, IOpenController, DefaultStyleController } from 'vs/base/browser/ui/list/listWidget';
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable, toDisposable, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { PagedList, IPagedRenderer } from 'vs/base/browser/ui/list/listPaging';
import { IDelegate, IRenderer, IListMouseEvent, IListTouchEvent } from 'vs/base/browser/ui/list/list';
import { IVirtualDelegate, IRenderer, IListMouseEvent, IListTouchEvent } from 'vs/base/browser/ui/list/list';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { attachListStyler, defaultListStyles, computeStyles } from 'vs/platform/theme/common/styler';
import { attachListStyler, defaultListStyles, computeStyles, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { InputFocusedContextKey } from 'vs/platform/workbench/common/contextkeys';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -23,8 +23,14 @@ import { DefaultController, IControllerOptions, OpenMode, ClickBehavior, Default
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { Event, Emitter } from 'vs/base/common/event';
import { createStyleSheet } from 'vs/base/browser/dom';
import { createStyleSheet, addStandardDisposableListener, getTotalHeight, removeClass, addClass } from 'vs/base/browser/dom';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { TPromise } from 'vs/base/common/winjs.base';
import { onUnexpectedError, canceled } from 'vs/base/common/errors';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
export type ListWidget = List<any> | PagedList<any> | ITree;
@@ -90,16 +96,12 @@ export class ListService implements IListService {
const RawWorkbenchListFocusContextKey = new RawContextKey<boolean>('listFocus', true);
export const WorkbenchListSupportsMultiSelectContextKey = new RawContextKey<boolean>('listSupportsMultiselect', true);
export const WorkbenchListFocusContextKey = ContextKeyExpr.and(RawWorkbenchListFocusContextKey, ContextKeyExpr.not(InputFocusedContextKey));
export const WorkbenchListHasSelectionOrFocus = new RawContextKey<boolean>('listHasSelectionOrFocus', false);
export const WorkbenchListDoubleSelection = new RawContextKey<boolean>('listDoubleSelection', false);
export const WorkbenchListMultiSelection = new RawContextKey<boolean>('listMultiSelection', false);
function createScopedContextKeyService(contextKeyService: IContextKeyService, widget: ListWidget): IContextKeyService {
const result = contextKeyService.createScoped(widget.getHTMLElement());
if (widget instanceof List || widget instanceof PagedList) {
WorkbenchListSupportsMultiSelectContextKey.bindTo(result);
}
RawWorkbenchListFocusContextKey.bindTo(result);
return result;
}
@@ -199,6 +201,7 @@ export class WorkbenchList<T> extends List<T> {
readonly contextKeyService: IContextKeyService;
private listHasSelectionOrFocus: IContextKey<boolean>;
private listDoubleSelection: IContextKey<boolean>;
private listMultiSelection: IContextKey<boolean>;
@@ -206,7 +209,7 @@ export class WorkbenchList<T> extends List<T> {
constructor(
container: HTMLElement,
delegate: IDelegate<T>,
delegate: IVirtualDelegate<T>,
renderers: IRenderer<T, any>[],
options: IListOptions<T>,
@IContextKeyService contextKeyService: IContextKeyService,
@@ -225,6 +228,11 @@ export class WorkbenchList<T> extends List<T> {
);
this.contextKeyService = createScopedContextKeyService(contextKeyService, this);
const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService);
listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false));
this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService);
this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService);
this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService);
@@ -236,8 +244,17 @@ export class WorkbenchList<T> extends List<T> {
attachListStyler(this, themeService),
this.onSelectionChange(() => {
const selection = this.getSelection();
const focus = this.getFocus();
this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0);
this.listMultiSelection.set(selection.length > 1);
this.listDoubleSelection.set(selection.length === 2);
}),
this.onFocusChange(() => {
const selection = this.getSelection();
const focus = this.getFocus();
this.listHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0);
})
]));
@@ -267,7 +284,7 @@ export class WorkbenchPagedList<T> extends PagedList<T> {
constructor(
container: HTMLElement,
delegate: IDelegate<number>,
delegate: IVirtualDelegate<number>,
renderers: IPagedRenderer<T, any>[],
options: IListOptions<T>,
@IContextKeyService contextKeyService: IContextKeyService,
@@ -287,6 +304,9 @@ export class WorkbenchPagedList<T> extends PagedList<T> {
this.contextKeyService = createScopedContextKeyService(contextKeyService, this);
const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService);
listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false));
this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService);
this.disposables.push(combinedDisposable([
@@ -323,6 +343,7 @@ export class WorkbenchTree extends Tree {
protected disposables: IDisposable[];
private listHasSelectionOrFocus: IContextKey<boolean>;
private listDoubleSelection: IContextKey<boolean>;
private listMultiSelection: IContextKey<boolean>;
@@ -352,6 +373,10 @@ export class WorkbenchTree extends Tree {
this.disposables = [];
this.contextKeyService = createScopedContextKeyService(contextKeyService, this);
WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService);
this.listHasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService);
this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService);
this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService);
@@ -366,10 +391,20 @@ export class WorkbenchTree extends Tree {
this.disposables.push(this.onDidChangeSelection(() => {
const selection = this.getSelection();
const focus = this.getFocus();
this.listHasSelectionOrFocus.set((selection && selection.length > 0) || !!focus);
this.listDoubleSelection.set(selection && selection.length === 2);
this.listMultiSelection.set(selection && selection.length > 1);
}));
this.disposables.push(this.onDidChangeFocus(() => {
const selection = this.getSelection();
const focus = this.getFocus();
this.listHasSelectionOrFocus.set((selection && selection.length > 0) || !!focus);
}));
this.disposables.push(configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(openModeSettingKey)) {
this._openOnSingleClick = useSingleClickToOpen(configurationService);
@@ -526,6 +561,171 @@ export class TreeResourceNavigator extends Disposable {
}
}
export interface IHighlightingRenderer extends ITreeRenderer {
/**
* Update hightlights and return the best matching element
*/
updateHighlights(tree: ITree, pattern: string): any;
}
export interface IHighlightingTreeConfiguration extends ITreeConfiguration {
renderer: IHighlightingRenderer & ITreeRenderer;
}
export class HighlightingTreeController extends WorkbenchTreeController {
constructor(
options: IControllerOptions,
private readonly onType: () => any,
@IConfigurationService configurationService: IConfigurationService
) {
super(options, configurationService);
}
onKeyDown(tree: ITree, event: IKeyboardEvent) {
let handled = super.onKeyDown(tree, event);
if (handled) {
return true;
}
if (this.upKeyBindingDispatcher.has(event.keyCode)) {
return false;
}
if (event.ctrlKey || event.metaKey) {
// ignore ctrl/cmd-combination but not shift/alt-combinatios
return false;
}
// crazy -> during keydown focus moves to the input box
// and because of that the keyup event is handled by the
// input field
if (event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z) {
// todo@joh this is much weaker than using the KeyboardMapperFactory
// but due to layering-challanges that's not available here...
this.onType();
return true;
}
return false;
}
}
export class HighlightingWorkbenchTree extends WorkbenchTree {
protected readonly domNode: HTMLElement;
protected readonly inputContainer: HTMLElement;
protected readonly input: InputBox;
protected readonly renderer: IHighlightingRenderer;
constructor(
parent: HTMLElement,
treeConfiguration: IHighlightingTreeConfiguration,
treeOptions: ITreeOptions,
listOptions: IInputOptions,
@IContextKeyService contextKeyService: IContextKeyService,
@IContextViewService contextViewService: IContextViewService,
@IListService listService: IListService,
@IThemeService themeService: IThemeService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService
) {
// build html skeleton
const container = document.createElement('div');
container.className = 'highlighting-tree';
const inputContainer = document.createElement('div');
inputContainer.className = 'input';
const treeContainer = document.createElement('div');
treeContainer.className = 'tree';
container.appendChild(inputContainer);
container.appendChild(treeContainer);
parent.appendChild(container);
// create tree
treeConfiguration.controller = treeConfiguration.controller || instantiationService.createInstance(HighlightingTreeController, {}, () => this.onTypeInTree());
super(treeContainer, treeConfiguration, treeOptions, contextKeyService, listService, themeService, instantiationService, configurationService);
this.renderer = treeConfiguration.renderer;
this.domNode = container;
addClass(this.domNode, 'inactive');
// create input
this.inputContainer = inputContainer;
this.input = new InputBox(inputContainer, contextViewService, listOptions);
this.input.setEnabled(false);
this.input.onDidChange(this.updateHighlights, this, this.disposables);
this.disposables.push(attachInputBoxStyler(this.input, themeService));
this.disposables.push(this.input);
this.disposables.push(addStandardDisposableListener(this.input.inputElement, 'keydown', event => {
//todo@joh make this command/context-key based
switch (event.keyCode) {
case KeyCode.DownArrow:
case KeyCode.Tab:
this.domFocus();
event.preventDefault();
break;
case KeyCode.Enter:
this.setSelection(this.getSelection());
event.preventDefault();
break;
case KeyCode.Escape:
this.input.value = '';
this.domFocus();
event.preventDefault();
break;
}
}));
}
setInput(element: any): TPromise<any> {
this.input.setEnabled(false);
return super.setInput(element).then(value => {
if (!this.input.inputElement) {
// has been disposed in the meantime -> cancel
return Promise.reject(canceled());
}
this.input.setEnabled(true);
return value;
});
}
layout(height?: number, width?: number): void {
this.input.layout();
super.layout(isNaN(height) ? height : height - getTotalHeight(this.inputContainer), width);
}
private onTypeInTree(): void {
removeClass(this.domNode, 'inactive');
this.input.focus();
this.layout();
}
private lastSelection: any[];
private updateHighlights(pattern: string): void {
// remember old selection
let defaultSelection: any[] = [];
if (!this.lastSelection && pattern) {
this.lastSelection = this.getSelection();
} else if (this.lastSelection && !pattern) {
defaultSelection = this.lastSelection;
this.lastSelection = [];
}
const topElement = this.renderer.updateHighlights(this, pattern);
this.refresh().then(() => {
if (topElement && pattern) {
this.reveal(topElement, .5).then(_ => {
this.setSelection([topElement], this);
this.setFocus(topElement, this);
});
} else {
this.setSelection(defaultSelection, this);
}
}, onUnexpectedError);
}
}
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
@@ -548,20 +748,16 @@ configurationRegistry.registerConfiguration({
'- `ctrlCmd` refers to a value the setting can take and should not be localized.',
'- `Control` and `Command` refer to the modifier keys Ctrl or Cmd on the keyboard and can be localized.'
]
}, "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). `ctrlCmd` maps to `Control` on Windows and Linux and to `Command` on macOS. The 'Open to Side' mouse gestures - if supported - will adapt such that they do not conflict with the multiselect modifier.")
}, "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'],
'enumDescriptions': [
localize('openMode.singleClick', "Opens items on mouse single click."),
localize('openMode.doubleClick', "Open items on mouse double click.")
],
'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). Set to `singleClick` to open items with a single mouse click and `doubleClick` to only open via mouse double click. For parents with children in trees, this setting will control if a single click expands the parent or a double click. Note that some trees and lists might choose to ignore this setting if it is not applicable. ")
}, "Controls how to open items in trees and lists using the mouse (if supported). For parents with children in trees, this setting will control if a single click expands the parent or a double click. Note that some trees and lists might choose to ignore this setting if it is not applicable. ")
},
[horizontalScrollingKey]: {
'type': 'boolean',

View File

@@ -11,8 +11,9 @@ import { Event } from 'vs/base/common/event';
export interface ILocalization {
languageId: string;
languageName?: string;
languageNameLocalized?: string;
localizedLanguageName?: string;
translations: ITranslation[];
minimalTranslations?: { [key: string]: string };
}
export interface ITranslation {
@@ -51,7 +52,7 @@ export function isValidLocalization(localization: ILocalization): boolean {
if (localization.languageName && typeof localization.languageName !== 'string') {
return false;
}
if (localization.languageNameLocalized && typeof localization.languageNameLocalized !== 'string') {
if (localization.localizedLanguageName && typeof localization.localizedLanguageName !== 'string') {
return false;
}
return true;

View File

@@ -6,12 +6,14 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event, buffer } from 'vs/base/common/event';
import { ILocalizationsService, LanguageType } from 'vs/platform/localizations/common/localizations';
export interface ILocalizationsChannel extends IChannel {
call(command: 'event:onDidLanguagesChange'): TPromise<void>;
listen(event: 'onDidLanguagesChange'): Event<void>;
listen<T>(event: string, arg?: any): Event<T>;
call(command: 'getLanguageIds'): TPromise<string[]>;
call(command: string, arg?: any): TPromise<any>;
}
@@ -24,9 +26,16 @@ export class LocalizationsChannel implements ILocalizationsChannel {
this.onDidLanguagesChange = buffer(service.onDidLanguagesChange, true);
}
listen<T>(event: string): Event<any> {
switch (event) {
case 'onDidLanguagesChange': return this.onDidLanguagesChange;
}
throw new Error('No event found');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'event:onDidLanguagesChange': return eventToCall(this.onDidLanguagesChange);
case 'getLanguageIds': return this.service.getLanguageIds(arg);
}
return undefined;
@@ -39,8 +48,7 @@ export class LocalizationsChannelClient implements ILocalizationsService {
constructor(private channel: ILocalizationsChannel) { }
private _onDidLanguagesChange = eventFromCall<void>(this.channel, 'event:onDidLanguagesChange');
get onDidLanguagesChange(): Event<void> { return this._onDidLanguagesChange; }
get onDidLanguagesChange(): Event<void> { return this.channel.listen('onDidLanguagesChange'); }
getLanguageIds(type?: LanguageType): TPromise<string[]> {
return this.channel.call('getLanguageIds', type);

View File

@@ -8,7 +8,6 @@ import { createHash } from 'crypto';
import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Disposable } from 'vs/base/common/lifecycle';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { join } from 'vs/base/common/paths';
import { TPromise } from 'vs/base/common/winjs.base';
import { Limiter } from 'vs/base/common/async';
import { areSameExtensions, getGalleryExtensionIdFromLocal, getIdFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
@@ -17,6 +16,8 @@ import { isValidLocalization, ILocalizationsService, LanguageType } from 'vs/pla
import product from 'vs/platform/node/product';
import { distinct, equals } from 'vs/base/common/arrays';
import { Event, Emitter } from 'vs/base/common/event';
import { Schemas } from 'vs/base/common/network';
import { posix } from 'path';
interface ILanguagePack {
hash: string;
@@ -27,7 +28,7 @@ interface ILanguagePack {
translations: { [id: string]: string };
}
const systemLanguages: string[] = ['de', 'en', 'en-US', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'zh-CN', 'zh-Hans', 'zh-TW', 'zh-Hant'];
const systemLanguages: string[] = ['de', 'en', 'en-US', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'zh-CN', 'zh-TW'];
if (product.quality !== 'stable') {
systemLanguages.push('hu');
}
@@ -106,7 +107,7 @@ class LanguagePacksCache extends Disposable {
@ILogService private logService: ILogService
) {
super();
this.languagePacksFilePath = join(environmentService.userDataPath, 'languagepacks.json');
this.languagePacksFilePath = posix.join(environmentService.userDataPath, 'languagepacks.json');
this.languagePacksFileLimiter = new Limiter(1);
}
@@ -138,7 +139,7 @@ class LanguagePacksCache extends Disposable {
private createLanguagePacksFromExtension(languagePacks: { [language: string]: ILanguagePack }, extension: ILocalExtension): void {
const extensionIdentifier = { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid };
for (const localizationContribution of extension.manifest.contributes.localizations) {
if (isValidLocalization(localizationContribution)) {
if (extension.location.scheme === Schemas.file && isValidLocalization(localizationContribution)) {
let languagePack = languagePacks[localizationContribution.languageId];
if (!languagePack) {
languagePack = { hash: '', extensions: [], translations: {} };
@@ -151,7 +152,7 @@ class LanguagePacksCache extends Disposable {
languagePack.extensions.push({ extensionIdentifier, version: extension.manifest.version });
}
for (const translation of localizationContribution.translations) {
languagePack.translations[translation.id] = join(extension.path, translation.path);
languagePack.translations[translation.id] = posix.join(extension.location.fsPath, translation.path);
}
}
}

View File

@@ -30,6 +30,15 @@ export class BufferLogService extends AbstractLogService implements ILogService
private buffer: ILog[] = [];
private _logger: ILogService | undefined = undefined;
constructor() {
super();
this._register(this.onDidChangeLogLevel(level => {
if (this._logger) {
this._logger.setLevel(level);
}
}));
}
set logger(logger: ILogService) {
this._logger = logger;

View File

@@ -3,14 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { TPromise } from 'vs/base/common/winjs.base';
import { LogLevel, ILogService, DelegatedLogService } from 'vs/platform/log/common/log';
import { Event, buffer } from 'vs/base/common/event';
export interface ILogLevelSetterChannel extends IChannel {
call(command: 'event:onDidChangeLogLevel'): TPromise<LogLevel>;
listen(event: 'onDidChangeLogLevel'): Event<LogLevel>;
listen<T>(event: string, arg?: any): Event<T>;
call(command: 'setLevel', logLevel: LogLevel): TPromise<void>;
call(command: string, arg?: any): TPromise<any>;
}
export class LogLevelSetterChannel implements ILogLevelSetterChannel {
@@ -21,9 +24,16 @@ export class LogLevelSetterChannel implements ILogLevelSetterChannel {
this.onDidChangeLogLevel = buffer(service.onDidChangeLogLevel, true);
}
listen<T>(event: string): Event<any> {
switch (event) {
case 'onDidChangeLogLevel': return this.onDidChangeLogLevel;
}
throw new Error('No event found');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'event:onDidChangeLogLevel': return eventToCall(this.onDidChangeLogLevel);
case 'setLevel': this.service.setLevel(arg); return TPromise.as(null);
}
return undefined;
@@ -34,8 +44,9 @@ export class LogLevelSetterChannelClient {
constructor(private channel: ILogLevelSetterChannel) { }
private _onDidChangeLogLevel = eventFromCall<LogLevel>(this.channel, 'event:onDidChangeLogLevel');
get onDidChangeLogLevel(): Event<LogLevel> { return this._onDidChangeLogLevel; }
get onDidChangeLogLevel(): Event<LogLevel> {
return this.channel.listen('onDidChangeLogLevel');
}
setLevel(level: LogLevel): TPromise<void> {
return this.channel.call('setLevel', level);

View File

@@ -183,7 +183,8 @@ export class MarkerService implements IMarkerService {
code, severity,
message, source,
startLineNumber, startColumn, endLineNumber, endColumn,
relatedInformation
relatedInformation,
tags,
} = data;
if (!message) {
@@ -208,7 +209,8 @@ export class MarkerService implements IMarkerService {
startColumn,
endLineNumber,
endColumn,
relatedInformation
relatedInformation,
tags,
};
}

View File

@@ -38,6 +38,10 @@ export interface IRelatedInformation {
endColumn: number;
}
export enum MarkerTag {
Unnecessary = 1,
}
export enum MarkerSeverity {
Hint = 1,
Info = 2,
@@ -83,6 +87,7 @@ export interface IMarkerData {
endLineNumber: number;
endColumn: number;
relatedInformation?: IRelatedInformation[];
tags?: MarkerTag[];
}
export interface IResourceMarker {
@@ -102,6 +107,7 @@ export interface IMarker {
endLineNumber: number;
endColumn: number;
relatedInformation?: IRelatedInformation[];
tags?: MarkerTag[];
}
export interface MarkerStatistics {

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IMenubarService = createDecorator<IMenubarService>('menubarService');
export interface IMenubarService {
_serviceBrand: any;
updateMenubar(windowId: number, menus: IMenubarData, additionalKeybindings?: Array<IMenubarKeybinding>): TPromise<void>;
}
export interface IMenubarData {
'Files'?: IMenubarMenu;
'Edit'?: IMenubarMenu;
[id: string]: IMenubarMenu;
}
export interface IMenubarMenu {
items: Array<MenubarMenuItem>;
}
export interface IMenubarKeybinding {
id: string;
label: string;
isNative: boolean;
}
export interface IMenubarMenuItemAction {
id: string;
label: string;
checked: boolean;
enabled: boolean;
keybinding?: IMenubarKeybinding;
}
export interface IMenubarMenuItemSubmenu {
id: string;
label: string;
submenu: IMenubarMenu;
}
export interface IMenubarMenuItemSeparator {
id: 'vscode.menubar.separator';
}
export type MenubarMenuItem = IMenubarMenuItemAction | IMenubarMenuItemSubmenu | IMenubarMenuItemSeparator;
export function isMenubarMenuItemSubmenu(menuItem: MenubarMenuItem): menuItem is IMenubarMenuItemSubmenu {
return (<IMenubarMenuItemSubmenu>menuItem).submenu !== undefined;
}
export function isMenubarMenuItemAction(menuItem: MenubarMenuItem): menuItem is IMenubarMenuItemAction {
return (<IMenubarMenuItemAction>menuItem).checked !== undefined || (<IMenubarMenuItemAction>menuItem).enabled !== undefined;
}
export function isMenubarMenuItemSeparator(menuItem: MenubarMenuItem): menuItem is IMenubarMenuItemSeparator {
return (<IMenubarMenuItemSeparator>menuItem).id === 'vscode.menubar.separator';
}

View File

@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { TPromise } from 'vs/base/common/winjs.base';
import { IMenubarService, IMenubarData, IMenubarKeybinding } from 'vs/platform/menubar/common/menubar';
import { Event } from 'vs/base/common/event';
export interface IMenubarChannel extends IChannel {
call(command: 'updateMenubar', arg: [number, IMenubarData]): TPromise<void>;
call(command: string, arg?: any): TPromise<any>;
}
export class MenubarChannel implements IMenubarChannel {
constructor(private service: IMenubarService) { }
listen<T>(event: string, arg?: any): Event<T> {
throw new Error('No events');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'updateMenubar': return this.service.updateMenubar(arg[0], arg[1], arg[2]);
}
return undefined;
}
}
export class MenubarChannelClient implements IMenubarService {
_serviceBrand: any;
constructor(private channel: IMenubarChannel) { }
updateMenubar(windowId: number, menus: IMenubarData, additionalKeybindings?: Array<IMenubarKeybinding>): TPromise<void> {
return this.channel.call('updateMenubar', [windowId, menus, additionalKeybindings]);
}
}

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IMenubarService, IMenubarData, IMenubarKeybinding } from 'vs/platform/menubar/common/menubar';
import { Menubar } from 'vs/code/electron-main/menubar';
import { ILogService } from 'vs/platform/log/common/log';
import { TPromise } from 'vs/base/common/winjs.base';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { isMacintosh, isWindows } from 'vs/base/common/platform';
export class MenubarService implements IMenubarService {
_serviceBrand: any;
private _menubar: Menubar;
constructor(
@IInstantiationService private instantiationService: IInstantiationService,
@ILogService private logService: ILogService
) {
// Install Menu
if (isMacintosh && isWindows) {
this._menubar = this.instantiationService.createInstance(Menubar);
}
}
updateMenubar(windowId: number, menus: IMenubarData, additionalKeybindings?: Array<IMenubarKeybinding>): TPromise<void> {
this.logService.trace('menubarService#updateMenubar', windowId);
if (this._menubar) {
this._menubar.updateMenu(menus, windowId, additionalKeybindings);
}
return TPromise.as(null);
}
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
// The strings localized in this file will get pulled into the manifest of the language packs.
// So that they are available for VS Code to use without downloading the entire language pack.
export const minimumTranslatedStrings = {
showLanguagePackExtensions: localize('showLanguagePackExtensions', "Search language packs in the Marketplace to change the display language to {0}."),
searchMarketplace: localize('searchMarketplace', "Search Marketplace"),
installAndRestartMessage: localize('installAndRestartMessage', "Install language pack to change the display language to {0}."),
installAndRestart: localize('installAndRestart', "Install and Restart")
};

View File

@@ -10,6 +10,10 @@ export interface IProductConfiguration {
nameShort: string;
nameLong: string;
applicationName: string;
win32AppId: string;
win32x64AppId: string;
win32UserAppId: string;
win32x64UserAppId: string;
win32AppUserModelId: string;
win32MutexName: string;
darwinBundleIdentifier: string;
@@ -18,9 +22,11 @@ export interface IProductConfiguration {
downloadUrl: string;
updateUrl?: string;
quality?: string;
target?: string;
commit?: string;
settingsSearchBuildId?: number;
settingsSearchUrl?: string;
experimentsUrl?: string;
date: string;
extensionsGallery: {
serviceUrl: string;
@@ -32,7 +38,7 @@ export interface IProductConfiguration {
// {{SQL CARBON EDIT}}
recommendedExtensions: string[];
extensionImportantTips: { [id: string]: { name: string; pattern: string; }; };
exeBasedExtensionTips: { [id: string]: any; };
exeBasedExtensionTips: { [id: string]: { friendlyName: string, windowsPath?: string, recommendations: string[] }; };
extensionKeywords: { [extension: string]: string[]; };
extensionAllowedBadgeProviders: string[];
extensionAllowedProposedApi: string[];
@@ -79,6 +85,7 @@ export interface IProductConfiguration {
'darwin': string;
};
logUploaderUrl: string;
portable?: string;
}
export interface ISurveyData {

View File

@@ -205,7 +205,7 @@ export class NoOpNotification implements INotificationHandle {
private readonly _onDidClose: Emitter<void> = new Emitter();
public get onDidClose(): Event<void> {
get onDidClose(): Event<void> {
return this._onDidClose.event;
}

View File

@@ -1,12 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { OpenerService } from 'vs/platform/opener/browser/openerService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
registerSingleton(IOpenerService, OpenerService);

View File

@@ -1,89 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import URI from 'vs/base/common/uri';
import * as dom from 'vs/base/browser/dom';
import { parse } from 'vs/base/common/marshalling';
import { Schemas } from 'vs/base/common/network';
import { TPromise } from 'vs/base/common/winjs.base';
import { IEditorService } from 'vs/platform/editor/common/editor';
import { normalize } from 'vs/base/common/paths';
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { optional } from 'vs/platform/instantiation/common/instantiation';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
export class OpenerService implements IOpenerService {
_serviceBrand: any;
constructor(
@IEditorService private readonly _editorService: IEditorService,
@ICommandService private readonly _commandService: ICommandService,
@optional(ITelemetryService) private _telemetryService: ITelemetryService = NullTelemetryService
) {
//
}
open(resource: URI, options?: { openToSide?: boolean }): TPromise<any> {
/* __GDPR__
"openerService" : {
"scheme" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this._telemetryService.publicLog('openerService', { scheme: resource.scheme });
const { scheme, path, query, fragment } = resource;
let promise: TPromise<any> = TPromise.wrap(void 0);
if (scheme === Schemas.http || scheme === Schemas.https || scheme === Schemas.mailto) {
// open http or default mail application
dom.windowOpenNoOpener(resource.toString(true));
} else if (scheme === 'command' && CommandsRegistry.getCommand(path)) {
// execute as command
let args: any = [];
try {
args = parse(query);
if (!Array.isArray(args)) {
args = [args];
}
} catch (e) {
//
}
promise = this._commandService.executeCommand(path, ...args);
} else {
let selection: {
startLineNumber: number;
startColumn: number;
};
const match = /^L?(\d+)(?:,(\d+))?/.exec(fragment);
if (match) {
// support file:///some/file.js#73,84
// support file:///some/file.js#L73
selection = {
startLineNumber: parseInt(match[1]),
startColumn: match[2] ? parseInt(match[2]) : 1
};
// remove fragment
resource = resource.with({ fragment: '' });
}
if (!resource.scheme) {
// we cannot handle those
return TPromise.as(undefined);
} else if (resource.scheme === Schemas.file) {
resource = resource.with({ path: normalize(resource.path) }); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954)
}
promise = this._editorService.openEditor({ resource, options: { selection, } }, options && options.openToSide);
}
return promise;
}
}

View File

@@ -10,7 +10,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
export const IOpenerService = createDecorator<IOpenerService>('openerService');
export interface IOpenerService {
_serviceBrand: any;

View File

@@ -1,117 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import URI from 'vs/base/common/uri';
import * as assert from 'assert';
import { TPromise } from 'vs/base/common/winjs.base';
import { IEditorService, IResourceInput } from 'vs/platform/editor/common/editor';
import { ICommandService, NullCommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { OpenerService } from 'vs/platform/opener/browser/openerService';
suite('OpenerService', function () {
let lastInput: IResourceInput;
const editorService = new class implements IEditorService {
_serviceBrand: any;
openEditor(input: IResourceInput): any {
lastInput = input;
}
};
let lastCommand: { id: string, args: any[] };
const commandService = new class implements ICommandService {
_serviceBrand: any;
onWillExecuteCommand = () => ({ dispose: () => { } });
executeCommand(id: string, ...args: any[]): TPromise<any> {
lastCommand = { id, args };
return TPromise.as(undefined);
}
};
setup(function () {
lastInput = undefined;
lastCommand = undefined;
});
test('delegate to editorService, scheme:///fff', function () {
const openerService = new OpenerService(editorService, NullCommandService);
openerService.open(URI.parse('another:///somepath'));
assert.equal(lastInput.options.selection, undefined);
});
test('delegate to editorService, scheme:///fff#L123', function () {
const openerService = new OpenerService(editorService, NullCommandService);
openerService.open(URI.parse('file:///somepath#L23'));
assert.equal(lastInput.options.selection.startLineNumber, 23);
assert.equal(lastInput.options.selection.startColumn, 1);
assert.equal(lastInput.options.selection.endLineNumber, undefined);
assert.equal(lastInput.options.selection.endColumn, undefined);
assert.equal(lastInput.resource.fragment, '');
openerService.open(URI.parse('another:///somepath#L23'));
assert.equal(lastInput.options.selection.startLineNumber, 23);
assert.equal(lastInput.options.selection.startColumn, 1);
openerService.open(URI.parse('another:///somepath#L23,45'));
assert.equal(lastInput.options.selection.startLineNumber, 23);
assert.equal(lastInput.options.selection.startColumn, 45);
assert.equal(lastInput.options.selection.endLineNumber, undefined);
assert.equal(lastInput.options.selection.endColumn, undefined);
assert.equal(lastInput.resource.fragment, '');
});
test('delegate to editorService, scheme:///fff#123,123', function () {
const openerService = new OpenerService(editorService, NullCommandService);
openerService.open(URI.parse('file:///somepath#23'));
assert.equal(lastInput.options.selection.startLineNumber, 23);
assert.equal(lastInput.options.selection.startColumn, 1);
assert.equal(lastInput.options.selection.endLineNumber, undefined);
assert.equal(lastInput.options.selection.endColumn, undefined);
assert.equal(lastInput.resource.fragment, '');
openerService.open(URI.parse('file:///somepath#23,45'));
assert.equal(lastInput.options.selection.startLineNumber, 23);
assert.equal(lastInput.options.selection.startColumn, 45);
assert.equal(lastInput.options.selection.endLineNumber, undefined);
assert.equal(lastInput.options.selection.endColumn, undefined);
assert.equal(lastInput.resource.fragment, '');
});
test('delegate to commandsService, command:someid', function () {
const openerService = new OpenerService(editorService, commandService);
// unknown command
openerService.open(URI.parse('command:foobar'));
assert.equal(lastCommand, undefined);
assert.equal(lastInput.resource.toString(), 'command:foobar');
assert.equal(lastInput.options.selection, undefined);
const id = `aCommand${Math.random()}`;
CommandsRegistry.registerCommand(id, function () { });
openerService.open(URI.parse('command:' + id));
assert.equal(lastCommand.id, id);
assert.equal(lastCommand.args.length, 0);
openerService.open(URI.parse('command:' + id).with({ query: '123' }));
assert.equal(lastCommand.id, id);
assert.equal(lastCommand.args.length, 1);
assert.equal(lastCommand.args[0], '123');
openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) }));
assert.equal(lastCommand.id, id);
assert.equal(lastCommand.args.length, 2);
assert.equal(lastCommand.args[0], 12);
assert.equal(lastCommand.args[1], true);
});
});

View File

@@ -4,8 +4,9 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
export const IProgressService = createDecorator<IProgressService>('progressService');
@@ -22,7 +23,7 @@ export interface IProgressService {
* Indicate progress for the duration of the provided promise. Progress will stop in
* any case of promise completion, error or cancellation.
*/
showWhile(promise: TPromise<any>, delay?: number): TPromise<void>;
showWhile(promise: Thenable<any>, delay?: number): Thenable<void>;
}
export interface IProgressRunner {
@@ -62,32 +63,66 @@ export class Progress<T> implements IProgress<T> {
}
}
export enum ProgressLocation {
Explorer = 1,
Scm = 3,
Extensions = 5,
Window = 10,
Notification = 15
/**
* A helper to show progress during a long running operation. If the operation
* is started multiple times, only the last invocation will drive the progress.
*/
export interface IOperation {
id: number;
isCurrent: () => boolean;
token: CancellationToken;
stop(): void;
}
export interface IProgressOptions {
location: ProgressLocation;
title?: string;
source?: string;
total?: number;
cancellable?: boolean;
}
export interface IProgressStep {
message?: string;
increment?: number;
}
export const IProgressService2 = createDecorator<IProgressService2>('progressService2');
export interface IProgressService2 {
_serviceBrand: any;
withProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => P, onDidCancel?: () => void): P;
export class LongRunningOperation {
private currentOperationId = 0;
private currentOperationDisposables: IDisposable[] = [];
private currentProgressRunner: IProgressRunner;
private currentProgressTimeout: number;
constructor(
private progressService: IProgressService
) { }
start(progressDelay: number): IOperation {
// Stop any previous operation
this.stop();
// Start new
const newOperationId = ++this.currentOperationId;
const newOperationToken = new CancellationTokenSource();
this.currentProgressTimeout = setTimeout(() => {
if (newOperationId === this.currentOperationId) {
this.currentProgressRunner = this.progressService.show(true);
}
}, progressDelay);
this.currentOperationDisposables.push(
toDisposable(() => clearTimeout(this.currentProgressTimeout)),
toDisposable(() => newOperationToken.cancel()),
toDisposable(() => this.currentProgressRunner ? this.currentProgressRunner.done() : void 0)
);
return {
id: newOperationId,
token: newOperationToken.token,
stop: () => this.doStop(newOperationId),
isCurrent: () => this.currentOperationId === newOperationId
};
}
stop(): void {
this.doStop(this.currentOperationId);
}
private doStop(operationId: number): void {
if (this.currentOperationId === operationId) {
this.currentOperationDisposables = dispose(this.currentOperationDisposables);
}
}
dispose(): void {
this.currentOperationDisposables = dispose(this.currentOperationDisposables);
}
}

View File

@@ -33,7 +33,6 @@ export interface IPickOpenEntry {
run?: (context: IEntryRunContext) => void;
action?: IAction;
payload?: any;
picked?: boolean;
}
export interface IPickOpenItem {
@@ -85,46 +84,14 @@ export interface IPickOptions {
* a context key to set when this picker is active
*/
contextKey?: string;
/**
* an optional flag to make this picker multi-select (honoured by extension API)
*/
canSelectMany?: boolean;
}
export interface IInputOptions {
export interface IStringPickOptions extends IPickOptions {
onDidFocus?: (item: string) => void;
}
/**
* the value to prefill in the input box
*/
value?: string;
/**
* the selection of value, default to the whole word
*/
valueSelection?: [number, number];
/**
* the text to display underneath the input box
*/
prompt?: string;
/**
* an optional string to show as place holder in the input box to guide the user what to type
*/
placeHolder?: string;
/**
* set to true to show a password prompt that will not show the typed value
*/
password?: boolean;
ignoreFocusLost?: boolean;
/**
* an optional function that is used to validate user input.
*/
validateInput?: (input: string) => TPromise<string>;
export interface ITypedPickOptions<T extends IPickOpenEntry> extends IPickOptions {
onDidFocus?: (entry: T) => void;
}
export interface IShowOptions {
@@ -155,21 +122,16 @@ export interface IQuickOpenService {
* Passing in a promise will allow you to resolve the elements in the background while quick open will show a
* progress bar spinning.
*/
pick(picks: TPromise<string[]>, options?: IPickOptions, token?: CancellationToken): TPromise<string>;
pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: IPickOptions, token?: CancellationToken): TPromise<T>;
pick(picks: string[], options?: IPickOptions, token?: CancellationToken): TPromise<string>;
pick<T extends IPickOpenEntry>(picks: T[], options?: IPickOptions, token?: CancellationToken): TPromise<T>;
pick(picks: TPromise<string[]>, options?: IStringPickOptions, token?: CancellationToken): TPromise<string>;
pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: ITypedPickOptions<T>, token?: CancellationToken): TPromise<T>;
pick(picks: string[], options?: IStringPickOptions, token?: CancellationToken): TPromise<string>;
pick<T extends IPickOpenEntry>(picks: T[], options?: ITypedPickOptions<T>, token?: CancellationToken): TPromise<T>;
/**
* Allows to navigate from the outside in an opened picker.
*/
navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void;
/**
* Opens the quick open box for user input and returns a promise with the user typed value if any.
*/
input(options?: IInputOptions, token?: CancellationToken): TPromise<string>;
/**
* Accepts the selected value in quick open if visible.
*/

View File

@@ -6,8 +6,169 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import { IPickOptions, IPickOpenEntry, IInputOptions } from 'vs/platform/quickOpen/common/quickOpen';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import URI from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
export interface IQuickPickItem {
id?: string;
label: string;
description?: string;
detail?: string;
picked?: boolean;
}
export interface IQuickNavigateConfiguration {
keybindings: ResolvedKeybinding[];
}
export interface IPickOptions<T extends IQuickPickItem> {
/**
* an optional string to show as place holder in the input box to guide the user what she picks on
*/
placeHolder?: string;
/**
* an optional flag to include the description when filtering the picks
*/
matchOnDescription?: boolean;
/**
* an optional flag to include the detail when filtering the picks
*/
matchOnDetail?: boolean;
/**
* an optional flag to not close the picker on focus lost
*/
ignoreFocusLost?: boolean;
/**
* an optional flag to make this picker multi-select
*/
canPickMany?: boolean;
onDidFocus?: (entry: T) => void;
}
export interface IInputOptions {
/**
* the value to prefill in the input box
*/
value?: string;
/**
* the selection of value, default to the whole word
*/
valueSelection?: [number, number];
/**
* the text to display underneath the input box
*/
prompt?: string;
/**
* an optional string to show as place holder in the input box to guide the user what to type
*/
placeHolder?: string;
/**
* set to true to show a password prompt that will not show the typed value
*/
password?: boolean;
ignoreFocusLost?: boolean;
/**
* an optional function that is used to validate user input.
*/
validateInput?: (input: string) => TPromise<string>;
}
export interface IQuickInput {
title: string | undefined;
step: number | undefined;
totalSteps: number | undefined;
enabled: boolean;
busy: boolean;
ignoreFocusOut: boolean;
show(): void;
hide(): void;
onDidHide: Event<void>;
dispose(): void;
}
export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
value: string;
placeholder: string | undefined;
readonly onDidChangeValue: Event<string>;
readonly onDidAccept: Event<string>;
buttons: ReadonlyArray<IQuickInputButton>;
readonly onDidTriggerButton: Event<IQuickInputButton>;
items: ReadonlyArray<T>;
canSelectMany: boolean;
matchOnDescription: boolean;
matchOnDetail: boolean;
activeItems: ReadonlyArray<T>;
readonly onDidChangeActive: Event<T[]>;
selectedItems: ReadonlyArray<T>;
readonly onDidChangeSelection: Event<T[]>;
}
export interface IInputBox extends IQuickInput {
value: string;
valueSelection: Readonly<[number, number]>;
placeholder: string | undefined;
password: boolean;
readonly onDidChangeValue: Event<string>;
readonly onDidAccept: Event<string>;
buttons: ReadonlyArray<IQuickInputButton>;
readonly onDidTriggerButton: Event<IQuickInputButton>;
prompt: string | undefined;
validationMessage: string | undefined;
}
export interface IQuickInputButton {
iconPath: { dark: URI; light?: URI; };
tooltip?: string | undefined;
}
export const IQuickInputService = createDecorator<IQuickInputService>('quickInputService');
@@ -15,9 +176,30 @@ export interface IQuickInputService {
_serviceBrand: any;
pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: IPickOptions, token?: CancellationToken): TPromise<T[]>;
/**
* Opens the quick input box for selecting items and returns a promise with the user selected item(s) if any.
*/
pick<T extends IQuickPickItem, O extends IPickOptions<T>>(picks: TPromise<T[]>, options?: O, token?: CancellationToken): TPromise<O extends { canPickMany: true } ? T[] : T>;
/**
* Opens the quick input box for text input and returns a promise with the user typed value if any.
*/
input(options?: IInputOptions, token?: CancellationToken): TPromise<string>;
backButton: IQuickInputButton;
createQuickPick<T extends IQuickPickItem>(): IQuickPick<T>;
createInputBox(): IInputBox;
focus(): void;
toggle(): void;
navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void;
accept(): TPromise<void>;
back(): TPromise<void>;
cancel(): TPromise<void>;
}

View File

@@ -37,12 +37,12 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration)
'http.proxy': {
type: 'string',
pattern: '^https?://([^:]*(:[^@]*)?@)?([^:]+)(:\\d+)?/?$|^$',
description: localize('proxy', "The proxy setting to use. If not set will be taken from the http_proxy and https_proxy environment variables")
description: localize('proxy', "The proxy setting to use. If not set will be taken from the http_proxy and https_proxy environment variables.")
},
'http.proxyStrictSSL': {
type: 'boolean',
default: true,
description: localize('strictSSL', "Whether the proxy server certificate should be verified against the list of supplied CAs.")
description: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs.")
},
'http.proxyAuthorization': {
type: ['null', 'string'],

View File

@@ -40,18 +40,21 @@ export class RequestService implements IRequestService {
this.authorization = config.http && config.http.proxyAuthorization;
}
async request(options: IRequestOptions, requestFn: IRequestFunction = request): TPromise<IRequestContext> {
request(options: IRequestOptions, requestFn: IRequestFunction = request): TPromise<IRequestContext> {
this.logService.trace('RequestService#request', options.url);
const { proxyUrl, strictSSL } = this;
const agentPromise = options.agent ? TPromise.wrap(options.agent) : TPromise.wrap(getProxyAgent(options.url, { proxyUrl, strictSSL }));
options.agent = options.agent || await getProxyAgent(options.url, { proxyUrl, strictSSL });
options.strictSSL = strictSSL;
return agentPromise.then(agent => {
options.agent = agent;
options.strictSSL = strictSSL;
if (this.authorization) {
options.headers = assign(options.headers || {}, { 'Proxy-Authorization': this.authorization });
}
if (this.authorization) {
options.headers = assign(options.headers || {}, { 'Proxy-Authorization': this.authorization });
}
return requestFn(options);
return requestFn(options);
});
}
}

View File

@@ -4,45 +4,71 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import uri from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
import * as glob from 'vs/base/common/glob';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as objects from 'vs/base/common/objects';
import * as paths from 'vs/base/common/paths';
import * as glob from 'vs/base/common/glob';
import uri, { UriComponents } from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { IFilesConfiguration } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle';
export const ID = 'searchService';
export const VIEW_ID = 'workbench.view.search';
export const ISearchService = createDecorator<ISearchService>(ID);
export const ISearchHistoryService = createDecorator<ISearchHistoryService>('searchHistoryService');
export const ISearchService = createDecorator<ISearchService>('searchService');
/**
* A service that enables to search for files or with in files.
*/
export interface ISearchService {
_serviceBrand: any;
search(query: ISearchQuery): PPromise<ISearchComplete, ISearchProgressItem>;
search(query: ISearchQuery, onProgress?: (result: ISearchProgressItem) => void): TPromise<ISearchComplete>;
extendQuery(query: ISearchQuery): void;
clearCache(cacheKey: string): TPromise<void>;
registerSearchResultProvider(provider: ISearchResultProvider): IDisposable;
registerSearchResultProvider(scheme: string, type: SearchProviderType, provider: ISearchResultProvider): IDisposable;
}
export interface ISearchHistoryValues {
search?: string[];
replace?: string[];
include?: string[];
exclude?: string[];
}
export interface ISearchHistoryService {
_serviceBrand: any;
onDidClearHistory: Event<void>;
clearHistory(): void;
load(): ISearchHistoryValues;
save(history: ISearchHistoryValues): void;
}
/**
* TODO@roblou - split text from file search entirely, or share code in a more natural way.
*/
export enum SearchProviderType {
file,
fileIndex,
text
}
export interface ISearchResultProvider {
search(query: ISearchQuery): PPromise<ISearchComplete, ISearchProgressItem>;
search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void): TPromise<ISearchComplete>;
clearCache(cacheKey: string): TPromise<void>;
}
export interface IFolderQuery {
folder: uri;
export interface IFolderQuery<U extends UriComponents=uri> {
folder: U;
excludePattern?: glob.IExpression;
includePattern?: glob.IExpression;
fileEncoding?: string;
disregardIgnoreFiles?: boolean;
}
export interface ICommonQueryOptions {
extraFileResources?: uri[];
export interface ICommonQueryOptions<U> {
extraFileResources?: U[];
filePattern?: string; // file search only
fileEncoding?: string;
maxResults?: number;
@@ -58,23 +84,27 @@ export interface ICommonQueryOptions {
disregardIgnoreFiles?: boolean;
disregardExcludeSettings?: boolean;
ignoreSymlinks?: boolean;
maxFileSize?: number;
}
export interface IQueryOptions extends ICommonQueryOptions {
export interface IQueryOptions extends ICommonQueryOptions<uri> {
excludePattern?: string;
includePattern?: string;
}
export interface ISearchQuery extends ICommonQueryOptions {
export interface ISearchQueryProps<U extends UriComponents> extends ICommonQueryOptions<U> {
type: QueryType;
excludePattern?: glob.IExpression;
includePattern?: glob.IExpression;
contentPattern?: IPatternInfo;
folderQueries?: IFolderQuery[];
folderQueries?: IFolderQuery<U>[];
usingSearchPaths?: boolean;
}
export type ISearchQuery = ISearchQueryProps<uri>;
export type IRawSearchQuery = ISearchQueryProps<UriComponents>;
export enum QueryType {
File = 1,
Text = 2
@@ -100,11 +130,13 @@ export interface IPatternInfo {
isSmartCase?: boolean;
}
export interface IFileMatch {
resource?: uri;
export interface IFileMatch<U extends UriComponents = uri> {
resource?: U;
lineMatches?: ILineMatch[];
}
export type IRawFileMatch2 = IFileMatch<UriComponents>;
export interface ILineMatch {
preview: string;
lineNumber: number;
@@ -121,10 +153,13 @@ export interface ISearchProgressItem extends IFileMatch, IProgress {
// Marker interface to indicate the possible values for progress calls from the engine
}
export interface ISearchComplete {
export interface ISearchCompleteStats {
limitHit?: boolean;
stats?: ISearchStats;
}
export interface ISearchComplete extends ISearchCompleteStats {
results: IFileMatch[];
stats: ISearchStats;
}
export interface ISearchStats {

View File

@@ -6,7 +6,7 @@
'use strict';
import * as path from 'path';
import * as fs from 'original-fs';
import * as fs from 'fs';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { writeFileAndFlushSync } from 'vs/base/node/extfs';
import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types';
@@ -25,7 +25,7 @@ export class FileStorage {
}
}
public getItem<T>(key: string, defaultValue?: T): T {
getItem<T>(key: string, defaultValue?: T): T {
this.ensureLoaded();
const res = this.database[key];
@@ -36,7 +36,7 @@ export class FileStorage {
return res;
}
public setItem(key: string, data: any): void {
setItem(key: string, data: any): void {
this.ensureLoaded();
// Remove an item when it is undefined or null
@@ -55,7 +55,7 @@ export class FileStorage {
this.saveSync();
}
public removeItem(key: string): void {
removeItem(key: string): void {
this.ensureLoaded();
// Only update if the key is actually present (not undefined)
@@ -96,15 +96,15 @@ export class StateService implements IStateService {
this.fileStorage = new FileStorage(path.join(environmentService.userDataPath, 'storage.json'), error => logService.error(error));
}
public getItem<T>(key: string, defaultValue?: T): T {
getItem<T>(key: string, defaultValue?: T): T {
return this.fileStorage.getItem(key, defaultValue);
}
public setItem(key: string, data: any): void {
setItem(key: string, data: any): void {
this.fileStorage.setItem(key, data);
}
public removeItem(key: string): void {
removeItem(key: string): void {
this.fileStorage.removeItem(key);
}
}

View File

@@ -21,13 +21,13 @@ export interface IStorage {
export class StorageService implements IStorageService {
public _serviceBrand: any;
_serviceBrand: any;
public static readonly COMMON_PREFIX = 'storage://';
public static readonly GLOBAL_PREFIX = `${StorageService.COMMON_PREFIX}global/`;
public static readonly WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/`;
public static readonly WORKSPACE_IDENTIFIER = 'workspaceidentifier';
public static readonly NO_WORKSPACE_IDENTIFIER = '__$noWorkspace__';
static readonly COMMON_PREFIX = 'storage://';
static readonly GLOBAL_PREFIX = `${StorageService.COMMON_PREFIX}global/`;
static readonly WORKSPACE_PREFIX = `${StorageService.COMMON_PREFIX}workspace/`;
static readonly WORKSPACE_IDENTIFIER = 'workspaceidentifier';
static readonly NO_WORKSPACE_IDENTIFIER = '__$noWorkspace__';
private _workspaceStorage: IStorage;
private _globalStorage: IStorage;
@@ -47,11 +47,11 @@ export class StorageService implements IStorageService {
this.setWorkspaceId(workspaceId, legacyWorkspaceId);
}
public get workspaceId(): string {
get workspaceId(): string {
return this._workspaceId;
}
public setWorkspaceId(workspaceId: string, legacyWorkspaceId?: number): void {
setWorkspaceId(workspaceId: string, legacyWorkspaceId?: number): void {
this._workspaceId = workspaceId;
// Calculate workspace storage key
@@ -64,11 +64,11 @@ export class StorageService implements IStorageService {
}
}
public get globalStorage(): IStorage {
get globalStorage(): IStorage {
return this._globalStorage;
}
public get workspaceStorage(): IStorage {
get workspaceStorage(): IStorage {
return this._workspaceStorage;
}
@@ -124,7 +124,7 @@ export class StorageService implements IStorageService {
}
}
public store(key: string, value: any, scope = StorageScope.GLOBAL): void {
store(key: string, value: any, scope = StorageScope.GLOBAL): void {
const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
if (types.isUndefinedOrNull(value)) {
@@ -142,7 +142,7 @@ export class StorageService implements IStorageService {
}
}
public get(key: string, scope = StorageScope.GLOBAL, defaultValue?: any): string {
get(key: string, scope = StorageScope.GLOBAL, defaultValue?: any): string {
const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
const value = storage.getItem(this.toStorageKey(key, scope));
@@ -153,7 +153,7 @@ export class StorageService implements IStorageService {
return value;
}
public getInteger(key: string, scope = StorageScope.GLOBAL, defaultValue?: number): number {
getInteger(key: string, scope = StorageScope.GLOBAL, defaultValue?: number): number {
const value = this.get(key, scope, defaultValue);
if (types.isUndefinedOrNull(value)) {
@@ -163,7 +163,7 @@ export class StorageService implements IStorageService {
return parseInt(value, 10);
}
public getBoolean(key: string, scope = StorageScope.GLOBAL, defaultValue?: boolean): boolean {
getBoolean(key: string, scope = StorageScope.GLOBAL, defaultValue?: boolean): boolean {
const value = this.get(key, scope, defaultValue);
if (types.isUndefinedOrNull(value)) {
@@ -177,7 +177,7 @@ export class StorageService implements IStorageService {
return value ? true : false;
}
public remove(key: string, scope = StorageScope.GLOBAL): void {
remove(key: string, scope = StorageScope.GLOBAL): void {
const storage = (scope === StorageScope.GLOBAL) ? this._globalStorage : this._workspaceStorage;
const storageKey = this.toStorageKey(key, scope);
@@ -201,11 +201,11 @@ export class InMemoryLocalStorage implements IStorage {
this.store = {};
}
public get length() {
get length() {
return Object.keys(this.store).length;
}
public key(index: number): string {
key(index: number): string {
const keys = Object.keys(this.store);
if (keys.length > index) {
return keys[index];
@@ -214,11 +214,11 @@ export class InMemoryLocalStorage implements IStorage {
return null;
}
public setItem(key: string, value: any): void {
setItem(key: string, value: any): void {
this.store[key] = value.toString();
}
public getItem(key: string): string {
getItem(key: string): string {
const item = this.store[key];
if (!types.isUndefinedOrNull(item)) {
return item;
@@ -227,7 +227,7 @@ export class InMemoryLocalStorage implements IStorage {
return null;
}
public removeItem(key: string): void {
removeItem(key: string): void {
delete this.store[key];
}
}

View File

@@ -1,92 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { deepClone } from 'vs/base/common/objects';
/* __GDPR__FRAGMENT__
"IExperiments" : {
}
*/
export interface IExperiments {
}
export const IExperimentService = createDecorator<IExperimentService>('experimentService');
export interface IExperimentService {
_serviceBrand: any;
getExperiments(): IExperiments;
}
export class ExperimentService implements IExperimentService {
_serviceBrand: any;
private experiments: IExperiments = {}; // Shortcut while there are no experiments.
constructor(
@IStorageService private storageService: IStorageService,
@IConfigurationService private configurationService: IConfigurationService,
) { }
getExperiments() {
if (!this.experiments) {
this.experiments = loadExperiments(this.storageService, this.configurationService);
}
return this.experiments;
}
}
function loadExperiments(storageService: IStorageService, configurationService: IConfigurationService): IExperiments {
const experiments = splitExperimentsRandomness(storageService);
return applyOverrides(experiments, configurationService);
}
function applyOverrides(experiments: IExperiments, configurationService: IConfigurationService): IExperiments {
const experimentsConfig = getExperimentsOverrides(configurationService);
Object.keys(experiments).forEach(key => {
if (key in experimentsConfig) {
experiments[key] = experimentsConfig[key];
}
});
return experiments;
}
function splitExperimentsRandomness(storageService: IStorageService): IExperiments {
const random1 = getExperimentsRandomness(storageService);
const [/* random2 */, /* ripgrepQuickSearch */] = splitRandom(random1);
// const [/* random3 */, /* deployToAzureQuickLink */] = splitRandom(random2);
// const [random4, /* mergeQuickLinks */] = splitRandom(random3);
// const [random5, /* enableWelcomePage */] = splitRandom(random4);
return {
// ripgrepQuickSearch,
};
}
function getExperimentsRandomness(storageService: IStorageService) {
const key = 'experiments.randomness';
let valueString = storageService.get(key);
if (!valueString) {
valueString = Math.random().toString();
storageService.store(key, valueString);
}
return parseFloat(valueString);
}
function splitRandom(random: number): [number, boolean] {
const scaled = random * 2;
const i = Math.floor(scaled);
return [scaled - i, i === 1];
}
function getExperimentsOverrides(configurationService: IConfigurationService): IExperiments {
return deepClone(configurationService.getValue<any>('experiments')) || {};
}

View File

@@ -8,6 +8,7 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import { Event } from 'vs/base/common/event';
export interface ITelemetryLog {
eventName: string;
@@ -23,6 +24,10 @@ export class TelemetryAppenderChannel implements ITelemetryAppenderChannel {
constructor(private appender: ITelemetryAppender) { }
listen<T>(event: string, arg?: any): Event<T> {
throw new Error('No events');
}
call(command: string, { eventName, data }: ITelemetryLog): TPromise<any> {
this.appender.log(eventName, data);
return TPromise.as(null);

View File

@@ -21,7 +21,6 @@ export interface ITelemetryServiceConfig {
appender: ITelemetryAppender;
commonProperties?: TPromise<{ [name: string]: any }>;
piiPaths?: string[];
userOptIn?: boolean;
}
export class TelemetryService implements ITelemetryService {
@@ -46,7 +45,7 @@ export class TelemetryService implements ITelemetryService {
this._appender = config.appender;
this._commonProperties = config.commonProperties || TPromise.as({});
this._piiPaths = config.piiPaths || [];
this._userOptIn = typeof config.userOptIn === 'undefined' ? true : config.userOptIn;
this._userOptIn = true;
// static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information`
this._cleanupPatterns = [/file:\/\/\/.*?\/resources\/app\//gi];
@@ -167,8 +166,9 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat
'properties': {
'telemetry.enableTelemetry': {
'type': 'boolean',
'description': localize('telemetry.enableTelemetry', "Enable usage data and errors to be sent to Microsoft."),
'default': true
'description': localize('telemetry.enableTelemetry', "Enable usage data and errors to be sent to a Microsoft online service."),
'default': true,
'tags': ['usesOnlineServices']
}
}
});

View File

@@ -12,6 +12,7 @@ import URI from 'vs/base/common/uri';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IKeybindingService, KeybindingSource } from 'vs/platform/keybinding/common/keybinding';
import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { ILogService } from 'vs/platform/log/common/log';
export const NullTelemetryService = new class implements ITelemetryService {
_serviceBrand: undefined;
@@ -30,13 +31,38 @@ export const NullTelemetryService = new class implements ITelemetryService {
export interface ITelemetryAppender {
log(eventName: string, data: any): void;
dispose(): TPromise<any>;
}
export function combinedAppender(...appenders: ITelemetryAppender[]): ITelemetryAppender {
return { log: (e, d) => appenders.forEach(a => a.log(e, d)) };
return {
log: (e, d) => appenders.forEach(a => a.log(e, d)),
dispose: () => TPromise.join(appenders.map(a => a.dispose()))
};
}
export const NullAppender: ITelemetryAppender = { log: () => null };
export const NullAppender: ITelemetryAppender = { log: () => null, dispose: () => TPromise.as(null) };
export class LogAppender implements ITelemetryAppender {
private commonPropertiesRegex = /^sessionID$|^version$|^timestamp$|^commitHash$|^common\./;
constructor(@ILogService private readonly _logService: ILogService) { }
dispose(): TPromise<any> {
return TPromise.as(undefined);
}
log(eventName: string, data: any): void {
const strippedData = {};
Object.keys(data).forEach(key => {
if (!this.commonPropertiesRegex.test(key)) {
strippedData[key] = data[key];
}
});
this._logService.trace(`telemetry/${eventName}`, strippedData);
}
}
/* __GDPR__FRAGMENT__
"URIDescriptor" : {
@@ -126,35 +152,39 @@ const configurationValueWhitelist = [
'editor.formatOnSave',
'editor.colorDecorators',
'window.zoomLevel',
'files.autoSave',
'files.hotExit',
'breadcrumbs.enabled',
'breadcrumbs.filePath',
'breadcrumbs.symbolPath',
'breadcrumbs.useQuickPick',
'explorer.openEditors.visible',
'extensions.autoUpdate',
'files.associations',
'workbench.statusBar.visible',
'files.autoGuessEncoding',
'files.autoSave',
'files.autoSaveDelay',
'files.encoding',
'files.eol',
'files.hotExit',
'files.trimTrailingWhitespace',
'git.confirmSync',
'workbench.sideBar.location',
'window.openFilesInNewWindow',
'javascript.validate.enable',
'window.restoreWindows',
'extensions.autoUpdate',
'files.eol',
'explorer.openEditors.visible',
'workbench.editor.enablePreview',
'files.autoSaveDelay',
'workbench.editor.showTabs',
'files.encoding',
'files.autoGuessEncoding',
'git.enabled',
'http.proxyStrictSSL',
'terminal.integrated.fontFamily',
'workbench.editor.enablePreviewFromQuickOpen',
'workbench.editor.swipeToNavigate',
'javascript.validate.enable',
'php.builtInCompletions.enable',
'php.validate.enable',
'php.validate.run',
'workbench.welcome.enabled',
'terminal.integrated.fontFamily',
'window.openFilesInNewWindow',
'window.restoreWindows',
'window.zoomLevel',
'workbench.editor.enablePreview',
'workbench.editor.enablePreviewFromQuickOpen',
'workbench.editor.showTabs',
'workbench.editor.swipeToNavigate',
'workbench.sideBar.location',
'workbench.startupEditor',
'workbench.statusBar.visible',
'workbench.welcome.enabled',
];
export function configurationTelemetry(telemetryService: ITelemetryService, configurationService: IConfigurationService): IDisposable {

View File

@@ -9,6 +9,7 @@ import { isObject } from 'vs/base/common/types';
import { safeStringify, mixin } from 'vs/base/common/objects';
import { TPromise } from 'vs/base/common/winjs.base';
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import { ILogService } from 'vs/platform/log/common/log';
let _initialized = false;
@@ -64,7 +65,8 @@ export class AppInsightsAppender implements ITelemetryAppender {
constructor(
private _eventPrefix: string,
private _defaultData: { [key: string]: any },
aiKeyOrClientFactory: string | (() => typeof appInsights.client) // allow factory function for testing
aiKeyOrClientFactory: string | (() => typeof appInsights.client), // allow factory function for testing
@ILogService private _logService?: ILogService
) {
if (!this._defaultData) {
this._defaultData = Object.create(null);
@@ -144,8 +146,12 @@ export class AppInsightsAppender implements ITelemetryAppender {
return;
}
data = mixin(data, this._defaultData);
let { properties, measurements } = AppInsightsAppender._getData(data);
this._aiClient.trackEvent(this._eventPrefix + '/' + eventName, properties, measurements);
data = AppInsightsAppender._getData(data);
if (this._logService) {
this._logService.trace(`telemetry/${eventName}`, data);
}
this._aiClient.trackEvent(this._eventPrefix + '/' + eventName, data.properties, data.measurements);
}
dispose(): TPromise<any> {

View File

@@ -6,6 +6,7 @@
import * as assert from 'assert';
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
import { ILogService, AbstractLogService, LogLevel, DEFAULT_LOG_LEVEL } from 'vs/platform/log/common/log';
interface IAppInsightsEvent {
eventName: string;
@@ -39,11 +40,61 @@ class AppInsightsMock {
}
}
class TestableLogService extends AbstractLogService implements ILogService {
_serviceBrand: any;
public logs: string[] = [];
constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
super();
this.setLevel(logLevel);
}
trace(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Trace) {
this.logs.push(message + JSON.stringify(args));
}
}
debug(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Debug) {
this.logs.push(message);
}
}
info(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Info) {
this.logs.push(message);
}
}
warn(message: string | Error, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Warning) {
this.logs.push(message.toString());
}
}
error(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Error) {
this.logs.push(message);
}
}
critical(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Critical) {
this.logs.push(message);
}
}
dispose(): void { }
}
suite('AIAdapter', () => {
var appInsightsMock: AppInsightsMock;
var adapter: AppInsightsAppender;
var prefix = 'prefix';
setup(() => {
appInsightsMock = new AppInsightsMock();
adapter = new AppInsightsAppender(prefix, undefined, () => appInsightsMock);
@@ -141,4 +192,27 @@ suite('AIAdapter', () => {
assert.equal(appInsightsMock.events[0].properties['nestedObj.nestedObj2.nestedObj3'], JSON.stringify({ 'testProperty': 'test' }));
assert.equal(appInsightsMock.events[0].measurements['nestedObj.testMeasurement'], 1);
});
test('Do not Log Telemetry if log level is not trace', () => {
const logService = new TestableLogService(LogLevel.Info);
adapter = new AppInsightsAppender(prefix, { 'common.platform': 'Windows' }, () => appInsightsMock, logService);
adapter.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 });
assert.equal(logService.logs.length, 0);
});
test('Log Telemetry if log level is trace', () => {
const logService = new TestableLogService(LogLevel.Trace);
adapter = new AppInsightsAppender(prefix, { 'common.platform': 'Windows' }, () => appInsightsMock, logService);
adapter.log('testEvent', { hello: 'world', isTrue: true, numberBetween1And3: 2 });
assert.equal(logService.logs.length, 1);
assert.equal(logService.logs[0], 'telemetry/testEvent' + JSON.stringify([{
properties: {
hello: 'world',
'common.platform': 'Windows'
},
measurements: {
isTrue: 1, numberBetween1And3: 2
}
}]));
});
});

View File

@@ -14,8 +14,6 @@ import * as Errors from 'vs/base/common/errors';
import * as sinon from 'sinon';
import { getConfigurationValue } from 'vs/platform/configuration/common/configuration';
const optInStatusEventName: string = 'optInStatus';
class TestTelemetryAppender implements ITelemetryAppender {
public events: any[];
@@ -34,8 +32,9 @@ class TestTelemetryAppender implements ITelemetryAppender {
return this.events.length;
}
public dispose() {
public dispose(): TPromise<any> {
this.isDisposed = true;
return TPromise.as(null);
}
}
@@ -718,29 +717,9 @@ suite('TelemetryService', () => {
}
}));
test('Telemetry Service respects user opt-in settings', sinon.test(function () {
test('Telemetry Service sends events when enableTelemetry is on', sinon.test(function () {
let testAppender = new TestTelemetryAppender();
let service = new TelemetryService({ userOptIn: false, appender: testAppender }, undefined);
return service.publicLog('testEvent').then(() => {
assert.equal(testAppender.getEventsCount(), 0);
service.dispose();
});
}));
test('Telemetry Service does not sent optInStatus when user opted out', sinon.test(function () {
let testAppender = new TestTelemetryAppender();
let service = new TelemetryService({ userOptIn: false, appender: testAppender }, undefined);
return service.publicLog(optInStatusEventName, { optIn: false }).then(() => {
assert.equal(testAppender.getEventsCount(), 0);
service.dispose();
});
}));
test('Telemetry Service sends events when enableTelemetry is on even user optin is on', sinon.test(function () {
let testAppender = new TestTelemetryAppender();
let service = new TelemetryService({ userOptIn: true, appender: testAppender }, undefined);
let service = new TelemetryService({ appender: testAppender }, undefined);
return service.publicLog('testEvent').then(() => {
assert.equal(testAppender.getEventsCount(), 1);

View File

@@ -20,6 +20,7 @@ export interface ColorContribution {
readonly description: string;
readonly defaults: ColorDefaults;
readonly needsTransparency: boolean;
readonly deprecationMessage: string;
}
@@ -87,7 +88,7 @@ class ColorRegistry implements IColorRegistry {
}
public registerColor(id: string, defaults: ColorDefaults, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier {
let colorContribution = { id, description, defaults, needsTransparency };
let colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage };
this.colorsById[id] = colorContribution;
let propertySchema: IJSONSchema = { type: 'string', description, format: 'color-hex', default: '#ff0000' };
if (deprecationMessage) {
@@ -148,9 +149,9 @@ export function getColorRegistry(): IColorRegistry {
// ----- base colors
export const foreground = registerColor('foreground', { dark: '#CCCCCC', light: '#6C6C6C', hc: '#FFFFFF' }, nls.localize('foreground', "Overall foreground color. This color is only used if not overridden by a component."));
export const foreground = registerColor('foreground', { dark: '#CCCCCC', light: '#616161', hc: '#FFFFFF' }, nls.localize('foreground', "Overall foreground color. This color is only used if not overridden by a component."));
export const errorForeground = registerColor('errorForeground', { dark: '#F48771', light: '#A1260D', hc: '#F48771' }, nls.localize('errorForeground', "Overall foreground color for error messages. This color is only used if not overridden by a component."));
export const descriptionForeground = registerColor('descriptionForeground', { light: transparent(foreground, 0.7), dark: transparent(foreground, 0.7), hc: transparent(foreground, 0.7) }, nls.localize('descriptionForeground', "Foreground color for description text providing additional information, for example for a label."));
export const descriptionForeground = registerColor('descriptionForeground', { light: '#717171', dark: transparent(foreground, 0.7), hc: transparent(foreground, 0.7) }, nls.localize('descriptionForeground', "Foreground color for description text providing additional information, for example for a label."));
export const focusBorder = registerColor('focusBorder', { dark: Color.fromHex('#0E639C').transparent(0.6), light: Color.fromHex('#007ACC').transparent(0.4), hc: '#F38518' }, nls.localize('focusBorder', "Overall border color for focused elements. This color is only used if not overridden by a component."));
@@ -164,8 +165,8 @@ export const selectionBackground = registerColor('selection.background', { light
// ------ text colors
export const textSeparatorForeground = registerColor('textSeparator.foreground', { light: '#0000002e', dark: '#ffffff2e', hc: Color.black }, nls.localize('textSeparatorForeground', "Color for text separators."));
export const textLinkForeground = registerColor('textLink.foreground', { light: '#4080D0', dark: '#4080D0', hc: '#4080D0' }, nls.localize('textLinkForeground', "Foreground color for links in text."));
export const textLinkActiveForeground = registerColor('textLink.activeForeground', { light: '#4080D0', dark: '#4080D0', hc: '#4080D0' }, nls.localize('textLinkActiveForeground', "Foreground color for active links in text."));
export const textLinkForeground = registerColor('textLink.foreground', { light: '#006AB1', dark: '#3794FF', hc: '006AB1' }, nls.localize('textLinkForeground', "Foreground color for links in text."));
export const textLinkActiveForeground = registerColor('textLink.activeForeground', { light: '#007ACC', dark: '#3794FF', hc: '#007ACC' }, nls.localize('textLinkActiveForeground', "Foreground color for links in text when clicked on and on mouse hover."));
export const textPreformatForeground = registerColor('textPreformat.foreground', { light: '#A31515', dark: '#D7BA7D', hc: '#D7BA7D' }, nls.localize('textPreformatForeground', "Foreground color for preformatted text segments."));
export const textBlockQuoteBackground = registerColor('textBlockQuote.background', { light: '#7f7f7f1a', dark: '#7f7f7f1a', hc: null }, nls.localize('textBlockQuoteBackground', "Background color for block quotes in text."));
export const textBlockQuoteBorder = registerColor('textBlockQuote.border', { light: '#007acc80', dark: '#007acc80', hc: Color.white }, nls.localize('textBlockQuoteBorder', "Border color for block quotes in text."));
@@ -178,7 +179,7 @@ export const inputBackground = registerColor('input.background', { dark: '#3C3C3
export const inputForeground = registerColor('input.foreground', { dark: foreground, light: foreground, hc: foreground }, nls.localize('inputBoxForeground', "Input box foreground."));
export const inputBorder = registerColor('input.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('inputBoxBorder', "Input box border."));
export const inputActiveOptionBorder = registerColor('inputOption.activeBorder', { dark: '#007ACC', light: '#007ACC', hc: activeContrastBorder }, nls.localize('inputBoxActiveOptionBorder', "Border color of activated options in input fields."));
export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { dark: null, light: null, hc: null }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text."));
export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { light: transparent(foreground, 0.5), dark: transparent(foreground, 0.5), hc: transparent(foreground, 0.7) }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text."));
export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', { dark: '#063B49', light: '#D6ECF2', hc: Color.black }, nls.localize('inputValidationInfoBackground', "Input validation background color for information severity."));
export const inputValidationInfoBorder = registerColor('inputValidation.infoBorder', { dark: '#007acc', light: '#007acc', hc: contrastBorder }, nls.localize('inputValidationInfoBorder', "Input validation border color for information severity."));
@@ -192,36 +193,43 @@ export const selectListBackground = registerColor('dropdown.listBackground', { d
export const selectForeground = registerColor('dropdown.foreground', { dark: '#F0F0F0', light: null, hc: Color.white }, nls.localize('dropdownForeground', "Dropdown foreground."));
export const selectBorder = registerColor('dropdown.border', { dark: selectBackground, light: '#CECECE', hc: contrastBorder }, nls.localize('dropdownBorder', "Dropdown border."));
export const listFocusBackground = registerColor('list.focusBackground', { dark: '#073655', light: '#DCEBFC', hc: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listFocusBackground = registerColor('list.focusBackground', { dark: '#062F4A', light: '#DFF0FF', hc: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listFocusForeground = registerColor('list.focusForeground', { dark: null, light: null, hc: null }, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#3399FF', hc: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#2477CE', hc: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', { dark: Color.white, light: Color.white, hc: null }, nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#3F3F46', light: '#CCCEDB', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not."));
export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#dddfea', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not."));
export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hc: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not."));
export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: '#313135', light: '#d8dae6', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not."));
export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: '#313135', light: '#d8dae6', hc: null }, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not."));
export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hc: null }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse."));
export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hc: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse."));
export const listDropBackground = registerColor('list.dropBackground', { dark: listFocusBackground, light: listFocusBackground, hc: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items around using the mouse."));
export const listHighlightForeground = registerColor('list.highlightForeground', { dark: '#0097fb', light: '#007acc', hc: focusBorder }, nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.'));
export const listInvalidItemForeground = registerColor('list.invalidItemForeground', { dark: '#B89500', light: '#B89500', hc: '#B89500' }, nls.localize('invalidItemForeground', 'List/Tree foreground color for invalid items, for example an unresolved root in explorer.'));
export const listErrorForeground = registerColor('list.errorForeground', { dark: '#F88070', light: '#B01011', hc: null }, nls.localize('listErrorForeground', 'Foreground color of list items containing errors.'));
export const listWarningForeground = registerColor('list.warningForeground', { dark: '#4d9e4d', light: '#117711', hc: null }, nls.localize('listWarningForeground', 'Foreground color of list items containing warnings.'));
export const pickerGroupForeground = registerColor('pickerGroup.foreground', { dark: Color.fromHex('#0097FB').transparent(0.6), light: Color.fromHex('#007ACC').transparent(0.6), hc: Color.white }, nls.localize('pickerGroupForeground', "Quick picker color for grouping labels."));
export const pickerGroupForeground = registerColor('pickerGroup.foreground', { dark: '#3794FF', light: '#006AB1', hc: Color.white }, nls.localize('pickerGroupForeground', "Quick picker color for grouping labels."));
export const pickerGroupBorder = registerColor('pickerGroup.border', { dark: '#3F3F46', light: '#CCCEDB', hc: Color.white }, nls.localize('pickerGroupBorder', "Quick picker color for grouping borders."));
export const buttonForeground = registerColor('button.foreground', { dark: Color.white, light: Color.white, hc: Color.white }, nls.localize('buttonForeground', "Button foreground color."));
export const buttonBackground = registerColor('button.background', { dark: '#0E639C', light: '#007ACC', hc: null }, nls.localize('buttonBackground', "Button background color."));
export const buttonHoverBackground = registerColor('button.hoverBackground', { dark: lighten(buttonBackground, 0.2), light: darken(buttonBackground, 0.2), hc: null }, nls.localize('buttonHoverBackground', "Button background color when hovering."));
export const badgeBackground = registerColor('badge.background', { dark: '#4D4D4D', light: '#BEBEBE', hc: Color.black }, nls.localize('badgeBackground', "Badge background color. Badges are small information labels, e.g. for search results count."));
export const badgeForeground = registerColor('badge.foreground', { dark: Color.white, light: Color.white, hc: Color.white }, nls.localize('badgeForeground', "Badge foreground color. Badges are small information labels, e.g. for search results count."));
export const badgeBackground = registerColor('badge.background', { dark: '#4D4D4D', light: '#C4C4C4', hc: Color.black }, nls.localize('badgeBackground', "Badge background color. Badges are small information labels, e.g. for search results count."));
export const badgeForeground = registerColor('badge.foreground', { dark: Color.white, light: '#333', hc: Color.white }, nls.localize('badgeForeground', "Badge foreground color. Badges are small information labels, e.g. for search results count."));
export const scrollbarShadow = registerColor('scrollbar.shadow', { dark: '#000000', light: '#DDDDDD', hc: null }, nls.localize('scrollbarShadow', "Scrollbar shadow to indicate that the view is scrolled."));
export const scrollbarSliderBackground = registerColor('scrollbarSlider.background', { dark: Color.fromHex('#797979').transparent(0.4), light: Color.fromHex('#646464').transparent(0.4), hc: transparent(contrastBorder, 0.6) }, nls.localize('scrollbarSliderBackground', "Scrollbar slider background color."));
export const scrollbarSliderHoverBackground = registerColor('scrollbarSlider.hoverBackground', { dark: Color.fromHex('#646464').transparent(0.7), light: Color.fromHex('#646464').transparent(0.7), hc: transparent(contrastBorder, 0.8) }, nls.localize('scrollbarSliderHoverBackground', "Scrollbar slider background color when hovering."));
export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.activeBackground', { dark: Color.fromHex('#BFBFBF').transparent(0.4), light: Color.fromHex('#000000').transparent(0.6), hc: contrastBorder }, nls.localize('scrollbarSliderActiveBackground', "Scrollbar slider background color when active."));
export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.activeBackground', { dark: Color.fromHex('#BFBFBF').transparent(0.4), light: Color.fromHex('#000000').transparent(0.6), hc: contrastBorder }, nls.localize('scrollbarSliderActiveBackground', "Scrollbar slider background color when clicked on."));
export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hc: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations."));
export const breadcrumbsForeground = registerColor('breadcrumb.foreground', { light: Color.fromHex('#6C6C6C').transparent(.7), dark: Color.fromHex('#CCCCCC').transparent(.7), hc: Color.white.transparent(.7) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items."));
export const breadcrumbsFocusForeground = registerColor('breadcrumb.focusForeground', { light: '#6C6C6C', dark: '#CCCCCC', hc: Color.white }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items."));
export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.activeSelectionForeground', { light: '#6C6C6C', dark: '#CCCCCC', hc: Color.white }, nls.localize('breadcrumbsSelectedForegound', "Color of selected breadcrumb items."));
export const breadcrumbsPickerBackground = registerColor('breadcrumbPicker.background', { light: '#ECECEC', dark: '#252526', hc: Color.black }, nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker."));
/**
* Editor background color.
* Because of bug https://monacotools.visualstudio.com/DefaultCollection/Monaco/_workitems/edit/13254
@@ -240,6 +248,7 @@ export const editorForeground = registerColor('editor.foreground', { light: '#33
export const editorWidgetBackground = registerColor('editorWidget.background', { dark: '#2D2D30', light: '#EFEFF2', hc: '#0C141F' }, nls.localize('editorWidgetBackground', 'Background color of editor widgets, such as find/replace.'));
export const editorWidgetBorder = registerColor('editorWidget.border', { dark: '#454545', light: '#C8C8C8', hc: contrastBorder }, nls.localize('editorWidgetBorder', 'Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.'));
export const editorWidgetResizeBorder = registerColor('editorWidget.resizeBorder', { light: null, dark: null, hc: null }, nls.localize('editorWidgetResizeBorder', "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget."));
/**
* Editor selection colors.
@@ -285,6 +294,8 @@ export const diffRemoved = registerColor('diffEditor.removedTextBackground', { d
export const diffInsertedOutline = registerColor('diffEditor.insertedTextBorder', { dark: null, light: null, hc: '#33ff2eff' }, nls.localize('diffEditorInsertedOutline', 'Outline color for the text that got inserted.'));
export const diffRemovedOutline = registerColor('diffEditor.removedTextBorder', { dark: null, light: null, hc: '#FF008F' }, nls.localize('diffEditorRemovedOutline', 'Outline color for text that got removed.'));
export const diffBorder = registerColor('diffEditor.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('diffEditorBorder', 'Border color between the two text editors.'));
/**
* Merge-conflict colors
*/

View File

@@ -6,7 +6,7 @@
'use strict';
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, lighten, badgeBackground, badgeForeground, progressBarBackground } from 'vs/platform/theme/common/colorRegistry';
import { focusBorder, inputBackground, inputForeground, ColorIdentifier, selectForeground, selectBackground, selectListBackground, selectBorder, inputBorder, foreground, editorBackground, contrastBorder, inputActiveOptionBorder, listFocusBackground, listFocusForeground, listActiveSelectionBackground, listActiveSelectionForeground, listInactiveSelectionForeground, listInactiveSelectionBackground, listInactiveFocusBackground, listHoverBackground, listHoverForeground, listDropBackground, pickerGroupBorder, pickerGroupForeground, widgetShadow, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationErrorBorder, inputValidationErrorBackground, activeContrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, ColorFunction, badgeBackground, badgeForeground, progressBarBackground, breadcrumbsForeground, breadcrumbsFocusForeground, breadcrumbsActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
@@ -177,7 +177,7 @@ export function attachQuickOpenStyler(widget: IThemable, themeService: IThemeSer
inputValidationErrorBackground: (style && style.inputValidationErrorBackground) || inputValidationErrorBackground,
listFocusBackground: (style && style.listFocusBackground) || listFocusBackground,
listFocusForeground: (style && style.listFocusForeground) || listFocusForeground,
listActiveSelectionBackground: (style && style.listActiveSelectionBackground) || lighten(listActiveSelectionBackground, 0.1),
listActiveSelectionBackground: (style && style.listActiveSelectionBackground) || listActiveSelectionBackground,
listActiveSelectionForeground: (style && style.listActiveSelectionForeground) || listActiveSelectionForeground,
listFocusAndSelectionBackground: style && style.listFocusAndSelectionBackground || listActiveSelectionBackground,
listFocusAndSelectionForeground: (style && style.listFocusAndSelectionForeground) || listActiveSelectionForeground,
@@ -219,7 +219,7 @@ export function attachListStyler(widget: IThemable, themeService: IThemeService,
export const defaultListStyles: IColorMapping = {
listFocusBackground: listFocusBackground,
listFocusForeground: listFocusForeground,
listActiveSelectionBackground: lighten(listActiveSelectionBackground, 0.1),
listActiveSelectionBackground: listActiveSelectionBackground,
listActiveSelectionForeground: listActiveSelectionForeground,
listFocusAndSelectionBackground: listActiveSelectionBackground,
listFocusAndSelectionForeground: listActiveSelectionForeground,
@@ -261,4 +261,24 @@ export function attachProgressBarStyler(widget: IThemable, themeService: IThemeS
export function attachStylerCallback(themeService: IThemeService, colors: { [name: string]: ColorIdentifier }, callback: styleFn): IDisposable {
return attachStyler(themeService, colors, callback);
}
}
export interface IBreadcrumbsWidgetStyleOverrides extends IStyleOverrides {
breadcrumbsBackground?: ColorIdentifier;
breadcrumbsForeground?: ColorIdentifier;
breadcrumbsHoverForeground?: ColorIdentifier;
breadcrumbsFocusForeground?: ColorIdentifier;
breadcrumbsFocusAndSelectionForeground?: ColorIdentifier;
}
export const defaultBreadcrumbsStyles = <IBreadcrumbsWidgetStyleOverrides>{
breadcrumbsBackground: editorBackground,
breadcrumbsForeground: breadcrumbsForeground,
breadcrumbsHoverForeground: breadcrumbsFocusForeground,
breadcrumbsFocusForeground: breadcrumbsFocusForeground,
breadcrumbsFocusAndSelectionForeground: breadcrumbsActiveSelectionForeground,
};
export function attachBreadcrumbsStyler(widget: IThemable, themeService: IThemeService, style?: IBreadcrumbsWidgetStyleOverrides): IDisposable {
return attachStyler(themeService, { ...defaultBreadcrumbsStyles, ...style }, widget);
}

View File

@@ -6,10 +6,11 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Color } from 'vs/base/common/color';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import * as platform from 'vs/platform/registry/common/platform';
import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';
import { Event, Emitter } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
export const IThemeService = createDecorator<IThemeService>('themeService');
@@ -66,7 +67,7 @@ export interface ICssStyleCollector {
}
export interface IThemingParticipant {
(theme: ITheme, collector: ICssStyleCollector): void;
(theme: ITheme, collector: ICssStyleCollector, environment: IEnvironmentService): void;
}
export interface IThemeService {
@@ -110,12 +111,10 @@ class ThemingRegistry implements IThemingRegistry {
public onThemeChange(participant: IThemingParticipant): IDisposable {
this.themingParticipants.push(participant);
this.onThemingParticipantAddedEmitter.fire(participant);
return {
dispose: () => {
const idx = this.themingParticipants.indexOf(participant);
this.themingParticipants.splice(idx, 1);
}
};
return toDisposable(() => {
const idx = this.themingParticipants.indexOf(participant);
this.themingParticipants.splice(idx, 1);
});
}
public get onThemingParticipantAdded(): Event<IThemingParticipant> {

View File

@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Registry } from 'vs/platform/registry/common/platform';
import { IColorRegistry, Extensions, ColorContribution } from 'vs/platform/theme/common/colorRegistry';
import { editorMarkerNavigationError } from 'vs/editor/contrib/gotoError/gotoErrorWidget';
import { overviewRulerModifiedForeground } from 'vs/workbench/parts/scm/electron-browser/dirtydiffDecorator';
import { STATUS_BAR_DEBUGGING_BACKGROUND } from 'vs/workbench/parts/debug/browser/statusbarColorProvider';
import { debugExceptionWidgetBackground } from 'vs/workbench/parts/debug/browser/exceptionWidget';
import { debugToolBarBackground } from 'vs/workbench/parts/debug/browser/debugActionsWidget';
import { buttonBackground } from 'vs/workbench/parts/welcome/page/electron-browser/welcomePage';
import { embeddedEditorBackground } from 'vs/workbench/parts/welcome/walkThrough/electron-browser/walkThroughPart';
import { request, asText } from 'vs/base/node/request';
import * as pfs from 'vs/base/node/pfs';
import * as path from 'path';
import * as assert from 'assert';
interface ColorInfo {
description: string;
offset: number;
length: number;
}
interface DescriptionDiff {
docDescription: string;
specDescription: string;
}
// add artificial dependencies to some files that are not loaded yet
export const forceColorLoad = [editorMarkerNavigationError, overviewRulerModifiedForeground, STATUS_BAR_DEBUGGING_BACKGROUND,
debugExceptionWidgetBackground, debugToolBarBackground, buttonBackground, embeddedEditorBackground];
export const experimental = []; // 'settings.modifiedItemForeground', 'editorUnnecessary.foreground' ];
suite('Color Registry', function () {
test('all colors documented', async function () {
const reqContext = await request({ url: 'https://raw.githubusercontent.com/Microsoft/vscode-docs/vnext/docs/getstarted/theme-color-reference.md' });
const content = await asText(reqContext);
const expression = /\-\s*\`([\w\.]+)\`: (.*)/g;
let m: RegExpExecArray;
let colorsInDoc: { [id: string]: ColorInfo } = Object.create(null);
while (m = expression.exec(content)) {
colorsInDoc[m[1]] = { description: m[2], offset: m.index, length: m.length };
}
let missing = Object.create(null);
let descriptionDiffs: { [id: string]: DescriptionDiff } = Object.create(null);
let themingRegistry = Registry.as<IColorRegistry>(Extensions.ColorContribution);
for (let color of themingRegistry.getColors()) {
if (!colorsInDoc[color.id]) {
if (!color.deprecationMessage) {
missing[color.id] = getDescription(color);
}
} else {
let docDescription = colorsInDoc[color.id].description;
let specDescription = getDescription(color);
if (docDescription !== specDescription) {
descriptionDiffs[color.id] = { docDescription, specDescription };
}
delete colorsInDoc[color.id];
}
}
let colorsInExtensions = await getColorsFromExtension();
for (let colorId in colorsInExtensions) {
if (!colorsInDoc[colorId]) {
missing[colorId] = colorsInExtensions[colorId];
} else {
delete colorsInDoc[colorId];
}
}
for (let colorId of experimental) {
if (missing[colorId]) {
delete missing[colorId];
}
if (colorsInDoc[colorId]) {
assert.fail(`Color ${colorId} found in doc but marked experimental. Please remove from experimental list.`);
}
}
let undocumentedKeys = Object.keys(missing).map(k => `${k}: ${missing[k]}`);
assert.deepEqual(undocumentedKeys, [], 'Undocumented colors ids');
let superfluousKeys = Object.keys(colorsInDoc);
assert.deepEqual(superfluousKeys, [], 'Colors ids in doc that do not exist');
});
});
function getDescription(color: ColorContribution) {
let specDescription = color.description;
if (color.deprecationMessage) {
specDescription = specDescription + ' ' + color.deprecationMessage;
}
return specDescription;
}
async function getColorsFromExtension(): Promise<{ [id: string]: string }> {
let extPath = require.toUrl('../../../../../../extensions');
let extFolders = await pfs.readDirsInDir(extPath);
let result: { [id: string]: string } = Object.create(null);
for (let folder of extFolders) {
try {
let packageJSON = JSON.parse((await pfs.readFile(path.join(extPath, folder, 'package.json'))).toString());
let contributes = packageJSON['contributes'];
if (contributes) {
let colors = contributes['colors'];
if (colors) {
for (let color of colors) {
let colorId = color['id'];
if (colorId) {
result[colorId] = colorId['description'];
}
}
}
}
} catch (e) {
// ignore
}
}
return result;
}

View File

@@ -48,8 +48,13 @@ export enum StateType {
Ready = 'ready',
}
export enum UpdateType {
Setup,
Archive
}
export type Uninitialized = { type: StateType.Uninitialized };
export type Idle = { type: StateType.Idle };
export type Idle = { type: StateType.Idle, updateType: UpdateType };
export type CheckingForUpdates = { type: StateType.CheckingForUpdates, context: any };
export type AvailableForDownload = { type: StateType.AvailableForDownload, update: IUpdate };
export type Downloading = { type: StateType.Downloading, update: IUpdate };
@@ -61,7 +66,7 @@ export type State = Uninitialized | Idle | CheckingForUpdates | AvailableForDown
export const State = {
Uninitialized: { type: StateType.Uninitialized } as Uninitialized,
Idle: { type: StateType.Idle } as Idle,
Idle: (updateType: UpdateType) => ({ type: StateType.Idle, updateType }) as Idle,
CheckingForUpdates: (context: any) => ({ type: StateType.CheckingForUpdates, context } as CheckingForUpdates),
AvailableForDownload: (update: IUpdate) => ({ type: StateType.AvailableForDownload, update } as AvailableForDownload),
Downloading: (update: IUpdate) => ({ type: StateType.Downloading, update } as Downloading),
@@ -89,4 +94,6 @@ export interface IUpdateService {
downloadUpdate(): TPromise<void>;
applyUpdate(): TPromise<void>;
quitAndInstall(): TPromise<void>;
}
isLatestVersion(): TPromise<boolean | undefined>;
}

View File

@@ -6,17 +6,21 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event, Emitter } from 'vs/base/common/event';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IUpdateService, State } from './update';
export interface IUpdateChannel extends IChannel {
listen(event: 'onStateChange'): Event<State>;
listen<T>(command: string, arg?: any): Event<T>;
call(command: 'checkForUpdates', arg: any): TPromise<void>;
call(command: 'downloadUpdate'): TPromise<void>;
call(command: 'applyUpdate'): TPromise<void>;
call(command: 'quitAndInstall'): TPromise<void>;
call(command: '_getInitialState'): TPromise<State>;
call(command: 'isLatestVersion'): TPromise<boolean>;
call(command: string, arg?: any): TPromise<any>;
}
@@ -24,14 +28,22 @@ export class UpdateChannel implements IUpdateChannel {
constructor(private service: IUpdateService) { }
listen<T>(event: string, arg?: any): Event<any> {
switch (event) {
case 'onStateChange': return this.service.onStateChange;
}
throw new Error('No event found');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'event:onStateChange': return eventToCall(this.service.onStateChange);
case 'checkForUpdates': return this.service.checkForUpdates(arg);
case 'downloadUpdate': return this.service.downloadUpdate();
case 'applyUpdate': return this.service.applyUpdate();
case 'quitAndInstall': return this.service.quitAndInstall();
case '_getInitialState': return TPromise.as(this.service.state);
case 'isLatestVersion': return this.service.isLatestVersion();
}
return undefined;
}
@@ -41,8 +53,6 @@ export class UpdateChannelClient implements IUpdateService {
_serviceBrand: any;
private _onRemoteStateChange = eventFromCall<State>(this.channel, 'event:onStateChange');
private _onStateChange = new Emitter<State>();
get onStateChange(): Event<State> { return this._onStateChange.event; }
@@ -58,7 +68,8 @@ export class UpdateChannelClient implements IUpdateService {
this._onStateChange.fire(state);
// fire subsequent states as they come in from remote
this._onRemoteStateChange(state => this._onStateChange.fire(state));
this.channel.listen('onStateChange')(state => this._onStateChange.fire(state));
}, onUnexpectedError);
}
@@ -77,4 +88,8 @@ export class UpdateChannelClient implements IUpdateService {
quitAndInstall(): TPromise<void> {
return this.channel.call('quitAndInstall');
}
}
isLatestVersion(): TPromise<boolean> {
return this.channel.call('isLatestVersion');
}
}

View File

@@ -11,9 +11,10 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import product from 'vs/platform/node/product';
import { TPromise } from 'vs/base/common/winjs.base';
import { IUpdateService, State, StateType, AvailableForDownload } from 'vs/platform/update/common/update';
import { IUpdateService, State, StateType, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ILogService } from 'vs/platform/log/common/log';
import { IRequestService } from 'vs/platform/request/node/request';
export function createUpdateURL(platform: string, quality: string): string {
return `${product.updateUrl}/api/update/${platform}/${quality}/${product.commit}`;
@@ -23,6 +24,8 @@ export abstract class AbstractUpdateService implements IUpdateService {
_serviceBrand: any;
protected readonly url: string | undefined;
private _state: State = State.Uninitialized;
private throttler: Throttler = new Throttler();
@@ -43,7 +46,8 @@ export abstract class AbstractUpdateService implements IUpdateService {
@ILifecycleService private lifecycleService: ILifecycleService,
@IConfigurationService protected configurationService: IConfigurationService,
@IEnvironmentService private environmentService: IEnvironmentService,
@ILogService protected logService: ILogService
@IRequestService protected requestService: IRequestService,
@ILogService protected logService: ILogService,
) {
if (this.environmentService.disableUpdates) {
this.logService.info('update#ctor - updates are disabled');
@@ -62,12 +66,13 @@ export abstract class AbstractUpdateService implements IUpdateService {
return;
}
if (!this.setUpdateFeedUrl(quality)) {
this.url = this.buildUpdateFeedUrl(quality);
if (!this.url) {
this.logService.info('update#ctor - updates are disabled');
return;
}
this.setState({ type: StateType.Idle });
this.setState(State.Idle(this.getUpdateType()));
// Start checking for updates after 30 seconds
this.scheduleCheckForUpdates(30 * 1000)
@@ -153,10 +158,29 @@ export abstract class AbstractUpdateService implements IUpdateService {
return TPromise.as(null);
}
isLatestVersion(): TPromise<boolean | undefined> {
if (!this.url) {
return TPromise.as(undefined);
}
return this.requestService.request({ url: this.url }).then(context => {
// The update server replies with 204 (No Content) when no
// update is available - that's all we want to know.
if (context.res.statusCode === 204) {
return true;
} else {
return false;
}
});
}
protected getUpdateType(): UpdateType {
return UpdateType.Archive;
}
protected doQuitAndInstall(): void {
// noop
}
protected abstract setUpdateFeedUrl(quality: string): boolean;
protected abstract buildUpdateFeedUrl(quality: string): string | undefined;
protected abstract doCheckForUpdates(context: any): void;
}

View File

@@ -11,11 +11,12 @@ import { Event, fromNodeEventEmitter } from 'vs/base/common/event';
import { memoize } from 'vs/base/common/decorators';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { State, IUpdate, StateType } from 'vs/platform/update/common/update';
import { State, IUpdate, StateType, UpdateType } from 'vs/platform/update/common/update';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ILogService } from 'vs/platform/log/common/log';
import { AbstractUpdateService, createUpdateURL } from 'vs/platform/update/electron-main/abstractUpdateService';
import { IRequestService } from 'vs/platform/request/node/request';
export class DarwinUpdateService extends AbstractUpdateService {
@@ -33,9 +34,10 @@ export class DarwinUpdateService extends AbstractUpdateService {
@IConfigurationService configurationService: IConfigurationService,
@ITelemetryService private telemetryService: ITelemetryService,
@IEnvironmentService environmentService: IEnvironmentService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService
) {
super(lifecycleService, configurationService, environmentService, logService);
super(lifecycleService, configurationService, environmentService, requestService, logService);
this.onRawError(this.onError, this, this.disposables);
this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables);
this.onRawUpdateDownloaded(this.onUpdateDownloaded, this, this.disposables);
@@ -44,19 +46,19 @@ export class DarwinUpdateService extends AbstractUpdateService {
private onError(err: string): void {
this.logService.error('UpdateService error: ', err);
this.setState(State.Idle);
this.setState(State.Idle(UpdateType.Archive));
}
protected setUpdateFeedUrl(quality: string): boolean {
protected buildUpdateFeedUrl(quality: string): string | undefined {
const url = createUpdateURL('darwin', quality);
try {
electron.autoUpdater.setFeedURL(createUpdateURL('darwin', quality));
electron.autoUpdater.setFeedURL({ url });
} catch (e) {
// application is very likely not signed
this.logService.error('Failed to set update feed URL');
return false;
this.logService.error('Failed to set update feed URL', e);
return undefined;
}
return true;
return url;
}
protected doCheckForUpdates(context: any): void {
@@ -99,7 +101,7 @@ export class DarwinUpdateService extends AbstractUpdateService {
*/
this.telemetryService.publicLog('update:notAvailable', { explicit: !!this.state.context });
this.setState(State.Idle);
this.setState(State.Idle(UpdateType.Archive));
}
protected doQuitAndInstall(): void {

View File

@@ -9,7 +9,7 @@ import product from 'vs/platform/node/product';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { IRequestService } from 'vs/platform/request/node/request';
import { State, IUpdate, AvailableForDownload } from 'vs/platform/update/common/update';
import { State, IUpdate, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ILogService } from 'vs/platform/log/common/log';
@@ -22,22 +22,19 @@ export class LinuxUpdateService extends AbstractUpdateService {
_serviceBrand: any;
private url: string | undefined;
constructor(
@ILifecycleService lifecycleService: ILifecycleService,
@IConfigurationService configurationService: IConfigurationService,
@ITelemetryService private telemetryService: ITelemetryService,
@IEnvironmentService environmentService: IEnvironmentService,
@IRequestService private requestService: IRequestService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService
) {
super(lifecycleService, configurationService, environmentService, logService);
super(lifecycleService, configurationService, environmentService, requestService, logService);
}
protected setUpdateFeedUrl(quality: string): boolean {
this.url = createUpdateURL(`linux-${process.arch}`, quality);
return true;
protected buildUpdateFeedUrl(quality: string): string {
return createUpdateURL(`linux-${process.arch}`, quality);
}
protected doCheckForUpdates(context: any): void {
@@ -58,7 +55,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
*/
this.telemetryService.publicLog('update:notAvailable', { explicit: !!context });
this.setState(State.Idle);
this.setState(State.Idle(UpdateType.Archive));
} else {
this.setState(State.AvailableForDownload(update));
}
@@ -72,7 +69,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
}
*/
this.telemetryService.publicLog('update:notAvailable', { explicit: !!context });
this.setState(State.Idle);
this.setState(State.Idle(UpdateType.Archive));
});
}
@@ -84,8 +81,8 @@ export class LinuxUpdateService extends AbstractUpdateService {
} else {
shell.openExternal(state.update.url);
}
this.setState(State.Idle);
this.setState(State.Idle(UpdateType.Archive));
return TPromise.as(null);
}
}

View File

@@ -5,7 +5,7 @@
'use strict';
import * as fs from 'original-fs';
import * as fs from 'fs';
import * as path from 'path';
import * as pfs from 'vs/base/node/pfs';
import { memoize } from 'vs/base/common/decorators';
@@ -14,7 +14,7 @@ import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycle
import { IRequestService } from 'vs/platform/request/node/request';
import product from 'vs/platform/node/product';
import { TPromise, Promise } from 'vs/base/common/winjs.base';
import { State, IUpdate, StateType } from 'vs/platform/update/common/update';
import { State, IUpdate, StateType, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ILogService } from 'vs/platform/log/common/log';
@@ -23,6 +23,7 @@ import { download, asJson } from 'vs/base/node/request';
import { checksum } from 'vs/base/node/crypto';
import { tmpdir } from 'os';
import { spawn } from 'child_process';
import { shell } from 'electron';
function pollUntil(fn: () => boolean, timeout = 1000): TPromise<void> {
return new TPromise<void>(c => {
@@ -43,17 +44,27 @@ interface IAvailableUpdate {
updateFilePath?: string;
}
let _updateType: UpdateType | undefined = undefined;
function getUpdateType(): UpdateType {
if (typeof _updateType === 'undefined') {
_updateType = fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))
? UpdateType.Setup
: UpdateType.Archive;
}
return _updateType;
}
export class Win32UpdateService extends AbstractUpdateService {
_serviceBrand: any;
private url: string | undefined;
private availableUpdate: IAvailableUpdate | undefined;
@memoize
get cachePath(): TPromise<string> {
// {{SQL CARBON EDIT}}
const result = path.join(tmpdir(), `sqlops-update-${process.arch}`);
const result = path.join(tmpdir(), `sqlops-update-${product.target}-${process.arch}`);
return pfs.mkdirp(result, null).then(() => result);
}
@@ -62,19 +73,40 @@ export class Win32UpdateService extends AbstractUpdateService {
@IConfigurationService configurationService: IConfigurationService,
@ITelemetryService private telemetryService: ITelemetryService,
@IEnvironmentService environmentService: IEnvironmentService,
@IRequestService private requestService: IRequestService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService
) {
super(lifecycleService, configurationService, environmentService, logService);
super(lifecycleService, configurationService, environmentService, requestService, logService);
if (getUpdateType() === UpdateType.Setup) {
/* __GDPR__
"update:win32SetupTarget" : {
"target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
/* __GDPR__
"update:win<NUMBER>SetupTarget" : {
"target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
telemetryService.publicLog('update:win32SetupTarget', { target: product.target });
}
}
protected setUpdateFeedUrl(quality: string): boolean {
if (!fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) {
return false;
protected buildUpdateFeedUrl(quality: string): string | undefined {
let platform = 'win32';
if (process.arch === 'x64') {
platform += '-x64';
}
this.url = createUpdateURL(process.arch === 'x64' ? 'win32-x64' : 'win32', quality);
return true;
if (getUpdateType() === UpdateType.Archive) {
platform += '-archive';
} else if (product.target === 'user') {
platform += '-user';
}
return createUpdateURL(platform, quality);
}
protected doCheckForUpdates(context: any): void {
@@ -87,7 +119,9 @@ export class Win32UpdateService extends AbstractUpdateService {
this.requestService.request({ url: this.url })
.then<IUpdate>(asJson)
.then(update => {
if (!update || !update.url || !update.version) {
const updateType = getUpdateType();
if (!update || !update.url || !update.version || !update.productVersion) {
/* __GDPR__
"update:notAvailable" : {
"explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
@@ -95,7 +129,12 @@ export class Win32UpdateService extends AbstractUpdateService {
*/
this.telemetryService.publicLog('update:notAvailable', { explicit: !!context });
this.setState(State.Idle);
this.setState(State.Idle(updateType));
return TPromise.as(null);
}
if (updateType === UpdateType.Archive) {
this.setState(State.AvailableForDownload(update));
return TPromise.as(null);
}
@@ -124,7 +163,11 @@ export class Win32UpdateService extends AbstractUpdateService {
this.availableUpdate = { packagePath };
if (fastUpdatesEnabled && update.supportsFastUpdate) {
this.setState(State.Downloaded(update));
if (product.target === 'user') {
this.doApplyUpdate();
} else {
this.setState(State.Downloaded(update));
}
} else {
this.setState(State.Ready(update));
}
@@ -139,10 +182,16 @@ export class Win32UpdateService extends AbstractUpdateService {
}
*/
this.telemetryService.publicLog('update:notAvailable', { explicit: !!context });
this.setState(State.Idle);
this.setState(State.Idle(getUpdateType()));
});
}
protected doDownloadUpdate(state: AvailableForDownload): TPromise<void> {
shell.openExternal(state.update.url);
this.setState(State.Idle(getUpdateType()));
return TPromise.as(null);
}
private getUpdatePackagePath(version: string): TPromise<string> {
// {{SQL CARBON EDIT}}
return this.cachePath.then(cachePath => path.join(cachePath, `SqlOpsStudioSetup-${product.quality}-${version}.exe`));
@@ -161,7 +210,11 @@ export class Win32UpdateService extends AbstractUpdateService {
}
protected doApplyUpdate(): TPromise<void> {
if (this.state.type !== StateType.Downloaded || !this.availableUpdate) {
if (this.state.type !== StateType.Downloaded && this.state.type !== StateType.Downloading) {
return TPromise.as(null);
}
if (!this.availableUpdate) {
return TPromise.as(null);
}
@@ -180,7 +233,7 @@ export class Win32UpdateService extends AbstractUpdateService {
child.once('exit', () => {
this.availableUpdate = undefined;
this.setState(State.Idle);
this.setState(State.Idle(getUpdateType()));
});
const readyMutexName = `${product.win32MutexName}-ready`;
@@ -209,4 +262,8 @@ export class Win32UpdateService extends AbstractUpdateService {
});
}
}
protected getUpdateType(): UpdateType {
return getUpdateType();
}
}

View File

@@ -21,13 +21,21 @@ configurationRegistry.registerConfiguration({
'enum': ['none', 'default'],
'default': 'default',
'scope': ConfigurationScope.APPLICATION,
'description': nls.localize('updateChannel', "Configure whether you receive automatic updates from an update channel. Requires a restart after change.")
'description': nls.localize('updateChannel', "Configure whether you receive automatic updates from an update channel. Requires a restart after change. The updates are fetched from an online service."),
'tags': ['usesOnlineServices']
},
'update.enableWindowsBackgroundUpdates': {
'type': 'boolean',
'default': true,
'scope': ConfigurationScope.APPLICATION,
'description': nls.localize('enableWindowsBackgroundUpdates', "Enables Windows background updates.")
'description': nls.localize('enableWindowsBackgroundUpdates', "Enables Windows background updates. The updates are fetched from an online service."),
'tags': ['usesOnlineServices']
},
'update.showReleaseNotes': {
'type': 'boolean',
'default': true,
'description': nls.localize('showReleaseNotes', "Show Release Notes after an update. The Release Notes are fetched from an online service."),
'tags': ['usesOnlineServices']
}
}
});

View File

@@ -0,0 +1,120 @@
/*---------------------------------------------------------------------------------------------
* 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 { IDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { isEqual, basenameOrAuthority } from 'vs/base/common/resources';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { tildify, getPathLabel } from 'vs/base/common/labels';
import { ltrim } from 'vs/base/common/strings';
export interface IUriDisplayService {
_serviceBrand: any;
getLabel(resource: URI, relative?: boolean): string;
registerFormater(schema: string, formater: UriDisplayRules): IDisposable;
onDidRegisterFormater: Event<{ scheme: string, formater: UriDisplayRules }>;
}
export interface UriDisplayRules {
label: string; // myLabel:/${path}
separator: '/' | '\\' | '';
tildify?: boolean;
normalizeDriveLetter?: boolean;
}
const URI_DISPLAY_SERVICE_ID = 'uriDisplay';
const sepRegexp = /\//g;
const labelMatchingRegexp = /\$\{scheme\}|\$\{authority\}|\$\{path\}/g;
function hasDriveLetter(path: string): boolean {
return isWindows && path && path[2] === ':';
}
export class UriDisplayService implements IUriDisplayService {
_serviceBrand: any;
private readonly formaters = new Map<string, UriDisplayRules>();
private readonly _onDidRegisterFormater = new Emitter<{ scheme: string, formater: UriDisplayRules }>();
constructor(
@IEnvironmentService private environmentService: IEnvironmentService,
@IWorkspaceContextService private contextService: IWorkspaceContextService
) { }
get onDidRegisterFormater(): Event<{ scheme: string, formater: UriDisplayRules }> {
return this._onDidRegisterFormater.event;
}
getLabel(resource: URI, relative: boolean): string {
if (!resource) {
return undefined;
}
const formater = this.formaters.get(resource.scheme);
if (!formater) {
return getPathLabel(resource.path, this.environmentService, relative ? this.contextService : undefined);
}
if (relative) {
const baseResource = this.contextService && this.contextService.getWorkspaceFolder(resource);
if (baseResource) {
let relativeLabel: string;
if (isEqual(baseResource.uri, resource, !isLinux)) {
relativeLabel = ''; // no label if resources are identical
} else {
const baseResourceLabel = this.formatUri(baseResource.uri, formater);
relativeLabel = ltrim(this.formatUri(resource, formater).substring(baseResourceLabel.length), formater.separator);
}
const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1;
if (hasMultipleRoots) {
const rootName = (baseResource && baseResource.name) ? baseResource.name : basenameOrAuthority(baseResource.uri);
relativeLabel = relativeLabel ? (rootName + ' • ' + relativeLabel) : rootName; // always show root basename if there are multiple
}
return relativeLabel;
}
}
return this.formatUri(resource, formater);
}
registerFormater(scheme: string, formater: UriDisplayRules): IDisposable {
this.formaters.set(scheme, formater);
this._onDidRegisterFormater.fire({ scheme, formater });
return {
dispose: () => this.formaters.delete(scheme)
};
}
private formatUri(resource: URI, formater: UriDisplayRules): string {
let label = formater.label.replace(labelMatchingRegexp, match => {
switch (match) {
case '${scheme}': return resource.scheme;
case '${authority}': return resource.authority;
case '${path}': return resource.path;
default: return '';
}
});
// convert \c:\something => C:\something
if (formater.normalizeDriveLetter && hasDriveLetter(label)) {
label = label.charAt(1).toUpperCase() + label.substr(2);
}
if (formater.tildify) {
label = tildify(label, this.environmentService.userHome);
}
return label.replace(sepRegexp, formater.separator);
}
}
export const IUriDisplayService = createDecorator<IUriDisplayService>(URI_DISPLAY_SERVICE_ID);

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
import { ipcRenderer as ipc } from 'electron';
import { Registry } from 'vs/platform/registry/common/platform';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
/**
* Uri display registration needs to be shared from renderer to main.
* Since there will be another instance of the uri display service running on main.
*/
class UriDisplayRegistrationContribution implements IWorkbenchContribution {
constructor(@IUriDisplayService uriDisplayService: IUriDisplayService) {
uriDisplayService.onDidRegisterFormater(data => {
ipc.send('vscode:uriDisplayRegisterFormater', data);
});
}
}
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(UriDisplayRegistrationContribution, LifecyclePhase.Starting);

View File

@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* 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 { IUriDisplayService, UriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
import { TestEnvironmentService, TestContextService } from 'vs/workbench/test/workbenchTestServices';
import { Schemas } from 'vs/base/common/network';
import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace';
import URI from 'vs/base/common/uri';
import { nativeSep } from 'vs/base/common/paths';
import { isWindows } from 'vs/base/common/platform';
suite('URI Display', () => {
let uriDisplayService: IUriDisplayService;
setup(() => {
uriDisplayService = new UriDisplayService(TestEnvironmentService, new TestContextService());
});
test('file scheme', function () {
uriDisplayService.registerFormater(Schemas.file, {
label: '${path}',
separator: nativeSep,
tildify: !isWindows,
normalizeDriveLetter: isWindows
});
const uri1 = TestWorkspace.folders[0].uri.with({ path: TestWorkspace.folders[0].uri.path.concat('/a/b/c/d') });
assert.equal(uriDisplayService.getLabel(uri1, true), isWindows ? 'a\\b\\c\\d' : 'a/b/c/d');
assert.equal(uriDisplayService.getLabel(uri1, false), isWindows ? 'C:\\testWorkspace\\a\\b\\c\\d' : '/testWorkspace/a/b/c/d');
const uri2 = URI.file('c:\\1/2/3');
assert.equal(uriDisplayService.getLabel(uri2, false), isWindows ? 'C:\\1\\2\\3' : '/c:\\1/2/3');
});
test('custom scheme', function () {
uriDisplayService.registerFormater(Schemas.vscode, {
label: 'LABEL/${path}/${authority}/END',
separator: '/',
tildify: true,
normalizeDriveLetter: true
});
const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5');
assert.equal(uriDisplayService.getLabel(uri1, false), 'LABEL//1/2/3/4/5/microsoft.com/END');
});
});

View File

@@ -10,6 +10,7 @@ import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IURLHandler, IURLService } from './url';
import URI from 'vs/base/common/uri';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
export interface IURLServiceChannel extends IChannel {
call(command: 'open', url: string): TPromise<boolean>;
@@ -20,6 +21,10 @@ export class URLServiceChannel implements IURLServiceChannel {
constructor(private service: IURLService) { }
listen<T>(event: string, arg?: any): Event<T> {
throw new Error('No events');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'open': return this.service.open(URI.revive(arg));
@@ -52,6 +57,10 @@ export class URLHandlerChannel implements IURLHandlerChannel {
constructor(private handler: IURLHandler) { }
listen<T>(event: string, arg?: any): Event<T> {
throw new Error('No events');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'handleURL': return this.handler.handleURL(URI.revive(arg));

View File

@@ -9,6 +9,7 @@ import { IURLService, IURLHandler } from 'vs/platform/url/common/url';
import URI from 'vs/base/common/uri';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { TPromise } from 'vs/base/common/winjs.base';
import { first } from 'vs/base/common/async';
declare module Array {
function from<T>(set: Set<T>): T[];
@@ -20,16 +21,9 @@ export class URLService implements IURLService {
private handlers = new Set<IURLHandler>();
async open(uri: URI): TPromise<boolean> {
open(uri: URI): TPromise<boolean> {
const handlers = Array.from(this.handlers);
for (const handler of handlers) {
if (await handler.handleURL(uri)) {
return true;
}
}
return false;
return first(handlers.map(h => () => h.handleURL(uri)), undefined, false);
}
registerHandler(handler: IURLHandler): IDisposable {
@@ -44,7 +38,7 @@ export class RelayURLService extends URLService implements IURLHandler {
super();
}
async open(uri: URI): TPromise<boolean> {
open(uri: URI): TPromise<boolean> {
return this.urlService.open(uri);
}

View File

@@ -57,56 +57,57 @@ export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
]);
}
async handleURL(uri: URI): TPromise<boolean> {
handleURL(uri: URI): TPromise<boolean> {
if (!isExtensionId(uri.authority)) {
return false;
return TPromise.as(false);
}
const extensionId = uri.authority;
const wasHandlerAvailable = this.extensionHandlers.has(extensionId);
const extensions = await this.extensionService.getExtensions();
const extension = extensions.filter(e => e.id === extensionId)[0];
return this.extensionService.getExtensions().then(extensions => {
const extension = extensions.filter(e => e.id === extensionId)[0];
if (!extension) {
return false;
}
const result = await this.dialogService.confirm({
message: localize('confirmUrl', "Allow an extension to open this URL?", extensionId),
detail: `${extension.displayName || extension.name} (${extensionId}) wants to open a URL:\n\n${uri.toString()}`
});
if (!result.confirmed) {
return true;
}
const handler = this.extensionHandlers.get(extensionId);
if (handler) {
if (!wasHandlerAvailable) {
// forward it directly
return handler.handleURL(uri);
if (!extension) {
return TPromise.as(false);
}
// let the ExtensionUrlHandler instance handle this
return TPromise.as(false);
}
return this.dialogService.confirm({
message: localize('confirmUrl', "Allow an extension to open this URL?", extensionId),
detail: `${extension.displayName || extension.name} (${extensionId}) wants to open a URL:\n\n${uri.toString()}`
}).then(result => {
// collect URI for eventual extension activation
const timestamp = new Date().getTime();
let uris = this.uriBuffer.get(extensionId);
if (!result.confirmed) {
return TPromise.as(true);
}
if (!uris) {
uris = [];
this.uriBuffer.set(extensionId, uris);
}
const handler = this.extensionHandlers.get(extensionId);
if (handler) {
if (!wasHandlerAvailable) {
// forward it directly
return handler.handleURL(uri);
}
uris.push({ timestamp, uri });
// let the ExtensionUrlHandler instance handle this
return TPromise.as(false);
}
// activate the extension
await this.extensionService.activateByEvent(`onUri:${extensionId}`);
// collect URI for eventual extension activation
const timestamp = new Date().getTime();
let uris = this.uriBuffer.get(extensionId);
return true;
if (!uris) {
uris = [];
this.uriBuffer.set(extensionId, uris);
}
uris.push({ timestamp, uri });
// activate the extension
return this.extensionService.activateByEvent(`onUri:${extensionId}`)
.then(() => true);
});
});
}
registerExtensionHandler(extensionId: string, handler: IURLHandler): void {

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IContextKeyService, ContextKeyDefinedExpr, ContextKeyExpr, ContextKeyAndExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { HistoryInputBox, IHistoryInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { IContextScopedWidget, getContextScopedWidget, createWidgetScopedContextKeyService, bindContextScopedWidget } from 'vs/platform/widget/common/contextScopedWidget';
import { IHistoryNavigationWidget } from 'vs/base/browser/history';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
export const HistoryNavigationWidgetContext = 'historyNavigationWidget';
export const HistoryNavigationEnablementContext = 'historyNavigationEnabled';
export interface IContextScopedHistoryNavigationWidget extends IContextScopedWidget {
historyNavigator: IHistoryNavigationWidget;
}
export function createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService: IContextKeyService, widget: IContextScopedHistoryNavigationWidget): { scopedContextKeyService: IContextKeyService, historyNavigationEnablement: IContextKey<boolean> } {
const scopedContextKeyService = createWidgetScopedContextKeyService(contextKeyService, widget);
bindContextScopedWidget(scopedContextKeyService, widget, HistoryNavigationWidgetContext);
const historyNavigationEnablement = new RawContextKey<boolean>(HistoryNavigationEnablementContext, true).bindTo(scopedContextKeyService);
return { scopedContextKeyService, historyNavigationEnablement };
}
export class ContextScopedHistoryInputBox extends HistoryInputBox {
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, options: IHistoryInputOptions,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(container, contextViewProvider, options);
this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, <IContextScopedHistoryNavigationWidget>{ target: this.element, historyNavigator: this }).scopedContextKeyService);
}
}
export class ContextScopedFindInput extends FindInput {
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, options: IFindInputOptions,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(container, contextViewProvider, options);
this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, <IContextScopedHistoryNavigationWidget>{ target: this.inputBox.element, historyNavigator: this.inputBox }).scopedContextKeyService);
}
}
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'history.showPrevious',
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(new ContextKeyDefinedExpr(HistoryNavigationWidgetContext), new ContextKeyEqualsExpr(HistoryNavigationEnablementContext, true)),
primary: KeyCode.UpArrow,
secondary: [KeyMod.Alt | KeyCode.UpArrow],
handler: (accessor, arg2) => {
const historyInputBox: IHistoryNavigationWidget = getContextScopedWidget<IContextScopedHistoryNavigationWidget>(accessor.get(IContextKeyService), HistoryNavigationWidgetContext).historyNavigator;
historyInputBox.showPreviousValue();
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'history.showNext',
weight: KeybindingWeight.WorkbenchContrib,
when: new ContextKeyAndExpr([new ContextKeyDefinedExpr(HistoryNavigationWidgetContext), new ContextKeyEqualsExpr(HistoryNavigationEnablementContext, true)]),
primary: KeyCode.DownArrow,
secondary: [KeyMod.Alt | KeyCode.DownArrow],
handler: (accessor, arg2) => {
const historyInputBox: IHistoryNavigationWidget = getContextScopedWidget<IContextScopedHistoryNavigationWidget>(accessor.get(IContextKeyService), HistoryNavigationWidgetContext).historyNavigator;
historyInputBox.showNextValue();
}
});

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IContextKeyService, RawContextKey, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey';
export function bindContextScopedWidget(contextKeyService: IContextKeyService, widget: IContextScopedWidget, contextKey: string): void {
new RawContextKey<IContextScopedWidget>(contextKey, widget).bindTo(contextKeyService);
}
export function createWidgetScopedContextKeyService(contextKeyService: IContextKeyService, widget: IContextScopedWidget): IContextKeyService {
return contextKeyService.createScoped(widget.target);
}
export function getContextScopedWidget<T extends IContextScopedWidget>(contextKeyService: IContextKeyService, contextKey: string): T {
return contextKeyService.getContext(document.activeElement).getValue(contextKey);
}
export interface IContextScopedWidget {
readonly target: IContextKeyServiceTarget;
}

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