diff --git a/product.json b/product.json index c596fff2b81defb929854bc3a373da61cd848833..670b6d8ab4e31b856d22d420706a48caf6ff9b5f 100644 --- a/product.json +++ b/product.json @@ -6,6 +6,10 @@ "win32MutexName": "vscodeoss", "licenseUrl": "https://github.com/Microsoft/vscode/blob/master/LICENSE.txt", "darwinBundleIdentifier": "com.visualstudio.code.oss", - "welcomePage": "https://go.microsoft.com/fwlink/?LinkId=723048", "reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new" + ,"extensionsGallery": { + "serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery", + "cacheUrl": "https://vscode.blob.core.windows.net/gallery/index", + "itemUrl": "https://marketplace.visualstudio.com/items" + } } \ No newline at end of file diff --git a/src/bootstrap.js b/src/bootstrap.js index dbee2f968653d958ca3ca366a80eb026d5022e5e..ae4e67c1488b3c24610555b973254777d138947d 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -90,16 +90,18 @@ if (!!process.send && process.env.PIPE_LOGGING === 'true') { console.error = function () { safeSend({ type: '__$console', severity: 'error', arguments: safeStringify(arguments) }); }; } -// Let stdout, stderr and stdin be no-op streams. This prevents an issue where we would get an EBADF -// error when we are inside a forked process and this process tries to access those channels. -var stream = require('stream'); -var writable = new stream.Writable({ - write: function () { /* No OP */ } -}); - -process.__defineGetter__('stdout', function() { return writable; }); -process.__defineGetter__('stderr', function() { return writable; }); -process.__defineGetter__('stdin', function() { return writable; }); +if (!process.env['VSCODE_ALLOW_IO']) { + // Let stdout, stderr and stdin be no-op streams. This prevents an issue where we would get an EBADF + // error when we are inside a forked process and this process tries to access those channels. + var stream = require('stream'); + var writable = new stream.Writable({ + write: function () { /* No OP */ } + }); + + process.__defineGetter__('stdout', function() { return writable; }); + process.__defineGetter__('stderr', function() { return writable; }); + process.__defineGetter__('stdin', function() { return writable; }); +} // Handle uncaught exceptions process.on('uncaughtException', function (err) { diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index df54c181db07ff126356066ec8cfef94afb5707f..cc17dacbde25da20a37e5b3e6741bbe6880f4cc3 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -922,7 +922,7 @@ export function append(parent: HTMLElement, child: T): T { const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((.([\w\-]+))*)/; // Similar to builder, but much more lightweight -export function emmet(description: string): HTMLElement { +export function emmet(description: string): T { let match = SELECTOR_REGEX.exec(description); if (!match) { @@ -930,6 +930,7 @@ export function emmet(description: string): HTMLElement { } let result = document.createElement(match[1] || 'div'); + if (match[3]) { result.id = match[3]; } @@ -937,7 +938,7 @@ export function emmet(description: string): HTMLElement { result.className = match[4].replace(/\./g, ' ').trim(); } - return result; + return result as T; } export function show(...elements: HTMLElement[]): void { @@ -985,4 +986,12 @@ export function removeTabIndexAndUpdateFocus(node: HTMLElement): void { export function getElementsByTagName(tag: string): HTMLElement[] { return Array.prototype.slice.call(document.getElementsByTagName(tag), 0); +} + +export function finalHandler(fn: (event: T)=>any): (event: T)=>any { + return e => { + e.preventDefault(); + e.stopPropagation(); + fn(e); + }; } \ No newline at end of file diff --git a/src/vs/base/browser/event.ts b/src/vs/base/browser/event.ts new file mode 100644 index 0000000000000000000000000000000000000000..34c37b47102898d6a558fc55a623126ea8486b84 --- /dev/null +++ b/src/vs/base/browser/event.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import _Event, { Emitter } from 'vs/base/common/event'; + +export interface IDomEvent { + (element: HTMLElement, type: "MSContentZoom", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSGestureChange", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSGestureDoubleTap", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSGestureEnd", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSGestureHold", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSGestureStart", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSGestureTap", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSGotPointerCapture", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSInertiaStart", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSLostPointerCapture", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSManipulationStateChanged", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSPointerCancel", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSPointerDown", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSPointerEnter", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSPointerLeave", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSPointerMove", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSPointerOut", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSPointerOver", useCapture?: boolean): _Event; + (element: HTMLElement, type: "MSPointerUp", useCapture?: boolean): _Event; + (element: HTMLElement, type: "abort", useCapture?: boolean): _Event; + (element: HTMLElement, type: "activate", useCapture?: boolean): _Event; + (element: HTMLElement, type: "ariarequest", useCapture?: boolean): _Event; + (element: HTMLElement, type: "beforeactivate", useCapture?: boolean): _Event; + (element: HTMLElement, type: "beforecopy", useCapture?: boolean): _Event; + (element: HTMLElement, type: "beforecut", useCapture?: boolean): _Event; + (element: HTMLElement, type: "beforedeactivate", useCapture?: boolean): _Event; + (element: HTMLElement, type: "beforepaste", useCapture?: boolean): _Event; + (element: HTMLElement, type: "blur", useCapture?: boolean): _Event; + (element: HTMLElement, type: "canplay", useCapture?: boolean): _Event; + (element: HTMLElement, type: "canplaythrough", useCapture?: boolean): _Event; + (element: HTMLElement, type: "change", useCapture?: boolean): _Event; + (element: HTMLElement, type: "click", useCapture?: boolean): _Event; + (element: HTMLElement, type: "command", useCapture?: boolean): _Event; + (element: HTMLElement, type: "contextmenu", useCapture?: boolean): _Event; + (element: HTMLElement, type: "copy", useCapture?: boolean): _Event; + (element: HTMLElement, type: "cuechange", useCapture?: boolean): _Event; + (element: HTMLElement, type: "cut", useCapture?: boolean): _Event; + (element: HTMLElement, type: "dblclick", useCapture?: boolean): _Event; + (element: HTMLElement, type: "deactivate", useCapture?: boolean): _Event; + (element: HTMLElement, type: "drag", useCapture?: boolean): _Event; + (element: HTMLElement, type: "dragend", useCapture?: boolean): _Event; + (element: HTMLElement, type: "dragenter", useCapture?: boolean): _Event; + (element: HTMLElement, type: "dragleave", useCapture?: boolean): _Event; + (element: HTMLElement, type: "dragover", useCapture?: boolean): _Event; + (element: HTMLElement, type: "dragstart", useCapture?: boolean): _Event; + (element: HTMLElement, type: "drop", useCapture?: boolean): _Event; + (element: HTMLElement, type: "durationchange", useCapture?: boolean): _Event; + (element: HTMLElement, type: "emptied", useCapture?: boolean): _Event; + (element: HTMLElement, type: "ended", useCapture?: boolean): _Event; + (element: HTMLElement, type: "error", useCapture?: boolean): _Event; + (element: HTMLElement, type: "focus", useCapture?: boolean): _Event; + (element: HTMLElement, type: "gotpointercapture", useCapture?: boolean): _Event; + (element: HTMLElement, type: "input", useCapture?: boolean): _Event; + (element: HTMLElement, type: "keydown", useCapture?: boolean): _Event; + (element: HTMLElement, type: "keypress", useCapture?: boolean): _Event; + (element: HTMLElement, type: "keyup", useCapture?: boolean): _Event; + (element: HTMLElement, type: "load", useCapture?: boolean): _Event; + (element: HTMLElement, type: "loadeddata", useCapture?: boolean): _Event; + (element: HTMLElement, type: "loadedmetadata", useCapture?: boolean): _Event; + (element: HTMLElement, type: "loadstart", useCapture?: boolean): _Event; + (element: HTMLElement, type: "lostpointercapture", useCapture?: boolean): _Event; + (element: HTMLElement, type: "mousedown", useCapture?: boolean): _Event; + (element: HTMLElement, type: "mouseenter", useCapture?: boolean): _Event; + (element: HTMLElement, type: "mouseleave", useCapture?: boolean): _Event; + (element: HTMLElement, type: "mousemove", useCapture?: boolean): _Event; + (element: HTMLElement, type: "mouseout", useCapture?: boolean): _Event; + (element: HTMLElement, type: "mouseover", useCapture?: boolean): _Event; + (element: HTMLElement, type: "mouseup", useCapture?: boolean): _Event; + (element: HTMLElement, type: "mousewheel", useCapture?: boolean): _Event; + (element: HTMLElement, type: "paste", useCapture?: boolean): _Event; + (element: HTMLElement, type: "pause", useCapture?: boolean): _Event; + (element: HTMLElement, type: "play", useCapture?: boolean): _Event; + (element: HTMLElement, type: "playing", useCapture?: boolean): _Event; + (element: HTMLElement, type: "pointercancel", useCapture?: boolean): _Event; + (element: HTMLElement, type: "pointerdown", useCapture?: boolean): _Event; + (element: HTMLElement, type: "pointerenter", useCapture?: boolean): _Event; + (element: HTMLElement, type: "pointerleave", useCapture?: boolean): _Event; + (element: HTMLElement, type: "pointermove", useCapture?: boolean): _Event; + (element: HTMLElement, type: "pointerout", useCapture?: boolean): _Event; + (element: HTMLElement, type: "pointerover", useCapture?: boolean): _Event; + (element: HTMLElement, type: "pointerup", useCapture?: boolean): _Event; + (element: HTMLElement, type: "progress", useCapture?: boolean): _Event; + (element: HTMLElement, type: "ratechange", useCapture?: boolean): _Event; + (element: HTMLElement, type: "reset", useCapture?: boolean): _Event; + (element: HTMLElement, type: "scroll", useCapture?: boolean): _Event; + (element: HTMLElement, type: "seeked", useCapture?: boolean): _Event; + (element: HTMLElement, type: "seeking", useCapture?: boolean): _Event; + (element: HTMLElement, type: "select", useCapture?: boolean): _Event; + (element: HTMLElement, type: "selectstart", useCapture?: boolean): _Event; + (element: HTMLElement, type: "stalled", useCapture?: boolean): _Event; + (element: HTMLElement, type: "submit", useCapture?: boolean): _Event; + (element: HTMLElement, type: "suspend", useCapture?: boolean): _Event; + (element: HTMLElement, type: "timeupdate", useCapture?: boolean): _Event; + (element: HTMLElement, type: "touchcancel", useCapture?: boolean): _Event; + (element: HTMLElement, type: "touchend", useCapture?: boolean): _Event; + (element: HTMLElement, type: "touchmove", useCapture?: boolean): _Event; + (element: HTMLElement, type: "touchstart", useCapture?: boolean): _Event; + (element: HTMLElement, type: "volumechange", useCapture?: boolean): _Event; + (element: HTMLElement, type: "waiting", useCapture?: boolean): _Event; + (element: HTMLElement, type: "webkitfullscreenchange", useCapture?: boolean): _Event; + (element: HTMLElement, type: "webkitfullscreenerror", useCapture?: boolean): _Event; + (element: HTMLElement, type: "wheel", useCapture?: boolean): _Event; + (element: HTMLElement, type: string, useCapture?: boolean): _Event; +} + +export const domEvent: IDomEvent = (element: HTMLElement, type: string, useCapture?) => { + const fn = e => emitter.fire(e); + const emitter = new Emitter({ + onFirstListenerAdd: () => { + element.addEventListener(type, fn); + }, + onLastListenerRemove: () => { + element.removeEventListener(type, fn); + } + }); + + return emitter.event; +}; diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 2cfe0ecafe7bd89476c40d1688559294d4de56f5..a58a08217a84ed5e5e7d79de878fcc6529495ab2 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -36,7 +36,7 @@ cursor: default; } -.monaco-action-bar .action-item.active { +.monaco-action-bar.animated .action-item.active { -ms-transform: translate(0, -3px); -webkit-transform: translate(0, -3px); -moz-transform: translate(0, -3px); @@ -77,7 +77,7 @@ margin-right: .8em; } -.monaco-action-bar.vertical .action-item.active { +.monaco-action-bar.animated.vertical .action-item.active { -ms-transform: translate(5px, 0); -webkit-transform: translate(5px, 0); -moz-transform: translate(5px, 0); diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 8c73c19bfdeace5f05b5d5a703798595d50ac155..46207f71a75c576e13a926f806188eafa8bd81ee 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -137,7 +137,7 @@ export class BaseActionItem extends EventEmitter implements IActionItem { public onClick(event: Event): void { DOM.EventHelper.stop(event, true); - + let context: any; if (types.isUndefinedOrNull(this._context)) { context = event; @@ -388,6 +388,7 @@ export interface IActionBarOptions { actionItemProvider?: IActionItemProvider; actionRunner?: IActionRunner; ariaLabel?: string; + animated?: boolean; } let defaultOptions: IActionBarOptions = { @@ -440,6 +441,10 @@ export class ActionBar extends EventEmitter implements IActionRunner { this.domNode = document.createElement('div'); this.domNode.className = 'monaco-action-bar'; + if (options.animated !== false) { + DOM.addClass(this.domNode, 'animated'); + } + let isVertical = this.options.orientation === ActionsOrientation.VERTICAL; if (isVertical) { this.domNode.className += ' vertical'; diff --git a/src/vs/base/browser/ui/list/list.css b/src/vs/base/browser/ui/list/list.css index 64bbf7167c6f9496b3614ac0d1b1c5ddf02cf11b..a10b9a831032641fce7ed5813773b9164ac7ecea 100644 --- a/src/vs/base/browser/ui/list/list.css +++ b/src/vs/base/browser/ui/list/list.css @@ -47,12 +47,12 @@ .vs-dark .monaco-list-row:hover { background-color: rgba(255, 255, 255, 0.08); } .hc-black .monaco-list-row:hover { outline: 1px dashed #f38518; background: transparent; } -/* Selection */ -.monaco-list-row.selected { background-color: #4FA7FF; color: white; } -.vs-dark .monaco-list-row.selected { background-color: #0E639C; color: white; } -.hc-black .monaco-list-row.selected { border: 1px solid #f38518; } - /* Focus */ .monaco-list-row.focused { background-color: #DCEBFC; } .vs-dark .monaco-list-row.focused { background-color: #073655; } .hc-black .monaco-list-row.focused { outline: 1px dotted #f38518; background: transparent } + +/* Selection */ +.monaco-list-row.selected { background-color: #4FA7FF; color: white; } +.vs-dark .monaco-list-row.selected { background-color: #0E639C; color: white; } +.hc-black .monaco-list-row.selected { border: 1px solid #f38518; } diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts new file mode 100644 index 0000000000000000000000000000000000000000..7da5370aec353acde8179cd88486d2a809d087ec --- /dev/null +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./list'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { range } from 'vs/base/common/arrays'; +import { IDelegate, IRenderer, IFocusChangeEvent, ISelectionChangeEvent } from './list'; +import { List } from './listWidget'; +import { PagedModel } from 'vs/base/common/paging'; +import Event, { mapEvent } from 'vs/base/common/event'; + +export interface IPagedRenderer extends IRenderer { + renderPlaceholder(index: number, templateData: TTemplateData): void; +} + +export interface ITemplateData { + data: T; + disposable: IDisposable; +} + +class PagedRenderer implements IRenderer> { + + get templateId(): string { return this.renderer.templateId; } + + constructor( + private renderer: IPagedRenderer, + private modelProvider: () => PagedModel + ) {} + + renderTemplate(container: HTMLElement): ITemplateData { + const data = this.renderer.renderTemplate(container); + return { data, disposable: { dispose: () => {} } }; + } + + renderElement(index: number, _: number, data: ITemplateData): void { + data.disposable.dispose(); + + const model = this.modelProvider(); + + if (model.isResolved(index)) { + return this.renderer.renderElement(model.get(index), index, data.data); + } + + const promise = model.resolve(index); + data.disposable = { dispose: () => promise.cancel() }; + + this.renderer.renderPlaceholder(index, data.data); + promise.done(entry => this.renderer.renderElement(entry, index, data.data)); + } + + disposeTemplate(data: ITemplateData): void { + data.disposable.dispose(); + data.disposable = null; + this.renderer.disposeTemplate(data.data); + data.data = null; + } +} + +export class PagedList { + + private list: List; + private _model: PagedModel; + get onDOMFocus(): Event { return this.list.onDOMFocus; } + + constructor( + container: HTMLElement, + delegate: IDelegate, + renderers: IPagedRenderer[] + ) { + const pagedRenderers = renderers.map(r => new PagedRenderer>(r, () => this.model)); + this.list = new List(container, delegate, pagedRenderers); + } + + get onFocusChange(): Event> { + return mapEvent(this.list.onFocusChange, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); + } + + get onSelectionChange(): Event> { + return mapEvent(this.list.onSelectionChange, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); + } + + get model(): PagedModel { + return this._model; + } + + set model(model: PagedModel) { + this._model = model; + this.list.splice(0, this.list.length, ...range(model.length)); + } + + get scrollTop(): number { + return this.list.scrollTop; + } + + focusNext(n?: number, loop?: boolean): void { + this.list.focusNext(n, loop); + } + + focusPrevious(n?: number, loop?: boolean): void { + this.list.focusPrevious(n, loop); + } + + selectNext(n?: number, loop?: boolean): void { + this.list.selectNext(n, loop); + } + + selectPrevious(n?: number, loop?: boolean): void { + this.list.selectPrevious(n, loop); + } + + getFocus(): number[] { + return this.list.getFocus(); + } + + setSelection(...indexes: number[]): void { + this.list.setSelection(...indexes); + } + + layout(height?: number): void { + this.list.layout(height); + } +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index efaf86b1a4f7c33a1ea25cccf9a431a9051cce22..988684bfa38f416cef5d8c54ac2a6b6706cdec85 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { toObject, assign } from 'vs/base/common/objects'; +import { toObject, assign, getOrDefault } from 'vs/base/common/objects'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Gesture } from 'vs/base/browser/touch'; import * as DOM from 'vs/base/browser/dom'; @@ -38,6 +38,14 @@ const MouseEventTypes = [ 'contextmenu' ]; +export interface IListViewOptions { + useShadows?: boolean; +} + +const DefaultOptions: IListViewOptions = { + useShadows: true +}; + export class ListView implements IDisposable { private items: IItem[]; @@ -56,7 +64,8 @@ export class ListView implements IDisposable { constructor( container: HTMLElement, private delegate: IDelegate, - renderers: IRenderer[] + renderers: IRenderer[], + options: IListViewOptions = DefaultOptions ) { this.items = []; this.itemId = 0; @@ -69,7 +78,6 @@ export class ListView implements IDisposable { this._domNode = document.createElement('div'); this._domNode.className = 'monaco-list'; - this._domNode.tabIndex = 0; this.rowsContainer = document.createElement('div'); this.rowsContainer.className = 'monaco-list-rows'; @@ -79,7 +87,7 @@ export class ListView implements IDisposable { canUseTranslate3d: false, horizontal: ScrollbarVisibility.Hidden, vertical: ScrollbarVisibility.Auto, - useShadows: false, + useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows), saveLastScrollTimeOnClassName: 'monaco-list-row' }); diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 8facec26665b4cfe2f80188d80201985e93d5b4c..cba1e19027a07eaa1be0fa746cf2082c64339ca0 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -8,8 +8,9 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { isNumber } from 'vs/base/common/types'; import * as DOM from 'vs/base/browser/dom'; import Event, { Emitter, mapEvent, EventBufferer } from 'vs/base/common/event'; +import { domEvent } from 'vs/base/browser/event'; import { IDelegate, IRenderer, IListMouseEvent, IFocusChangeEvent, ISelectionChangeEvent } from './list'; -import { ListView } from './listView'; +import { ListView, IListViewOptions } from './listView'; interface ITraitTemplateData { container: HTMLElement; @@ -138,6 +139,8 @@ class Controller implements IDisposable { private onClick(e: IListMouseEvent) { e.preventDefault(); e.stopPropagation(); + this.view.domNode.focus(); + this.list.setFocus(e.index); this.list.setSelection(e.index); } @@ -146,6 +149,11 @@ class Controller implements IDisposable { } } +export interface IListOptions extends IListViewOptions { +} + +const DefaultOptions: IListOptions = {}; + export class List implements IDisposable { private static InstanceCount = 0; @@ -165,10 +173,14 @@ export class List implements IDisposable { return this.eventBufferer.wrapEvent(mapEvent(this.selection.onChange, e => this.toListEvent(e))); } + private _onDOMFocus: Event; + get onDOMFocus(): Event { return this._onDOMFocus; } + constructor( container: HTMLElement, delegate: IDelegate, - renderers: IRenderer[] + renderers: IRenderer[], + options: IListOptions = DefaultOptions ) { this.focus = new FocusTrait(i => this.getElementId(i)); this.selection = new Trait('selected'); @@ -180,9 +192,12 @@ export class List implements IDisposable { return r; }); - this.view = new ListView(container, delegate, renderers); + this.view = new ListView(container, delegate, renderers, options); this.view.domNode.setAttribute('role', 'listbox'); + this.view.domNode.tabIndex = 0; this.controller = new Controller(this, this.view); + + this._onDOMFocus = domEvent(this.view.domNode, 'focus'); } splice(start: number, deleteCount: number, ...elements: T[]): void { @@ -201,6 +216,10 @@ export class List implements IDisposable { return this.view.getContentHeight(); } + get scrollTop(): number { + return this.view.getScrollTop(); + } + layout(height?: number): void { this.view.layout(height); } @@ -229,6 +248,10 @@ export class List implements IDisposable { this.setSelection(Math.max(index, 0)); } + getSelection(): number[] { + return this.selection.get(); + } + setFocus(...indexes: number[]): void { this.eventBufferer.bufferEvents(() => { indexes = indexes.concat(this.focus.set(...indexes)); @@ -255,7 +278,7 @@ export class List implements IDisposable { let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight); lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1; const lastPageElement = this.view.element(lastPageIndex); - const currentlyFocusedElement = this.getFocus()[0]; + const currentlyFocusedElement = this.getFocusedElements()[0]; if (currentlyFocusedElement !== lastPageElement) { this.setFocus(lastPageIndex); @@ -281,7 +304,7 @@ export class List implements IDisposable { } const firstPageElement = this.view.element(firstPageIndex); - const currentlyFocusedElement = this.getFocus()[0]; + const currentlyFocusedElement = this.getFocusedElements()[0]; if (currentlyFocusedElement !== firstPageElement) { this.setFocus(firstPageIndex); @@ -296,8 +319,12 @@ export class List implements IDisposable { } } - getFocus(): T[] { - return this.focus.get().map(i => this.view.element(i)); + getFocus(): number[] { + return this.focus.get(); + } + + getFocusedElements(): T[] { + return this.getFocus().map(i => this.view.element(i)); } reveal(index: number, relativeTop?: number): void { diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index fa44d0667f33a71aa71b8b9a73d7f5b1b0d3ed87..bf47f5db4f91096c95608efadbe5189ac854c717 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -216,10 +216,26 @@ export function flatten(arr: T[][]): T[] { return arr.reduce((r, v) => r.concat(v), []); } +export function range(to: number, from = 0): number[] { + const result = []; + + for (let i = from; i < to; i++) { + result.push(i); + } + + return result; +} + export function fill(num: number, valueFn: () => T, arr: T[] = []): T[] { for (let i = 0; i < num; i++) { arr[i] = valueFn(); } return arr; +} + +export function index(array: T[], indexer: (t: T) => string): { [key: string]: T; } { + const result = Object.create(null); + array.forEach(t => result[indexer(t)] = t); + return result; } \ No newline at end of file diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index c6dee602171c37f842091f1a3b1552cfb21390fd..fc550668b0afdc30df59546766f3dd36756eeba1 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -152,8 +152,11 @@ export function fromEventEmitter(emitter: EventEmitter, eventType: string): E } export function mapEvent(event: Event, map: (i:I)=>O): Event { - return (listener, thisArgs?, disposables?) => - event(i => listener(map(i)), thisArgs, disposables); + return (listener, thisArgs?, disposables?) => event(i => listener(map(i)), thisArgs, disposables); +} + +export function filterEvent(event: Event, filter: (e:T)=>boolean): Event { + return (listener, thisArgs?, disposables?) => event(e => filter(e) && listener(e), thisArgs, disposables); } enum EventDelayerState { diff --git a/src/vs/base/common/paging.ts b/src/vs/base/common/paging.ts index ee6d1b28ddc56cf662c0d57de216b16c2146850b..d0f923dc91be33da830c84bd8abc8a2b16321ce7 100644 --- a/src/vs/base/common/paging.ts +++ b/src/vs/base/common/paging.ts @@ -8,6 +8,9 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { ArraySet } from 'vs/base/common/set'; +/** + * A Pager is a stateless abstraction over a paged collection. + */ export interface IPager { firstPage: T[]; total: number; @@ -22,7 +25,17 @@ interface IPage { elements: T[]; } -export class PagedModel { +/** + * A PagedModel is a stateful model over an abstracted paged collection. + */ +export interface IPagedModel { + length: number; + isResolved(index: number): boolean; + get(index: number): T; + resolve(index: number): TPromise; +} + +export class PagedModel implements IPagedModel { private pages: IPage[] = []; @@ -92,6 +105,21 @@ export class PagedModel { } } +export class SinglePagePagedModel extends PagedModel { + constructor(elements: T[]) { + super({ + firstPage: elements, + total: elements.length, + pageSize: elements.length, + getPage: null + }); + } +} + +/** + * Similar to array.map, `mapPager` lets you map the elements of an + * abstract paged collection to another type. + */ export function mapPager(pager: IPager, fn: (t: T) => R): IPager { return { firstPage: pager.firstPage.map(fn), diff --git a/src/vs/base/node/request.ts b/src/vs/base/node/request.ts index 41b3aee353a890b8ce482cc2e4d84e3de8e2b1d7..077c526ce5975da80d51c1b4e807937cfda43a5b 100644 --- a/src/vs/base/node/request.ts +++ b/src/vs/base/node/request.ts @@ -86,6 +86,23 @@ export function download(filePath: string, opts: IRequestOptions): TPromise { + return request(opts).then(pair => new Promise((c, e) => { + if (!((pair.res.statusCode >= 200 && pair.res.statusCode < 300) || pair.res.statusCode === 1223)) { + return e('Server returned ' + pair.res.statusCode); + } + + if (pair.res.statusCode === 204) { + return c(null); + } + + let buffer: string[] = []; + pair.res.on('data', d => buffer.push(d)); + pair.res.on('end', () => c(buffer.join(''))); + pair.res.on('error', e); + })); +} + export function json(opts: IRequestOptions): TPromise { return request(opts).then(pair => new Promise((c, e) => { if (!((pair.res.statusCode >= 200 && pair.res.statusCode < 300) || pair.res.statusCode === 1223)) { diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 93887c775440d42a1fcde1a5368158b6cad9095b..a4d3a75fcb4f2cc59f3eb4b0c69b34669ae238d8 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -109,7 +109,7 @@ function main(accessor: ServicesAccessor, ipcServer: Server, userEnv: IProcessEn process.env['VSCODE_SHARED_IPC_HOOK'] = envService.sharedIPCHandle; // Spawn shared process - const sharedProcess = instantiationService.invokeFunction(spawnSharedProcess); + const sharedProcess = spawnSharedProcess(!envService.isBuilt || envService.cliArgs.verboseLogging); // Make sure we associate the program with the app user model id // This will help Windows to associate the running program with diff --git a/src/vs/code/electron-main/sharedProcess.ts b/src/vs/code/electron-main/sharedProcess.ts index fcaa716732c6e42db0579ca4d45516cbe2fb6bdc..d6274c6c984f358197825d1ebd85ccf5df1f8988 100644 --- a/src/vs/code/electron-main/sharedProcess.ts +++ b/src/vs/code/electron-main/sharedProcess.ts @@ -7,15 +7,18 @@ import * as cp from 'child_process'; import URI from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { assign } from 'vs/base/common/objects'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; const boostrapPath = URI.parse(require.toUrl('bootstrap')).fsPath; -function _spawnSharedProcess(): cp.ChildProcess { +function _spawnSharedProcess(allowOutput: boolean): cp.ChildProcess { const env = assign({}, process.env, { AMD_ENTRYPOINT: 'vs/code/node/sharedProcessMain' }); + if (allowOutput) { + env['VSCODE_ALLOW_IO'] = 'true'; + } + const result = cp.fork(boostrapPath, ['--type=SharedProcess'], { env }); // handshake @@ -26,7 +29,7 @@ function _spawnSharedProcess(): cp.ChildProcess { let spawnCount = 0; -export function spawnSharedProcess(accessor: ServicesAccessor): IDisposable { +export function spawnSharedProcess(allowOutput: boolean): IDisposable { let child: cp.ChildProcess; const spawn = () => { @@ -34,7 +37,7 @@ export function spawnSharedProcess(accessor: ServicesAccessor): IDisposable { return; } - child = _spawnSharedProcess(); + child = _spawnSharedProcess(allowOutput); child.on('exit', spawn); }; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 08c16cc57a3869892bc49626bc33d10af88cc4f4..dea192101df2f0d06d8407eb757146779eaaa451 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -9,6 +9,7 @@ import pkg from 'vs/platform/package'; import { ParsedArgs } from 'vs/code/node/argv'; import { TPromise } from 'vs/base/common/winjs.base'; import { sequence } from 'vs/base/common/async'; +import { IPager } from 'vs/base/common/paging'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -17,8 +18,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { IEventService } from 'vs/platform/event/common/event'; import { EventService } from 'vs/platform/event/common/eventService'; -import { IExtensionManagementService, IExtensionGalleryService, IQueryResult } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; +import { IExtensionManagementService, IExtensionGalleryService, IExtensionManifest, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/node/extensionGalleryService'; import { ITelemetryService, combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -34,6 +34,10 @@ const notFound = id => localize('notFound', "Extension '{0}' not found.", id); const notInstalled = id => localize('notInstalled', "Extension '{0}' is not installed.", id); const useId = localize('useId', "Make sure you use the full extension ID, eg: {0}", 'ms-vscode.csharp'); +function getId(manifest: IExtensionManifest): string { + return `${ manifest.publisher }.${ manifest.name }`; +} + class Main { constructor( @@ -59,22 +63,22 @@ class Main { private listExtensions(): TPromise { return this.extensionManagementService.getInstalled().then(extensions => { - extensions.forEach(e => console.log(getExtensionId(e))); + extensions.forEach(e => console.log(getId(e.manifest))); }); } private installExtension(ids: string[]): TPromise { return sequence(ids.map(id => () => { return this.extensionManagementService.getInstalled().then(installed => { - const isInstalled = installed.some(e => getExtensionId(e) === id); + const isInstalled = installed.some(e => getId(e.manifest) === id); if (isInstalled) { console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", id)); return TPromise.as(null); } - return this.extensionGalleryService.query({ ids: [id] }) - .then(null, err => { + return this.extensionGalleryService.query({ names: [id] }) + .then>(null, err => { if (err.responseText) { try { const response = JSON.parse(err.responseText); @@ -94,8 +98,8 @@ class Main { console.log(localize('foundExtension', "Found '{0}' in the marketplace.", id)); console.log(localize('installing', "Installing...")); - return this.extensionManagementService.install(extension).then(extension => { - console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed!", id, extension.version)); + return this.extensionManagementService.install(extension).then(() => { + console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed!", id, extension.versions[0].version)); }); }); }); @@ -105,7 +109,7 @@ class Main { private uninstallExtension(ids: string[]): TPromise { return sequence(ids.map(id => () => { return this.extensionManagementService.getInstalled().then(installed => { - const [extension] = installed.filter(e => getExtensionId(e) === id); + const [extension] = installed.filter(e => getId(e.manifest) === id); if (!extension) { return TPromise.wrapError(`${ notInstalled(id) }\n${ useId }`); diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 1830cf200aa68e09ce3995dc96d3bf785bfdf3f2..1ec4ae1e8b2ff1de334fe1631018a2cadcdae766 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -148,7 +148,7 @@ class Delegate implements IDelegate { constructor(private listProvider: () => List) { } getHeight(element: CompletionItem): number { - const focus = this.listProvider().getFocus()[0]; + const focus = this.listProvider().getFocusedElements()[0]; if (element.suggestion.documentationLabel && element === focus) { return FocusHeight; @@ -369,7 +369,9 @@ export class SuggestWidget implements IContentWidget, IDisposable { let renderer: IRenderer = instantiationService.createInstance(Renderer, this, this.editor); this.delegate = new Delegate(() => this.list); - this.list = new List(this.listElement, this.delegate, [renderer]); + this.list = new List(this.listElement, this.delegate, [renderer], { + useShadows: false + }); this.toDispose = [ editor.onDidBlurEditorText(() => this.onEditorBlur()), @@ -738,7 +740,7 @@ export class SuggestWidget implements IContentWidget, IDisposable { case State.Loading: return !this.isAuto; default: - const focus = this.list.getFocus()[0]; + const focus = this.list.getFocusedElements()[0]; if (focus) { this.list.setSelection(this.completionModel.items.indexOf(focus)); } else { @@ -759,7 +761,7 @@ export class SuggestWidget implements IContentWidget, IDisposable { return; } - const item = this.list.getFocus()[0]; + const item = this.list.getFocusedElements()[0]; if (!item || !item.suggestion.documentationLabel) { return; @@ -823,7 +825,7 @@ export class SuggestWidget implements IContentWidget, IDisposable { } else if (this.state === State.Details) { height = 12 * UnfocusedHeight; } else { - const focus = this.list.getFocus()[0]; + const focus = this.list.getFocusedElements()[0]; const focusHeight = focus ? this.delegate.getHeight(focus) : UnfocusedHeight; height = focusHeight; @@ -842,7 +844,7 @@ export class SuggestWidget implements IContentWidget, IDisposable { if (this.state !== State.Details) { this.details.render(null); } else { - this.details.render(this.list.getFocus()[0]); + this.details.render(this.list.getFocusedElements()[0]); } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 689a6514885f79cd198a58833f4e362186b0727d..4d652721b4b6ff82b06a25b2dc875d557c99f07a 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -8,6 +8,7 @@ import nls = require('vs/nls'); import { TPromise } from 'vs/base/common/winjs.base'; import Event from 'vs/base/common/event'; +import { IPager } from 'vs/base/common/paging'; import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; export interface IExtensionManifest { @@ -18,70 +19,108 @@ export interface IExtensionManifest { displayName?: string; description?: string; main?: string; + icon?: string; } export interface IGalleryVersion { version: string; date: string; manifestUrl: string; + readmeUrl: string; downloadUrl: string; + iconUrl: string; downloadHeaders: { [key: string]: string; }; } -export interface IGalleryMetadata { - galleryApiUrl: string; +export interface IExtensionIdentity { + name: string; + publisher: string; +} + +export interface IGalleryExtension { id: string; + name: string; + displayName: string; publisherId: string; + publisher: string; publisherDisplayName: string; + description: string; installCount: number; + rating: number; + ratingCount: number; versions: IGalleryVersion[]; } -export interface IExtension extends IExtensionManifest { - galleryInformation?: IGalleryMetadata; - path?: string; +export interface IGalleryMetadata { + id: string; + publisherId: string; + publisherDisplayName: string; +} + +export interface ILocalExtension { + id: string; + manifest: IExtensionManifest; + metadata: IGalleryMetadata; + path: string; + readmeUrl: string; } export const IExtensionManagementService = createDecorator('extensionManagementService'); export const IExtensionGalleryService = createDecorator('extensionGalleryService'); +export enum SortBy { + NoneOrRelevance = 0, + LastUpdatedDate = 1, + Title = 2, + PublisherName = 3, + InstallCount = 4, + PublishedDate = 5, + AverageRating = 6 +} + +export enum SortOrder { + Default = 0, + Ascending = 1, + Descending = 2 +} + export interface IQueryOptions { text?: string; ids?: string[]; + names?: string[]; pageSize?: number; -} - -export interface IQueryResult { - firstPage: IExtension[]; - total: number; - pageSize: number; - getPage(pageNumber: number): TPromise; + sortBy?: SortBy; + sortOrder?: SortOrder; } export interface IExtensionGalleryService { serviceId: ServiceIdentifier; isEnabled(): boolean; - query(options?: IQueryOptions): TPromise; + query(options?: IQueryOptions): TPromise>; } +export type InstallExtensionEvent = { id: string; gallery?: IGalleryExtension; }; +export type DidInstallExtensionEvent = { id: string; local?: ILocalExtension; error?: Error; }; + export interface IExtensionManagementService { serviceId: ServiceIdentifier; - onInstallExtension: Event; - onDidInstallExtension: Event<{ extension: IExtension; error?: Error; }>; - onUninstallExtension: Event; - onDidUninstallExtension: Event; - - install(extension: IExtension): TPromise; - install(zipPath: string): TPromise; - uninstall(extension: IExtension): TPromise; - getInstalled(includeDuplicateVersions?: boolean): TPromise; + + onInstallExtension: Event; + onDidInstallExtension: Event; + onUninstallExtension: Event; + onDidUninstallExtension: Event; + + install(extension: IGalleryExtension): TPromise; + install(zipPath: string): TPromise; + uninstall(extension: ILocalExtension): TPromise; + getInstalled(includeDuplicateVersions?: boolean): TPromise; } export const IExtensionTipsService = createDecorator('extensionTipsService'); export interface IExtensionTipsService { serviceId: ServiceIdentifier; - getRecommendations(): TPromise; + getRecommendations(): TPromise; } export const ExtensionsLabel = nls.localize('extensions', "Extensions"); diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index debb987fea18a76901f3af473b8f1aa4ca700575..c4f4c7bcca89e2fce688eb797a2a4fa115eb4542 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionManagementService, IExtension, IExtensionManifest } from './extensionManagement'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent } from './extensionManagement'; import Event from 'vs/base/common/event'; export interface IExtensionManagementChannel extends IChannel { @@ -15,9 +15,9 @@ export interface IExtensionManagementChannel extends IChannel { call(command: 'event:onDidInstallExtension'): TPromise; call(command: 'event:onUninstallExtension'): TPromise; call(command: 'event:onDidUninstallExtension'): TPromise; - call(command: 'install', extensionOrPath: IExtension | string): TPromise; - call(command: 'uninstall', extension: IExtension): TPromise; - call(command: 'getInstalled', includeDuplicateVersions: boolean): TPromise; + call(command: 'install', extensionOrPath: ILocalExtension | string): TPromise; + call(command: 'uninstall', extension: ILocalExtension): TPromise; + call(command: 'getInstalled', includeDuplicateVersions: boolean): TPromise; call(command: string, arg: any): TPromise; } @@ -44,29 +44,29 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer constructor(private channel: IExtensionManagementChannel) { } - private _onInstallExtension = eventFromCall(this.channel, 'event:onInstallExtension'); - get onInstallExtension(): Event { return this._onInstallExtension; } + private _onInstallExtension = eventFromCall(this.channel, 'event:onInstallExtension'); + get onInstallExtension(): Event { return this._onInstallExtension; } - private _onDidInstallExtension = eventFromCall(this.channel, 'event:onDidInstallExtension'); - get onDidInstallExtension(): Event<{ extension: IExtension; error?: Error; }> { return this._onDidInstallExtension; } + private _onDidInstallExtension = eventFromCall(this.channel, 'event:onDidInstallExtension'); + get onDidInstallExtension(): Event { return this._onDidInstallExtension; } - private _onUninstallExtension = eventFromCall(this.channel, 'event:onUninstallExtension'); - get onUninstallExtension(): Event { return this._onUninstallExtension; } + private _onUninstallExtension = eventFromCall(this.channel, 'event:onUninstallExtension'); + get onUninstallExtension(): Event { return this._onUninstallExtension; } - private _onDidUninstallExtension = eventFromCall(this.channel, 'event:onDidUninstallExtension'); - get onDidUninstallExtension(): Event { return this._onDidUninstallExtension; } + private _onDidUninstallExtension = eventFromCall(this.channel, 'event:onDidUninstallExtension'); + get onDidUninstallExtension(): Event { return this._onDidUninstallExtension; } - install(extension: IExtension): TPromise; - install(zipPath: string): TPromise; - install(arg: any): TPromise { + install(extension: IGalleryExtension): TPromise; + install(zipPath: string): TPromise; + install(arg: any): TPromise { return this.channel.call('install', arg); } - uninstall(extension: IExtension): TPromise { + uninstall(extension: ILocalExtension): TPromise { return this.channel.call('uninstall', extension); } - getInstalled(includeDuplicateVersions?: boolean): TPromise { + getInstalled(includeDuplicateVersions?: boolean): TPromise { return this.channel.call('getInstalled', includeDuplicateVersions); } } \ No newline at end of file diff --git a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts index ccb2c36292fe8c92ccc6a0e242ce97247f367fa7..054ad3ecf2343bc3883076b183887994fb65e433 100644 --- a/src/vs/platform/extensionManagement/node/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/node/extensionGalleryService.ts @@ -4,37 +4,37 @@ *--------------------------------------------------------------------------------------------*/ import { TPromise } from 'vs/base/common/winjs.base'; -import { IExtension, IExtensionGalleryService, IGalleryVersion, IQueryOptions, IQueryResult } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IGalleryExtension, IExtensionGalleryService, IGalleryVersion, IQueryOptions, SortBy, SortOrder } from 'vs/platform/extensionManagement/common/extensionManagement'; import { isUndefined } from 'vs/base/common/types'; import { assign, getOrDefault } from 'vs/base/common/objects'; import { IRequestService } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IPager } from 'vs/base/common/paging'; import pkg from 'vs/platform/package'; import product from 'vs/platform/product'; -export interface IGalleryExtensionFile { +interface IRawGalleryExtensionFile { assetType: string; } -export interface IGalleryExtensionVersion { +interface IRawGalleryExtensionVersion { version: string; lastUpdated: string; assetUri: string; - files: IGalleryExtensionFile[]; + files: IRawGalleryExtensionFile[]; } -export interface IGalleryExtension { +interface IRawGalleryExtension { extensionId: string; extensionName: string; displayName: string; shortDescription: string; publisher: { displayName: string, publisherId: string, publisherName: string; }; - versions: IGalleryExtensionVersion[]; - galleryApiUrl: string; - statistics: IGalleryExtensionStatistics[]; + versions: IRawGalleryExtensionVersion[]; + statistics: IRawGalleryExtensionStatistics[]; } -export interface IGalleryExtensionStatistics { +interface IRawGalleryExtensionStatistics { statisticName: string; value: number; } @@ -63,22 +63,6 @@ enum FilterType { SearchText = 10 } -enum SortBy { - NoneOrRelevance = 0, - LastUpdatedDate = 1, - Title = 2, - PublisherName = 3, - InstallCount = 4, - PublishedDate = 5, - AverageRating = 6 -} - -enum SortOrder { - Default = 0, - Ascending = 1, - Descending = 2 -} - interface ICriterium { filterType: FilterType; value?: string; @@ -130,8 +114,12 @@ class Query { return new Query(assign({}, this.state, { criteria })); } - withSort(sortBy: SortBy, sortOrder = SortOrder.Default): Query { - return new Query(assign({}, this.state, { sortBy, sortOrder })); + withSortBy(sortBy: SortBy): Query { + return new Query(assign({}, this.state, { sortBy })); + } + + withSortOrder(sortOrder): Query { + return new Query(assign({}, this.state, { sortOrder })); } withFlags(...flags: Flags[]): Query { @@ -152,39 +140,34 @@ class Query { } } -function getInstallCount(statistics: IGalleryExtensionStatistics[]): number { - if (!statistics) { - return 0; - } - - const result = statistics.filter(s => s.statisticName === 'install')[0]; +function getStatistic(statistics: IRawGalleryExtensionStatistics[], name: string): number { + const result = (statistics || []).filter(s => s.statisticName === name)[0]; return result ? result.value : 0; } -function toExtension(galleryExtension: IGalleryExtension, extensionsGalleryUrl: string, downloadHeaders: any): IExtension { +function toExtension(galleryExtension: IRawGalleryExtension, extensionsGalleryUrl: string, downloadHeaders: any): IGalleryExtension { const versions = galleryExtension.versions.map(v => ({ version: v.version, date: v.lastUpdated, downloadHeaders, downloadUrl: `${ v.assetUri }/Microsoft.VisualStudio.Services.VSIXPackage?install=true`, - manifestUrl: `${ v.assetUri }/Microsoft.VisualStudio.Code.Manifest` + manifestUrl: `${ v.assetUri }/Microsoft.VisualStudio.Code.Manifest`, + readmeUrl: `${ v.assetUri }/Microsoft.VisualStudio.Services.Content.Details`, + iconUrl: `${ v.assetUri }/Microsoft.VisualStudio.Services.Icons.Default` })); return { + id: galleryExtension.extensionId, name: galleryExtension.extensionName, - displayName: galleryExtension.displayName || galleryExtension.extensionName, + displayName: galleryExtension.displayName, + publisherId: galleryExtension.publisher.publisherId, publisher: galleryExtension.publisher.publisherName, - version: versions[0].version, - engines: { vscode: void 0 }, // TODO: ugly + publisherDisplayName: galleryExtension.publisher.displayName, description: galleryExtension.shortDescription || '', - galleryInformation: { - galleryApiUrl: extensionsGalleryUrl, - id: galleryExtension.extensionId, - publisherId: galleryExtension.publisher.publisherId, - publisherDisplayName: galleryExtension.publisher.displayName, - installCount: getInstallCount(galleryExtension.statistics), - versions - } + installCount: getStatistic(galleryExtension.statistics, 'install'), + rating: getStatistic(galleryExtension.statistics, 'averagerating'), + ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'), + versions }; } @@ -212,12 +195,12 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return !!this.extensionsGalleryUrl; } - query(options: IQueryOptions = {}): TPromise { + query(options: IQueryOptions = {}): TPromise> { if (!this.isEnabled()) { return TPromise.wrapError(new Error('No extension gallery service configured.')); } - const type = options.ids ? 'ids' : (options.text ? 'text' : 'all'); + const type = options.names ? 'ids' : (options.text ? 'text' : 'all'); const text = options.text || ''; const pageSize = getOrDefault(options, o => o.pageSize, 50); @@ -229,14 +212,21 @@ export class ExtensionGalleryService implements IExtensionGalleryService { .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code'); if (text) { - query = query.withFilter(FilterType.SearchText, text) - .withSort(SortBy.NoneOrRelevance); + query = query.withFilter(FilterType.SearchText, text).withSortBy(SortBy.NoneOrRelevance); } else if (options.ids) { - options.ids.forEach(id => { - query = query.withFilter(FilterType.ExtensionName, id); - }); + query = options.ids.reduce((query, id) => query.withFilter(FilterType.ExtensionId, id), query); + } else if (options.names) { + query = options.names.reduce((query, name) => query.withFilter(FilterType.ExtensionName, name), query); } else { - query = query.withSort(SortBy.InstallCount); + query = query.withSortBy(SortBy.InstallCount); + } + + if (typeof options.sortBy === 'number') { + query = query.withSortBy(options.sortBy); + } + + if (typeof options.sortOrder === 'number') { + query = query.withSortOrder(options.sortOrder); } return this.queryGallery(query).then(({ galleryExtensions, total }) => { @@ -251,7 +241,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { }); } - private queryGallery(query: Query): TPromise<{ galleryExtensions: IGalleryExtension[], total: number; }> { + private queryGallery(query: Query): TPromise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> { const data = JSON.stringify(query.raw); return this.getRequestHeaders() diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 029f959417ee530c27ed226eedb4f897f02d1bb7..c784b141ac344e5e66398074083abd66a40484d7 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -15,8 +15,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { flatten } from 'vs/base/common/arrays'; import { extract, buffer } from 'vs/base/node/zip'; import { Promise, TPromise } from 'vs/base/common/winjs.base'; -import { IExtensionManagementService, IExtension, IExtensionManifest, IGalleryMetadata, IGalleryVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { extensionEquals, getTelemetryData } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IExtensionIdentity, IExtensionManifest, IGalleryVersion, IGalleryMetadata, InstallExtensionEvent, DidInstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; import { download, json, IRequestOptions } from 'vs/base/node/request'; import { getProxyAgent } from 'vs/base/node/proxy'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -29,20 +28,23 @@ import { groupBy, values } from 'vs/base/common/collections'; import { isValidExtensionVersion } from 'vs/platform/extensions/node/extensionValidator'; import pkg from 'vs/platform/package'; -function parseManifest(raw: string): TPromise { +function parseManifest(raw: string): TPromise<{ manifest: IExtensionManifest; metadata: IGalleryMetadata; }> { return new Promise((c, e) => { try { - c(JSON.parse(raw)); + const manifest = JSON.parse(raw); + const metadata = manifest.__metadata || null; + delete manifest.__metadata; + c({ manifest, metadata }); } catch (err) { e(new Error(nls.localize('invalidManifest', "Extension invalid: package.json is not a JSON file."))); } }); } -function validate(zipPath: string, extension?: IExtension, version = extension && extension.version): TPromise { +function validate(zipPath: string, extension?: IExtensionIdentity, version?: string): TPromise { return buffer(zipPath, 'extension/package.json') .then(buffer => parseManifest(buffer.toString('utf8'))) - .then(manifest => { + .then(({ manifest }) => { if (extension) { if (extension.name !== manifest.name) { return Promise.wrapError(Error(nls.localize('invalidName', "Extension invalid: manifest name mismatch."))); @@ -61,28 +63,7 @@ function validate(zipPath: string, extension?: IExtension, version = extension & }); } -function createExtension(manifest: IExtensionManifest, galleryInformation?: IGalleryMetadata, path?: string): IExtension { - const extension: IExtension = { - name: manifest.name, - displayName: manifest.displayName || manifest.name, - publisher: manifest.publisher, - version: manifest.version, - engines: { vscode: manifest.engines.vscode }, - description: manifest.description || '' - }; - - if (galleryInformation) { - extension.galleryInformation = galleryInformation; - } - - if (path) { - extension.path = path; - } - - return extension; -} - -function getExtensionId(extension: IExtensionManifest, version = extension.version): string { +function getExtensionId(extension: IExtensionIdentity, version: string): string { return `${ extension.publisher }.${ extension.name }-${ version }`; } @@ -95,17 +76,17 @@ export class ExtensionManagementService implements IExtensionManagementService { private obsoleteFileLimiter: Limiter; private disposables: IDisposable[]; - private _onInstallExtension = new Emitter(); - onInstallExtension: Event = this._onInstallExtension.event; + private _onInstallExtension = new Emitter(); + onInstallExtension: Event = this._onInstallExtension.event; - private _onDidInstallExtension = new Emitter<{ extension: IExtension; isUpdate: boolean; error?: Error; }>(); - onDidInstallExtension: Event<{ extension: IExtension; isUpdate: boolean; error?: Error; }> = this._onDidInstallExtension.event; + private _onDidInstallExtension = new Emitter(); + onDidInstallExtension: Event = this._onDidInstallExtension.event; - private _onUninstallExtension = new Emitter(); - onUninstallExtension: Event = this._onUninstallExtension.event; + private _onUninstallExtension = new Emitter(); + onUninstallExtension: Event = this._onUninstallExtension.event; - private _onDidUninstallExtension = new Emitter(); - onDidUninstallExtension: Event = this._onDidUninstallExtension.event; + private _onDidUninstallExtension = new Emitter(); + onDidUninstallExtension: Event = this._onDidUninstallExtension.event; constructor( @IEnvironmentService private environmentService: IEnvironmentService, @@ -115,71 +96,73 @@ export class ExtensionManagementService implements IExtensionManagementService { this.obsoletePath = path.join(this.extensionsPath, '.obsolete'); this.obsoleteFileLimiter = new Limiter(1); - this.disposables = [ - this.onDidInstallExtension(({ extension, isUpdate, error }) => telemetryService.publicLog( - isUpdate ? 'extensionGallery2:update' : 'extensionGallery2:install', - assign(getTelemetryData(extension), { success: !error }) - )), - this.onDidUninstallExtension(extension => telemetryService.publicLog( - 'extensionGallery2:uninstall', - assign(getTelemetryData(extension), { success: true }) - )) - ]; + // this.disposables = [ + // this.onDidInstallExtension(({ extension, isUpdate, error }) => telemetryService.publicLog( + // isUpdate ? 'extensionGallery2:update' : 'extensionGallery2:install', + // assign(getTelemetryData(extension), { success: !error }) + // )), + // this.onDidUninstallExtension(extension => telemetryService.publicLog( + // 'extensionGallery2:uninstall', + // assign(getTelemetryData(extension), { success: true }) + // )) + // ]; } - install(extension: IExtension): TPromise; - install(zipPath: string): TPromise; - install(arg: any): TPromise { + install(extension: IGalleryExtension): TPromise; + install(zipPath: string): TPromise; + install(arg: any): TPromise { if (types.isString(arg)) { - return this.installFromZip(arg); + const zipPath = arg as string; + + return validate(zipPath).then(manifest => { + const id = getExtensionId(manifest, manifest.version); + this._onInstallExtension.fire({ id }); + + return this.installValidExtension(zipPath, id); + }); } - const extension = arg as IExtension; - return this.isObsolete(extension).then(obsolete => { + const extension = arg as IGalleryExtension; + const id = getExtensionId(extension, extension.versions[0].version); + this._onInstallExtension.fire({ id, gallery: extension }); + + return this.isObsolete(id).then(obsolete => { if (obsolete) { - return TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", extension.name))); + return TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", extension.displayName || extension.name))); } return this.installFromGallery(arg); }); } - private installFromGallery(extension: IExtension): TPromise { - const galleryInformation = extension.galleryInformation; - - if (!galleryInformation) { - return TPromise.wrapError(new Error(nls.localize('missingGalleryInformation', "Gallery information is missing"))); - } + private installFromGallery(extension: IGalleryExtension): TPromise { + return this.getLastValidExtensionVersion(extension).then(versionInfo => { + const version = versionInfo.version; + const url = versionInfo.downloadUrl; + const headers = versionInfo.downloadHeaders; + const zipPath = path.join(tmpdir(), extension.id); + const id = getExtensionId(extension, version); + const metadata = { + id: extension.id, + publisherId: extension.publisherId, + publisherDisplayName: extension.publisherDisplayName + }; - this._onInstallExtension.fire(extension); - - return this.getLastValidExtensionVersion(extension, extension.galleryInformation.versions).then(versionInfo => { - return this.getInstalled() - .then(installed => installed.some(e => extensionEquals(e, extension))) - .then(isUpdate => { - const version = versionInfo.version; - const url = versionInfo.downloadUrl; - const headers = versionInfo.downloadHeaders; - const zipPath = path.join(tmpdir(), galleryInformation.id); - const extensionPath = path.join(this.extensionsPath, getExtensionId(extension, version)); - const manifestPath = path.join(extensionPath, 'package.json'); - - return this.request(url) - .then(opts => assign(opts, { headers })) - .then(opts => download(zipPath, opts)) - .then(() => validate(zipPath, extension, version)) - .then(manifest => extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true }).then(() => manifest)) - .then(manifest => assign({ __metadata: galleryInformation }, manifest)) - .then(manifest => pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'))) - .then(() => { this._onDidInstallExtension.fire({ extension, isUpdate }); return extension; }) - .then(null, error => { this._onDidInstallExtension.fire({ extension, isUpdate, error }); return TPromise.wrapError(error); }); - }); + return this.request(url) + .then(opts => assign(opts, { headers })) + .then(opts => download(zipPath, opts)) + .then(() => validate(zipPath, extension, version)) + .then(() => this.installValidExtension(zipPath, id, metadata)); }); } - private getLastValidExtensionVersion(extension: IExtension, versions: IGalleryVersion[]): TPromise { + private getLastValidExtensionVersion(extension: IGalleryExtension): TPromise { + return this._getLastValidExtensionVersion(extension, extension.versions); + } + + private _getLastValidExtensionVersion(extension: IGalleryExtension, versions: IGalleryVersion[]): TPromise { if (!versions.length) { - return TPromise.wrapError(new Error(nls.localize('noCompatible', "Couldn't find a compatible version of {0} with this version of Code.", extension.displayName))); + return TPromise.wrapError(new Error(nls.localize('noCompatible', "Couldn't find a compatible version of {0} with this version of Code.", extension.displayName || extension.name))); } const version = versions[0]; @@ -193,42 +176,49 @@ export class ExtensionManagementService implements IExtensionManagementService { }; if (!isValidExtensionVersion(pkg.version, desc, [])) { - return this.getLastValidExtensionVersion(extension, versions.slice(1)); + return this._getLastValidExtensionVersion(extension, versions.slice(1)); } return version; }); } - private installFromZip(zipPath: string): TPromise { - return validate(zipPath).then(manifest => { - const extensionPath = path.join(this.extensionsPath, getExtensionId(manifest)); - this._onInstallExtension.fire(manifest); + private installValidExtension(zipPath: string, id: string, metadata: IGalleryMetadata = null): TPromise { + const extensionPath = path.join(this.extensionsPath, id); + const manifestPath = path.join(extensionPath, 'package.json'); - return this.getInstalled() - .then(installed => installed.some(e => extensionEquals(e, manifest))) - .then(isUpdate => { + return extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true }) + .then(() => pfs.readFile(manifestPath, 'utf8')) + .then(raw => parseManifest(raw)) + .then(({ manifest }) => { + return pfs.readdir(extensionPath).then(children => { + const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; + const readmeUrl = readme ? `file://${ extensionPath }/${ readme }` : null; - return extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true }) - .then(() => createExtension(manifest, ( manifest).__metadata, extensionPath)) - .then(extension => { this._onDidInstallExtension.fire({ extension, isUpdate }); return extension; }); - }); - }); + const local: ILocalExtension = { id, manifest, metadata, path: extensionPath, readmeUrl }; + const rawManifest = assign(manifest, { __metadata: metadata }); + + return pfs.writeFile(manifestPath, JSON.stringify(rawManifest, null, '\t')) + .then(() => this._onDidInstallExtension.fire({ id, local })); + }); + }) + .then(null, error => { this._onDidInstallExtension.fire({ id, error }); return TPromise.wrapError(error); }); } - uninstall(extension: IExtension): TPromise { - const extensionPath = extension.path || path.join(this.extensionsPath, getExtensionId(extension)); + uninstall(extension: ILocalExtension): TPromise { + const id = extension.id; + const extensionPath = path.join(this.extensionsPath, id); return pfs.exists(extensionPath) .then(exists => exists ? null : Promise.wrapError(new Error(nls.localize('notExists', "Could not find extension")))) - .then(() => this._onUninstallExtension.fire(extension)) - .then(() => this.setObsolete(extension)) + .then(() => this._onUninstallExtension.fire(id)) + .then(() => this.setObsolete(id)) .then(() => pfs.rimraf(extensionPath)) - .then(() => this.unsetObsolete(extension)) - .then(() => this._onDidUninstallExtension.fire(extension)); + .then(() => this.unsetObsolete(id)) + .then(() => this._onDidUninstallExtension.fire(id)); } - getInstalled(includeDuplicateVersions: boolean = false): TPromise { + getInstalled(includeDuplicateVersions: boolean = false): TPromise { const all = this.getAllInstalled(); if (includeDuplicateVersions) { @@ -236,35 +226,39 @@ export class ExtensionManagementService implements IExtensionManagementService { } return all.then(extensions => { - const byId = values(groupBy(extensions, p => `${ p.publisher }.${ p.name }`)); - return byId.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]); + const byId = values(groupBy(extensions, p => `${ p.manifest.publisher }.${ p.manifest.name }`)); + return byId.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]); }); } - private getAllInstalled(): TPromise { + private getAllInstalled(): TPromise { const limiter = new Limiter(10); return this.getObsoleteExtensions() .then(obsolete => { return pfs.readdir(this.extensionsPath) - .then(extensions => extensions.filter(e => !obsolete[e])) - .then(extensions => Promise.join(extensions.map(e => { - const extensionPath = path.join(this.extensionsPath, e); + .then(extensions => extensions.filter(id => !obsolete[id])) + .then(extensionIds => Promise.join(extensionIds.map(id => { + const extensionPath = path.join(this.extensionsPath, id); + + const each = () => pfs.readdir(extensionPath).then(children => { + const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; + const readmeUrl = readme ? `file://${ extensionPath }/${ readme }` : null; - return limiter.queue( - () => pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8') + return pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8') .then(raw => parseManifest(raw)) - .then(manifest => createExtension(manifest, ( manifest).__metadata, extensionPath)) - .then(null, () => null) - ); + .then(({ manifest, metadata }) => ({ id, manifest, metadata, path: extensionPath, readmeUrl })); + }).then(null, () => null); + + return limiter.queue(each); }))) .then(result => result.filter(a => !!a)); }); } removeDeprecatedExtensions(): TPromise { - const outdated = this.getOutdatedExtensions() - .then(extensions => extensions.map(e => getExtensionId(e))); + const outdated = this.getOutdatedExtensionIds() + .then(extensions => extensions.map(e => getExtensionId(e.manifest, e.manifest.version))); const obsolete = this.getObsoleteExtensions() .then(obsolete => Object.keys(obsolete)); @@ -279,28 +273,21 @@ export class ExtensionManagementService implements IExtensionManagementService { }); } - private getOutdatedExtensions(): TPromise { - return this.getAllInstalled().then(plugins => { - const byId = values(groupBy(plugins, p => `${ p.publisher }.${ p.name }`)); - const extensions = flatten(byId.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version)).slice(1))); - - return extensions - .filter(e => !!e.path); - }); + private getOutdatedExtensionIds(): TPromise { + return this.getAllInstalled() + .then(extensions => values(groupBy(extensions, p => `${ p.manifest.publisher }.${ p.manifest.name }`))) + .then(versions => flatten(versions.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1)))); } - private isObsolete(extension: IExtension): TPromise { - const id = getExtensionId(extension); + private isObsolete(id: string): TPromise { return this.withObsoleteExtensions(obsolete => !!obsolete[id]); } - private setObsolete(extension: IExtension): TPromise { - const id = getExtensionId(extension); + private setObsolete(id: string): TPromise { return this.withObsoleteExtensions(obsolete => assign(obsolete, { [id]: true })); } - private unsetObsolete(extension: IExtension): TPromise { - const id = getExtensionId(extension); + private unsetObsolete(id: string): TPromise { return this.withObsoleteExtensions(obsolete => delete obsolete[id]); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts index 38f64108a0cb196899faa69824159c5fe27304a0..06984461a1b30c00923400fafc80150dd2ca6677 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts @@ -5,48 +5,62 @@ 'use strict'; -import { IExtension, IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentity, ILocalExtension, IGalleryExtension, IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { TPromise } from 'vs/base/common/winjs.base'; import * as semver from 'semver'; -export function getExtensionId(extension: IExtension): string { - return `${ extension.publisher }.${ extension.name }`; -} - -export function extensionEquals(one: IExtension, other: IExtension): boolean { +export function extensionEquals(one: IExtensionIdentity, other: IExtensionIdentity): boolean { return one.publisher === other.publisher && one.name === other.name; } -export function getTelemetryData(extension: IExtension): any { - return { - id: getExtensionId(extension), - name: extension.name, - galleryId: extension.galleryInformation ? extension.galleryInformation.id : null, - publisherId: extension.galleryInformation ? extension.galleryInformation.publisherId : null, - publisherName: extension.publisher, - publisherDisplayName: extension.galleryInformation ? extension.galleryInformation.publisherDisplayName : null - }; +export function getTelemetryData(extension: ILocalExtension | IGalleryExtension): any { + const local = extension as ILocalExtension; + const gallery = extension as IGalleryExtension; + + if (local.path) { + return { + id: `${ local.manifest.publisher }.${ local.manifest.name }`, + name: local.manifest.name, + galleryId: local.metadata ? local.metadata.id : null, + publisherId: local.metadata ? local.metadata.publisherId : null, + publisherName: local.manifest.publisher, + publisherDisplayName: local.metadata ? local.metadata.publisherDisplayName : null + }; + } else { + return { + id: `${ gallery.publisher }.${ gallery.name }`, + name: gallery.name, + galleryId: gallery.id, + publisherId: gallery.publisherId, + publisherName: gallery.publisher, + publisherDisplayName: gallery.publisherDisplayName + }; + } } -export function getOutdatedExtensions(extensionsService: IExtensionManagementService, galleryService: IExtensionGalleryService): TPromise { +export function getOutdatedExtensions(extensionsService: IExtensionManagementService, galleryService: IExtensionGalleryService): TPromise { if (!galleryService.isEnabled()) { return TPromise.as([]); } return extensionsService.getInstalled().then(installed => { - const ids = installed.map(getExtensionId); + const names = installed.map(({ manifest }) => `${ manifest.publisher }.${ manifest.name }`); if (installed.length === 0) { return TPromise.as([]); } - return galleryService.query({ ids, pageSize: ids.length }).then(result => { + return galleryService.query({ names, pageSize: names.length }).then(result => { const available = result.firstPage; - return available.filter(extension => { - const local = installed.filter(local => extensionEquals(local, extension))[0]; - return local && semver.lt(local.version, extension.version); - }); + return available.map(extension => { + const local = installed.filter(local => extensionEquals(local.manifest, extension))[0]; + if (local && semver.lt(local.manifest.version, extension.versions[0].version)) { + return local; + } else { + return null; + } + }).filter(e => !!e); }); }); } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index c556c688779af62095fdc731e2835f12f28132aa..3853cff4dcf22483c1bbdbf64fe430c0190f5160 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -12,7 +12,7 @@ import {Builder, $} from 'vs/base/browser/builder'; import {Action} from 'vs/base/common/actions'; import errors = require('vs/base/common/errors'); import {ActionsOrientation, ActionBar, IActionItem} from 'vs/base/browser/ui/actionbar/actionbar'; -import {ToolBar} from 'vs/base/browser/ui/toolbar/toolbar'; +import {CONTEXT, ToolBar} from 'vs/base/browser/ui/toolbar/toolbar'; import {Registry} from 'vs/platform/platform'; import {CompositeEvent, EventType} from 'vs/workbench/common/events'; import {ViewletDescriptor, ViewletRegistry, Extensions as ViewletExtensions} from 'vs/workbench/browser/viewlet'; @@ -27,10 +27,15 @@ import {IInstantiationService} from 'vs/platform/instantiation/common/instantiat import {IMessageService} from 'vs/platform/message/common/message'; import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService'; +import {Scope, IActionBarRegistry, Extensions as ActionBarExtensions, prepareActions} from 'vs/workbench/browser/actionBarRegistry'; +import Severity from 'vs/base/common/severity'; +import {IAction} from 'vs/base/common/actions'; +import events = require('vs/base/common/events'); export class ActivitybarPart extends Part implements IActivityService { public serviceId = IActivityService; private viewletSwitcherBar: ActionBar; + private globalViewletSwitcherBar: ActionBar; private globalToolBar: ToolBar; private activityActionItems: { [actionId: string]: IActionItem; }; private viewletIdToActions: { [viewletId: string]: ActivityAction; }; @@ -103,7 +108,7 @@ export class ActivitybarPart extends Part implements IActivityService { this.createViewletSwitcher($result.clone()); // Bottom Toolbar with action items for global actions - // this.createGlobalToolBarArea($result.clone()); // not used currently + this.createGlobalToolBarArea($result.clone()); // not used currently return $result; } @@ -118,106 +123,125 @@ export class ActivitybarPart extends Part implements IActivityService { }); this.viewletSwitcherBar.getContainer().addClass('position-top'); + // Global viewlet switcher is right below + this.globalViewletSwitcherBar = new ActionBar(div, { + actionItemProvider: (action: Action) => this.activityActionItems[action.id], + orientation: ActionsOrientation.VERTICAL, + ariaLabel: nls.localize('globalActivityBarAriaLabel', "Active Global View Switcher") + }); + this.globalViewletSwitcherBar.getContainer().addClass('position-bottom'); + // Build Viewlet Actions in correct order - let activeViewlet = this.viewletService.getActiveViewlet(); - let registry = (Registry.as(ViewletExtensions.Viewlets)); - let viewletActions: Action[] = registry.getViewlets() - .sort((v1: ViewletDescriptor, v2: ViewletDescriptor) => v1.order - v2.order) - .map((viewlet: ViewletDescriptor) => { - let action = this.instantiationService.createInstance(ViewletActivityAction, viewlet.id + '.activity-bar-action', viewlet); + const activeViewlet = this.viewletService.getActiveViewlet(); + const registry = (Registry.as(ViewletExtensions.Viewlets)); + const allViewletActions = registry.getViewlets(); + const actionOptions = { label: true, icon: true }; + + const toAction = (viewlet: ViewletDescriptor) => { + let action = this.instantiationService.createInstance(ViewletActivityAction, viewlet.id + '.activity-bar-action', viewlet); + + let keybinding: string = null; + let keys = this.keybindingService.lookupKeybindings(viewlet.id).map(k => this.keybindingService.getLabelFor(k)); + if (keys && keys.length) { + keybinding = keys[0]; + } + + this.activityActionItems[action.id] = new ActivityActionItem(action, viewlet.name, keybinding); + this.viewletIdToActions[viewlet.id] = action; + + // Mark active viewlet action as active + if (activeViewlet && activeViewlet.getId() === viewlet.id) { + action.activate(); + } + + return action; + }; + + // Add to viewlet switcher + this.viewletSwitcherBar.push(allViewletActions + .filter(v => !v.isGlobal) + .sort((v1, v2) => v1.order - v2.order) + .map(toAction) + , actionOptions); + + // Add to viewlet switcher + this.globalViewletSwitcherBar.push(allViewletActions + .filter(v => v.isGlobal) + .sort((v1, v2) => v1.order - v2.order) + .map(toAction), + actionOptions); + } + + private createGlobalToolBarArea(div: Builder): void { + + // Global action bar is on the bottom + this.globalToolBar = new ToolBar(div.getHTMLElement(), this.contextMenuService, { + actionItemProvider: (action: Action) => this.activityActionItems[action.id], + orientation: ActionsOrientation.VERTICAL + }); + this.globalToolBar.getContainer().addClass('global'); + this.globalToolBar.actionRunner.addListener2(events.EventType.RUN, (e: any) => { + + // Check for Error + if (e.error && !errors.isPromiseCanceledError(e.error)) { + this.messageService.show(Severity.Error, e.error); + } + + // Log in telemetry + if (this.telemetryService) { + this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'activityBar' }); + } + }); + + // Build Global Actions in correct order + let primaryActions = this.getGlobalActions(true); + let secondaryActions = this.getGlobalActions(false); + + if (primaryActions.length + secondaryActions.length > 0) { + this.globalToolBar.getContainer().addClass('position-bottom'); + } + + // Add to global action bar + this.globalToolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions))(); + } + + private getGlobalActions(primary: boolean): IAction[] { + let actionBarRegistry = Registry.as(ActionBarExtensions.Actionbar); + + // Collect actions from actionbar contributor + let actions: IAction[]; + if (primary) { + actions = actionBarRegistry.getActionBarActionsForContext(Scope.GLOBAL, CONTEXT); + } else { + actions = actionBarRegistry.getSecondaryActionBarActionsForContext(Scope.GLOBAL, CONTEXT); + } + + return actions.map((action: Action) => { + if (primary) { let keybinding: string = null; - let keys = this.keybindingService.lookupKeybindings(viewlet.id).map(k => this.keybindingService.getLabelFor(k)); + let keys = this.keybindingService.lookupKeybindings(action.id).map(k => this.keybindingService.getLabelFor(k)); if (keys && keys.length) { keybinding = keys[0]; } - this.activityActionItems[action.id] = new ActivityActionItem(action, viewlet.name, keybinding); - this.viewletIdToActions[viewlet.id] = action; + let actionItem = actionBarRegistry.getActionItemForContext(Scope.GLOBAL, CONTEXT, action); - // Mark active viewlet action as active - if (activeViewlet && activeViewlet.getId() === viewlet.id) { - action.activate(); + if (!actionItem) { + actionItem = new ActivityActionItem(action, action.label, keybinding); } - return action; + if (actionItem instanceof ActivityActionItem) { + ( actionItem).keybinding = keybinding; + } + + this.activityActionItems[action.id] = actionItem; } - ); - // Add to viewlet switcher - this.viewletSwitcherBar.push(viewletActions, { label: true, icon: true }); + return action; + }); } - // private createGlobalToolBarArea(div: Builder): void { - - // // Global action bar is on the bottom - // this.globalToolBar = new ToolBar(div.getHTMLElement(), this.contextMenuService, { - // actionItemProvider: (action: Action) => this.activityActionItems[action.id], - // orientation: ActionsOrientation.VERTICAL - // }); - // this.globalToolBar.getContainer().addClass('global'); - - // this.globalToolBar.actionRunner.addListener(events.EventType.RUN, (e: any) => { - - // // Check for Error - // if (e.error && !errors.isPromiseCanceledError(e.error)) { - // this.messageService.show(Severity.Error, e.error); - // } - - // // Log in telemetry - // if (this.telemetryService) { - // this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'activityBar' }); - // } - // }); - - // // Build Global Actions in correct order - // let primaryActions = this.getGlobalActions(true); - // let secondaryActions = this.getGlobalActions(false); - - // if (primaryActions.length + secondaryActions.length > 0) { - // this.globalToolBar.getContainer().addClass('position-bottom'); - // } - - // // Add to global action bar - // this.globalToolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions))(); - // } - - // private getGlobalActions(primary: boolean): IAction[] { - // let actionBarRegistry = Registry.as(ActionBarExtensions.Actionbar); - - // // Collect actions from actionbar contributor - // let actions: IAction[]; - // if (primary) { - // actions = actionBarRegistry.getActionBarActionsForContext(Scope.GLOBAL, CONTEXT); - // } else { - // actions = actionBarRegistry.getSecondaryActionBarActionsForContext(Scope.GLOBAL, CONTEXT); - // } - - // return actions.map((action: Action) => { - // if (primary) { - // let keybinding: string = null; - // let keys = this.keybindingService.lookupKeybindings(action.id).map(k => this.keybindingService.getLabelFor(k)); - // if (keys && keys.length) { - // keybinding = keys[0]; - // } - - // let actionItem = actionBarRegistry.getActionItemForContext(Scope.GLOBAL, CONTEXT, action); - - // if (!actionItem) { - // actionItem = new ActivityActionItem(action, action.label, keybinding); - // } - - // if (actionItem instanceof ActivityActionItem) { - // ( actionItem).keybinding = keybinding; - // } - - // this.activityActionItems[action.id] = actionItem; - // } - - // return action; - // }); - // } - public dispose(): void { if (this.viewletSwitcherBar) { this.viewletSwitcherBar.dispose(); diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 93331ce5d74309b69f0160bd0d3b28a6df047d6b..27e52d0c7f5ae4fd3084ae4277927decff9f1804 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -151,7 +151,14 @@ export abstract class ViewerViewlet extends Viewlet { /** * A viewlet descriptor is a leightweight descriptor of a viewlet in the workbench. */ -export class ViewletDescriptor extends CompositeDescriptor { } +export class ViewletDescriptor extends CompositeDescriptor { + public isGlobal: boolean; + + constructor(moduleId: string, ctorName: string, id: string, name: string, cssClass?: string, order?: number, isGlobal?: boolean) { + super(moduleId, ctorName, id, name, cssClass, order); + this.isGlobal = isGlobal || false; + } +} export const Extensions = { Viewlets: 'workbench.contributions.viewlets' @@ -171,14 +178,14 @@ export class ViewletRegistry extends CompositeRegistry { * Returns the viewlet descriptor for the given id or null if none. */ public getViewlet(id: string): ViewletDescriptor { - return this.getComposite(id); + return this.getComposite(id) as ViewletDescriptor; } /** * Returns an array of registered viewlets known to the platform. */ public getViewlets(): ViewletDescriptor[] { - return this.getComposits(); + return this.getComposits() as ViewletDescriptor[]; } /** diff --git a/src/vs/workbench/parts/extensions/common/extensionsInput.ts b/src/vs/workbench/parts/extensions/common/extensionsInput.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6121bde693d8a0c509195d1c63a3f2bfd4fb47a --- /dev/null +++ b/src/vs/workbench/parts/extensions/common/extensionsInput.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { EditorInput } from 'vs/workbench/common/editor'; + +// TODO@joao: layer breaker +import { IExtension, ExtensionsModel } from '../electron-browser/extensionsModel'; + +export class ExtensionsInput extends EditorInput { + + static get ID() { return 'workbench.extensions.input2'; } + get model(): ExtensionsModel { return this._model; } + get extension(): IExtension { return this._extension; } + + constructor(private _model: ExtensionsModel, private _extension: IExtension) { + super(); + } + + getTypeId(): string { + return ExtensionsInput.ID; + } + + getName(): string { + return this.extension.displayName; + } + + matches(other: any): boolean { + if (!(other instanceof ExtensionsInput)) { + return false; + } + + const otherExtensionInput = other as ExtensionsInput; + + // TODO@joao is this correct? + return this.extension === otherExtensionInput.extension; + } + + resolve(refresh?: boolean): TPromise { + return TPromise.as(null); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts new file mode 100644 index 0000000000000000000000000000000000000000..4597e07d319dd1acbe4144de1a3c6aa7690eb15b --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./media/extensionEditor'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { marked } from 'vs/base/common/marked/marked'; +import { IDisposable, empty, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { Builder } from 'vs/base/browser/builder'; +import { append, emmet as $, addClass, removeClass, finalHandler } from 'vs/base/browser/dom'; +import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { IViewletService } from 'vs/workbench/services/viewlet/common/viewletService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsInput } from '../common/extensionsInput'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ITemplateData } from './extensionsList'; +import { RatingsWidget, Label } from './extensionsWidgets'; +import { EditorOptions } from 'vs/workbench/common/editor'; +import { shell } from 'electron'; +import product from 'vs/platform/product'; +import { IExtensionsViewlet } from './extensions'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { CombinedInstallAction, UpdateAction } from './extensionsActions'; + +const actionOptions = { icon: true, label: true }; + +export class ExtensionEditor extends BaseEditor { + + static ID: string = 'workbench.editor.extension'; + + private icon: HTMLElement; + private name: HTMLAnchorElement; + private publisher: HTMLAnchorElement; + private installCount: HTMLElement; + private rating: HTMLAnchorElement; + private description: HTMLElement; + private actionBar: ActionBar; + private body: HTMLElement; + + private _highlight: ITemplateData; + private highlightDisposable: IDisposable; + + private transientDisposables: IDisposable[]; + private disposables: IDisposable[]; + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IExtensionGalleryService private galleryService: IExtensionGalleryService, + @IConfigurationService private configurationService: IConfigurationService, + @IInstantiationService private instantiationService: IInstantiationService, + @IRequestService private requestService: IRequestService, + @IViewletService private viewletService: IViewletService + ) { + super(ExtensionEditor.ID, telemetryService); + this._highlight = null; + this.highlightDisposable = empty; + this.disposables = []; + } + + createEditor(parent: Builder): void { + const container = parent.getHTMLElement(); + + const root = append(container, $('.extension-editor')); + const header = append(root, $('.header')); + + this.icon = append(header, $('.icon')); + + const details = append(header, $('.details')); + this.name = append(details, $('a.name')); + this.name.href = '#'; + + const subtitle = append(details, $('.subtitle')); + this.publisher = append(subtitle, $('a.publisher')); + this.publisher.href = '#'; + + const install = append(subtitle, $('span.install')); + append(install, $('span.octicon.octicon-cloud-download')); + this.installCount = append(install, $('span.count')); + + this.rating = append(subtitle, $('a.rating')); + this.rating.href = '#'; + + this.description = append(details, $('.description')); + + const actions = append(details, $('.actions')); + this.actionBar = new ActionBar(actions, { animated: false }); + + this.body = append(root, $('.body')); + } + + setInput(input: ExtensionsInput, options: EditorOptions): TPromise { + this.transientDisposables = dispose(this.transientDisposables); + + this.body.innerHTML = ''; + + let promise = TPromise.as(null); + const extension = input.extension; + + this.icon.style.backgroundImage = `url("${ extension.iconUrl }")`; + this.name.textContent = extension.displayName; + this.publisher.textContent = extension.publisherDisplayName; + this.description.textContent = extension.description; + + if (product.extensionsGallery) { + const extensionUrl = `${ product.extensionsGallery.itemUrl }?itemName=${ extension.publisher }.${ extension.name }`; + + this.name.onclick = finalHandler(() => shell.openExternal(extensionUrl)); + this.rating.onclick = finalHandler(() => shell.openExternal(`${ extensionUrl }#review-details`)); + this.publisher.onclick = finalHandler(() => { + this.viewletService.openViewlet('workbench.viewlet.extensions', true) + .then(viewlet => viewlet as IExtensionsViewlet) + .done(viewlet => viewlet.search(`publisher:"${ extension.publisherDisplayName }"`, true)); + }); + } + + const install = new Label(this.installCount, input.model, extension, e => `${ e.installCount }`); + this.transientDisposables.push(install); + + const ratings = new RatingsWidget(this.rating, input.model, extension); + this.transientDisposables.push(ratings); + + const installAction = new CombinedInstallAction(input.model, extension); + const updateAction = new UpdateAction(input.model, extension); + this.actionBar.clear(); + this.actionBar.push([updateAction, installAction], actionOptions); + this.transientDisposables.push(updateAction, installAction); + + addClass(this.body, 'loading'); + + if (extension.readmeUrl) { + promise = super.setInput(input, options) + .then(() => this.requestService.makeRequest({ url: extension.readmeUrl })) + .then(response => response.responseText) + .then(marked.parse) + .then(html => this.body.innerHTML = html) + .then(null, () => null) + .then(() => removeClass(this.body, 'loading')); + } + + this.transientDisposables.push(toDisposable(() => promise.cancel())); + + return TPromise.as(null); + } + + layout(): void { + return; + } + + dispose(): void { + this._highlight = null; + this.transientDisposables = dispose(this.transientDisposables); + this.disposables = dispose(this.disposables); + super.dispose(); + } +} diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index 276ffc285b87f027467523975bb93590e69f75b7..e7d7be2de8640fc681d21022d20ac3098a088588 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -8,7 +8,7 @@ import {forEach} from 'vs/base/common/collections'; import {IDisposable, dispose} from 'vs/base/common/lifecycle'; import {TPromise as Promise} from 'vs/base/common/winjs.base'; import {match} from 'vs/base/common/glob'; -import {IExtensionGalleryService, IExtensionTipsService, IExtension} from 'vs/platform/extensionManagement/common/extensionManagement'; +import {IExtensionGalleryService, IExtensionTipsService, ILocalExtension} from 'vs/platform/extensionManagement/common/extensionManagement'; import {IModelService} from 'vs/editor/common/services/modelService'; import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage'; import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; @@ -62,10 +62,10 @@ export class ExtensionTipsService implements IExtensionTipsService { } } - getRecommendations(): Promise { - const ids = Object.keys(this._recommendations); + getRecommendations(): Promise { + const names = Object.keys(this._recommendations); - return this._galleryService.query({ ids, pageSize: ids.length }) + return this._galleryService.query({ names, pageSize: names.length }) .then(result => result.firstPage, () => []); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts index c24649dc1bff84b22691e4b37a2f0caea9fdfca7..7652935305beaccc641bcc782c9e2a5ce1172959 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts @@ -4,16 +4,34 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/extensions'; +import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/platform'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IStatusbarRegistry, Extensions as StatusbarExtensions, StatusbarItemDescriptor, StatusbarAlignment } from 'vs/workbench/browser/parts/statusbar/statusbar'; -import { ExtensionsStatusbarItem } from 'vs/workbench/parts/extensions/electron-browser/extensionsWidgets'; import { IExtensionGalleryService, IExtensionTipsService, ExtensionsLabel, ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/node/extensionGalleryService'; import { ExtensionTipsService } from 'vs/workbench/parts/extensions/electron-browser/extensionTipsService'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { ExtensionsWorkbenchExtension } from 'vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchExtension'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/parts/output/common/output'; +import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput'; +// import { EditorInput } from 'vs/workbench/common/editor'; +// import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; + +// class ExtensionsInputFactory implements IEditorInputFactory { + +// constructor() {} + +// public serialize(editorInput: EditorInput): string { +// return ''; +// } + +// public deserialize(instantiationService: IInstantiationService, resourceRaw: string): EditorInput { +// return instantiationService.createInstance(ExtensionsInput); +// } +// } registerSingleton(IExtensionGalleryService, ExtensionGalleryService); registerSingleton(IExtensionTipsService, ExtensionTipsService); @@ -21,8 +39,31 @@ registerSingleton(IExtensionTipsService, ExtensionTipsService); Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(ExtensionsWorkbenchExtension); -Registry.as(StatusbarExtensions.Statusbar) - .registerStatusbarItem(new StatusbarItemDescriptor(ExtensionsStatusbarItem, StatusbarAlignment.LEFT,10000)); - Registry.as(OutputExtensions.OutputChannels) - .registerChannel(ExtensionsChannelId, ExtensionsLabel); \ No newline at end of file + .registerChannel(ExtensionsChannelId, ExtensionsLabel); + +// Registry.as(EditorExtensions.Editors) +// .registerEditorInputFactory(ExtensionsInput.ID, ExtensionsInputFactory); + +const editorDescriptor = new EditorDescriptor( + 'workbench.editor.extension', + localize('extension', "Extension"), + 'vs/workbench/parts/extensions/electron-browser/extensionEditor', + 'ExtensionEditor' +); + +Registry.as(EditorExtensions.Editors) + .registerEditor(editorDescriptor, [new SyncDescriptor(ExtensionsInput)]); + +const viewletDescriptor = new ViewletDescriptor( + 'vs/workbench/parts/extensions/electron-browser/extensionsViewlet', + 'ExtensionsViewlet', + 'workbench.viewlet.extensions', + localize('extensions', "Extensions"), + 'extensions', + 100, + true +); + +Registry.as(ViewletExtensions.Viewlets) + .registerViewlet(viewletDescriptor); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensions.ts new file mode 100644 index 0000000000000000000000000000000000000000..6888cfe54dff915135ff47e3c1de0aac6e4ac55e --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import {IViewlet} from 'vs/workbench/common/viewlet'; + +export interface IExtensionsViewlet extends IViewlet { + search(text: string, immediate?: boolean): void; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts index a7e0780b4eaa80270925ea5f3621a05de7b98580..b3ef7418e98460bdb12c3154cc0989b8e7f222de 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsActions.ts @@ -3,216 +3,360 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/extensionActions'; import nls = require('vs/nls'); -import { Promise, TPromise } from 'vs/base/common/winjs.base'; +import { TPromise } from 'vs/base/common/winjs.base'; import { Action } from 'vs/base/common/actions'; -import { assign } from 'vs/base/common/objects'; -import Severity from 'vs/base/common/severity'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IMessageService } from 'vs/platform/message/common/message'; -import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions'; -import { IExtensionManagementService, IExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { extensionEquals, getTelemetryData } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; -import { IQuickOpenService } from 'vs/workbench/services/quickopen/common/quickOpenService'; +// import { assign } from 'vs/base/common/objects'; +// import Severity from 'vs/base/common/severity'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +// import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +// import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +// import { IMessageService } from 'vs/platform/message/common/message'; +// import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions'; +import { IExtension, ExtensionsModel, ExtensionState } from './extensionsModel'; +// import { extensionEquals, getTelemetryData } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; +// import { IQuickOpenService } from 'vs/workbench/services/quickopen/common/quickOpenService'; + +// const CloseAction = new Action('action.close', nls.localize('close', "Close")); + +// export class ListExtensionsAction extends Action { + +// static ID = 'workbench.extensions.action.listExtensions'; +// static LABEL = nls.localize('showInstalledExtensions', "Show Installed Extensions"); + +// constructor( +// id: string, +// label: string, +// @IExtensionManagementService private extensionManagementService: IExtensionManagementService, +// @IQuickOpenService private quickOpenService: IQuickOpenService +// ) { +// super(id, label, null, true); +// } + +// run(): Promise { +// return this.quickOpenService.show('ext '); +// } + +// protected isEnabled(): boolean { +// return true; +// } +// } + +// export class InstallExtensionAction extends Action { + +// static ID = 'workbench.extensions.action.installExtension'; +// static LABEL = nls.localize('installExtension', "Install Extension"); + +// constructor( +// id: string, +// label: string, +// @IExtensionManagementService private extensionManagementService: IExtensionManagementService, +// @IQuickOpenService private quickOpenService: IQuickOpenService +// ) { +// super(id, label, null, true); +// } + +// run(): Promise { +// return this.quickOpenService.show('ext install '); +// } + +// protected isEnabled(): boolean { +// return true; +// } +// } + +// export class ListOutdatedExtensionsAction extends Action { + +// static ID = 'workbench.extensions.action.listOutdatedExtensions'; +// static LABEL = nls.localize('showOutdatedExtensions', "Show Outdated Extensions"); + +// constructor( +// id: string, +// label: string, +// @IExtensionManagementService private extensionManagementService: IExtensionManagementService, +// @IQuickOpenService private quickOpenService: IQuickOpenService +// ) { +// super(id, label, null, true); +// } + +// run(): Promise { +// return this.quickOpenService.show('ext update '); +// } + +// protected isEnabled(): boolean { +// return true; +// } +// } + +// export class ListSuggestedExtensionsAction extends Action { + +// static ID = 'workbench.extensions.action.listSuggestedExtensions'; +// static LABEL = nls.localize('showExtensionRecommendations', "Show Extension Recommendations"); + +// constructor( +// id: string, +// label: string, +// @IExtensionManagementService private extensionManagementService: IExtensionManagementService, +// @IQuickOpenService private quickOpenService: IQuickOpenService +// ) { +// super(id, label, null, true); +// } + +// run(): Promise { +// return this.quickOpenService.show('ext recommend '); +// } + +// protected isEnabled(): boolean { +// return true; +// } +// } -const CloseAction = new Action('action.close', nls.localize('close', "Close")); - -export class ListExtensionsAction extends Action { +export class InstallAction extends Action { - static ID = 'workbench.extensions.action.listExtensions'; - static LABEL = nls.localize('showInstalledExtensions', "Show Installed Extensions"); + private disposables: IDisposable[] = []; - constructor( - id: string, - label: string, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IQuickOpenService private quickOpenService: IQuickOpenService - ) { - super(id, label, null, true); - } + constructor(private model: ExtensionsModel, private extension: IExtension) { + super('extensions.install', nls.localize('installAction', "Install"), 'extension-action install', false); - public run(): Promise { - return this.quickOpenService.show('ext '); + this.disposables.push(this.model.onChange(() => this.updateEnablement())); + this.updateEnablement(); } - protected isEnabled(): boolean { - return true; + private updateEnablement(): void { + this.enabled = this.model.canInstall(this.extension) && this.extension.state === ExtensionState.Uninstalled; } -} -export class InstallExtensionAction extends Action { + run(): TPromise { + return this.model.install(this.extension); - static ID = 'workbench.extensions.action.installExtension'; - static LABEL = nls.localize('installExtension', "Install Extension"); + // this.enabled = false; - constructor( - id: string, - label: string, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IQuickOpenService private quickOpenService: IQuickOpenService - ) { - super(id, label, null, true); + // return this.extensionManagementService.getInstalled() + // .then(installed => installed.some(({ manifest }) => extensionEquals(manifest, extension))) + // .then(isUpdate => { + // return this.extensionManagementService + // .install(extension) + // .then(() => this.onSuccess(extension, isUpdate), err => this.onError(err, extension, isUpdate)) + // .then(() => this.enabled = true) + // .then(() => null); + // }); } - public run(): Promise { - return this.quickOpenService.show('ext install '); - } - - protected isEnabled(): boolean { - return true; + // private onSuccess(extension: IGalleryExtension, isUpdate: boolean) { + // this.reportTelemetry(extension, isUpdate, true); + // this.messageService.show(Severity.Info, { + // message: nls.localize('success-installed', "'{0}' was successfully installed. Restart to enable it.", extension.displayName || extension.name), + // actions: [ + // CloseAction, + // this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, nls.localize('restartNow', "Restart Now")) + // ] + // }); + // } + + // private onError(err: Error, extension: IGalleryExtension, isUpdate: boolean) { + // this.reportTelemetry(extension, isUpdate, false); + // this.messageService.show(Severity.Error, err); + // } + + // private reportTelemetry(extension: IGalleryExtension, isUpdate: boolean, success: boolean) { + // const event = isUpdate ? 'extensionGallery:update' : 'extensionGallery:install'; + // const data = assign(getTelemetryData(extension), { success }); + + // this.telemetryService.publicLog(event, data); + // } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); } } -export class ListOutdatedExtensionsAction extends Action { +export class UninstallAction extends Action { - static ID = 'workbench.extensions.action.listOutdatedExtensions'; - static LABEL = nls.localize('showOutdatedExtensions', "Show Outdated Extensions"); + private disposables: IDisposable[] = []; - constructor( - id: string, - label: string, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IQuickOpenService private quickOpenService: IQuickOpenService - ) { - super(id, label, null, true); - } + constructor(private model: ExtensionsModel, private extension: IExtension) { + super('extensions.uninstall', nls.localize('uninstall', "Uninstall"), 'extension-action uninstall', false); - public run(): Promise { - return this.quickOpenService.show('ext update '); + this.disposables.push(this.model.onChange(() => this.updateEnablement())); + this.updateEnablement(); } - protected isEnabled(): boolean { - return true; + private updateEnablement(): void { + this.enabled = this.extension.state === ExtensionState.Installed; } -} -export class ListSuggestedExtensionsAction extends Action { + run(): TPromise { + // const name = extension.manifest.displayName || extension.manifest.name; - static ID = 'workbench.extensions.action.listSuggestedExtensions'; - static LABEL = nls.localize('showExtensionRecommendations', "Show Extension Recommendations"); + if (!window.confirm(nls.localize('deleteSure', "Are you sure you want to uninstall '{0}'?", this.extension.displayName))) { + return TPromise.as(null); + } - constructor( - id: string, - label: string, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IQuickOpenService private quickOpenService: IQuickOpenService - ) { - super(id, label, null, true); - } + return this.model.uninstall(this.extension); - public run(): Promise { - return this.quickOpenService.show('ext recommend '); + // this.enabled = false; + + // return this.extensionManagementService.getInstalled().then(localExtensions => { + // const [local] = localExtensions.filter(local => extensionEquals(local.manifest, extension.manifest)); + + // if (!local) { + // return TPromise.wrapError(nls.localize('notFound', "Extension '{0}' not installed.", name)); + // } + + // return this.extensionManagementService.uninstall(local) + // .then(() => this.onSuccess(local), err => this.onError(err, local)) + // .then(() => this.enabled = true) + // .then(() => null); + // }); } - protected isEnabled(): boolean { - return true; +// private onSuccess(extension: ILocalExtension) { +// const name = extension.manifest.displayName || extension.manifest.name; +// this.reportTelemetry(extension, true); + +// this.messageService.show(Severity.Info, { +// message: nls.localize('success-uninstalled', "'{0}' was successfully uninstalled. Restart to deactivate it.", name), +// actions: [ +// CloseAction, +// this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, nls.localize('restartNow2', "Restart Now")) +// ] +// }); +// } + +// private onError(err: Error, extension: ILocalExtension) { +// this.reportTelemetry(extension, false); +// this.messageService.show(Severity.Error, err); +// } + +// private reportTelemetry(extension: ILocalExtension, success: boolean) { +// const data = assign(getTelemetryData(extension), { success }); + +// this.telemetryService.publicLog('extensionGallery:uninstall', data); +// } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); } } -export class InstallAction extends Action { +export class CombinedInstallAction extends Action { - constructor( - label: string, - @IQuickOpenService protected quickOpenService: IQuickOpenService, - @IExtensionManagementService protected extensionManagementService: IExtensionManagementService, - @IMessageService protected messageService: IMessageService, - @ITelemetryService protected telemetryService: ITelemetryService, - @IInstantiationService protected instantiationService: IInstantiationService - ) { - super('extensions.install', label, 'octicon octicon-cloud-download', true); - } + private installAction: InstallAction; + private uninstallAction: UninstallAction; + private disposables: IDisposable[] = []; - public run(extension: IExtension): TPromise { - this.enabled = false; + constructor(private model: ExtensionsModel, private extension: IExtension) { + super('extensions.combinedInstall', '', '', false); - return this.extensionManagementService.getInstalled() - .then(installed => installed.some(e => extensionEquals(e, extension))) - .then(isUpdate => { - return this.extensionManagementService - .install(extension) - .then(() => this.onSuccess(extension, isUpdate), err => this.onError(err, extension, isUpdate)) - .then(() => this.enabled = true) - .then(() => null); - }); - } + this.installAction = new InstallAction(model, extension); + this.uninstallAction = new UninstallAction(model, extension); + this.disposables.push(this.installAction, this.uninstallAction); - private onSuccess(extension: IExtension, isUpdate: boolean) { - this.reportTelemetry(extension, isUpdate, true); - this.messageService.show(Severity.Info, { - message: nls.localize('success-installed', "'{0}' was successfully installed. Restart to enable it.", extension.displayName), - actions: [ - CloseAction, - this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, nls.localize('restartNow', "Restart Now")) - ] - }); + this.disposables.push(this.installAction.addListener2(Action.ENABLED, () => this.update())); + this.disposables.push(this.uninstallAction.addListener2(Action.ENABLED, () => this.update())); + this.update(); } - private onError(err: Error, extension: IExtension, isUpdate: boolean) { - this.reportTelemetry(extension, isUpdate, false); - this.messageService.show(Severity.Error, err); + private update(): void { + if (this.installAction.enabled) { + this.enabled = true; + this.label = this.installAction.label; + this.class = this.installAction.class; + } else if (this.uninstallAction.enabled) { + this.enabled = true; + this.label = this.uninstallAction.label; + this.class = this.uninstallAction.class; + } else if (this.extension.state === ExtensionState.Installing) { + this.enabled = false; + this.label = this.installAction.label; + this.class = this.installAction.class; + } else { + this.enabled = false; + } } - private reportTelemetry(extension: IExtension, isUpdate: boolean, success: boolean) { - const event = isUpdate ? 'extensionGallery:update' : 'extensionGallery:install'; - const data = assign(getTelemetryData(extension), { success }); + run(): TPromise { + if (this.installAction.enabled) { + return this.installAction.run(); + } else if (this.uninstallAction.enabled) { + return this.uninstallAction.run(); + } - this.telemetryService.publicLog(event, data); + return TPromise.as(null); } -} - -export class UninstallAction extends Action { - constructor( - @IQuickOpenService protected quickOpenService: IQuickOpenService, - @IExtensionManagementService protected extensionManagementService: IExtensionManagementService, - @IMessageService protected messageService: IMessageService, - @ITelemetryService protected telemetryService: ITelemetryService, - @IInstantiationService protected instantiationService: IInstantiationService - ) { - super('extensions.uninstall', nls.localize('uninstall', "Uninstall Extension"), 'octicon octicon-x', true); + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); } +} - public run(extension: IExtension): TPromise { - if (!window.confirm(nls.localize('deleteSure', "Are you sure you want to uninstall '{0}'?", extension.displayName))) { - return TPromise.as(null); - } +export class UpdateAction extends Action { - this.enabled = false; + private static EnabledClass = 'extension-action update'; + private static DisabledClass = `${ UpdateAction.EnabledClass } disabled`; - return this.extensionManagementService.getInstalled().then(localExtensions => { - const [local] = localExtensions.filter(local => extensionEquals(local, extension)); + private disposables: IDisposable[] = []; - if (!local) { - return TPromise.wrapError(nls.localize('notFound', "Extension '{0}' not installed.", extension.displayName)); - } + constructor(private model: ExtensionsModel, private extension: IExtension) { + super('extensions.update', nls.localize('updateAction', "Update"), UpdateAction.DisabledClass, false); - return this.extensionManagementService.uninstall(local) - .then(() => this.onSuccess(local), err => this.onError(err, local)) - .then(() => this.enabled = true) - .then(() => null); - }); + this.disposables.push(this.model.onChange(() => this.updateEnablement())); + this.updateEnablement(); } - private onSuccess(extension: IExtension) { - this.reportTelemetry(extension, true); - this.messageService.show(Severity.Info, { - message: nls.localize('success-uninstalled', "'{0}' was successfully uninstalled. Restart to deactivate it.", extension.displayName), - actions: [ - CloseAction, - this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, nls.localize('restartNow2', "Restart Now")) - ] - }); - } + private updateEnablement(): void { + const canInstall = this.model.canInstall(this.extension); + const isInstalled = this.extension.state === ExtensionState.Installed; - private onError(err: Error, extension: IExtension) { - this.reportTelemetry(extension, false); - this.messageService.show(Severity.Error, err); + this.enabled = canInstall && isInstalled && this.extension.outdated; + this.class = this.enabled ? UpdateAction.EnabledClass : UpdateAction.DisabledClass; } - private reportTelemetry(extension: IExtension, success: boolean) { - const data = assign(getTelemetryData(extension), { success }); + run(): TPromise { + return this.model.install(this.extension); + + // this.enabled = false; - this.telemetryService.publicLog('extensionGallery:uninstall', data); + // return this.extensionManagementService.getInstalled() + // .then(installed => installed.some(({ manifest }) => extensionEquals(manifest, extension))) + // .then(isUpdate => { + // return this.extensionManagementService + // .install(extension) + // .then(() => this.onSuccess(extension, isUpdate), err => this.onError(err, extension, isUpdate)) + // .then(() => this.enabled = true) + // .then(() => null); + // }); } -} + + // private onSuccess(extension: IGalleryExtension, isUpdate: boolean) { + // this.reportTelemetry(extension, isUpdate, true); + // this.messageService.show(Severity.Info, { + // message: nls.localize('success-installed', "'{0}' was successfully installed. Restart to enable it.", extension.displayName || extension.name), + // actions: [ + // CloseAction, + // this.instantiationService.createInstance(ReloadWindowAction, ReloadWindowAction.ID, nls.localize('restartNow', "Restart Now")) + // ] + // }); + // } + + // private onError(err: Error, extension: IGalleryExtension, isUpdate: boolean) { + // this.reportTelemetry(extension, isUpdate, false); + // this.messageService.show(Severity.Error, err); + // } + + // private reportTelemetry(extension: IGalleryExtension, isUpdate: boolean, success: boolean) { + // const event = isUpdate ? 'extensionGallery:update' : 'extensionGallery:install'; + // const data = assign(getTelemetryData(extension), { success }); + + // this.telemetryService.publicLog(event, data); + // } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts new file mode 100644 index 0000000000000000000000000000000000000000..69cf962cceb45730760f4d772cc61bbb44656275 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsList.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { append, emmet as $, addClass, removeClass } from 'vs/base/browser/dom'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IDelegate } from 'vs/base/browser/ui/list/list'; +import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; +import { IExtension, ExtensionsModel } from './extensionsModel'; +import { CombinedInstallAction, UpdateAction } from './extensionsActions'; +import { Label } from './extensionsWidgets'; + +export interface ITemplateData { + extension: IExtension; + element: HTMLElement; + icon: HTMLElement; + name: HTMLElement; + version: HTMLElement; + author: HTMLElement; + description: HTMLElement; + actionbar: ActionBar; + disposables: IDisposable[]; +} + +export class Delegate implements IDelegate { + getHeight() { return 62; } + getTemplateId() { return 'extension'; } +} + +const actionOptions = { icon: true, label: true }; + +export class Renderer implements IPagedRenderer { + + private _templates: ITemplateData[]; + get templates(): ITemplateData[] { return this._templates; } + + constructor( + private model: ExtensionsModel, + @IInstantiationService private instantiationService: IInstantiationService + ) { + this._templates = []; + } + + get templateId() { return 'extension'; } + + renderTemplate(root: HTMLElement): ITemplateData { + const element = append(root, $('.extension')); + const icon = append(element, $('.icon')); + const details = append(element, $('.details')); + const header = append(details, $('.header')); + const name = append(header, $('span.name.ellipsis')); + const version = append(header, $('span.version.ellipsis')); + const author = append(header, $('span.author.ellipsis')); + const description = append(details, $('.description.ellipsis')); + const actionbar = new ActionBar(details, { animated: false }); + const disposables = []; + + const result = { extension: null, element, icon, name, version, author, description, actionbar, disposables }; + this._templates.push(result); + return result; + } + + renderPlaceholder(index: number, data: ITemplateData): void { + data.disposables = dispose(data.disposables); + + addClass(data.element, 'loading'); + data.extension = null; + data.icon.style.backgroundImage = ''; + data.name.textContent = ''; + data.version.textContent = ''; + data.author.textContent = ''; + data.description.textContent = ''; + data.actionbar.clear(); + } + + renderElement(extension: IExtension, index: number, data: ITemplateData): void { + data.disposables = dispose(data.disposables); + + removeClass(data.element, 'loading'); + data.extension = extension; + data.icon.style.backgroundImage = `url("${ extension.iconUrl }")`; + data.name.textContent = extension.displayName; + data.author.textContent = extension.publisherDisplayName; + data.description.textContent = extension.description; + + const version = new Label(data.version, this.model, extension, e => e.version); + const installAction = new CombinedInstallAction(this.model, extension); + const updateAction = new UpdateAction(this.model, extension); + data.actionbar.clear(); + data.actionbar.push([updateAction, installAction], actionOptions); + + data.disposables.push(version, installAction, updateAction); + } + + disposeTemplate(data: ITemplateData): void { + const index = this._templates.indexOf(data); + + if (index > -1) { + this._templates.splice(index, 1); + } + } +} diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsModel.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsModel.ts new file mode 100644 index 0000000000000000000000000000000000000000..036361edfa0fbb54cb2e6dc3ec198e41524a80ca --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsModel.ts @@ -0,0 +1,333 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./media/extensionsViewlet'; +import Event, { Emitter } from 'vs/base/common/event'; +import { index } from 'vs/base/common/arrays'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IPager, mapPager } from 'vs/base/common/paging'; +import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import * as semver from 'semver'; + +export enum ExtensionState { + Installing, + Installed, + // Uninstalling, + Uninstalled +} + +export interface IExtension { + state: ExtensionState; + name: string; + displayName: string; + publisher: string; + publisherDisplayName: string; + version: string; + latestVersion: string; + description: string; + readmeUrl: string; + iconUrl: string; + installCount: number; + rating: number; + ratingCount: number; + outdated: boolean; +} + +interface IExtensionStateProvider { + (extension: Extension): ExtensionState; +} + +class Extension implements IExtension { + + constructor( + private stateProvider: IExtensionStateProvider, + public local: ILocalExtension, + public gallery: IGalleryExtension = null + ) {} + + get name(): string { + return this.local ? this.local.manifest.name : this.gallery.name; + } + + get displayName(): string { + if (this.local) { + return this.local.manifest.displayName || this.local.manifest.name; + } + + return this.gallery.displayName || this.gallery.name; + } + + get publisher(): string { + return this.local ? this.local.manifest.publisher : this.gallery.publisher; + } + + get publisherDisplayName(): string { + if (this.local) { + if (this.local.metadata && this.local.metadata.publisherDisplayName) { + return this.local.metadata.publisherDisplayName; + } + + return this.local.manifest.publisher; + } + + return this.gallery.publisherDisplayName || this.gallery.publisher; + } + + get version(): string { + return this.local ? this.local.manifest.version : this.gallery.versions[0].version; + } + + get latestVersion(): string { + return this.gallery ? this.gallery.versions[0].version : this.local.manifest.version; + } + + get description(): string { + return this.local ? this.local.manifest.description : this.gallery.description; + } + + get readmeUrl(): string { + if (this.local && this.local.readmeUrl) { + return this.local.readmeUrl; + } + + if (this.gallery && this.gallery.versions[0].readmeUrl) { + return this.gallery.versions[0].readmeUrl; + } + + return null; + } + + get iconUrl(): string { + if (this.local && this.local.manifest.icon) { + return `file://${ this.local.path }/${ this.local.manifest.icon }`; + } + + if (this.gallery && this.gallery.versions[0].iconUrl) { + return this.gallery.versions[0].iconUrl; + } + + return require.toUrl('./media/defaultIcon.png'); + } + + get state(): ExtensionState { + return this.stateProvider(this); + } + + get installCount(): number { + return this.gallery ? this.gallery.installCount : null; + } + + get rating(): number { + return this.gallery ? this.gallery.rating : null; + } + + get ratingCount(): number { + return this.gallery ? this.gallery.ratingCount : null; + } + + get outdated(): boolean { + return semver.gt(this.latestVersion, this.version); + } +} + +export class ExtensionsModel { + + private static SyncPeriod = 1000 * 60 * 60 * 12; // 12 hours + + private stateProvider: IExtensionStateProvider; + private installing: { id: string; extension: Extension; }[] = []; + private installed: Extension[] = []; + private didTriggerSync: boolean = false; + private syncDelayer: ThrottledDelayer; + private disposables: IDisposable[] = []; + + private _onChange: Emitter = new Emitter(); + get onChange(): Event { return this._onChange.event; } + + constructor( + @IExtensionManagementService private extensionService: IExtensionManagementService, + @IExtensionGalleryService private galleryService: IExtensionGalleryService + ) { + this.stateProvider = ext => this.getExtensionState(ext); + + this.disposables.push(extensionService.onInstallExtension(({ id, gallery }) => this.onInstallExtension(id, gallery))); + this.disposables.push(extensionService.onDidInstallExtension(({ id, local, error }) => this.onDidInstallExtension(id, local, error))); + this.disposables.push(extensionService.onUninstallExtension((id) => this.onUninstallExtension(id))); + + this.syncDelayer = new ThrottledDelayer(ExtensionsModel.SyncPeriod); + } + + getLocal(): TPromise { + return this.extensionService.getInstalled().then(result => { + const installedById = index(this.installed, e => e.local.id); + + this.installed = result.map(local => { + const extension = installedById[local.id] || new Extension(this.stateProvider, local); + extension.local = local; + return extension; + }); + + const installing = this.installing + .filter(e => !this.installed.some(installed => installed.local.id === e.id)) + .map(e => e.extension); + + if (!this.didTriggerSync) { + this.didTriggerSync = true; + this.syncWithGallery(true); + } + + this._onChange.fire(); + return [...this.installed, ...installing]; + }); + } + + queryGallery(options: IQueryOptions = {}): TPromise> { + return this.galleryService.query(options).then(result => { + const installedByGalleryId = index(this.installed, e => e.local.metadata ? e.local.metadata.id : ''); + + return mapPager(result, gallery => { + const id = gallery.id; + const installed = installedByGalleryId[id]; + + if (installed) { + installed.gallery = gallery; + this._onChange.fire(); + return installed; + } + + return new Extension(this.stateProvider, null, gallery); + }); + }); + } + + private syncWithGallery(immediate = false): void { + const loop = () => this.doSyncWithGallery().then(() => this.syncWithGallery()); + const delay = immediate ? 0 : ExtensionsModel.SyncPeriod; + + this.syncDelayer.trigger(loop, delay); + } + + private doSyncWithGallery(): TPromise { + const ids = this.installed + .filter(e => !!(e.local && e.local.metadata)) + .map(e => e.local.metadata.id); + + if (ids.length === 0) { + return TPromise.as(null); + } + + return this.queryGallery({ ids, pageSize: ids.length }) as TPromise; + } + + canInstall(extension: IExtension): boolean { + if (!(extension instanceof Extension)) { + return; + } + + return !!(extension as Extension).gallery; + } + + install(extension: IExtension): TPromise { + if (!(extension instanceof Extension)) { + return; + } + + const ext = extension as Extension; + const gallery = ext.gallery; + + if (!gallery) { + return TPromise.wrapError(new Error('Missing gallery')); + } + + return this.extensionService.install(gallery); + } + + uninstall(extension: IExtension): TPromise { + if (!(extension instanceof Extension)) { + return; + } + + const ext = extension as Extension; + const local = ext.local || this.installed.filter(e => e.local.metadata && ext.gallery && e.local.metadata.id === ext.gallery.id)[0].local; + + if (!local) { + return TPromise.wrapError(new Error('Missing local')); + } + + return this.extensionService.uninstall(local); + } + + private onInstallExtension(id: string, gallery: IGalleryExtension): void { + if (!gallery) { + return; + } + + let extension = this.installed.filter(e => (e.local.metadata && e.local.metadata.id) === gallery.id)[0]; + + if (!extension) { + extension = new Extension(this.stateProvider, null, gallery); + } + + extension.gallery = gallery; + this.installing.push({ id, extension }); + + this._onChange.fire(); + } + + private onDidInstallExtension(id: string, local: ILocalExtension, error: Error): void { + const installing = this.installing.filter(e => e.id === id)[0]; + + if (!installing) { + return; + } + + const extension = installing.extension; + extension.local = local; + + this.installing = this.installing.filter(e => e.id !== id); + + const galleryId = local.metadata && local.metadata.id; + const installed = this.installed.filter(e => (e.local.metadata && e.local.metadata.id) === galleryId)[0]; + + if (galleryId && installed) { + installed.local = local; + } else { + this.installed.push(extension); + } + + this._onChange.fire(); + } + + private onUninstallExtension(id: string): void { + const previousLength = this.installed.length; + this.installed = this.installed.filter(e => e.local.id !== id); + + if (previousLength === this.installed.length) { + return; + } + + this._onChange.fire(); + } + + private getExtensionState(extension: Extension): ExtensionState { + if (this.installed.some(e => e === extension || (e.gallery && extension.gallery && e.gallery.id === extension.gallery.id))) { + return ExtensionState.Installed; + } + + if (extension.gallery && this.installing.some(e => e.extension.gallery.id === extension.gallery.id)) { + return ExtensionState.Installing; + } + + return ExtensionState.Uninstalled; + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen.ts deleted file mode 100644 index 87bc1a3bcd5d04b7a8052b0b7222d19f825eb7ae..0000000000000000000000000000000000000000 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen.ts +++ /dev/null @@ -1,617 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import nls = require('vs/nls'); -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { isNumber } from 'vs/base/common/types'; -import { PagedModel, mapPager } from 'vs/base/common/paging'; -import { ThrottledDelayer } from 'vs/base/common/async'; -import * as dom from 'vs/base/browser/dom'; -import Severity from 'vs/base/common/severity'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IAutoFocus, Mode, IModel, IDataSource, IRenderer, IRunner, IEntryRunContext, IAccessiblityProvider } from 'vs/base/parts/quickopen/common/quickOpen'; -import { QuickOpenPagedModel, IPagedRenderer } from 'vs/base/parts/quickopen/common/quickOpenPaging'; -import { matchesContiguousSubString } from 'vs/base/common/filters'; -import { QuickOpenHandler } from 'vs/workbench/browser/quickopen'; -import { IHighlight } from 'vs/base/parts/quickopen/browser/quickOpenModel'; -import { IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, IExtension, IQueryResult } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { InstallAction, UninstallAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; -import { IMessageService } from 'vs/platform/message/common/message'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceContextService } from 'vs/workbench/services/workspace/common/contextService'; -import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { Action } from 'vs/base/common/actions'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { shell } from 'electron'; -import { extensionEquals, getOutdatedExtensions } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; - -const $ = dom.emmet; - -const InstallLabel = nls.localize('install', "Install Extension"); -const UpdateLabel = nls.localize('update', "Update Extension"); - -export interface IHighlights { - id: IHighlight[]; - name: IHighlight[]; - displayName: IHighlight[]; - description: IHighlight[]; -} - -export enum ExtensionState { - Uninstalled, - Installed, - Outdated -} - -export interface IExtensionEntry { - extension: IExtension; - highlights: IHighlights; - state: ExtensionState; -} - -interface ITemplateData { - root: HTMLElement; - displayName: HighlightedLabel; - version: HTMLElement; - installCount: HTMLElement; - installCountLabel: HTMLElement; - author: HTMLElement; - actionbar: ActionBar; - description: HighlightedLabel; - disposables: IDisposable[]; -} - -function getHighlights(input: string, extension: IExtension, nullIfEmpty = true): IHighlights { - const id = matchesContiguousSubString(input, `${ extension.publisher }.${ extension.name }`) || []; - const name = matchesContiguousSubString(input, extension.name) || []; - const displayName = matchesContiguousSubString(input, extension.displayName) || []; - const description = matchesContiguousSubString(input, extension.description) || []; - - if (nullIfEmpty && !id.length && !name.length && !displayName.length && !description.length) { - return null; - } - - return { id, name, displayName, description }; -} - -function extensionEntryCompare(one: IExtensionEntry, other: IExtensionEntry): number { - const oneInstallCount = one.extension.galleryInformation ? one.extension.galleryInformation.installCount : 0; - const otherInstallCount = other.extension.galleryInformation ? other.extension.galleryInformation.installCount : 0; - const diff = otherInstallCount - oneInstallCount; - - if (diff !== 0) { - return diff; - } - - return one.extension.displayName.localeCompare(other.extension.displayName); -} - -class OpenLicenseAction extends Action { - - constructor( - @IWorkspaceContextService private contextService: IWorkspaceContextService - ) { - super('extensions.open-license', nls.localize('license', "License"), '', true); - } - - public run(extension: IExtension): TPromise { - const url = `${ this.contextService.getConfiguration().env.extensionsGallery.itemUrl }/${ extension.publisher }.${ extension.name }/license`; - shell.openExternal(url); - return TPromise.as(null); - } -} - -class OpenInGalleryAction extends Action { - - constructor( - private promptToInstall: boolean, - @IMessageService protected messageService: IMessageService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IInstantiationService protected instantiationService: IInstantiationService - ) { - super('extensions.open-in-gallery', nls.localize('readme', "Readme"), '', true); - } - - public run(extension: IExtension): TPromise { - const url = `${this.contextService.getConfiguration().env.extensionsGallery.itemUrl}/${ extension.publisher }.${ extension.name }`; - shell.openExternal(url); - - if (!this.promptToInstall) { - return TPromise.as(null); - } - - const hideMessage = this.messageService.show(Severity.Info, { - message: nls.localize('installPrompt', "Would you like to install '{0}'?", extension.displayName), - actions: [ - new Action('cancelaction', nls.localize('cancel', 'Cancel')), - new Action('installNow', nls.localize('installNow', 'Install Now'), null, true, () => { - hideMessage(); - - const hideInstallMessage = this.messageService.show(Severity.Info, nls.localize('nowInstalling', "'{0}' is being installed...", extension.displayName)); - - const action = this.instantiationService.createInstance(InstallAction, ''); - return action.run(extension).then(r => { - hideInstallMessage(); - return TPromise.as(r); - }, e => { - hideInstallMessage(); - return TPromise.wrapError(e); - }); - }) - ] - }); - - return TPromise.as(null); - } -} - -class InstallRunner implements IRunner { - - private action: InstallAction; - - constructor( - @IInstantiationService private instantiationService: IInstantiationService - ) {} - - run(entry: IExtensionEntry, mode: Mode, context: IEntryRunContext): boolean { - if (mode === Mode.PREVIEW) { - return false; - } - - if (entry.state === ExtensionState.Installed) { - return false; - } - - if (!this.action) { - this.action = this.instantiationService.createInstance(InstallAction, InstallLabel); - } - - this.action.run(entry.extension).done(null, onUnexpectedError); - return true; - } -} - -class AccessibilityProvider implements IAccessiblityProvider { - - public getAriaLabel(entry: IExtensionEntry): string { - return nls.localize('extensionAriaLabel', "{0}, {1}, extensions picker", entry.extension.displayName, entry.extension.description); - } -} - -class Renderer implements IPagedRenderer { - - constructor( - @IInstantiationService private instantiationService: IInstantiationService, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService - ) {} - - getHeight(entry: IExtensionEntry): number { - return 48; - } - - getTemplateId(entry: IExtensionEntry): string { - return 'extension'; - } - - renderTemplate(templateId: string, container: HTMLElement): ITemplateData { - // Important to preserve order here. - const root = dom.append(container, $('.extension')); - const firstRow = dom.append(root, $('.row')); - const secondRow = dom.append(root, $('.row')); - const published = dom.append(firstRow, $('.published')); - const displayName = new HighlightedLabel(dom.append(firstRow, $('span.name'))); - const installCount = dom.append(firstRow, $('span.install')); - dom.append(installCount, $('span.octicon.octicon-cloud-download')); - const installCountLabel = dom.append(installCount, $('span.installCount')); - const version = dom.append(published, $('span.version')); - const author = dom.append(published, $('span.author')); - - return { - root, - author, - displayName, - version, - installCount, - installCountLabel, - actionbar: new ActionBar(dom.append(secondRow, $('.actions'))), - description: new HighlightedLabel(dom.append(secondRow, $('span.description'))), - disposables: [] - }; - } - - renderPlaceholder(index: number, templateId: string, data: ITemplateData): void { - dom.addClass(data.root, 'loading'); - - data.author.textContent = nls.localize('author', 'Author'); - data.displayName.set(nls.localize('name', 'Name')); - data.version.textContent = '0.0.1'; - data.installCount.style.display = 'none'; - data.installCountLabel.textContent = ''; - data.actionbar.clear(); - data.description.set(nls.localize('description', 'Description')); - data.disposables = dispose(data.disposables); - } - - renderElement(entry: IExtensionEntry, templateId: string, data: ITemplateData): void { - dom.removeClass(data.root, 'loading'); - - const extension = entry.extension; - const publisher = extension.galleryInformation ? extension.galleryInformation.publisherDisplayName : extension.publisher; - const installCount = extension.galleryInformation ? extension.galleryInformation.installCount : null; - const actionOptions = { icon: true, label: false }; - - const updateActions = () => { - data.actionbar.clear(); - - if (entry.extension.galleryInformation) { - data.actionbar.push(this.instantiationService.createInstance(OpenInGalleryAction, entry.state === ExtensionState.Uninstalled), { label: true, icon: false }); - data.actionbar.push(this.instantiationService.createInstance(OpenLicenseAction), { label: true, icon: false }); - } - - switch (entry.state) { - case ExtensionState.Uninstalled: - if (entry.extension.galleryInformation) { - data.actionbar.push(this.instantiationService.createInstance(InstallAction, InstallLabel), actionOptions); - } - break; - case ExtensionState.Installed: - data.actionbar.push(this.instantiationService.createInstance(UninstallAction), actionOptions); - break; - case ExtensionState.Outdated: - data.actionbar.push(this.instantiationService.createInstance(UninstallAction), actionOptions); - data.actionbar.push(this.instantiationService.createInstance(InstallAction, UpdateLabel), actionOptions); - break; - } - }; - - const onExtensionStateChange = (e: IExtension, state: ExtensionState) => { - if (extensionEquals(e, extension)) { - entry.state = state; - updateActions(); - } - }; - - data.actionbar.context = extension; - updateActions(); - - data.disposables = dispose(data.disposables); - data.disposables.push(this.extensionManagementService.onDidInstallExtension(e => onExtensionStateChange(e.extension, ExtensionState.Installed))); - data.disposables.push(this.extensionManagementService.onDidUninstallExtension(e => onExtensionStateChange(e, ExtensionState.Uninstalled))); - - data.displayName.set(extension.displayName, entry.highlights.displayName); - data.displayName.element.title = extension.name; - data.version.textContent = extension.version; - - if (isNumber(installCount)) { - data.installCount.style.display = 'inline'; - data.installCountLabel.textContent = String(installCount); - - if (!installCount) { - data.installCount.title = nls.localize('installCountZero', "{0} wasn't downloaded yet.", extension.displayName); - } else if (installCount === 1) { - data.installCount.title = nls.localize('installCountOne', "{0} was downloaded once.", extension.displayName); - } else { - data.installCount.title = nls.localize('installCountMultiple', "{0} was downloaded {1} times.", extension.displayName, installCount); - } - } else { - data.installCount.style.display = 'none'; - data.installCountLabel.textContent = ''; - } - - data.author.textContent = publisher; - data.description.set(extension.description, entry.highlights.description); - data.description.element.title = extension.description; - } - - disposeTemplate(templateId: string, data: ITemplateData): void { - data.displayName.dispose(); - data.description.dispose(); - data.disposables = dispose(data.disposables); - } -} - -class DataSource implements IDataSource { - - getId(entry: IExtensionEntry): string { - const extension = entry.extension; - - if (!extension) { - throw new Error(`Not an extension entry. Found ${ Object.keys(entry).slice(5) },... instead.`); - } - - if (extension.galleryInformation) { - return `${ extension.galleryInformation.id }-${ extension.version }`; - } - - return `local@${ extension.publisher }.${ extension.name }-${ extension.version }@${ extension.path || '' }`; - } - - getLabel(entry: IExtensionEntry): string { - return entry.extension.name; - } -} - -class LocalExtensionsModel implements IModel { - - public dataSource = new DataSource(); - public renderer: IRenderer; - public accessibilityProvider: IAccessiblityProvider = new AccessibilityProvider(); - public runner = { run: () => false }; - public entries: IExtensionEntry[]; - - constructor( - private installedExtensions: IExtension[], - private outdatedExtensions: IExtension[], - @IInstantiationService instantiationService: IInstantiationService - ) { - this.renderer = instantiationService.createInstance(Renderer); - this.entries = []; - } - - public set input(input: string) { - this.entries = this.installedExtensions - .map(extension => ({ extension, highlights: getHighlights(input.trim(), extension) })) - .filter(({ highlights }) => !!highlights) - .map(({ extension, highlights }: { extension: IExtension, highlights: IHighlights }) => { - const [outdatedExt] = this.outdatedExtensions.filter(outdatedExt => extensionEquals(outdatedExt, extension)); - - return { - extension, - highlights, - state: outdatedExt - ? ExtensionState.Outdated - : ExtensionState.Installed - }; - }) - .sort(extensionEntryCompare); - } -} - -export class LocalExtensionsHandler extends QuickOpenHandler { - - private modelPromise: TPromise; - - constructor( - @IInstantiationService private instantiationService: IInstantiationService, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IExtensionGalleryService private galleryService: IExtensionGalleryService - ) { - super(); - this.modelPromise = null; - } - - public getAriaLabel(): string { - return nls.localize('localExtensionsHandlerAriaLabel', "Type to narrow down the list of installed extensions"); - } - - getResults(input: string): TPromise> { - if (!this.modelPromise) { - this.modelPromise = TPromise.join([this.extensionManagementService.getInstalled(), - getOutdatedExtensions(this.extensionManagementService, this.galleryService)]) - .then(result => this.instantiationService.createInstance(LocalExtensionsModel, result[0], result[1])); - } - - return this.modelPromise.then(model => { - model.input = input; - return model; - }); - } - - getEmptyLabel(input: string): string { - return nls.localize('noExtensionsInstalled', "No extensions found"); - } - - getAutoFocus(searchValue: string): IAutoFocus { - return { autoFocusFirstEntry: true }; - } - - onClose(canceled: boolean): void { - this.modelPromise = null; - } -} - -export class GalleryExtensionsHandler extends QuickOpenHandler { - - private delayer: ThrottledDelayer; - - constructor( - @IInstantiationService private instantiationService: IInstantiationService, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IExtensionGalleryService private galleryService: IExtensionGalleryService, - @ITelemetryService private telemetryService: ITelemetryService - ) { - super(); - this.delayer = new ThrottledDelayer(500); - } - - public getAriaLabel(): string { - return nls.localize('galleryExtensionsHandlerAriaLabel', "Type to narrow down the list of extensions from the gallery"); - } - - getResults(text: string): TPromise> { - return this.extensionManagementService.getInstalled().then(localExtensions => { - return this.delayer.trigger(() => this.galleryService.query({ text })).then((result: IQueryResult) => { - const pager = mapPager(result, extension => { - const [local] = localExtensions.filter(local => extensionEquals(local, extension)); - - return { - extension, - highlights: getHighlights(text.trim(), extension, false), - state: local - ? (local.version === extension.version ? ExtensionState.Installed : ExtensionState.Outdated) - : ExtensionState.Uninstalled - }; - }); - - return new QuickOpenPagedModel( - new PagedModel(pager), - new DataSource(), - this.instantiationService.createInstance(Renderer), - this.instantiationService.createInstance(InstallRunner) - ); - }); - }); - } - - getEmptyLabel(input: string): string { - return nls.localize('noExtensionsToInstall', "No extensions found"); - } - - getAutoFocus(searchValue: string): IAutoFocus { - return { autoFocusFirstEntry: true }; - } -} - -class OutdatedExtensionsModel implements IModel { - - public dataSource = new DataSource(); - public accessibilityProvider: IAccessiblityProvider = new AccessibilityProvider(); - public renderer: IRenderer; - public runner: IRunner; - public entries: IExtensionEntry[]; - - constructor( - private outdatedExtensions: IExtension[], - @IInstantiationService instantiationService: IInstantiationService - ) { - this.renderer = instantiationService.createInstance(Renderer); - this.runner = instantiationService.createInstance(InstallRunner); - this.entries = []; - } - - public set input(input: string) { - this.entries = this.outdatedExtensions - .map(extension => ({ extension, highlights: getHighlights(input.trim(), extension) })) - .filter(({ highlights }) => !!highlights) - .map(({ extension, highlights }) => ({ - extension, - highlights, - state: ExtensionState.Outdated - })) - .sort(extensionEntryCompare); - } -} - -export class OutdatedExtensionsHandler extends QuickOpenHandler { - - private modelPromise: TPromise; - - constructor( - @IInstantiationService private instantiationService: IInstantiationService, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IExtensionGalleryService private galleryService: IExtensionGalleryService, - @ITelemetryService private telemetryService: ITelemetryService - ) { - super(); - } - - public getAriaLabel(): string { - return nls.localize('outdatedExtensionsHandlerAriaLabel', "Type to narrow down the list of outdated extensions"); - } - - getResults(input: string): TPromise> { - if (!this.modelPromise) { - this.telemetryService.publicLog('extensionGallery:open'); - - this.modelPromise = getOutdatedExtensions(this.extensionManagementService, this.galleryService) - .then(outdated => this.instantiationService.createInstance(OutdatedExtensionsModel, outdated)); - } - - return this.modelPromise.then(model => { - model.input = input; - return model; - }); - } - - onClose(canceled: boolean): void { - this.modelPromise = null; - } - - getEmptyLabel(input: string): string { - return nls.localize('noOutdatedExtensions', "No outdated extensions found"); - } - - getAutoFocus(searchValue: string): IAutoFocus { - return { autoFocusFirstEntry: true }; - } -} - - -class SuggestedExtensionsModel implements IModel { - - public dataSource = new DataSource(); - public renderer: IRenderer; - public runner: IRunner; - public entries: IExtensionEntry[]; - - constructor( - private suggestedExtensions: IExtension[], - private localExtensions: IExtension[], - @IInstantiationService instantiationService: IInstantiationService - ) { - this.renderer = instantiationService.createInstance(Renderer); - this.runner = instantiationService.createInstance(InstallRunner); - this.entries = []; - } - - public set input(input: string) { - this.entries = this.suggestedExtensions - .map(extension => ({ extension, highlights: getHighlights(input.trim(), extension) })) - .filter(({ extension, highlights }) => { - const local = this.localExtensions.filter(local => extensionEquals(local, extension))[0]; - return !local && !!highlights; - }) - .map(({ extension, highlights }: { extension: IExtension, highlights: IHighlights }) => { - return { - extension, - highlights, - state: ExtensionState.Uninstalled - }; - }) - .sort(extensionEntryCompare); - } -} - - -export class SuggestedExtensionHandler extends QuickOpenHandler { - - private modelPromise: TPromise; - - constructor( - @IExtensionTipsService private extensionTipsService: IExtensionTipsService, - @IInstantiationService private instantiationService: IInstantiationService, - @ITelemetryService private telemetryService: ITelemetryService, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService - ) { - super(); - } - - getResults(input: string): TPromise> { - if (!this.modelPromise) { - this.telemetryService.publicLog('extensionRecommendations:open'); - this.modelPromise = TPromise.join([this.extensionTipsService.getRecommendations(), this.extensionManagementService.getInstalled()]) - .then(result => this.instantiationService.createInstance(SuggestedExtensionsModel, result[0], result[1])); - } - - return this.modelPromise.then(model => { - model.input = input; - return model; - }); - } - - onClose(canceled: boolean): void { - this.modelPromise = null; - } - - getEmptyLabel(input: string): string { - return nls.localize('noRecommendedExtensions', "No recommended extensions"); - } - - getAutoFocus(searchValue: string): IAutoFocus { - return { autoFocusFirstEntry: true }; - } -} diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts new file mode 100644 index 0000000000000000000000000000000000000000..40e04f6c4e6e7479ca11159f4d003348bbeee78c --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./media/extensionsViewlet'; +import { localize } from 'vs/nls'; +import { ThrottledDelayer, always } from 'vs/base/common/async'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Builder, Dimension } from 'vs/base/browser/builder'; +import { mapEvent, filterEvent } from 'vs/base/common/event'; +import { domEvent } from 'vs/base/browser/event'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Viewlet } from 'vs/workbench/browser/viewlet'; +import { append, emmet as $ } from 'vs/base/browser/dom'; +import { PagedModel, SinglePagePagedModel } from 'vs/base/common/paging'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { PagedList } from 'vs/base/browser/ui/list/listPaging'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Delegate, Renderer } from './extensionsList'; +import { ExtensionsModel, IExtension } from './extensionsModel'; +import { IExtensionsViewlet } from './extensions'; +import { IExtensionManagementService, IExtensionGalleryService, SortBy } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsInput } from '../common/extensionsInput'; +import { IProgressService } from 'vs/platform/progress/common/progress'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class ExtensionsViewlet extends Viewlet implements IExtensionsViewlet { + + static ID: string = 'workbench.viewlet.extensions'; + + private searchDelayer: ThrottledDelayer; + private root: HTMLElement; + private searchBox: HTMLInputElement; + private extensionsBox: HTMLElement; + private model: ExtensionsModel; + private list: PagedList; + private disposables: IDisposable[] = []; + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IExtensionGalleryService private galleryService: IExtensionGalleryService, + @IExtensionManagementService private extensionService: IExtensionManagementService, + @IProgressService private progressService: IProgressService, + @IInstantiationService private instantiationService: IInstantiationService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService + ) { + super(ExtensionsViewlet.ID, telemetryService); + this.searchDelayer = new ThrottledDelayer(500); + this.model = instantiationService.createInstance(ExtensionsModel); + } + + create(parent: Builder): TPromise { + super.create(parent); + parent.addClass('extensions-viewlet'); + this.root = parent.getHTMLElement(); + + const header = append(this.root, $('.header')); + + this.searchBox = append(header, $('input.search-box')); + this.searchBox.type = 'search'; + this.searchBox.placeholder = localize('searchExtensions', "Search Extensions in Marketplace"); + this.extensionsBox = append(this.root, $('.extensions')); + + const delegate = new Delegate(); + const renderer = this.instantiationService.createInstance(Renderer, this.model); + this.list = new PagedList(this.extensionsBox, delegate, [renderer]); + + const onRawKeyDown = domEvent(this.searchBox, 'keydown'); + const onKeyDown = mapEvent(onRawKeyDown, e => new StandardKeyboardEvent(e)); + const onEnter = filterEvent(onKeyDown, e => e.keyCode === KeyCode.Enter); + const onEscape = filterEvent(onKeyDown, e => e.keyCode === KeyCode.Escape); + const onUpArrow = filterEvent(onKeyDown, e => e.keyCode === KeyCode.UpArrow); + const onDownArrow = filterEvent(onKeyDown, e => e.keyCode === KeyCode.DownArrow); + + onEnter(() => this.onEnter(), null, this.disposables); + onEscape(() => this.onEscape(), null, this.disposables); + onUpArrow(() => this.onUpArrow(), null, this.disposables); + onDownArrow(() => this.onDownArrow(), null, this.disposables); + + const onInput = domEvent(this.searchBox, 'input'); + onInput(() => this.triggerSearch(), null, this.disposables); + + this.list.onDOMFocus(() => this.searchBox.focus(), null, this.disposables); + + this.list.onSelectionChange(e => { + const [extension] = e.elements; + + if (!extension) { + return; + } + + return this.editorService.openEditor(new ExtensionsInput(this.model, extension)); + }, null, this.disposables); + + return TPromise.as(null); + } + + setVisible(visible:boolean): TPromise { + return super.setVisible(visible).then(() => { + if (visible) { + this.searchBox.focus(); + this.searchBox.setSelectionRange(0,this.searchBox.value.length); + this.triggerSearch(true); + } else { + this.list.model = new SinglePagePagedModel([]); + } + }); + } + + focus(): void { + this.searchBox.focus(); + } + + layout({ height }: Dimension):void { + this.list.layout(height - 38); + } + + search(text: string, immediate = false): void { + this.searchBox.value = text; + this.triggerSearch(immediate); + } + + private triggerSearch(immediate = false): void { + const text = this.searchBox.value; + this.searchDelayer.trigger(() => this.doSearch(text), immediate || !text ? 0 : 500); + } + + private doSearch(text: string = ''): TPromise { + const progressRunner = this.progressService.show(true); + let promise: TPromise>; + + if (!text) { + promise = this.model.getLocal() + .then(result => new SinglePagePagedModel(result)); + } else if (/@outdated/i.test(text)) { + promise = this.model.getLocal() + .then(result => result.filter(e => e.outdated)) + .then(result => new SinglePagePagedModel(result)); + } else if (/@popular/i.test(text)) { + promise = this.model.queryGallery({ sortBy: SortBy.InstallCount }) + .then(result => new PagedModel(result)); + } else { + promise = this.model.queryGallery({ text }) + .then(result => new PagedModel(result)); + } + + return always(promise, () => progressRunner.done()) + .then(model => this.list.model = model); + } + + private onEnter(): void { + this.list.setSelection(...this.list.getFocus()); + } + + private onEscape(): void { + this.searchBox.value = ''; + this.triggerSearch(true); + } + + private onUpArrow(): void { + this.list.focusPrevious(); + } + + private onDownArrow(): void { + this.list.focusNext(); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + super.dispose(); + } +} diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts index 2e3255c4f95c64d5af96ebcc60311c792864a231..6905187c2a9b661d37908cdd139a6716967544df 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsWidgets.ts @@ -3,168 +3,82 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import Severity from 'vs/base/common/severity'; -import { ThrottledDelayer } from 'vs/base/common/async'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { emmet as $, append, toggleClass } from 'vs/base/browser/dom'; -import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { onUnexpectedPromiseError } from 'vs/base/common/errors'; -import { assign } from 'vs/base/common/objects'; -import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; -import { IOutputService } from 'vs/workbench/parts/output/common/output'; -import { IExtensionService, IMessage } from 'vs/platform/extensions/common/extensions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionManagementService, IExtensionGalleryService, ExtensionsLabel, ExtensionsChannelId, IExtension, IExtensionManifest } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IQuickOpenService } from 'vs/workbench/services/quickopen/common/quickOpenService'; -import { getOutdatedExtensions } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; - -interface IState { - errors: IMessage[]; - installing: IExtensionManifest[]; - outdated: IExtension[]; -} +'use strict'; -const InitialState: IState = { - errors: [], - installing: [], - outdated: [] -}; +import 'vs/css!./media/extensionsWidgets'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IExtension, ExtensionsModel } from './extensionsModel'; +import { append, emmet as $, addClass } from 'vs/base/browser/dom'; -function extensionEquals(one: IExtensionManifest, other: IExtensionManifest): boolean { - return one.publisher === other.publisher && one.name === other.name; +export interface IOptions { + small?: boolean; } -const OutdatedPeriod = 12 * 60 * 60 * 1000; // every 12 hours - -export class ExtensionsStatusbarItem implements IStatusbarItem { +export class Label implements IDisposable { - private domNode: HTMLElement; - private state: IState = InitialState; - private outdatedDelayer = new ThrottledDelayer(OutdatedPeriod); + private listener: IDisposable; constructor( - @IExtensionService private extensionService: IExtensionService, - @IOutputService private outputService: IOutputService, - @IExtensionManagementService protected extensionManagementService: IExtensionManagementService, - @IExtensionGalleryService protected extensionGalleryService: IExtensionGalleryService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IQuickOpenService protected quickOpenService: IQuickOpenService, - @ITelemetryService protected telemetrService: ITelemetryService - ) {} - - render(container: HTMLElement): IDisposable { - this.domNode = append(container, $('a.extensions-statusbar')); - append(this.domNode, $('.icon')); - this.domNode.onclick = () => this.onClick(); - - this.checkErrors(); - this.checkOutdated(); - - const disposables = []; - this.extensionManagementService.onInstallExtension(this.onInstallExtension, this, disposables); - this.extensionManagementService.onDidInstallExtension(this.onDidInstallExtension, this, disposables); - this.extensionManagementService.onDidUninstallExtension(this.onDidUninstallExtension, this, disposables); - - return combinedDisposable(disposables); + element: HTMLElement, + model: ExtensionsModel, + extension: IExtension, + fn: (extension: IExtension) => string + ) { + const render = () => element.textContent = fn(extension); + render(); + this.listener = model.onChange(render); } - private updateState(obj: any): void { - this.state = assign(this.state, obj); - this.onStateChange(); + dispose(): void { + this.listener = dispose(this.listener); } +} - private get hasErrors() { return this.state.errors.length > 0; } - private get isInstalling() { return this.state.installing.length > 0; } - private get hasUpdates() { return this.state.outdated.length > 0; } - - private onStateChange(): void { - toggleClass(this.domNode, 'has-errors', this.hasErrors); - toggleClass(this.domNode, 'is-installing', !this.hasErrors && this.isInstalling); - toggleClass(this.domNode, 'has-updates', !this.hasErrors && !this.isInstalling && this.hasUpdates); - - if (this.hasErrors) { - const singular = nls.localize('oneIssue', "Extensions (1 issue)"); - const plural = nls.localize('multipleIssues', "Extensions ({0} issues)", this.state.errors.length); - this.domNode.title = this.state.errors.length > 1 ? plural : singular; - } else if (this.isInstalling) { - this.domNode.title = nls.localize('extensionsInstalling', "Extensions ({0} installing...)", this.state.installing.length); - } else if (this.hasUpdates) { - const singular = nls.localize('oneUpdate', "Extensions (1 update available)"); - const plural = nls.localize('multipleUpdates', "Extensions ({0} updates available)", this.state.outdated.length); - this.domNode.title = this.state.outdated.length > 1 ? plural : singular; - } else { - this.domNode.title = nls.localize('extensions', "Extensions"); - } - } +export class RatingsWidget implements IDisposable { + + static ID: string = 'workbench.editor.extension'; - private onClick(): void { - if (this.hasErrors) { - this.telemetrService.publicLog('extensionWidgetClick', {mode : 'hasErrors'}); - this.showErrors(this.state.errors); - this.updateState({ errors: [] }); - } else if (this.hasUpdates) { - this.telemetrService.publicLog('extensionWidgetClick', {mode : 'hasUpdate'}); - this.quickOpenService.show(`ext update `); - } else { - this.telemetrService.publicLog('extensionWidgetClick', {mode : 'none'}); - this.quickOpenService.show(`>${ExtensionsLabel}: `); + private disposables: IDisposable[] = []; + + constructor( + private container: HTMLElement, + private model: ExtensionsModel, + private extension: IExtension, + options: IOptions = {} + ) { + this.disposables.push(this.model.onChange(() => this.render())); + addClass(container, 'extension-ratings'); + + if (options.small) { + addClass(container, 'small'); } - } - private showErrors(errors: IMessage[]): void { - const promise = onUnexpectedPromiseError(this.extensionManagementService.getInstalled()); - promise.done(installed => { - errors.forEach(m => { - const extension = installed.filter(ext => ext.path === m.source).pop(); - const name = extension && extension.name; - const message = name ? `${ name }: ${ m.message }` : m.message; - - const outputChannel = this.outputService.getChannel(ExtensionsChannelId); - outputChannel.append(message); - outputChannel.show(true); - }); - }); + this.render(); } - private onInstallExtension(manifest: IExtensionManifest): void { - const installing = [...this.state.installing, manifest]; - this.updateState({ installing }); - } + private render(): void { + const rating = this.extension.rating; + this.container.innerHTML = ''; - private onDidInstallExtension({ extension }: { extension: IExtension; }): void { - const installing = this.state.installing - .filter(e => !extensionEquals(extension, e)); - this.updateState({ installing }); - this.outdatedDelayer.trigger(() => this.checkOutdated(), 0); - } + if (rating === null) { + return; + } - private onDidUninstallExtension(): void { - this.outdatedDelayer.trigger(() => this.checkOutdated(), 0); - } + for (let i = 1; i <= 5; i++) { + if (rating >= i) { + append(this.container, $('span.full.star')); + } else if (rating >= i - 0.5) { + append(this.container, $('span.half.star')); + } else { + append(this.container, $('span.empty.star')); + } + } - private checkErrors(): void { - const promise = onUnexpectedPromiseError(this.extensionService.onReady()); - promise.done(() => { - const status = this.extensionService.getExtensionsStatus(); - const errors = Object.keys(status) - .map(k => status[k].messages) - .reduce((r, m) => r.concat(m), []) - .filter(m => m.type > Severity.Info); - - this.updateState({ errors }); - }); + const count = append(this.container, $('span.count')); + count.textContent = String(this.extension.ratingCount); } - private checkOutdated(): TPromise { - return getOutdatedExtensions(this.extensionManagementService, this.extensionGalleryService) - .then(null, _ => []) // ignore errors - .then(outdated => { - this.updateState({ outdated }); - - // repeat this later - this.outdatedDelayer.trigger(() => this.checkOutdated()); - }); + dispose(): void { + this.disposables = dispose(this.disposables); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchExtension.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchExtension.ts index 1fdb6ab233781ae0b8075283d518bf5e8b926332..7d791c17894759377b9682a74fca38e00a1866d7 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchExtension.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsWorkbenchExtension.ts @@ -5,19 +5,14 @@ import nls = require('vs/nls'); import errors = require('vs/base/common/errors'); -import platform = require('vs/platform/platform'); import { Promise } from 'vs/base/common/winjs.base'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMessageService } from 'vs/platform/message/common/message'; import Severity from 'vs/base/common/severity'; import { IWorkspaceContextService } from 'vs/workbench/services/workspace/common/contextService'; import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions'; -import wbaregistry = require('vs/workbench/common/actionRegistry'); -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { ListExtensionsAction, InstallExtensionAction, ListOutdatedExtensionsAction, ListSuggestedExtensionsAction } from './extensionsActions'; -import { IQuickOpenRegistry, Extensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import {ipcRenderer as ipc} from 'electron'; interface IInstallExtensionsRequest { @@ -42,55 +37,8 @@ export class ExtensionsWorkbenchExtension implements IWorkbenchContribution { this.install(options.extensionsToInstall).done(null, errors.onUnexpectedError); } - const actionRegistry = ( platform.Registry.as(wbaregistry.Extensions.WorkbenchActions)); - actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListExtensionsAction, ListExtensionsAction.ID, ListExtensionsAction.LABEL), 'Extensions: Show Installed Extensions', ExtensionsLabel); - - (platform.Registry.as(Extensions.Quickopen)).registerQuickOpenHandler( - new QuickOpenHandlerDescriptor( - 'vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen', - 'LocalExtensionsHandler', - 'ext ', - nls.localize('localExtensionsCommands', "Show Local Extensions") - ) - ); - - if (galleryService.isEnabled()) { - - actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(InstallExtensionAction, InstallExtensionAction.ID, InstallExtensionAction.LABEL), 'Extensions: Install Extension', ExtensionsLabel); - - (platform.Registry.as(Extensions.Quickopen)).registerQuickOpenHandler( - new QuickOpenHandlerDescriptor( - 'vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen', - 'GalleryExtensionsHandler', - 'ext install ', - nls.localize('galleryExtensionsCommands', "Install Gallery Extensions"), - true - ) - ); - - actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListOutdatedExtensionsAction, ListOutdatedExtensionsAction.ID, ListOutdatedExtensionsAction.LABEL), 'Extensions: Show Outdated Extensions', ExtensionsLabel); - - (platform.Registry.as(Extensions.Quickopen)).registerQuickOpenHandler( - new QuickOpenHandlerDescriptor( - 'vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen', - 'OutdatedExtensionsHandler', - 'ext update ', - nls.localize('outdatedExtensionsCommands', "Update Outdated Extensions") - ) - ); - - // add extension tips services - actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListSuggestedExtensionsAction, ListSuggestedExtensionsAction.ID, ListSuggestedExtensionsAction.LABEL), 'Extensions: Show Extension Recommendations', ExtensionsLabel); - - (platform.Registry.as(Extensions.Quickopen)).registerQuickOpenHandler( - new QuickOpenHandlerDescriptor( - 'vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen', - 'SuggestedExtensionHandler', - 'ext recommend ', - nls.localize('suggestedExtensionsCommands', "Show Extension Recommendations") - ) - ); - } + // const actionRegistry = ( platform.Registry.as(wbaregistry.Extensions.WorkbenchActions)); + // actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ListExtensionsAction, ListExtensionsAction.ID, ListExtensionsAction.LABEL), 'Extensions: Show Installed Extensions', ExtensionsLabel); } private registerListeners(): void { diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/EmptyStar.svg b/src/vs/workbench/parts/extensions/electron-browser/media/EmptyStar.svg new file mode 100644 index 0000000000000000000000000000000000000000..f1e005907dec36a6ca302c0e18ed3960c84ccbf2 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/media/EmptyStar.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/FullStarLight.svg b/src/vs/workbench/parts/extensions/electron-browser/media/FullStarLight.svg new file mode 100644 index 0000000000000000000000000000000000000000..19c7da2e95938c73f73146b7dcdac102041b821d --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/media/FullStarLight.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/HalfStarLight.svg b/src/vs/workbench/parts/extensions/electron-browser/media/HalfStarLight.svg new file mode 100644 index 0000000000000000000000000000000000000000..cd84fdd9799011ee929701c973d6fb4574bdba64 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/media/HalfStarLight.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/defaultIcon.png b/src/vs/workbench/parts/extensions/electron-browser/media/defaultIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..2f36a94347418ca5485406b38fc515a2f6c947e5 Binary files /dev/null and b/src/vs/workbench/parts/extensions/electron-browser/media/defaultIcon.png differ diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionActions.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionActions.css new file mode 100644 index 0000000000000000000000000000000000000000..d27e14f9f8a300dd672329f1abd17c122fa90bd7 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionActions.css @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-action-bar .action-item .action-label.extension-action { + border: 1px solid #CCC; + color: #6C6C6C; + background-color: #E2E2E2; + padding: 0 5px; + line-height: initial; +} + +.monaco-action-bar .action-item:not(.disabled):hover .action-label.extension-action { + background-color: #D9D9D9; +} + +.monaco-action-bar .action-item:not(.disabled):active .action-label.extension-action { + background-color: #C9C9C9; +} + +.vs-dark .monaco-action-bar .action-item .action-label.extension-action { + border: 1px solid #545454; + color: #CCC; + background-color: #3A3A3A; +} + +.vs-dark .monaco-action-bar .action-item:not(.disabled):hover .action-label.extension-action { + background-color: #464646; +} + +.vs-dark .monaco-action-bar .action-item:not(.disabled):active .action-label.extension-action { + background-color: #505050; +} + +.monaco-action-bar .action-item .action-label.extension-action.install, +.monaco-action-bar .action-item .action-label.extension-action.update { + color: white; + background-color: #327e36; + border-color: #519A55; +} + +.monaco-action-bar .action-item:not(.disabled):hover .action-label.extension-action.install, +.monaco-action-bar .action-item:not(.disabled):hover .action-label.extension-action.update { + background-color: #478E4B; +} + +.monaco-action-bar .action-item:not(.disabled):active .action-label.extension-action.install, +.monaco-action-bar .action-item:not(.disabled):active .action-label.extension-action.update { + background-color: #6DA770; +} + +.monaco-action-bar .action-item.disabled .action-label.extension-action.update { + display: none; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css new file mode 100644 index 0000000000000000000000000000000000000000..e0d35b3296aff3f3ea20bbb6400b0e70a964dc8f --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionEditor.css @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.extension-editor { + height: 100%; + overflow-y: scroll; + display: flex; + flex-direction: column; +} + +.extension-editor a { + color: inherit; +} + +.extension-editor > .header { + display: flex; + height: 128px; + background: rgba(128, 128, 128, 0.15); + padding: 20px; + overflow: hidden; + font-size: 14px; +} + +.extension-editor > .header > .icon { + height: 128px; + width: 128px; + min-width: 128px; + background-size: 128px; + background-repeat: no-repeat; + background-position: center center; +} + +.extension-editor > .header > .details { + flex: 1; + padding-left: 20px; + overflow: hidden; +} + +.extension-editor > .header > .details > .name { + font-size: 26px; + font-weight: 600; + line-height: normal; + white-space: nowrap; +} + +.extension-editor > .header > .details > .subtitle { + padding-top: 10px; + white-space: nowrap; + height: 20px; + line-height: 20px; +} + +.extension-editor > .header > .details > .subtitle > .publisher { + font-size: 18px; +} + +.extension-editor > .header > .details > .subtitle > .install > .count { + margin-left: 6px; +} + +.extension-editor > .header > .details > .subtitle > span:not(:first-child):not(:empty), +.extension-editor > .header > .details > .subtitle > a:not(:first-child):not(:empty) { + border-left: 1px solid rgba(128, 128, 128, 0.7); + margin-left: 14px; + padding-left: 14px; +} + +.extension-editor > .header > .details > .description { + margin-top: 14px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.extension-editor > .header > .details > .actions { + margin-top: 14px; +} + +.extension-editor > .header > .details > .actions > .monaco-action-bar { + text-align: initial; +} + +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container { + justify-content: flex-start; +} + +.extension-editor > .body { + flex: 1; + overflow-y: scroll; + overflow-x: hidden; + padding: 20px; +} + +.extension-editor > .body img { + max-width: 100%; +} + +.extension-editor > .body.loading { + background-image: url('loading.svg'); + background-position: center center; + background-repeat: no-repeat; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css index 48ea4ea30663765e717ae5d8f374452c5b3a0b2a..333f95eda9ad4c2775da2ebca5a653133f38ae44 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensions.css @@ -198,4 +198,13 @@ background-size: 8px; background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.5) 75%, transparent 75%, transparent); animation: move-background 0.5s linear infinite; +} + +/* Global action */ + +.monaco-workbench > .activitybar .monaco-action-bar .action-label.extensions { + background: url('extensions-status.svg'); + background-size: 22px; + background-repeat: no-repeat; + background-position: 50% !important; } \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css new file mode 100644 index 0000000000000000000000000000000000000000..b66e7ae21427f6064ad10e623dbf0a137686add1 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.extensions-viewlet { + height: 100%; +} + +.extensions-viewlet > .header { + height: 38px; + box-sizing: border-box; + padding: 5px 9px 5px 16px; +} + +.extensions-viewlet > .header > .search-box { + width: 100%; + height: 26px; + box-sizing: border-box; + padding: 0 3px; +} + +.extensions-viewlet > .extensions { + height: calc(100% - 38px); +} + +.extensions-viewlet > .extensions .extension { + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 0 19px 0 16px; + overflow: hidden; + display: flex; +} + +.extensions-viewlet > .extensions .extension.loading { + background-image: url('loading.svg'); + background-position: center center; + background-repeat: no-repeat; +} + +.extensions-viewlet > .extensions .extension > .icon { + width: 42px; + height: 42px; + padding: 10px 14px 10px 0; + flex-shrink: 0; + background-repeat: no-repeat; + background-size: 42px; + background-position: left center; +} + +.extensions-viewlet > .extensions .extension.loading > .icon { + display: none; +} + +.extensions-viewlet > .extensions .extension > .details { + flex: 1; + padding: 4px 0; + overflow: hidden; +} + +.extensions-viewlet > .extensions .extension > .details > .header { + display: flex; +} + +.extensions-viewlet > .extensions .extension > .details > .header > .name { + font-weight: bold; +} + +.extensions-viewlet > .extensions .extension > .details > .header > .version { + opacity: 0.6; + font-size: 80%; + padding-left: 6px; +} + +.extensions-viewlet > .extensions .extension > .details > .header > .author { + flex: 1; + font-size: 90%; + text-align: right; + padding-left: 6px; +} + +.extensions-viewlet > .extensions .extension > .details > .monaco-action-bar .action-label { + margin-right: 0; + margin-left: 0.3em; +} + +.extensions-viewlet > .extensions .extension .ellipsis { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/*.extensions-viewlet > .extensions .extension > .details { + width: 100%; +}*/ \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionsWidgets.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsWidgets.css new file mode 100644 index 0000000000000000000000000000000000000000..680ce6acfd7d1414d019af07a8402577bec87ef9 --- /dev/null +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsWidgets.css @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.extension-ratings { + display: inline-block; +} + +.extension-ratings > .star { + display: inline-block; + width: 16px; + height: 16px; + background-repeat: no-repeat; + background-position: center center; +} + +.extension-ratings > .star:not(:first-child) { + margin-left: 3px; +} + +.extension-ratings.small > .star { + width: 12px; + height: 12px; +} + +.extension-ratings > .full { + background-image: url('./FullStarLight.svg'); +} + +.extension-ratings > .half { + background-image: url('./HalfStarLight.svg'); +} + +.extension-ratings > .empty { + background-image: url('./EmptyStar.svg'); +} + +.extension-ratings > .count { + margin-left: 6px; +} \ No newline at end of file diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 489914e4be0c63e18d1a644eccccc31ac9c7898b..099efea0993598bc5318f1474b6de57a361a5b72 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -44,7 +44,6 @@ import 'vs/workbench/parts/markers/markers.contribution'; import 'vs/workbench/parts/html/browser/html.contribution'; import 'vs/workbench/parts/extensions/electron-browser/extensions.contribution'; -import 'vs/workbench/parts/extensions/electron-browser/extensionsQuickOpen'; import 'vs/workbench/parts/output/browser/output.contribution';