mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-09 01:32:34 -05:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) === '#') {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, []);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
65
src/vs/platform/menubar/common/menubar.ts
Normal file
65
src/vs/platform/menubar/common/menubar.ts
Normal 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';
|
||||
}
|
||||
42
src/vs/platform/menubar/common/menubarIpc.ts
Normal file
42
src/vs/platform/menubar/common/menubarIpc.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
39
src/vs/platform/menubar/electron-main/menubarService.ts
Normal file
39
src/vs/platform/menubar/electron-main/menubarService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
17
src/vs/platform/node/minimalTranslations.ts
Normal file
17
src/vs/platform/node/minimalTranslations.ts
Normal 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")
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
|
||||
|
||||
export const IOpenerService = createDecorator<IOpenerService>('openerService');
|
||||
|
||||
|
||||
export interface IOpenerService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')) || {};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}]));
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
120
src/vs/platform/uriDisplay/common/uriDisplay.ts
Normal file
120
src/vs/platform/uriDisplay/common/uriDisplay.ts
Normal 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);
|
||||
@@ -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);
|
||||
50
src/vs/platform/uriDisplay/test/uriDisplay.test.ts
Normal file
50
src/vs/platform/uriDisplay/test/uriDisplay.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
76
src/vs/platform/widget/browser/contextScopedHistoryWidget.ts
Normal file
76
src/vs/platform/widget/browser/contextScopedHistoryWidget.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
25
src/vs/platform/widget/common/contextScopedWidget.ts
Normal file
25
src/vs/platform/widget/common/contextScopedWidget.ts
Normal 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
Reference in New Issue
Block a user