extensionEditor.ts 13.7 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 42 43 44 45 46 47 48 49 50

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

J
Joao Moreno 已提交
52 53 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
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',
95
	Contributions: 'contributions'
J
Joao Moreno 已提交
96 97
};

J
Joao Moreno 已提交
98 99 100 101
export class ExtensionEditor extends BaseEditor {

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

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

	private _highlight: ITemplateData;
	private highlightDisposable: IDisposable;

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

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

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

140
		this.disposables.push(viewletService.onDidViewletOpen(this.onViewletOpen, this, this.disposables));
J
Joao Moreno 已提交
141 142 143 144
	}

	createEditor(parent: Builder): void {
		const container = parent.getHTMLElement();
J
Joao Moreno 已提交
145 146 147

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

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

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

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

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

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

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

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

J
Joao Moreno 已提交
172 173 174 175 176 177
		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 已提交
178

J
Joao Moreno 已提交
179
		this.content = append(body, $('.content'));
J
Joao Moreno 已提交
180 181 182
	}

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

J
Joao Moreno 已提交
185
		const extension = input.extension;
186
		this.telemetryService.publicLog('extensionGallery:openExtension', extension.telemetryData);
J
Joao Moreno 已提交
187

J
Joao Moreno 已提交
188 189 190
		this.extensionReadme = new Cache(() => extension.getReadme());
		this.extensionManifest = new Cache(() => extension.getManifest());

191 192 193 194
		const onError = once(domEvent(this.icon, 'error'));
		onError(() => this.icon.src = extension.iconUrlFallback, null, this.transientDisposables);
		this.icon.src = extension.iconUrl;

J
Joao Moreno 已提交
195 196 197
		this.name.textContent = extension.displayName;
		this.publisher.textContent = extension.publisherDisplayName;
		this.description.textContent = extension.description;
J
Joao Moreno 已提交
198

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

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

			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 已提交
217 218
		}

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

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

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

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

		this.content.innerHTML = '';

		return super.setInput(input, options);
	}

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

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

				webview.style(this.themeService.getColorTheme());
J
Joao Moreno 已提交
265
				webview.contents = [body];
J
Joao Moreno 已提交
266 267 268 269 270

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

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

J
Joao Moreno 已提交
283
				ExtensionEditor.renderSettings(content, manifest);
J
Joao Moreno 已提交
284
				ExtensionEditor.renderThemes(content, manifest);
J
Joao Moreno 已提交
285
				ExtensionEditor.renderJSONValidation(content, manifest);
J
Joao Moreno 已提交
286
				ExtensionEditor.renderDebuggers(content, manifest);
287
			}));
J
Joao Moreno 已提交
288 289
	}

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

J
Joao Moreno 已提交
295
		if (!contrib.length) {
J
Joao Moreno 已提交
296 297 298 299
			return;
		}

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

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

J
Joao Moreno 已提交
311
		if (!contrib.length) {
J
Joao Moreno 已提交
312 313 314 315
			return;
		}

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

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

J
Joao Moreno 已提交
327
		if (!contrib.length) {
J
Joao Moreno 已提交
328 329 330 331
			return;
		}

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

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

J
Joao Moreno 已提交
357
		this.content.innerHTML = '';
J
Joao Moreno 已提交
358
		addClass(this.content, 'loading');
J
Joao Moreno 已提交
359

J
Joao Moreno 已提交
360 361
		let promise = loadingTask();
		promise = always(promise, () => removeClass(this.content, 'loading'));
J
Joao Moreno 已提交
362

J
Joao Moreno 已提交
363
		this.contentDisposables.push(toDisposable(() => promise.cancel()));
J
Joao Moreno 已提交
364 365 366 367 368 369
	}

	layout(): void {
		return;
	}

370
	private onViewletOpen(viewlet: IViewlet): void {
371 372 373 374 375 376 377
		if (!viewlet || viewlet.getId() === VIEWLET_ID) {
			return;
		}

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

J
Joao Moreno 已提交
378 379
	dispose(): void {
		this._highlight = null;
J
Joao Moreno 已提交
380 381
		this.transientDisposables = dispose(this.transientDisposables);
		this.disposables = dispose(this.disposables);
J
Joao Moreno 已提交
382 383 384
		super.dispose();
	}
}