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

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 已提交
53

J
Joao Moreno 已提交
54 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
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',
97
	Contributions: 'contributions'
J
Joao Moreno 已提交
98 99
};

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

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

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

	private _highlight: ITemplateData;
	private highlightDisposable: IDisposable;

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

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

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

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

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

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

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

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

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

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

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

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

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

J
Joao Moreno 已提交
175 176 177 178 179 180
		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 已提交
181

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

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

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

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

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

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

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

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

			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 已提交
220 221
		}

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

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

J
Joao Moreno 已提交
228 229 230 231 232 233 234 235
		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 已提交
236 237
		this.extensionActionBar.clear();
		this.extensionActionBar.push([enableAction, updateAction, installAction], { icon: true, label: true });
238
		this.transientDisposables.push(enableAction, updateAction, installAction);
J
Joao Moreno 已提交
239

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

		this.content.innerHTML = '';

		return super.setInput(input, options);
	}

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

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

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

				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 已提交
274 275 276
			.then(null, () => {
				const p = append(this.content, $('p'));
				p.textContent = localize('noReadme', "No README available.");
J
Joao Moreno 已提交
277
			}));
J
Joao Moreno 已提交
278
	}
J
Joao Moreno 已提交
279

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

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

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

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

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

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

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

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

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

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

		append(container, $('details', { open: true },
J
Joao Moreno 已提交
336
			$('summary', null, localize('themes', "Themes ({0})", contrib.length)),
J
Joao Moreno 已提交
337
			$('ul', null,
J
Joao Moreno 已提交
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
				...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 已提交
354 355 356 357
			)
		));
	}

J
Joao Moreno 已提交
358
	private renderCommands(container: HTMLElement, manifest: IExtensionManifest): void {
359 360 361
		interface Command {
			id: string;
			title: string;
J
Joao Moreno 已提交
362
			keybindings: string[];
363 364 365 366 367 368
			menus: string[];
		}

		const commands: Command[] = (manifest.contributes.commands || []).map(c => ({
			id: c.command,
			title: c.title,
J
Joao Moreno 已提交
369
			keybindings: [],
370 371 372 373 374 375 376 377 378 379 380
			menus: []
		}));

		const allCommands = commands.reduce<{ [id: string]: Command }>((r, c) => { r[c.id] = c; return r; }, {});

		const menus = manifest.contributes.menus || {};
		Object.keys(menus).forEach(context => {
			menus[context].forEach(menu => {
				let command = allCommands[menu.command];

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

J
Joao Moreno 已提交
390 391 392 393 394 395 396 397 398 399 400 401 402 403
		(manifest.contributes.keybindings || []).forEach(userString => {
			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);
			}
		});

404 405 406 407 408 409 410 411 412 413
		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 已提交
414
					$('th', null, localize('keyboard shortcuts', "Keyboard Shortcuts")),
415 416 417 418 419
					$('th', null, localize('menuContexts', "Menu Contexts"))
				),
				...commands.map(c => $('tr', null,
					$('td', null, c.id),
					$('td', null, c.title),
J
Joao Moreno 已提交
420
					$('td', null, ...c.keybindings.map(keybinding => $('code', null, keybinding))),
421 422 423 424 425 426
					$('td', null, ...c.menus.map(context => $('code', null, context)))
				))
			)
		));
	}

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

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

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

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

	layout(): void {
		return;
	}

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

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

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