extensionEditor.ts 16.0 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/extensionEditor';
J
Joao Moreno 已提交
9
import { localize } from 'vs/nls';
J
Joao Moreno 已提交
10 11
import { TPromise } from 'vs/base/common/winjs.base';
import { marked } from 'vs/base/common/marked/marked';
J
Joao Moreno 已提交
12
import { always } from 'vs/base/common/async';
J
Joao Moreno 已提交
13
import * as arrays from 'vs/base/common/arrays';
J
Joao Moreno 已提交
14
import Event, { Emitter, once } from 'vs/base/common/event';
J
Joao Moreno 已提交
15
import Cache from 'vs/base/common/cache';
J
Joao Moreno 已提交
16
import { Action } from 'vs/base/common/actions';
17
import { onUnexpectedError } from 'vs/base/common/errors';
J
Joao Moreno 已提交
18
import { IDisposable, empty, dispose, toDisposable } from 'vs/base/common/lifecycle';
J
Joao Moreno 已提交
19
import { Builder } from 'vs/base/browser/builder';
20
import { domEvent } from 'vs/base/browser/event';
J
Joao Moreno 已提交
21
import { append, $, addClass, removeClass, finalHandler } from 'vs/base/browser/dom';
J
Joao Moreno 已提交
22
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
23
import { IViewlet } from 'vs/workbench/common/viewlet';
J
Joao Moreno 已提交
24
import { IViewletService } from 'vs/workbench/services/viewlet/common/viewletService';
J
Joao Moreno 已提交
25 26
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
J
Joao Moreno 已提交
27
import { IExtensionGalleryService, IExtensionManifest } from 'vs/platform/extensionManagement/common/extensionManagement';
J
Joao Moreno 已提交
28
import { IThemeService } from 'vs/workbench/services/themes/common/themeService';
29
import { ExtensionsInput } from './extensionsInput';
J
Joao Moreno 已提交
30
import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, IExtension } from './extensions';
31
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
J
Joao Moreno 已提交
32
import { ITemplateData } from './extensionsList';
33
import { RatingsWidget, InstallWidget } from './extensionsWidgets';
J
Joao Moreno 已提交
34
import { EditorOptions } from 'vs/workbench/common/editor';
J
Joao Moreno 已提交
35 36
import { shell } from 'electron';
import product from 'vs/platform/product';
J
Joao Moreno 已提交
37
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
38
import { CombinedInstallAction, UpdateAction, EnableAction } from './extensionsActions';
J
Joao Moreno 已提交
39
import WebView from 'vs/workbench/parts/html/browser/webview';
40
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
J
Joao Moreno 已提交
41 42
import { Keybinding } from 'vs/base/common/keyCodes';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
J
Joao Moreno 已提交
43 44 45 46 47 48 49 50 51 52 53

function renderBody(body: string): string {
	return `<!DOCTYPE html>
		<html>
			<head>
				<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
				<link rel="stylesheet" type="text/css" href="${ require.toUrl('./media/markdown.css') }" >
			</head>
			<body>${ body }</body>
		</html>`;
}
J
Joao Moreno 已提交
54

J
Joao Moreno 已提交
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
class NavBar {

	private _onChange = new Emitter<string>();
	get onChange(): Event<string> { return this._onChange.event; }

	private actions: Action[];
	private actionbar: ActionBar;

	constructor(container: HTMLElement) {
		const element = append(container, $('.navbar'));
		this.actions = [];
		this.actionbar = new ActionBar(element, { animated: false });
	}

	push(id: string, label: string): void {
		const run = () => {
			this._onChange.fire(id);
			this.actions.forEach(a => a.enabled = a.id !== action.id);
			return TPromise.as(null);
		};

		const action = new Action(id, label, null, true, run);

		this.actions.push(action);
		this.actionbar.push(action);

		if (this.actions.length === 1) {
			run();
		}
	}

	clear(): void {
		this.actions = dispose(this.actions);
		this.actionbar.clear();
	}

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

const NavbarSection = {
	Readme: 'readme',
98
	Contributions: 'contributions'
J
Joao Moreno 已提交
99 100
};

J
Joao Moreno 已提交
101 102 103 104
export class ExtensionEditor extends BaseEditor {

	static ID: string = 'workbench.editor.extension';

105
	private icon: HTMLImageElement;
J
Joao Moreno 已提交
106
	private name: HTMLAnchorElement;
J
Joao Moreno 已提交
107
	private license: HTMLAnchorElement;
J
Joao Moreno 已提交
108
	private publisher: HTMLAnchorElement;
J
Joao Moreno 已提交
109
	private installCount: HTMLElement;
J
Joao Moreno 已提交
110
	private rating: HTMLAnchorElement;
J
Joao Moreno 已提交
111
	private description: HTMLElement;
J
Joao Moreno 已提交
112 113 114
	private extensionActionBar: ActionBar;
	private navbar: NavBar;
	private content: HTMLElement;
J
Joao Moreno 已提交
115 116 117 118

	private _highlight: ITemplateData;
	private highlightDisposable: IDisposable;

J
Joao Moreno 已提交
119 120 121
	private extensionReadme: Cache<string>;
	private extensionManifest: Cache<IExtensionManifest>;

J
Joao Moreno 已提交
122 123
	private contentDisposables: IDisposable[] = [];
	private transientDisposables: IDisposable[] = [];
J
Joao Moreno 已提交
124
	private disposables: IDisposable[];
J
Joao Moreno 已提交
125 126 127

	constructor(
		@ITelemetryService telemetryService: ITelemetryService,
128 129
		@IExtensionGalleryService private galleryService: IExtensionGalleryService,
		@IConfigurationService private configurationService: IConfigurationService,
J
Joao Moreno 已提交
130
		@IInstantiationService private instantiationService: IInstantiationService,
131
		@IViewletService private viewletService: IViewletService,
J
Joao Moreno 已提交
132
		@IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService,
133
		@IThemeService private themeService: IThemeService,
J
Joao Moreno 已提交
134 135
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
		@IKeybindingService private keybindingService: IKeybindingService
J
Joao Moreno 已提交
136 137 138 139
	) {
		super(ExtensionEditor.ID, telemetryService);
		this._highlight = null;
		this.highlightDisposable = empty;
J
Joao Moreno 已提交
140
		this.disposables = [];
J
Joao Moreno 已提交
141 142
		this.extensionReadme = null;
		this.extensionManifest = null;
143

144
		this.disposables.push(viewletService.onDidViewletOpen(this.onViewletOpen, this, this.disposables));
J
Joao Moreno 已提交
145 146 147 148
	}

	createEditor(parent: Builder): void {
		const container = parent.getHTMLElement();
J
Joao Moreno 已提交
149 150 151

		const root = append(container, $('.extension-editor'));
		const header = append(root, $('.header'));
J
Joao Moreno 已提交
152

153
		this.icon = append(header, $<HTMLImageElement>('img.icon'));
J
Joao Moreno 已提交
154

J
Joao Moreno 已提交
155
		const details = append(header, $('.details'));
J
Joao Moreno 已提交
156 157
		const title = append(details, $('.title'));
		this.name = append(title, $<HTMLAnchorElement>('a.name'));
J
Joao Moreno 已提交
158
		this.name.href = '#';
J
Joao Moreno 已提交
159 160

		const subtitle = append(details, $('.subtitle'));
J
Joao Moreno 已提交
161 162
		this.publisher = append(subtitle, $<HTMLAnchorElement>('a.publisher'));
		this.publisher.href = '#';
J
Joao Moreno 已提交
163

164
		this.installCount = append(subtitle, $('span.install'));
J
Joao Moreno 已提交
165

J
Joao Moreno 已提交
166 167
		this.rating = append(subtitle, $<HTMLAnchorElement>('a.rating'));
		this.rating.href = '#';
J
Joao Moreno 已提交
168

J
Joao Moreno 已提交
169 170 171
		this.license = append(subtitle, $<HTMLAnchorElement>('a.license'));
		this.license.href = '#';
		this.license.textContent = localize('license', 'License');
172
		this.license.style.display = 'none';
J
Joao Moreno 已提交
173

J
Joao Moreno 已提交
174 175
		this.description = append(details, $('.description'));

J
Joao Moreno 已提交
176 177 178 179 180 181
		const extensionActions = append(details, $('.actions'));
		this.extensionActionBar = new ActionBar(extensionActions, { animated: false });
		this.disposables.push(this.extensionActionBar);

		const body = append(root, $('.body'));
		this.navbar = new NavBar(body);
J
Joao Moreno 已提交
182

J
Joao Moreno 已提交
183
		this.content = append(body, $('.content'));
J
Joao Moreno 已提交
184 185 186
	}

	setInput(input: ExtensionsInput, options: EditorOptions): TPromise<void> {
J
Joao Moreno 已提交
187 188
		this.transientDisposables = dispose(this.transientDisposables);

J
Joao Moreno 已提交
189
		const extension = input.extension;
190
		this.telemetryService.publicLog('extensionGallery:openExtension', extension.telemetryData);
J
Joao Moreno 已提交
191

J
Joao Moreno 已提交
192 193 194
		this.extensionReadme = new Cache(() => extension.getReadme());
		this.extensionManifest = new Cache(() => extension.getManifest());

195 196 197 198
		const onError = once(domEvent(this.icon, 'error'));
		onError(() => this.icon.src = extension.iconUrlFallback, null, this.transientDisposables);
		this.icon.src = extension.iconUrl;

J
Joao Moreno 已提交
199 200 201
		this.name.textContent = extension.displayName;
		this.publisher.textContent = extension.publisherDisplayName;
		this.description.textContent = extension.description;
J
Joao Moreno 已提交
202

J
Joao Moreno 已提交
203 204 205
		if (product.extensionsGallery) {
			const extensionUrl = `${ product.extensionsGallery.itemUrl }?itemName=${ extension.publisher }.${ extension.name }`;

J
Joao Moreno 已提交
206 207 208
			this.name.onclick = finalHandler(() => shell.openExternal(extensionUrl));
			this.rating.onclick = finalHandler(() => shell.openExternal(`${ extensionUrl }#review-details`));
			this.publisher.onclick = finalHandler(() => {
209
				this.viewletService.openViewlet(VIEWLET_ID, true)
J
Joao Moreno 已提交
210 211 212
					.then(viewlet => viewlet as IExtensionsViewlet)
					.done(viewlet => viewlet.search(`publisher:"${ extension.publisherDisplayName }"`, true));
			});
213 214 215 216 217 218 219 220

			if (extension.licenseUrl) {
				this.license.onclick = finalHandler(() => shell.openExternal(extension.licenseUrl));
				this.license.style.display = 'initial';
			} else {
				this.license.onclick = null;
				this.license.style.display = 'none';
			}
J
Joao Moreno 已提交
221 222
		}

J
Joao Moreno 已提交
223
		const install = this.instantiationService.createInstance(InstallWidget, this.installCount, { extension });
J
Joao Moreno 已提交
224 225
		this.transientDisposables.push(install);

J
Joao Moreno 已提交
226
		const ratings = this.instantiationService.createInstance(RatingsWidget, this.rating, { extension });
J
Joao Moreno 已提交
227
		this.transientDisposables.push(ratings);
J
Joao Moreno 已提交
228

J
Joao Moreno 已提交
229 230 231 232 233 234 235 236
		const installAction = this.instantiationService.createInstance(CombinedInstallAction);
		const updateAction = this.instantiationService.createInstance(UpdateAction);
		const enableAction = this.instantiationService.createInstance(EnableAction);

		installAction.extension = extension;
		updateAction.extension = extension;
		enableAction.extension = extension;

J
Joao Moreno 已提交
237 238
		this.extensionActionBar.clear();
		this.extensionActionBar.push([enableAction, updateAction, installAction], { icon: true, label: true });
239
		this.transientDisposables.push(enableAction, updateAction, installAction);
J
Joao Moreno 已提交
240

J
Joao Moreno 已提交
241 242
		this.navbar.clear();
		this.navbar.onChange(this.onNavbarChange.bind(this, extension), this, this.transientDisposables);
243 244
		this.navbar.push(NavbarSection.Readme, localize('details', "Details"));
		this.navbar.push(NavbarSection.Contributions, localize('contributions', "Contributions"));
J
Joao Moreno 已提交
245 246 247 248 249 250 251 252 253

		this.content.innerHTML = '';

		return super.setInput(input, options);
	}

	private onNavbarChange(extension: IExtension, id: string): void {
		switch (id) {
			case NavbarSection.Readme: return this.openReadme(extension);
254
			case NavbarSection.Contributions: return this.openContributions(extension);
J
Joao Moreno 已提交
255
		}
J
Joao Moreno 已提交
256
	}
J
Joao Moreno 已提交
257

J
Joao Moreno 已提交
258
	private openReadme(extension: IExtension) {
J
Joao Moreno 已提交
259
		return this.loadContents(() => this.extensionReadme.get()
J
Joao Moreno 已提交
260
			.then(marked.parse)
J
Joao Moreno 已提交
261
			.then(renderBody)
J
Joao Moreno 已提交
262 263 264 265 266 267 268
			.then<void>(body => {
				const webview = new WebView(
					this.content,
					document.querySelector('.monaco-editor-background')
				);

				webview.style(this.themeService.getColorTheme());
J
Joao Moreno 已提交
269
				webview.contents = [body];
J
Joao Moreno 已提交
270 271 272 273 274

				const linkListener = webview.onDidClickLink(link => shell.openExternal(link.toString()));
				const themeListener = this.themeService.onDidColorThemeChange(themeId => webview.style(themeId));
				this.contentDisposables.push(webview, linkListener, themeListener);
			})
J
Joao Moreno 已提交
275 276 277
			.then(null, () => {
				const p = append(this.content, $('p'));
				p.textContent = localize('noReadme', "No README available.");
J
Joao Moreno 已提交
278
			}));
J
Joao Moreno 已提交
279
	}
J
Joao Moreno 已提交
280

281
	private openContributions(extension: IExtension) {
J
Joao Moreno 已提交
282
		return this.loadContents(() => this.extensionManifest.get()
283
			.then(manifest => {
J
Joao Moreno 已提交
284
				this.content.innerHTML = '';
285 286
				const content = append(this.content, $('div', { class: 'subcontent' }));

J
Joao Moreno 已提交
287
				ExtensionEditor.renderSettings(content, manifest);
J
Joao Moreno 已提交
288
				this.renderCommands(content, manifest);
J
Joao Moreno 已提交
289
				ExtensionEditor.renderThemes(content, manifest);
J
Joao Moreno 已提交
290
				ExtensionEditor.renderJSONValidation(content, manifest);
J
Joao Moreno 已提交
291
				ExtensionEditor.renderDebuggers(content, manifest);
292
			}));
J
Joao Moreno 已提交
293 294
	}

J
Joao Moreno 已提交
295 296 297
	private static renderSettings(container: HTMLElement, manifest: IExtensionManifest): void {
		const configuration = manifest.contributes.configuration;
		const properties = configuration && configuration.properties;
J
Joao Moreno 已提交
298
		const contrib = properties ? Object.keys(properties) : [];
J
Joao Moreno 已提交
299

J
Joao Moreno 已提交
300
		if (!contrib.length) {
J
Joao Moreno 已提交
301 302 303 304
			return;
		}

		append(container, $('details', { open: true },
J
Joao Moreno 已提交
305
			$('summary', null, localize('settings', "Settings ({0})", contrib.length)),
J
Joao Moreno 已提交
306
			$('table', null,
J
Joao Moreno 已提交
307
				$('tr', null, $('th', null, localize('setting name', "Name")), $('th', null, localize('description', "Description"))),
J
Joao Moreno 已提交
308
				...contrib.map(key => $('tr', null, $('td', null, $('code', null, key)), $('td', null, properties[key].description)))
J
Joao Moreno 已提交
309 310 311 312 313
			)
		));
	}

	private static renderDebuggers(container: HTMLElement, manifest: IExtensionManifest): void {
J
Joao Moreno 已提交
314
		const contrib = manifest.contributes.debuggers || [];
J
Joao Moreno 已提交
315

J
Joao Moreno 已提交
316
		if (!contrib.length) {
J
Joao Moreno 已提交
317 318 319 320
			return;
		}

		append(container, $('details', { open: true },
J
Joao Moreno 已提交
321
			$('summary', null, localize('debuggers', "Debuggers ({0})", contrib.length)),
J
Joao Moreno 已提交
322
			$('table', null,
J
Joao Moreno 已提交
323
				$('tr', null, $('th', null, localize('debugger name', "Name")), $('th', null, localize('runtime', "Runtime"))),
J
Joao Moreno 已提交
324
				...contrib.map(d => $('tr', null, $('td', null, d.label || d.type), $('td', null, d.runtime)))
J
Joao Moreno 已提交
325 326 327 328
			)
		));
	}

J
Joao Moreno 已提交
329
	private static renderThemes(container: HTMLElement, manifest: IExtensionManifest): void {
J
Joao Moreno 已提交
330
		const contrib = manifest.contributes.themes || [];
J
Joao Moreno 已提交
331

J
Joao Moreno 已提交
332
		if (!contrib.length) {
J
Joao Moreno 已提交
333 334 335 336
			return;
		}

		append(container, $('details', { open: true },
J
Joao Moreno 已提交
337
			$('summary', null, localize('themes', "Themes ({0})", contrib.length)),
J
Joao Moreno 已提交
338
			$('ul', null,
J
Joao Moreno 已提交
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
				...contrib.map(theme => $('li', null, theme.label))
			)
		));
	}

	private static renderJSONValidation(container: HTMLElement, manifest: IExtensionManifest): void {
		const contrib = manifest.contributes.jsonValidation || [];

		if (!contrib.length) {
			return;
		}

		append(container, $('details', { open: true },
			$('summary', null, localize('JSON Validation', "JSON Validation ({0})", contrib.length)),
			$('ul', null,
				...contrib.map(v => $('li', null, v.fileMatch))
J
Joao Moreno 已提交
355 356 357 358
			)
		));
	}

J
Joao Moreno 已提交
359
	private renderCommands(container: HTMLElement, manifest: IExtensionManifest): void {
J
Joao Moreno 已提交
360 361
		const rawCommands = manifest.contributes.commands || [];
		const commands = rawCommands.map(c => ({
362 363
			id: c.command,
			title: c.title,
J
Joao Moreno 已提交
364
			keybindings: [],
365 366 367
			menus: []
		}));

J
Joao Moreno 已提交
368
		const allCommands = arrays.index(commands, c => c.id);
369 370

		const menus = manifest.contributes.menus || {};
J
Joao Moreno 已提交
371

372 373 374 375 376
		Object.keys(menus).forEach(context => {
			menus[context].forEach(menu => {
				let command = allCommands[menu.command];

				if (!command) {
J
Joao Moreno 已提交
377
					command = { id: menu.command, title: '', keybindings: [], menus: [context] };
378 379 380 381 382 383 384 385
					allCommands[command.id] = command;
					commands.push(command);
				} else {
					command.menus.push(context);
				}
			});
		});

J
Joao Moreno 已提交
386 387 388
		const rawKeybindings = manifest.contributes.keybindings || [];

		rawKeybindings.forEach(userString => {
J
Joao Moreno 已提交
389 390 391 392 393 394 395 396 397 398 399 400 401
			let command = allCommands[userString.command];
			const keybinding = new Keybinding(Keybinding.fromUserSettingsLabel(userString.key));
			const key = this.keybindingService.getLabelFor(keybinding);

			if (!command) {
				command = { id: userString.command, title: '', keybindings: [key], menus: [] };
				allCommands[command.id] = command;
				commands.push(command);
			} else {
				command.keybindings.push(key);
			}
		});

402 403 404 405 406 407 408 409 410 411
		if (!commands.length) {
			return;
		}

		append(container, $('details', { open: true },
			$('summary', null, localize('commands', "Commands ({0})", commands.length)),
			$('table', null,
				$('tr', null,
					$('th', null, localize('command name', "Name")),
					$('th', null, localize('description', "Description")),
J
Joao Moreno 已提交
412
					$('th', null, localize('keyboard shortcuts', "Keyboard Shortcuts")),
413 414 415 416 417
					$('th', null, localize('menuContexts', "Menu Contexts"))
				),
				...commands.map(c => $('tr', null,
					$('td', null, c.id),
					$('td', null, c.title),
J
Joao Moreno 已提交
418
					$('td', null, ...c.keybindings.map(keybinding => $('code', null, keybinding))),
419 420 421 422 423 424
					$('td', null, ...c.menus.map(context => $('code', null, context)))
				))
			)
		));
	}

J
Joao Moreno 已提交
425 426 427
	private loadContents(loadingTask: ()=>TPromise<any>): void {
		this.contentDisposables = dispose(this.contentDisposables);

J
Joao Moreno 已提交
428
		this.content.innerHTML = '';
J
Joao Moreno 已提交
429
		addClass(this.content, 'loading');
J
Joao Moreno 已提交
430

J
Joao Moreno 已提交
431 432
		let promise = loadingTask();
		promise = always(promise, () => removeClass(this.content, 'loading'));
J
Joao Moreno 已提交
433

J
Joao Moreno 已提交
434
		this.contentDisposables.push(toDisposable(() => promise.cancel()));
J
Joao Moreno 已提交
435 436 437 438 439 440
	}

	layout(): void {
		return;
	}

441
	private onViewletOpen(viewlet: IViewlet): void {
442 443 444 445 446 447 448
		if (!viewlet || viewlet.getId() === VIEWLET_ID) {
			return;
		}

		this.editorService.closeEditor(this.position, this.input).done(null, onUnexpectedError);
	}

J
Joao Moreno 已提交
449 450
	dispose(): void {
		this._highlight = null;
J
Joao Moreno 已提交
451 452
		this.transientDisposables = dispose(this.transientDisposables);
		this.disposables = dispose(this.disposables);
J
Joao Moreno 已提交
453 454 455
		super.dispose();
	}
}