extensionsViewlet.ts 15.4 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';
12
import { isPromiseCanceledError, onUnexpectedError, create as createError } from 'vs/base/common/errors';
J
Joao Moreno 已提交
13
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
J
Joao Moreno 已提交
14 15
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Builder, Dimension } from 'vs/base/browser/builder';
16
import { assign } from 'vs/base/common/objects';
J
Joao Moreno 已提交
17
import EventOf, { mapEvent, chain } from 'vs/base/common/event';
18
import { IAction } from 'vs/base/common/actions';
J
Joao Moreno 已提交
19
import { domEvent } from 'vs/base/browser/event';
J
Joao Moreno 已提交
20
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
J
Joao Moreno 已提交
21 22
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
J
Joao Moreno 已提交
23
import { Viewlet } from 'vs/workbench/browser/viewlet';
24 25
import { IViewlet } from 'vs/workbench/common/viewlet';
import { IViewletService } from 'vs/workbench/services/viewlet/common/viewletService';
J
Joao Moreno 已提交
26
import { append, $, addStandardDisposableListener, EventType, addClass, removeClass, toggleClass } from 'vs/base/browser/dom';
27
import { PagedModel } from 'vs/base/common/paging';
J
Joao Moreno 已提交
28
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
J
Joao Moreno 已提交
29 30
import { PagedList } from 'vs/base/browser/ui/list/listPaging';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
J
Joao Moreno 已提交
31
import { Delegate, Renderer } from './extensionsList';
J
Joao Moreno 已提交
32
import { IExtensionsWorkbenchService, IExtension, IExtensionsViewlet, VIEWLET_ID, ExtensionState } from './extensions';
J
Joao Moreno 已提交
33
import { ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowInstalledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction } from './extensionsActions';
J
Joao Moreno 已提交
34
import { IExtensionManagementService, IExtensionGalleryService, IExtensionTipsService, SortBy, SortOrder, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
35
import { ExtensionsInput } from './extensionsInput';
J
Joao Moreno 已提交
36
import { Query } from '../common/extensionQuery';
J
Joao Moreno 已提交
37
import { OpenGlobalSettingsAction } from 'vs/workbench/browser/actions/openSettings';
J
Joao Moreno 已提交
38
import { IProgressService } from 'vs/platform/progress/common/progress';
J
Joao Moreno 已提交
39
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
40
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
J
Joao Moreno 已提交
41 42
import { IMessageService, CloseAction } from 'vs/platform/message/common/message';
import Severity from 'vs/base/common/severity';
J
Joao Moreno 已提交
43 44
import { IURLService } from 'vs/platform/url/common/url';
import URI from 'vs/base/common/uri';
J
Joao Moreno 已提交
45
import { IActivityService, ProgressBadge, NumberBadge } from 'vs/workbench/services/activity/common/activityService';
J
Joao Moreno 已提交
46

47 48 49 50
interface SearchInputEvent extends Event {
	target: HTMLInputElement;
	immediate?: boolean;
}
J
Joao Moreno 已提交
51

52
export class ExtensionsViewlet extends Viewlet implements IExtensionsViewlet {
J
Joao Moreno 已提交
53

54
	private onSearchChange: EventOf<string>;
J
Joao Moreno 已提交
55
	private searchDelayer: ThrottledDelayer<any>;
J
Joao Moreno 已提交
56
	private root: HTMLElement;
J
Joao Moreno 已提交
57 58
	private searchBox: HTMLInputElement;
	private extensionsBox: HTMLElement;
J
Joao Moreno 已提交
59
	private list: PagedList<IExtension>;
J
Joao Moreno 已提交
60 61
	private primaryActions: IAction[];
	private secondaryActions: IAction[];
J
Joao Moreno 已提交
62
	private disposables: IDisposable[] = [];
J
Joao Moreno 已提交
63

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

J
Joao Moreno 已提交
81 82 83
		chain(urlService.onOpenURL)
			.filter(uri => /^extension/.test(uri.path))
			.on(this.onOpenExtensionUrl, this, this.disposables);
84 85

		this.disposables.push(viewletService.onDidViewletOpen(this.onViewletOpen, this, this.disposables));
J
Joao Moreno 已提交
86 87 88 89
	}

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

J
Joao Moreno 已提交
93 94 95 96
		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 已提交
97 98
		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 已提交
99 100 101
		this.extensionsBox = append(this.root, $('.extensions'));

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

J
Joao Moreno 已提交
105 106 107 108 109 110 111 112 113
		const onKeyDown = chain(domEvent(this.searchBox, 'keydown'))
			.map(e => new StandardKeyboardEvent(e));

		onKeyDown.filter(e => e.keyCode === KeyCode.Enter).on(this.onEnter, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(this.onUpArrow, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(this.onDownArrow, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.PageUp).on(this.onPageUpArrow, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.PageDown).on(this.onPageDownArrow, this, this.disposables);
J
Joao Moreno 已提交
114

115 116 117 118
		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 已提交
119

J
Joao Moreno 已提交
120 121 122 123
		chain(this.list.onSelectionChange)
			.map(e => e.elements[0])
			.filter(e => !!e)
			.on(this.openExtension, this, this.disposables);
J
Joao Moreno 已提交
124

J
Joao Moreno 已提交
125 126 127 128
		return TPromise.as(null);
	}

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

	focus(): void {
J
Joao Moreno 已提交
141
		this.searchBox.focus();
J
Joao Moreno 已提交
142 143
	}

J
Joao Moreno 已提交
144
	layout({ height, width }: Dimension):void {
J
Joao Moreno 已提交
145
		this.list.layout(height - 38);
J
Joao Moreno 已提交
146 147 148 149 150
		toggleClass(this.root, 'narrow', width <= 300);
	}

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

J
Joao Moreno 已提交
153
	getActions(): IAction[] {
J
Joao Moreno 已提交
154 155 156 157
		if (!this.primaryActions) {
			this.primaryActions = [
				this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange)
			];
158 159
		}

J
Joao Moreno 已提交
160
		return this.primaryActions;
J
Joao Moreno 已提交
161 162
	}

163
	getSecondaryActions(): IAction[] {
J
Joao Moreno 已提交
164 165
		if (!this.secondaryActions) {
			this.secondaryActions = [
166
				this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL),
J
Joao Moreno 已提交
167
				new Separator(),
J
Joao Moreno 已提交
168 169 170
				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 已提交
171 172
				this.instantiationService.createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL),
				new Separator(),
J
Joao Moreno 已提交
173 174
				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 已提交
175
				new Separator(),
J
Joao Moreno 已提交
176 177
				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 已提交
178 179 180 181
			];
		}

		return this.secondaryActions;
182 183
	}

184
	search(value: string): void {
185
		const event = new Event('input', { bubbles: true }) as SearchInputEvent;
186
		event.immediate = true;
187 188 189

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

192
	private triggerSearch(value: string, immediate = false, suggestPopular = false): void {
J
Joao Moreno 已提交
193 194
		this.searchDelayer.trigger(() => this.doSearch(value, suggestPopular), immediate || !value ? 0 : 500)
			.done(null, err => this.onError(err));
J
Joao Moreno 已提交
195 196
	}

197
	private doSearch(value: string = '', suggestPopular = false): TPromise<any> {
198 199 200
		return this.progress(this.query(value))
			.then(model => {
				if (!value && model.length === 0 && suggestPopular) {
201
					return this.search('@sort:installs');
202 203 204 205 206 207 208 209 210 211
				}

				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();
212

213 214
			if (/@outdated/i.test(value)) {
				local = local.then(result => result.filter(e => e.outdated));
215 216
			}

217 218
			return local.then(result => new PagedModel(result));
		}
219

J
Joao Moreno 已提交
220
		const query = Query.parse(value);
221 222
		let options: IQueryOptions = {};

J
Joao Moreno 已提交
223 224 225 226 227 228 229 230 231 232
		switch(query.sortBy) {
			case 'installs': options = assign(options, { sortBy: SortBy.InstallCount }); break;
			case 'rating': options = assign(options, { sortBy: SortBy.AverageRating }); break;
		}

		switch (query.sortOrder) {
			case 'asc': options = assign(options, { sortOrder: SortOrder.Ascending }); break;
			case 'desc': options = assign(options, { sortOrder: SortOrder.Descending }); break;
		}

J
Joao Moreno 已提交
233
		if (/@recommended/i.test(query.value)) {
234
			const value = query.value.replace(/@recommended/g, '').trim().toLowerCase();
J
Joao Moreno 已提交
235 236 237 238

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

				this.telemetryService.publicLog('extensionRecommendations:open', { count: names.length });
J
Joao Moreno 已提交
242 243 244 245 246 247 248 249 250 251

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

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

J
Joao Moreno 已提交
252
		if (query.value) {
253
			options = assign(options, { text: query.value.substr(0, 200) });
J
Joao Moreno 已提交
254
		}
255 256 257

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

J
Joao Moreno 已提交
260
	private openExtension(extension: IExtension): void {
J
Joao Moreno 已提交
261
		this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension))
J
Joao Moreno 已提交
262
			.done(null, err => this.onError(err));
J
Joao Moreno 已提交
263 264
	}

J
Joao Moreno 已提交
265 266 267 268 269
	private onEnter(): void {
		this.list.setSelection(...this.list.getFocus());
	}

	private onEscape(): void {
270
		this.search('');
J
Joao Moreno 已提交
271 272 273 274
	}

	private onUpArrow(): void {
		this.list.focusPrevious();
J
Joao Moreno 已提交
275
		this.list.reveal(this.list.getFocus()[0]);
J
Joao Moreno 已提交
276 277 278 279
	}

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

J
Joao Moreno 已提交
283 284 285 286 287 288 289 290 291 292
	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 已提交
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
	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);
			});
	}

313 314 315 316 317
	private progress<T>(promise: TPromise<T>): TPromise<T> {
		const progressRunner = this.progressService.show(true);
		return always(promise, () => progressRunner.done());
	}

318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
	private onViewletOpen(viewlet: IViewlet): void {
		if (!viewlet || viewlet.getId() === VIEWLET_ID) {
			return;
		}

		const model = this.editorInputService.getStacksModel();

		const promises = model.groups.map(group => {
			const position = model.positionOfGroup(group);
			const inputs = group.getEditors().filter(input => input instanceof ExtensionsInput);
			const promises = inputs.map(input => this.editorService.closeEditor(position, input));

			return TPromise.join(promises);
		});

		TPromise.join(promises).done(null, onUnexpectedError);
	}

J
Joao Moreno 已提交
336 337 338 339 340 341 342
	private onError(err: any): void {
		if (isPromiseCanceledError(err)) {
			return;
		}

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

J
Joao Moreno 已提交
343
		if (/ECONNREFUSED/.test(message)) {
J
Joao Moreno 已提交
344 345
			const error = createError(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), {
				actions: [
346 347
					this.instantiationService.createInstance(OpenGlobalSettingsAction, OpenGlobalSettingsAction.ID, OpenGlobalSettingsAction.LABEL),
					CloseAction
J
Joao Moreno 已提交
348 349 350 351 352 353 354 355 356 357
				]
			});

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

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

J
Joao Moreno 已提交
358
	dispose(): void {
J
Joao Moreno 已提交
359
		this.disposables = dispose(this.disposables);
J
Joao Moreno 已提交
360 361 362
		super.dispose();
	}
}
J
Joao Moreno 已提交
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398

export class StatusUpdater implements IWorkbenchContribution {

	private disposables: IDisposable[];

	constructor(
		@IActivityService private activityService: IActivityService,
		@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService
	) {
		extensionsWorkbenchService.onChange(this.onServiceChange, this, this.disposables);
	}

	getId(): string {
		return 'vs.extensions.statusupdater';
	}

	private onServiceChange(): void {
		if (this.extensionsWorkbenchService.local.some(e => e.state === ExtensionState.Installing)) {
			this.activityService.showActivity(VIEWLET_ID, new ProgressBadge(() => localize('extensions', 'Extensions')), 'extensions-badge progress-badge');
			return;
		}

		const outdated = this.extensionsWorkbenchService.local.reduce((r, e) => r + (e.outdated ? 1 : 0), 0);

		if (outdated > 0) {
			const badge = new NumberBadge(outdated, n => localize('outdatedExtensions', '{0} Outdated Extensions', n));
			this.activityService.showActivity(VIEWLET_ID, badge, 'extensions-badge count-badge');
		} else {
			this.activityService.showActivity(VIEWLET_ID, null, 'extensions-badge');
		}
	}

	dispose(): void {
		this.disposables = dispose(this.disposables);
	}
}