extensionEditor.ts 13.2 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.renderDebuggers(content, manifest);
286
			}));
J
Joao Moreno 已提交
287 288
	}

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

		if (!settings.length) {
			return;
		}

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

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

		if (!debuggers.length) {
			return;
		}

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

J
Joao Moreno 已提交
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
	private static renderThemes(container: HTMLElement, manifest: IExtensionManifest): void {
		const themes = manifest.contributes.themes || [];

		if (!themes.length) {
			return;
		}

		append(container, $('details', { open: true },
			$('summary', null, localize('themes', "Themes ({0})", themes.length)),
			$('ul', null,
				...themes.map(theme => $('li', null, theme.label))
			)
		));
	}

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

J
Joao Moreno 已提交
341
		this.content.innerHTML = '';
J
Joao Moreno 已提交
342
		addClass(this.content, 'loading');
J
Joao Moreno 已提交
343

J
Joao Moreno 已提交
344 345
		let promise = loadingTask();
		promise = always(promise, () => removeClass(this.content, 'loading'));
J
Joao Moreno 已提交
346

J
Joao Moreno 已提交
347
		this.contentDisposables.push(toDisposable(() => promise.cancel()));
J
Joao Moreno 已提交
348 349 350 351 352 353
	}

	layout(): void {
		return;
	}

354
	private onViewletOpen(viewlet: IViewlet): void {
355 356 357 358 359 360 361
		if (!viewlet || viewlet.getId() === VIEWLET_ID) {
			return;
		}

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

J
Joao Moreno 已提交
362 363
	dispose(): void {
		this._highlight = null;
J
Joao Moreno 已提交
364 365
		this.transientDisposables = dispose(this.transientDisposables);
		this.disposables = dispose(this.disposables);
J
Joao Moreno 已提交
366 367 368
		super.dispose();
	}
}