extensionsViewlet.ts 13.3 KB
Newer Older
J
Joao Moreno 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

J
Joao Moreno 已提交
8
import 'vs/css!./media/extensionsViewlet';
J
Joao Moreno 已提交
9 10
import { localize } from 'vs/nls';
import { ThrottledDelayer, always } from 'vs/base/common/async';
J
Joao Moreno 已提交
11
import { TPromise } from 'vs/base/common/winjs.base';
J
Joao Moreno 已提交
12
import { isPromiseCanceledError, create as createError } from 'vs/base/common/errors';
J
Joao Moreno 已提交
13 14
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Builder, Dimension } from 'vs/base/browser/builder';
15
import { assign } from 'vs/base/common/objects';
16
import EventOf, { mapEvent, filterEvent } from 'vs/base/common/event';
17
import { IAction } from 'vs/base/common/actions';
J
Joao Moreno 已提交
18
import { domEvent } from 'vs/base/browser/event';
J
Joao Moreno 已提交
19
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
J
Joao Moreno 已提交
20 21
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
J
Joao Moreno 已提交
22
import { Viewlet } from 'vs/workbench/browser/viewlet';
J
Joao Moreno 已提交
23
import { append, $, addStandardDisposableListener, EventType, addClass, removeClass, toggleClass } from 'vs/base/browser/dom';
24
import { PagedModel } from 'vs/base/common/paging';
J
Joao Moreno 已提交
25
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
J
Joao Moreno 已提交
26 27
import { PagedList } from 'vs/base/browser/ui/list/listPaging';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
J
Joao Moreno 已提交
28
import { Delegate, Renderer } from './extensionsList';
J
Joao Moreno 已提交
29
import { IExtensionsWorkbenchService, IExtension, IExtensionsViewlet, VIEWLET_ID } from './extensions';
J
Joao Moreno 已提交
30
import { ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowInstalledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction } from './extensionsActions';
J
Joao Moreno 已提交
31
import { IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, SortBy, SortOrder, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
32
import { ExtensionsInput } from './extensionsInput';
J
Joao Moreno 已提交
33
import { Query } from '../common/extensionQuery';
J
Joao Moreno 已提交
34
import { OpenGlobalSettingsAction } from 'vs/workbench/browser/actions/openSettings';
J
Joao Moreno 已提交
35
import { IProgressService } from 'vs/platform/progress/common/progress';
J
Joao Moreno 已提交
36
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
J
Joao Moreno 已提交
37 38
import { IMessageService, CloseAction } from 'vs/platform/message/common/message';
import Severity from 'vs/base/common/severity';
J
Joao Moreno 已提交
39 40
import { IURLService } from 'vs/platform/url/common/url';
import URI from 'vs/base/common/uri';
J
Joao Moreno 已提交
41

42 43 44 45
interface SearchInputEvent extends Event {
	target: HTMLInputElement;
	immediate?: boolean;
}
J
Joao Moreno 已提交
46

47
export class ExtensionsViewlet extends Viewlet implements IExtensionsViewlet {
J
Joao Moreno 已提交
48

49
	private onSearchChange: EventOf<string>;
J
Joao Moreno 已提交
50
	private searchDelayer: ThrottledDelayer<any>;
J
Joao Moreno 已提交
51
	private root: HTMLElement;
J
Joao Moreno 已提交
52 53
	private searchBox: HTMLInputElement;
	private extensionsBox: HTMLElement;
J
Joao Moreno 已提交
54
	private list: PagedList<IExtension>;
J
Joao Moreno 已提交
55 56
	private primaryActions: IAction[];
	private secondaryActions: IAction[];
J
Joao Moreno 已提交
57
	private disposables: IDisposable[] = [];
J
Joao Moreno 已提交
58

J
Joao Moreno 已提交
59 60
	constructor(
		@ITelemetryService telemetryService: ITelemetryService,
61
		@IExtensionGalleryService private galleryService: IExtensionGalleryService,
J
Joao Moreno 已提交
62
		@IExtensionManagementService private extensionService: IExtensionManagementService,
J
Joao Moreno 已提交
63
		@IProgressService private progressService: IProgressService,
J
Joao Moreno 已提交
64
		@IInstantiationService private instantiationService: IInstantiationService,
65
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
J
Joao Moreno 已提交
66
		@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService,
67
		@IURLService urlService: IURLService,
J
Joao Moreno 已提交
68 69
		@IExtensionTipsService private tipsService: IExtensionTipsService,
		@IMessageService private messageService: IMessageService
J
Joao Moreno 已提交
70
	) {
J
Joao Moreno 已提交
71
		super(VIEWLET_ID, telemetryService);
J
Joao Moreno 已提交
72
		this.searchDelayer = new ThrottledDelayer(500);
J
Joao Moreno 已提交
73 74 75

		const onOpenExtensionUrl = filterEvent(urlService.onOpenURL, uri => /^extension/.test(uri.path));
		onOpenExtensionUrl(this.onOpenExtensionUrl, this, this.disposables);
J
Joao Moreno 已提交
76 77 78 79
	}

	create(parent: Builder): TPromise<void> {
		super.create(parent);
J
Joao Moreno 已提交
80 81 82
		parent.addClass('extensions-viewlet');
		this.root = parent.getHTMLElement();

J
Joao Moreno 已提交
83 84 85 86
		const header = append(this.root, $('.header'));

		this.searchBox = append(header, $<HTMLInputElement>('input.search-box'));
		this.searchBox.placeholder = localize('searchExtensions', "Search Extensions in Marketplace");
I
isidor 已提交
87 88
		this.disposables.push(addStandardDisposableListener(this.searchBox, EventType.FOCUS, () => addClass(this.searchBox, 'synthetic-focus')));
		this.disposables.push(addStandardDisposableListener(this.searchBox, EventType.BLUR, () => removeClass(this.searchBox, 'synthetic-focus')));
J
Joao Moreno 已提交
89 90 91
		this.extensionsBox = append(this.root, $('.extensions'));

		const delegate = new Delegate();
92
		const renderer = this.instantiationService.createInstance(Renderer);
J
Joao Moreno 已提交
93
		this.list = new PagedList(this.extensionsBox, delegate, [renderer]);
J
Joao Moreno 已提交
94

J
Joao Moreno 已提交
95 96 97 98 99 100
		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);
J
Joao Moreno 已提交
101 102
		const onPageUpArrow = filterEvent(onKeyDown, e => e.keyCode === KeyCode.PageUp);
		const onPageDownArrow = filterEvent(onKeyDown, e => e.keyCode === KeyCode.PageDown);
J
Joao Moreno 已提交
103

104 105 106 107
		onEnter(this.onEnter, this, this.disposables);
		onEscape(this.onEscape, this, this.disposables);
		onUpArrow(this.onUpArrow, this, this.disposables);
		onDownArrow(this.onDownArrow, this, this.disposables);
J
Joao Moreno 已提交
108 109
		onPageUpArrow(this.onPageUpArrow, this, this.disposables);
		onPageDownArrow(this.onPageDownArrow, this, this.disposables);
J
Joao Moreno 已提交
110

111 112 113 114
		const onSearchInput = domEvent(this.searchBox, 'input') as EventOf<SearchInputEvent>;
		onSearchInput(e => this.triggerSearch(e.target.value, e.immediate), null, this.disposables);

		this.onSearchChange = mapEvent(onSearchInput, e => e.target.value);
J
Joao Moreno 已提交
115

J
Joao Moreno 已提交
116
		const onSelectedExtension = filterEvent(mapEvent(this.list.onSelectionChange, e => e.elements[0]), e => !!e);
J
Joao Moreno 已提交
117
		onSelectedExtension(this.openExtension, this, this.disposables);
J
Joao Moreno 已提交
118

J
Joao Moreno 已提交
119 120 121 122
		return TPromise.as(null);
	}

	setVisible(visible:boolean): TPromise<void> {
J
Joao Moreno 已提交
123 124
		return super.setVisible(visible).then(() => {
			if (visible) {
J
Joao Moreno 已提交
125
				this.searchBox.focus();
126 127
				this.searchBox.setSelectionRange(0, this.searchBox.value.length);
				this.triggerSearch(this.searchBox.value, true, true);
J
Joao Moreno 已提交
128
			} else {
J
Joao Moreno 已提交
129
				this.list.model = new PagedModel([]);
J
Joao Moreno 已提交
130 131
			}
		});
J
Joao Moreno 已提交
132 133 134
	}

	focus(): void {
J
Joao Moreno 已提交
135
		this.searchBox.focus();
J
Joao Moreno 已提交
136 137
	}

J
Joao Moreno 已提交
138
	layout({ height, width }: Dimension):void {
J
Joao Moreno 已提交
139
		this.list.layout(height - 38);
J
Joao Moreno 已提交
140 141 142 143 144
		toggleClass(this.root, 'narrow', width <= 300);
	}

	getOptimalWidth(): number {
		return 400;
J
Joao Moreno 已提交
145 146
	}

J
Joao Moreno 已提交
147
	getActions(): IAction[] {
J
Joao Moreno 已提交
148 149 150 151
		if (!this.primaryActions) {
			this.primaryActions = [
				this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange)
			];
152 153
		}

J
Joao Moreno 已提交
154
		return this.primaryActions;
J
Joao Moreno 已提交
155 156
	}

157
	getSecondaryActions(): IAction[] {
J
Joao Moreno 已提交
158 159
		if (!this.secondaryActions) {
			this.secondaryActions = [
J
Joao Moreno 已提交
160 161
				this.instantiationService.createInstance(UpdateAllAction),
				new Separator(),
J
Joao Moreno 已提交
162 163 164
				this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL),
				this.instantiationService.createInstance(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, ShowOutdatedExtensionsAction.LABEL),
				this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL),
J
Joao Moreno 已提交
165 166
				this.instantiationService.createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL),
				new Separator(),
J
Joao Moreno 已提交
167 168
				this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.install', localize('sort by installs', "Sort By: Install Count"), this.onSearchChange, 'installs', undefined),
				this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort.rating', localize('sort by rating', "Sort By: Rating"), this.onSearchChange, 'rating', undefined),
J
Joao Moreno 已提交
169
				new Separator(),
J
Joao Moreno 已提交
170 171
				this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort..asc', localize('ascending', "Sort Order: ↑"), this.onSearchChange, undefined, 'asc'),
				this.instantiationService.createInstance(ChangeSortAction, 'extensions.sort..desc', localize('descending', "Sort Order: ↓"), this.onSearchChange, undefined, 'desc')
J
Joao Moreno 已提交
172 173 174 175
			];
		}

		return this.secondaryActions;
176 177
	}

178
	search(value: string): void {
179
		const event = new Event('input', { bubbles: true }) as SearchInputEvent;
180
		event.immediate = true;
181 182 183

		this.searchBox.value = value;
		this.searchBox.dispatchEvent(event);
J
Joao Moreno 已提交
184 185
	}

186
	private triggerSearch(value: string, immediate = false, suggestPopular = false): void {
J
Joao Moreno 已提交
187 188
		this.searchDelayer.trigger(() => this.doSearch(value, suggestPopular), immediate || !value ? 0 : 500)
			.done(null, err => this.onError(err));
J
Joao Moreno 已提交
189 190
	}

191
	private doSearch(value: string = '', suggestPopular = false): TPromise<any> {
192 193 194
		return this.progress(this.query(value))
			.then(model => {
				if (!value && model.length === 0 && suggestPopular) {
195
					return this.search('@sort:installs');
196 197 198 199 200 201 202 203 204 205
				}

				this.list.model = model;
				this.list.scrollTop = 0;
			});
	}

	private query(value: string): TPromise<PagedModel<IExtension>> {
		if (!value || /@outdated/i.test(value)) {
			let local = this.extensionsWorkbenchService.queryLocal();
206

207 208
			if (/@outdated/i.test(value)) {
				local = local.then(result => result.filter(e => e.outdated));
209 210
			}

211 212
			return local.then(result => new PagedModel(result));
		}
213

J
Joao Moreno 已提交
214
		const query = Query.parse(value);
215 216
		let options: IQueryOptions = {};

J
Joao Moreno 已提交
217
		if (/@recommended/i.test(query.value)) {
218
			const value = query.value.replace(/@recommended/g, '').trim().toLowerCase();
J
Joao Moreno 已提交
219 220 221 222

			return this.extensionsWorkbenchService.queryLocal().then(local => {
				const names = this.tipsService.getRecommendations()
					.filter(name => local.every(ext => `${ ext.publisher }.${ ext.name }` !== name))
223 224 225
					.filter(name => name.toLowerCase().indexOf(value) > -1);

				this.telemetryService.publicLog('extensionRecommendations:open', { count: names.length });
J
Joao Moreno 已提交
226 227 228 229 230 231 232 233 234 235

				if (!names.length) {
					return new PagedModel([]);
				}

				return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }))
					.then(result => new PagedModel(result));
			});
		}

J
Joao Moreno 已提交
236 237
		switch(query.sortBy) {
			case 'installs': options = assign(options, { sortBy: SortBy.InstallCount }); break;
J
Joao Moreno 已提交
238
			case 'rating': options = assign(options, { sortBy: SortBy.AverageRating }); break;
J
Joao Moreno 已提交
239
		}
J
Joao Moreno 已提交
240

J
Joao Moreno 已提交
241 242 243 244
		switch (query.sortOrder) {
			case 'asc': options = assign(options, { sortOrder: SortOrder.Ascending }); break;
			case 'desc': options = assign(options, { sortOrder: SortOrder.Descending }); break;
		}
J
Joao Moreno 已提交
245

J
Joao Moreno 已提交
246
		if (query.value) {
247
			options = assign(options, { text: query.value.substr(0, 200) });
J
Joao Moreno 已提交
248
		}
249 250 251

		return this.extensionsWorkbenchService.queryGallery(options)
			.then(result => new PagedModel(result));
J
Joao Moreno 已提交
252 253
	}

J
Joao Moreno 已提交
254
	private openExtension(extension: IExtension): void {
J
Joao Moreno 已提交
255
		this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension))
J
Joao Moreno 已提交
256
			.done(null, err => this.onError(err));
J
Joao Moreno 已提交
257 258
	}

J
Joao Moreno 已提交
259 260 261 262 263
	private onEnter(): void {
		this.list.setSelection(...this.list.getFocus());
	}

	private onEscape(): void {
264
		this.search('');
J
Joao Moreno 已提交
265 266 267 268
	}

	private onUpArrow(): void {
		this.list.focusPrevious();
J
Joao Moreno 已提交
269
		this.list.reveal(this.list.getFocus()[0]);
J
Joao Moreno 已提交
270 271 272 273
	}

	private onDownArrow(): void {
		this.list.focusNext();
J
Joao Moreno 已提交
274
		this.list.reveal(this.list.getFocus()[0]);
J
Joao Moreno 已提交
275 276
	}

J
Joao Moreno 已提交
277 278 279 280 281 282 283 284 285 286
	private onPageUpArrow(): void {
		this.list.focusPreviousPage();
		this.list.reveal(this.list.getFocus()[0]);
	}

	private onPageDownArrow(): void {
		this.list.focusNextPage();
		this.list.reveal(this.list.getFocus()[0]);
	}

J
Joao Moreno 已提交
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
	private onOpenExtensionUrl(uri: URI): void {
		const match = /^extension\/([^/]+)$/.exec(uri.path);

		if (!match) {
			return;
		}

		const extensionId = match[1];

		this.extensionsWorkbenchService.queryGallery({ names: [extensionId] })
			.done(result => {
				if (result.total < 1) {
					return;
				}

				const extension = result.firstPage[0];
				this.openExtension(extension);
			});
	}

307 308 309 310 311
	private progress<T>(promise: TPromise<T>): TPromise<T> {
		const progressRunner = this.progressService.show(true);
		return always(promise, () => progressRunner.done());
	}

J
Joao Moreno 已提交
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
	private onError(err: any): void {
		if (isPromiseCanceledError(err)) {
			return;
		}

		const message = err && err.message || '';

		if (!/ECONNREFUSED/.test(message)) {
			const error = createError(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), {
				actions: [
					CloseAction,
					this.instantiationService.createInstance(OpenGlobalSettingsAction, OpenGlobalSettingsAction.ID, OpenGlobalSettingsAction.LABEL)
				]
			});

			this.messageService.show(Severity.Error, error);
			return;
		}

		this.messageService.show(Severity.Error, err);
	}

J
Joao Moreno 已提交
334
	dispose(): void {
J
Joao Moreno 已提交
335
		this.disposables = dispose(this.disposables);
J
Joao Moreno 已提交
336 337 338
		super.dispose();
	}
}