@theia/vsx-registry 1.53.0-next.5 → 1.53.0-next.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -45
- package/lib/common/vsx-environment.d.ts +1 -0
- package/lib/common/vsx-environment.d.ts.map +1 -1
- package/lib/common/vsx-registry-common-module.d.ts.map +1 -1
- package/lib/common/vsx-registry-common-module.js +9 -3
- package/lib/common/vsx-registry-common-module.js.map +1 -1
- package/lib/node/vsx-cli.d.ts +1 -0
- package/lib/node/vsx-cli.d.ts.map +1 -1
- package/lib/node/vsx-cli.js +4 -0
- package/lib/node/vsx-cli.js.map +1 -1
- package/lib/node/vsx-environment-impl.d.ts +1 -0
- package/lib/node/vsx-environment-impl.d.ts.map +1 -1
- package/lib/node/vsx-environment-impl.js +3 -0
- package/lib/node/vsx-environment-impl.js.map +1 -1
- package/lib/node/vsx-extension-resolver.js +3 -3
- package/lib/node/vsx-extension-resolver.js.map +1 -1
- package/package.json +12 -11
- package/src/browser/recommended-extensions/preference-provider-overrides.ts +99 -99
- package/src/browser/recommended-extensions/recommended-extensions-json-schema.ts +74 -74
- package/src/browser/recommended-extensions/recommended-extensions-preference-contribution.ts +68 -68
- package/src/browser/style/extensions.svg +4 -4
- package/src/browser/style/index.css +436 -436
- package/src/browser/vsx-extension-argument-processor.ts +32 -32
- package/src/browser/vsx-extension-commands.ts +68 -68
- package/src/browser/vsx-extension-editor-manager.ts +42 -42
- package/src/browser/vsx-extension-editor.tsx +96 -96
- package/src/browser/vsx-extension.tsx +710 -710
- package/src/browser/vsx-extensions-contribution.ts +373 -373
- package/src/browser/vsx-extensions-model.ts +456 -456
- package/src/browser/vsx-extensions-preferences.ts +58 -58
- package/src/browser/vsx-extensions-search-bar.tsx +107 -107
- package/src/browser/vsx-extensions-search-model.ts +61 -61
- package/src/browser/vsx-extensions-source.ts +83 -83
- package/src/browser/vsx-extensions-view-container.ts +179 -179
- package/src/browser/vsx-extensions-widget.tsx +165 -165
- package/src/browser/vsx-language-quick-pick-service.ts +112 -112
- package/src/browser/vsx-registry-frontend-module.ts +113 -113
- package/src/common/index.ts +19 -19
- package/src/common/ovsx-client-provider.ts +35 -35
- package/src/common/vsx-environment.ts +28 -27
- package/src/common/vsx-extension-uri.ts +20 -20
- package/src/common/vsx-registry-common-module.ts +85 -78
- package/src/node/vsx-cli-deployer-participant.ts +46 -46
- package/src/node/vsx-cli.ts +55 -51
- package/src/node/vsx-environment-impl.ts +54 -50
- package/src/node/vsx-extension-resolver.ts +134 -134
- package/src/node/vsx-registry-backend-module.ts +38 -38
- package/src/node/vsx-remote-cli.ts +39 -39
- package/src/package.spec.ts +29 -29
|
@@ -1,710 +1,710 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2020 TypeFox and others.
|
|
3
|
-
//
|
|
4
|
-
// This program and the accompanying materials are made available under the
|
|
5
|
-
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
//
|
|
8
|
-
// This Source Code may also be made available under the following Secondary
|
|
9
|
-
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
// with the GNU Classpath Exception which is available at
|
|
12
|
-
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
//
|
|
14
|
-
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
-
// *****************************************************************************
|
|
16
|
-
|
|
17
|
-
import * as React from '@theia/core/shared/react';
|
|
18
|
-
import * as DOMPurify from '@theia/core/shared/dompurify';
|
|
19
|
-
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
20
|
-
import URI from '@theia/core/lib/common/uri';
|
|
21
|
-
import { TreeElement, TreeElementNode } from '@theia/core/lib/browser/source-tree';
|
|
22
|
-
import { OpenerService, open, OpenerOptions } from '@theia/core/lib/browser/opener-service';
|
|
23
|
-
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
|
24
|
-
import { PluginServer, DeployedPlugin, PluginType, PluginIdentifiers, PluginDeployOptions } from '@theia/plugin-ext/lib/common/plugin-protocol';
|
|
25
|
-
import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri';
|
|
26
|
-
import { ProgressService } from '@theia/core/lib/common/progress-service';
|
|
27
|
-
import { Endpoint } from '@theia/core/lib/browser/endpoint';
|
|
28
|
-
import { VSXEnvironment } from '../common/vsx-environment';
|
|
29
|
-
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
|
|
30
|
-
import { CommandRegistry, MenuPath, nls } from '@theia/core/lib/common';
|
|
31
|
-
import { codicon, ConfirmDialog, ContextMenuRenderer, HoverService, TreeWidget } from '@theia/core/lib/browser';
|
|
32
|
-
import { VSXExtensionNamespaceAccess, VSXUser } from '@theia/ovsx-client/lib/ovsx-types';
|
|
33
|
-
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
|
34
|
-
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
|
|
35
|
-
|
|
36
|
-
export const EXTENSIONS_CONTEXT_MENU: MenuPath = ['extensions_context_menu'];
|
|
37
|
-
|
|
38
|
-
export namespace VSXExtensionsContextMenu {
|
|
39
|
-
export const INSTALL = [...EXTENSIONS_CONTEXT_MENU, '1_install'];
|
|
40
|
-
export const COPY = [...EXTENSIONS_CONTEXT_MENU, '2_copy'];
|
|
41
|
-
export const CONTRIBUTION = [...EXTENSIONS_CONTEXT_MENU, '3_contribution'];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@injectable()
|
|
45
|
-
export class VSXExtensionData {
|
|
46
|
-
readonly version?: string;
|
|
47
|
-
readonly iconUrl?: string;
|
|
48
|
-
readonly publisher?: string;
|
|
49
|
-
readonly name?: string;
|
|
50
|
-
readonly displayName?: string;
|
|
51
|
-
readonly description?: string;
|
|
52
|
-
readonly averageRating?: number;
|
|
53
|
-
readonly downloadCount?: number;
|
|
54
|
-
readonly downloadUrl?: string;
|
|
55
|
-
readonly readmeUrl?: string;
|
|
56
|
-
readonly licenseUrl?: string;
|
|
57
|
-
readonly repository?: string;
|
|
58
|
-
readonly license?: string;
|
|
59
|
-
readonly readme?: string;
|
|
60
|
-
readonly preview?: boolean;
|
|
61
|
-
readonly verified?: boolean;
|
|
62
|
-
readonly namespaceAccess?: VSXExtensionNamespaceAccess;
|
|
63
|
-
readonly publishedBy?: VSXUser;
|
|
64
|
-
static KEYS: Set<(keyof VSXExtensionData)> = new Set([
|
|
65
|
-
'version',
|
|
66
|
-
'iconUrl',
|
|
67
|
-
'publisher',
|
|
68
|
-
'name',
|
|
69
|
-
'displayName',
|
|
70
|
-
'description',
|
|
71
|
-
'averageRating',
|
|
72
|
-
'downloadCount',
|
|
73
|
-
'downloadUrl',
|
|
74
|
-
'readmeUrl',
|
|
75
|
-
'licenseUrl',
|
|
76
|
-
'repository',
|
|
77
|
-
'license',
|
|
78
|
-
'readme',
|
|
79
|
-
'preview',
|
|
80
|
-
'verified',
|
|
81
|
-
'namespaceAccess',
|
|
82
|
-
'publishedBy'
|
|
83
|
-
]);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
@injectable()
|
|
87
|
-
export class VSXExtensionOptions {
|
|
88
|
-
readonly id: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export const VSXExtensionFactory = Symbol('VSXExtensionFactory');
|
|
92
|
-
export type VSXExtensionFactory = (options: VSXExtensionOptions) => VSXExtension;
|
|
93
|
-
|
|
94
|
-
@injectable()
|
|
95
|
-
export class VSXExtension implements VSXExtensionData, TreeElement {
|
|
96
|
-
/**
|
|
97
|
-
* Ensure the version string begins with `'v'`.
|
|
98
|
-
*/
|
|
99
|
-
static formatVersion(version: string | undefined): string | undefined {
|
|
100
|
-
if (version && !version.startsWith('v')) {
|
|
101
|
-
return `v${version}`;
|
|
102
|
-
}
|
|
103
|
-
return version;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
@inject(VSXExtensionOptions)
|
|
107
|
-
protected readonly options: VSXExtensionOptions;
|
|
108
|
-
|
|
109
|
-
@inject(OpenerService)
|
|
110
|
-
protected readonly openerService: OpenerService;
|
|
111
|
-
|
|
112
|
-
@inject(HostedPluginSupport)
|
|
113
|
-
protected readonly pluginSupport: HostedPluginSupport;
|
|
114
|
-
|
|
115
|
-
@inject(PluginServer)
|
|
116
|
-
protected readonly pluginServer: PluginServer;
|
|
117
|
-
|
|
118
|
-
@inject(ProgressService)
|
|
119
|
-
protected readonly progressService: ProgressService;
|
|
120
|
-
|
|
121
|
-
@inject(ContextMenuRenderer)
|
|
122
|
-
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
|
123
|
-
|
|
124
|
-
@inject(VSXEnvironment)
|
|
125
|
-
readonly environment: VSXEnvironment;
|
|
126
|
-
|
|
127
|
-
@inject(VSXExtensionsSearchModel)
|
|
128
|
-
readonly search: VSXExtensionsSearchModel;
|
|
129
|
-
|
|
130
|
-
@inject(HoverService)
|
|
131
|
-
protected readonly hoverService: HoverService;
|
|
132
|
-
|
|
133
|
-
@inject(WindowService)
|
|
134
|
-
readonly windowService: WindowService;
|
|
135
|
-
|
|
136
|
-
@inject(CommandRegistry)
|
|
137
|
-
readonly commandRegistry: CommandRegistry;
|
|
138
|
-
|
|
139
|
-
protected readonly data: Partial<VSXExtensionData> = {};
|
|
140
|
-
|
|
141
|
-
protected registryUri: Promise<string>;
|
|
142
|
-
|
|
143
|
-
@postConstruct()
|
|
144
|
-
protected postConstruct(): void {
|
|
145
|
-
this.registryUri = this.environment.getRegistryUri();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
get uri(): URI {
|
|
149
|
-
return VSCodeExtensionUri.fromId(this.id);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
get id(): string {
|
|
153
|
-
return this.options.id;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
get visible(): boolean {
|
|
157
|
-
return !!this.name;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
get plugin(): DeployedPlugin | undefined {
|
|
161
|
-
return this.pluginSupport.getPlugin(this.id as PluginIdentifiers.UnversionedId);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
get installed(): boolean {
|
|
165
|
-
return !!this.plugin;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
get builtin(): boolean {
|
|
169
|
-
return this.plugin?.type === PluginType.System;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
update(data: Partial<VSXExtensionData>): void {
|
|
173
|
-
for (const key of VSXExtensionData.KEYS) {
|
|
174
|
-
if (key in data) {
|
|
175
|
-
Object.assign(this.data, { [key]: data[key] });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
reloadWindow(): void {
|
|
181
|
-
this.windowService.reload();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
protected getData<K extends keyof VSXExtensionData>(key: K): VSXExtensionData[K] {
|
|
185
|
-
const model = this.plugin?.metadata.model;
|
|
186
|
-
if (model && key in model) {
|
|
187
|
-
return model[key as keyof typeof model] as VSXExtensionData[K];
|
|
188
|
-
}
|
|
189
|
-
return this.data[key];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
get iconUrl(): string | undefined {
|
|
193
|
-
const plugin = this.plugin;
|
|
194
|
-
const iconUrl = plugin && plugin.metadata.model.iconUrl;
|
|
195
|
-
if (iconUrl) {
|
|
196
|
-
return new Endpoint({ path: iconUrl }).getRestUrl().toString();
|
|
197
|
-
}
|
|
198
|
-
return this.data['iconUrl'];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
get publisher(): string | undefined {
|
|
202
|
-
return this.getData('publisher');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
get name(): string | undefined {
|
|
206
|
-
return this.getData('name');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
get displayName(): string | undefined {
|
|
210
|
-
return this.getData('displayName') || this.name;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
get description(): string | undefined {
|
|
214
|
-
return this.getData('description');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
get version(): string | undefined {
|
|
218
|
-
return this.getData('version');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
get averageRating(): number | undefined {
|
|
222
|
-
return this.getData('averageRating');
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
get downloadCount(): number | undefined {
|
|
226
|
-
return this.getData('downloadCount');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
get downloadUrl(): string | undefined {
|
|
230
|
-
return this.getData('downloadUrl');
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
get readmeUrl(): string | undefined {
|
|
234
|
-
const plugin = this.plugin;
|
|
235
|
-
const readmeUrl = plugin && plugin.metadata.model.readmeUrl;
|
|
236
|
-
if (readmeUrl) {
|
|
237
|
-
return new Endpoint({ path: readmeUrl }).getRestUrl().toString();
|
|
238
|
-
}
|
|
239
|
-
return this.data['readmeUrl'];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
get licenseUrl(): string | undefined {
|
|
243
|
-
let licenseUrl = this.data['licenseUrl'];
|
|
244
|
-
if (licenseUrl) {
|
|
245
|
-
return licenseUrl;
|
|
246
|
-
} else {
|
|
247
|
-
const plugin = this.plugin;
|
|
248
|
-
licenseUrl = plugin && plugin.metadata.model.licenseUrl;
|
|
249
|
-
if (licenseUrl) {
|
|
250
|
-
return new Endpoint({ path: licenseUrl }).getRestUrl().toString();
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
get repository(): string | undefined {
|
|
256
|
-
return this.getData('repository');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
get license(): string | undefined {
|
|
260
|
-
return this.getData('license');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
get readme(): string | undefined {
|
|
264
|
-
return this.getData('readme');
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
get preview(): boolean | undefined {
|
|
268
|
-
return this.getData('preview');
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
get verified(): boolean | undefined {
|
|
272
|
-
return this.getData('verified');
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
get namespaceAccess(): VSXExtensionNamespaceAccess | undefined {
|
|
276
|
-
return this.getData('namespaceAccess');
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
get publishedBy(): VSXUser | undefined {
|
|
280
|
-
return this.getData('publishedBy');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
get tooltip(): string {
|
|
284
|
-
let md = `__${this.displayName}__ ${VSXExtension.formatVersion(this.version)}\n\n${this.description}\n_____\n\n${nls.localizeByDefault('Publisher: {0}', this.publisher)}`;
|
|
285
|
-
|
|
286
|
-
if (this.license) {
|
|
287
|
-
md += ` \r${nls.localize('theia/vsx-registry/license', 'License: {0}', this.license)}`;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (this.downloadCount) {
|
|
291
|
-
md += ` \r${nls.localize('theia/vsx-registry/downloadCount', 'Download count: {0}', downloadCompactFormatter.format(this.downloadCount))}`;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (this.averageRating) {
|
|
295
|
-
md += ` \r${getAverageRatingTitle(this.averageRating)}`;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return md;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
protected _busy = 0;
|
|
302
|
-
get busy(): boolean {
|
|
303
|
-
return !!this._busy;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
async install(options?: PluginDeployOptions): Promise<void> {
|
|
307
|
-
if (!this.verified) {
|
|
308
|
-
const choice = await new ConfirmDialog({
|
|
309
|
-
title: nls.localize('theia/vsx-registry/confirmDialogTitle', 'Are you sure you want to proceed with the installation ?'),
|
|
310
|
-
msg: nls.localize('theia/vsx-registry/confirmDialogMessage', 'The extension "{0}" is unverified and might pose a security risk.', this.displayName)
|
|
311
|
-
}).open();
|
|
312
|
-
if (choice) {
|
|
313
|
-
this.doInstall(options);
|
|
314
|
-
}
|
|
315
|
-
} else {
|
|
316
|
-
this.doInstall(options);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async uninstall(): Promise<void> {
|
|
321
|
-
this._busy++;
|
|
322
|
-
try {
|
|
323
|
-
const { plugin } = this;
|
|
324
|
-
if (plugin) {
|
|
325
|
-
await this.progressService.withProgress(
|
|
326
|
-
nls.localizeByDefault('Uninstalling {0}...', this.id), 'extensions',
|
|
327
|
-
() => this.pluginServer.uninstall(PluginIdentifiers.componentsToVersionedId(plugin.metadata.model))
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
} finally {
|
|
331
|
-
this._busy--;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
protected async doInstall(options?: PluginDeployOptions): Promise<void> {
|
|
336
|
-
this._busy++;
|
|
337
|
-
try {
|
|
338
|
-
await this.progressService.withProgress(nls.localizeByDefault("Installing extension '{0}' v{1}...", this.id, this.version ?? 0), 'extensions', () =>
|
|
339
|
-
this.pluginServer.deploy(this.uri.toString(), undefined, options)
|
|
340
|
-
);
|
|
341
|
-
} finally {
|
|
342
|
-
this._busy--;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
handleContextMenu(e: React.MouseEvent<HTMLElement, MouseEvent>): void {
|
|
347
|
-
e.preventDefault();
|
|
348
|
-
this.contextMenuRenderer.render({
|
|
349
|
-
menuPath: EXTENSIONS_CONTEXT_MENU,
|
|
350
|
-
anchor: {
|
|
351
|
-
x: e.clientX,
|
|
352
|
-
y: e.clientY,
|
|
353
|
-
},
|
|
354
|
-
args: [this]
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Get the registry link for the given extension.
|
|
360
|
-
* @param path the url path.
|
|
361
|
-
* @returns the registry link for the given extension at the path.
|
|
362
|
-
*/
|
|
363
|
-
async getRegistryLink(path = ''): Promise<URI> {
|
|
364
|
-
const registryUri = new URI(await this.registryUri);
|
|
365
|
-
if (this.downloadUrl) {
|
|
366
|
-
const downloadUri = new URI(this.downloadUrl);
|
|
367
|
-
if (downloadUri.authority !== registryUri.authority) {
|
|
368
|
-
throw new Error('cannot generate a valid URL');
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return registryUri.resolve('extension/' + this.id.replace('.', '/')).resolve(path);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async serialize(): Promise<string> {
|
|
375
|
-
const serializedExtension: string[] = [];
|
|
376
|
-
serializedExtension.push(`Name: ${this.displayName}`);
|
|
377
|
-
serializedExtension.push(`Id: ${this.id}`);
|
|
378
|
-
serializedExtension.push(`Description: ${this.description}`);
|
|
379
|
-
serializedExtension.push(`Version: ${this.version}`);
|
|
380
|
-
serializedExtension.push(`Publisher: ${this.publisher}`);
|
|
381
|
-
if (this.downloadUrl !== undefined) {
|
|
382
|
-
const registryLink = await this.getRegistryLink();
|
|
383
|
-
serializedExtension.push(`Open VSX Link: ${registryLink.toString()}`);
|
|
384
|
-
};
|
|
385
|
-
return serializedExtension.join('\n');
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
async open(options: OpenerOptions = { mode: 'reveal' }): Promise<void> {
|
|
389
|
-
await this.doOpen(this.uri, options);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
async doOpen(uri: URI, options?: OpenerOptions): Promise<void> {
|
|
393
|
-
await open(this.openerService, uri, options);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
render(host: TreeWidget): React.ReactNode {
|
|
397
|
-
return <VSXExtensionComponent extension={this} host={host} hoverService={this.hoverService} />;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
export abstract class AbstractVSXExtensionComponent<Props extends AbstractVSXExtensionComponent.Props = AbstractVSXExtensionComponent.Props> extends React.Component<Props> {
|
|
402
|
-
|
|
403
|
-
readonly install = async (event?: React.MouseEvent) => {
|
|
404
|
-
event?.stopPropagation();
|
|
405
|
-
this.forceUpdate();
|
|
406
|
-
try {
|
|
407
|
-
const pending = this.props.extension.install();
|
|
408
|
-
this.forceUpdate();
|
|
409
|
-
await pending;
|
|
410
|
-
} finally {
|
|
411
|
-
this.forceUpdate();
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
readonly uninstall = async (event?: React.MouseEvent) => {
|
|
416
|
-
event?.stopPropagation();
|
|
417
|
-
try {
|
|
418
|
-
const pending = this.props.extension.uninstall();
|
|
419
|
-
this.forceUpdate();
|
|
420
|
-
await pending;
|
|
421
|
-
} finally {
|
|
422
|
-
this.forceUpdate();
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
readonly reloadWindow = (event?: React.MouseEvent) => {
|
|
427
|
-
event?.stopPropagation();
|
|
428
|
-
this.props.extension.reloadWindow();
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
protected readonly manage = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
|
432
|
-
e.stopPropagation();
|
|
433
|
-
this.props.extension.handleContextMenu(e);
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
protected renderAction(host?: TreeWidget): React.ReactNode {
|
|
437
|
-
const { builtin, busy, plugin } = this.props.extension;
|
|
438
|
-
const isFocused = (host?.model.getFocusedNode() as TreeElementNode)?.element === this.props.extension;
|
|
439
|
-
const tabIndex = (!host || isFocused) ? 0 : undefined;
|
|
440
|
-
const installed = !!plugin;
|
|
441
|
-
const outOfSynch = plugin?.metadata.outOfSync;
|
|
442
|
-
if (builtin) {
|
|
443
|
-
return <div className="codicon codicon-settings-gear action" tabIndex={tabIndex} onClick={this.manage}></div>;
|
|
444
|
-
}
|
|
445
|
-
if (busy) {
|
|
446
|
-
if (installed) {
|
|
447
|
-
return <button className="theia-button action theia-mod-disabled">{nls.localizeByDefault('Uninstalling')}</button>;
|
|
448
|
-
}
|
|
449
|
-
return <button className="theia-button action prominent theia-mod-disabled">{nls.localizeByDefault('Installing')}</button>;
|
|
450
|
-
}
|
|
451
|
-
if (installed) {
|
|
452
|
-
return <div>
|
|
453
|
-
{
|
|
454
|
-
outOfSynch
|
|
455
|
-
? <button className="theia-button action" onClick={this.reloadWindow}>{nls.localizeByDefault('Reload Window')}</button>
|
|
456
|
-
: <button className="theia-button action" onClick={this.uninstall}>{nls.localizeByDefault('Uninstall')}</button>
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
<div className="codicon codicon-settings-gear action" onClick={this.manage}></div>
|
|
460
|
-
</div>;
|
|
461
|
-
}
|
|
462
|
-
return <button className="theia-button prominent action" onClick={this.install}>{nls.localizeByDefault('Install')}</button>;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
export namespace AbstractVSXExtensionComponent {
|
|
467
|
-
export interface Props {
|
|
468
|
-
extension: VSXExtension;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const downloadFormatter = new Intl.NumberFormat();
|
|
473
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
474
|
-
const downloadCompactFormatter = new Intl.NumberFormat('en-US', { notation: 'compact', compactDisplay: 'short' } as any);
|
|
475
|
-
const averageRatingFormatter = (averageRating: number): number => Math.round(averageRating * 2) / 2;
|
|
476
|
-
const getAverageRatingTitle = (averageRating: number): string =>
|
|
477
|
-
nls.localizeByDefault('Average rating: {0} out of 5', averageRatingFormatter(averageRating));
|
|
478
|
-
|
|
479
|
-
export namespace VSXExtensionComponent {
|
|
480
|
-
export interface Props extends AbstractVSXExtensionComponent.Props {
|
|
481
|
-
host: TreeWidget;
|
|
482
|
-
hoverService: HoverService;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
export class VSXExtensionComponent<Props extends VSXExtensionComponent.Props = VSXExtensionComponent.Props> extends AbstractVSXExtensionComponent<Props> {
|
|
487
|
-
override render(): React.ReactNode {
|
|
488
|
-
const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating, tooltip, verified } = this.props.extension;
|
|
489
|
-
|
|
490
|
-
return <div
|
|
491
|
-
className='theia-vsx-extension noselect'
|
|
492
|
-
onMouseEnter={event => {
|
|
493
|
-
this.props.hoverService.requestHover({
|
|
494
|
-
content: new MarkdownStringImpl(tooltip),
|
|
495
|
-
target: event.currentTarget,
|
|
496
|
-
position: 'right'
|
|
497
|
-
});
|
|
498
|
-
}}
|
|
499
|
-
onMouseUp={event => {
|
|
500
|
-
if (event.button === 2) {
|
|
501
|
-
this.manage(event);
|
|
502
|
-
}
|
|
503
|
-
}}
|
|
504
|
-
>
|
|
505
|
-
{iconUrl ?
|
|
506
|
-
<img className='theia-vsx-extension-icon' src={iconUrl} /> :
|
|
507
|
-
<div className='theia-vsx-extension-icon placeholder' />}
|
|
508
|
-
<div className='theia-vsx-extension-content'>
|
|
509
|
-
<div className='title'>
|
|
510
|
-
<div className='noWrapInfo'>
|
|
511
|
-
<span className='name'>{displayName}</span> <span className='version'>{VSXExtension.formatVersion(version)}</span>
|
|
512
|
-
</div>
|
|
513
|
-
<div className='stat'>
|
|
514
|
-
{!!downloadCount && <span className='download-count'><i className={codicon('cloud-download')} />{downloadCompactFormatter.format(downloadCount)}</span>}
|
|
515
|
-
{!!averageRating && <span className='average-rating'><i className={codicon('star-full')} />{averageRatingFormatter(averageRating)}</span>}
|
|
516
|
-
</div>
|
|
517
|
-
</div>
|
|
518
|
-
<div className='noWrapInfo theia-vsx-extension-description'>{description}</div>
|
|
519
|
-
<div className='theia-vsx-extension-action-bar'>
|
|
520
|
-
<div className='theia-vsx-extension-publisher-container'>
|
|
521
|
-
{verified === true ? (
|
|
522
|
-
<i className={codicon('verified-filled')} />
|
|
523
|
-
) : verified === false ? (
|
|
524
|
-
<i className={codicon('verified')} />
|
|
525
|
-
) : (
|
|
526
|
-
<i className={codicon('question')} />
|
|
527
|
-
)}
|
|
528
|
-
<span className='noWrapInfo theia-vsx-extension-publisher'>{publisher}</span>
|
|
529
|
-
</div>
|
|
530
|
-
{this.renderAction(this.props.host)}
|
|
531
|
-
</div>
|
|
532
|
-
</div>
|
|
533
|
-
</div >;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export class VSXExtensionEditorComponent extends AbstractVSXExtensionComponent {
|
|
538
|
-
protected header: HTMLElement | undefined;
|
|
539
|
-
protected body: HTMLElement | undefined;
|
|
540
|
-
protected _scrollContainer: HTMLElement | undefined;
|
|
541
|
-
|
|
542
|
-
get scrollContainer(): HTMLElement | undefined {
|
|
543
|
-
return this._scrollContainer;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
override render(): React.ReactNode {
|
|
547
|
-
const {
|
|
548
|
-
builtin, preview, id, iconUrl, publisher, displayName, description, version,
|
|
549
|
-
averageRating, downloadCount, repository, license, readme
|
|
550
|
-
} = this.props.extension;
|
|
551
|
-
|
|
552
|
-
const sanitizedReadme = !!readme ? DOMPurify.sanitize(readme) : undefined;
|
|
553
|
-
|
|
554
|
-
return <React.Fragment>
|
|
555
|
-
<div className='header' ref={ref => this.header = (ref || undefined)}>
|
|
556
|
-
{iconUrl ?
|
|
557
|
-
<img className='icon-container' src={iconUrl} /> :
|
|
558
|
-
<div className='icon-container placeholder' />}
|
|
559
|
-
<div className='details'>
|
|
560
|
-
<div className='title'>
|
|
561
|
-
<span title='Extension name' className='name' onClick={this.openExtension}>{displayName}</span>
|
|
562
|
-
<span title='Extension identifier' className='identifier'>{id}</span>
|
|
563
|
-
{preview && <span className='preview'>Preview</span>}
|
|
564
|
-
{builtin && <span className='builtin'>Built-in</span>}
|
|
565
|
-
</div>
|
|
566
|
-
<div className='subtitle'>
|
|
567
|
-
<span title='Publisher name' className='publisher' onClick={this.searchPublisher}>
|
|
568
|
-
{this.renderNamespaceAccess()}
|
|
569
|
-
{publisher}
|
|
570
|
-
</span>
|
|
571
|
-
{!!downloadCount && <span className='download-count' onClick={this.openExtension}>
|
|
572
|
-
<i className={codicon('cloud-download')} />{downloadFormatter.format(downloadCount)}</span>}
|
|
573
|
-
{
|
|
574
|
-
averageRating !== undefined &&
|
|
575
|
-
<span className='average-rating' title={getAverageRatingTitle(averageRating)} onClick={this.openAverageRating}>{this.renderStars()}</span>
|
|
576
|
-
}
|
|
577
|
-
{repository && <span className='repository' onClick={this.openRepository}>Repository</span>}
|
|
578
|
-
{license && <span className='license' onClick={this.openLicense}>{license}</span>}
|
|
579
|
-
{version && <span className='version'>{VSXExtension.formatVersion(version)}</span>}
|
|
580
|
-
</div>
|
|
581
|
-
<div className='description noWrapInfo'>{description}</div>
|
|
582
|
-
{this.renderAction()}
|
|
583
|
-
</div>
|
|
584
|
-
</div>
|
|
585
|
-
{
|
|
586
|
-
sanitizedReadme &&
|
|
587
|
-
<div className='scroll-container'
|
|
588
|
-
ref={ref => this._scrollContainer = (ref || undefined)}>
|
|
589
|
-
<div className='body'
|
|
590
|
-
ref={ref => this.body = (ref || undefined)}
|
|
591
|
-
onClick={this.openLink}
|
|
592
|
-
// eslint-disable-next-line react/no-danger
|
|
593
|
-
dangerouslySetInnerHTML={{ __html: sanitizedReadme }}
|
|
594
|
-
/>
|
|
595
|
-
</div>
|
|
596
|
-
}
|
|
597
|
-
</React.Fragment >;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
protected renderNamespaceAccess(): React.ReactNode {
|
|
601
|
-
const { publisher, namespaceAccess, publishedBy } = this.props.extension;
|
|
602
|
-
if (namespaceAccess === undefined) {
|
|
603
|
-
return undefined;
|
|
604
|
-
}
|
|
605
|
-
let tooltip = publishedBy ? ` Published by "${publishedBy.loginName}".` : '';
|
|
606
|
-
let icon;
|
|
607
|
-
if (namespaceAccess === 'public') {
|
|
608
|
-
icon = 'globe';
|
|
609
|
-
tooltip = `Everyone can publish to "${publisher}" namespace.` + tooltip;
|
|
610
|
-
} else {
|
|
611
|
-
icon = 'shield';
|
|
612
|
-
tooltip = `Only verified owners can publish to "${publisher}" namespace.` + tooltip;
|
|
613
|
-
}
|
|
614
|
-
return <i className={`${codicon(icon)} namespace-access`} title={tooltip} onClick={this.openPublishedBy} />;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
protected renderStars(): React.ReactNode {
|
|
618
|
-
const rating = this.props.extension.averageRating || 0;
|
|
619
|
-
|
|
620
|
-
const renderStarAt = (position: number) => position <= rating ?
|
|
621
|
-
<i className={codicon('star-full')} /> :
|
|
622
|
-
position > rating && position - rating < 1 ?
|
|
623
|
-
<i className={codicon('star-half')} /> :
|
|
624
|
-
<i className={codicon('star-empty')} />;
|
|
625
|
-
return <React.Fragment>
|
|
626
|
-
{renderStarAt(1)}{renderStarAt(2)}{renderStarAt(3)}{renderStarAt(4)}{renderStarAt(5)}
|
|
627
|
-
</React.Fragment>;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// TODO replace with webview
|
|
631
|
-
readonly openLink = (event: React.MouseEvent) => {
|
|
632
|
-
if (!this.body) {
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
const target = event.nativeEvent.target;
|
|
636
|
-
if (!(target instanceof HTMLElement)) {
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
let node = target;
|
|
640
|
-
while (node.tagName.toLowerCase() !== 'a') {
|
|
641
|
-
if (node === this.body) {
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
if (!(node.parentElement instanceof HTMLElement)) {
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
node = node.parentElement;
|
|
648
|
-
}
|
|
649
|
-
const href = node.getAttribute('href');
|
|
650
|
-
if (href && !href.startsWith('#')) {
|
|
651
|
-
event.preventDefault();
|
|
652
|
-
this.props.extension.doOpen(new URI(href));
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
|
|
656
|
-
readonly openExtension = async (e: React.MouseEvent) => {
|
|
657
|
-
e.stopPropagation();
|
|
658
|
-
e.preventDefault();
|
|
659
|
-
|
|
660
|
-
const extension = this.props.extension;
|
|
661
|
-
const uri = await extension.getRegistryLink();
|
|
662
|
-
extension.doOpen(uri);
|
|
663
|
-
};
|
|
664
|
-
readonly searchPublisher = (e: React.MouseEvent) => {
|
|
665
|
-
e.stopPropagation();
|
|
666
|
-
e.preventDefault();
|
|
667
|
-
|
|
668
|
-
const extension = this.props.extension;
|
|
669
|
-
if (extension.publisher) {
|
|
670
|
-
extension.search.query = extension.publisher;
|
|
671
|
-
}
|
|
672
|
-
};
|
|
673
|
-
readonly openPublishedBy = async (e: React.MouseEvent) => {
|
|
674
|
-
e.stopPropagation();
|
|
675
|
-
e.preventDefault();
|
|
676
|
-
|
|
677
|
-
const extension = this.props.extension;
|
|
678
|
-
const homepage = extension.publishedBy && extension.publishedBy.homepage;
|
|
679
|
-
if (homepage) {
|
|
680
|
-
extension.doOpen(new URI(homepage));
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
readonly openAverageRating = async (e: React.MouseEvent) => {
|
|
684
|
-
e.stopPropagation();
|
|
685
|
-
e.preventDefault();
|
|
686
|
-
|
|
687
|
-
const extension = this.props.extension;
|
|
688
|
-
const uri = await extension.getRegistryLink('reviews');
|
|
689
|
-
extension.doOpen(uri);
|
|
690
|
-
};
|
|
691
|
-
readonly openRepository = (e: React.MouseEvent) => {
|
|
692
|
-
e.stopPropagation();
|
|
693
|
-
e.preventDefault();
|
|
694
|
-
|
|
695
|
-
const extension = this.props.extension;
|
|
696
|
-
if (extension.repository) {
|
|
697
|
-
extension.doOpen(new URI(extension.repository));
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
readonly openLicense = (e: React.MouseEvent) => {
|
|
701
|
-
e.stopPropagation();
|
|
702
|
-
e.preventDefault();
|
|
703
|
-
|
|
704
|
-
const extension = this.props.extension;
|
|
705
|
-
const licenseUrl = extension.licenseUrl;
|
|
706
|
-
if (licenseUrl) {
|
|
707
|
-
extension.doOpen(new URI(licenseUrl));
|
|
708
|
-
}
|
|
709
|
-
};
|
|
710
|
-
}
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2020 TypeFox and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import * as React from '@theia/core/shared/react';
|
|
18
|
+
import * as DOMPurify from '@theia/core/shared/dompurify';
|
|
19
|
+
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
20
|
+
import URI from '@theia/core/lib/common/uri';
|
|
21
|
+
import { TreeElement, TreeElementNode } from '@theia/core/lib/browser/source-tree';
|
|
22
|
+
import { OpenerService, open, OpenerOptions } from '@theia/core/lib/browser/opener-service';
|
|
23
|
+
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
|
24
|
+
import { PluginServer, DeployedPlugin, PluginType, PluginIdentifiers, PluginDeployOptions } from '@theia/plugin-ext/lib/common/plugin-protocol';
|
|
25
|
+
import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri';
|
|
26
|
+
import { ProgressService } from '@theia/core/lib/common/progress-service';
|
|
27
|
+
import { Endpoint } from '@theia/core/lib/browser/endpoint';
|
|
28
|
+
import { VSXEnvironment } from '../common/vsx-environment';
|
|
29
|
+
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
|
|
30
|
+
import { CommandRegistry, MenuPath, nls } from '@theia/core/lib/common';
|
|
31
|
+
import { codicon, ConfirmDialog, ContextMenuRenderer, HoverService, TreeWidget } from '@theia/core/lib/browser';
|
|
32
|
+
import { VSXExtensionNamespaceAccess, VSXUser } from '@theia/ovsx-client/lib/ovsx-types';
|
|
33
|
+
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
|
34
|
+
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
|
|
35
|
+
|
|
36
|
+
export const EXTENSIONS_CONTEXT_MENU: MenuPath = ['extensions_context_menu'];
|
|
37
|
+
|
|
38
|
+
export namespace VSXExtensionsContextMenu {
|
|
39
|
+
export const INSTALL = [...EXTENSIONS_CONTEXT_MENU, '1_install'];
|
|
40
|
+
export const COPY = [...EXTENSIONS_CONTEXT_MENU, '2_copy'];
|
|
41
|
+
export const CONTRIBUTION = [...EXTENSIONS_CONTEXT_MENU, '3_contribution'];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@injectable()
|
|
45
|
+
export class VSXExtensionData {
|
|
46
|
+
readonly version?: string;
|
|
47
|
+
readonly iconUrl?: string;
|
|
48
|
+
readonly publisher?: string;
|
|
49
|
+
readonly name?: string;
|
|
50
|
+
readonly displayName?: string;
|
|
51
|
+
readonly description?: string;
|
|
52
|
+
readonly averageRating?: number;
|
|
53
|
+
readonly downloadCount?: number;
|
|
54
|
+
readonly downloadUrl?: string;
|
|
55
|
+
readonly readmeUrl?: string;
|
|
56
|
+
readonly licenseUrl?: string;
|
|
57
|
+
readonly repository?: string;
|
|
58
|
+
readonly license?: string;
|
|
59
|
+
readonly readme?: string;
|
|
60
|
+
readonly preview?: boolean;
|
|
61
|
+
readonly verified?: boolean;
|
|
62
|
+
readonly namespaceAccess?: VSXExtensionNamespaceAccess;
|
|
63
|
+
readonly publishedBy?: VSXUser;
|
|
64
|
+
static KEYS: Set<(keyof VSXExtensionData)> = new Set([
|
|
65
|
+
'version',
|
|
66
|
+
'iconUrl',
|
|
67
|
+
'publisher',
|
|
68
|
+
'name',
|
|
69
|
+
'displayName',
|
|
70
|
+
'description',
|
|
71
|
+
'averageRating',
|
|
72
|
+
'downloadCount',
|
|
73
|
+
'downloadUrl',
|
|
74
|
+
'readmeUrl',
|
|
75
|
+
'licenseUrl',
|
|
76
|
+
'repository',
|
|
77
|
+
'license',
|
|
78
|
+
'readme',
|
|
79
|
+
'preview',
|
|
80
|
+
'verified',
|
|
81
|
+
'namespaceAccess',
|
|
82
|
+
'publishedBy'
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@injectable()
|
|
87
|
+
export class VSXExtensionOptions {
|
|
88
|
+
readonly id: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const VSXExtensionFactory = Symbol('VSXExtensionFactory');
|
|
92
|
+
export type VSXExtensionFactory = (options: VSXExtensionOptions) => VSXExtension;
|
|
93
|
+
|
|
94
|
+
@injectable()
|
|
95
|
+
export class VSXExtension implements VSXExtensionData, TreeElement {
|
|
96
|
+
/**
|
|
97
|
+
* Ensure the version string begins with `'v'`.
|
|
98
|
+
*/
|
|
99
|
+
static formatVersion(version: string | undefined): string | undefined {
|
|
100
|
+
if (version && !version.startsWith('v')) {
|
|
101
|
+
return `v${version}`;
|
|
102
|
+
}
|
|
103
|
+
return version;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@inject(VSXExtensionOptions)
|
|
107
|
+
protected readonly options: VSXExtensionOptions;
|
|
108
|
+
|
|
109
|
+
@inject(OpenerService)
|
|
110
|
+
protected readonly openerService: OpenerService;
|
|
111
|
+
|
|
112
|
+
@inject(HostedPluginSupport)
|
|
113
|
+
protected readonly pluginSupport: HostedPluginSupport;
|
|
114
|
+
|
|
115
|
+
@inject(PluginServer)
|
|
116
|
+
protected readonly pluginServer: PluginServer;
|
|
117
|
+
|
|
118
|
+
@inject(ProgressService)
|
|
119
|
+
protected readonly progressService: ProgressService;
|
|
120
|
+
|
|
121
|
+
@inject(ContextMenuRenderer)
|
|
122
|
+
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
|
123
|
+
|
|
124
|
+
@inject(VSXEnvironment)
|
|
125
|
+
readonly environment: VSXEnvironment;
|
|
126
|
+
|
|
127
|
+
@inject(VSXExtensionsSearchModel)
|
|
128
|
+
readonly search: VSXExtensionsSearchModel;
|
|
129
|
+
|
|
130
|
+
@inject(HoverService)
|
|
131
|
+
protected readonly hoverService: HoverService;
|
|
132
|
+
|
|
133
|
+
@inject(WindowService)
|
|
134
|
+
readonly windowService: WindowService;
|
|
135
|
+
|
|
136
|
+
@inject(CommandRegistry)
|
|
137
|
+
readonly commandRegistry: CommandRegistry;
|
|
138
|
+
|
|
139
|
+
protected readonly data: Partial<VSXExtensionData> = {};
|
|
140
|
+
|
|
141
|
+
protected registryUri: Promise<string>;
|
|
142
|
+
|
|
143
|
+
@postConstruct()
|
|
144
|
+
protected postConstruct(): void {
|
|
145
|
+
this.registryUri = this.environment.getRegistryUri();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get uri(): URI {
|
|
149
|
+
return VSCodeExtensionUri.fromId(this.id);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get id(): string {
|
|
153
|
+
return this.options.id;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get visible(): boolean {
|
|
157
|
+
return !!this.name;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get plugin(): DeployedPlugin | undefined {
|
|
161
|
+
return this.pluginSupport.getPlugin(this.id as PluginIdentifiers.UnversionedId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get installed(): boolean {
|
|
165
|
+
return !!this.plugin;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get builtin(): boolean {
|
|
169
|
+
return this.plugin?.type === PluginType.System;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
update(data: Partial<VSXExtensionData>): void {
|
|
173
|
+
for (const key of VSXExtensionData.KEYS) {
|
|
174
|
+
if (key in data) {
|
|
175
|
+
Object.assign(this.data, { [key]: data[key] });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
reloadWindow(): void {
|
|
181
|
+
this.windowService.reload();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
protected getData<K extends keyof VSXExtensionData>(key: K): VSXExtensionData[K] {
|
|
185
|
+
const model = this.plugin?.metadata.model;
|
|
186
|
+
if (model && key in model) {
|
|
187
|
+
return model[key as keyof typeof model] as VSXExtensionData[K];
|
|
188
|
+
}
|
|
189
|
+
return this.data[key];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
get iconUrl(): string | undefined {
|
|
193
|
+
const plugin = this.plugin;
|
|
194
|
+
const iconUrl = plugin && plugin.metadata.model.iconUrl;
|
|
195
|
+
if (iconUrl) {
|
|
196
|
+
return new Endpoint({ path: iconUrl }).getRestUrl().toString();
|
|
197
|
+
}
|
|
198
|
+
return this.data['iconUrl'];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get publisher(): string | undefined {
|
|
202
|
+
return this.getData('publisher');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
get name(): string | undefined {
|
|
206
|
+
return this.getData('name');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
get displayName(): string | undefined {
|
|
210
|
+
return this.getData('displayName') || this.name;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
get description(): string | undefined {
|
|
214
|
+
return this.getData('description');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
get version(): string | undefined {
|
|
218
|
+
return this.getData('version');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
get averageRating(): number | undefined {
|
|
222
|
+
return this.getData('averageRating');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
get downloadCount(): number | undefined {
|
|
226
|
+
return this.getData('downloadCount');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
get downloadUrl(): string | undefined {
|
|
230
|
+
return this.getData('downloadUrl');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
get readmeUrl(): string | undefined {
|
|
234
|
+
const plugin = this.plugin;
|
|
235
|
+
const readmeUrl = plugin && plugin.metadata.model.readmeUrl;
|
|
236
|
+
if (readmeUrl) {
|
|
237
|
+
return new Endpoint({ path: readmeUrl }).getRestUrl().toString();
|
|
238
|
+
}
|
|
239
|
+
return this.data['readmeUrl'];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
get licenseUrl(): string | undefined {
|
|
243
|
+
let licenseUrl = this.data['licenseUrl'];
|
|
244
|
+
if (licenseUrl) {
|
|
245
|
+
return licenseUrl;
|
|
246
|
+
} else {
|
|
247
|
+
const plugin = this.plugin;
|
|
248
|
+
licenseUrl = plugin && plugin.metadata.model.licenseUrl;
|
|
249
|
+
if (licenseUrl) {
|
|
250
|
+
return new Endpoint({ path: licenseUrl }).getRestUrl().toString();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
get repository(): string | undefined {
|
|
256
|
+
return this.getData('repository');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
get license(): string | undefined {
|
|
260
|
+
return this.getData('license');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
get readme(): string | undefined {
|
|
264
|
+
return this.getData('readme');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get preview(): boolean | undefined {
|
|
268
|
+
return this.getData('preview');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
get verified(): boolean | undefined {
|
|
272
|
+
return this.getData('verified');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get namespaceAccess(): VSXExtensionNamespaceAccess | undefined {
|
|
276
|
+
return this.getData('namespaceAccess');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
get publishedBy(): VSXUser | undefined {
|
|
280
|
+
return this.getData('publishedBy');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get tooltip(): string {
|
|
284
|
+
let md = `__${this.displayName}__ ${VSXExtension.formatVersion(this.version)}\n\n${this.description}\n_____\n\n${nls.localizeByDefault('Publisher: {0}', this.publisher)}`;
|
|
285
|
+
|
|
286
|
+
if (this.license) {
|
|
287
|
+
md += ` \r${nls.localize('theia/vsx-registry/license', 'License: {0}', this.license)}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (this.downloadCount) {
|
|
291
|
+
md += ` \r${nls.localize('theia/vsx-registry/downloadCount', 'Download count: {0}', downloadCompactFormatter.format(this.downloadCount))}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (this.averageRating) {
|
|
295
|
+
md += ` \r${getAverageRatingTitle(this.averageRating)}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return md;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
protected _busy = 0;
|
|
302
|
+
get busy(): boolean {
|
|
303
|
+
return !!this._busy;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async install(options?: PluginDeployOptions): Promise<void> {
|
|
307
|
+
if (!this.verified) {
|
|
308
|
+
const choice = await new ConfirmDialog({
|
|
309
|
+
title: nls.localize('theia/vsx-registry/confirmDialogTitle', 'Are you sure you want to proceed with the installation ?'),
|
|
310
|
+
msg: nls.localize('theia/vsx-registry/confirmDialogMessage', 'The extension "{0}" is unverified and might pose a security risk.', this.displayName)
|
|
311
|
+
}).open();
|
|
312
|
+
if (choice) {
|
|
313
|
+
this.doInstall(options);
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
this.doInstall(options);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async uninstall(): Promise<void> {
|
|
321
|
+
this._busy++;
|
|
322
|
+
try {
|
|
323
|
+
const { plugin } = this;
|
|
324
|
+
if (plugin) {
|
|
325
|
+
await this.progressService.withProgress(
|
|
326
|
+
nls.localizeByDefault('Uninstalling {0}...', this.id), 'extensions',
|
|
327
|
+
() => this.pluginServer.uninstall(PluginIdentifiers.componentsToVersionedId(plugin.metadata.model))
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
} finally {
|
|
331
|
+
this._busy--;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
protected async doInstall(options?: PluginDeployOptions): Promise<void> {
|
|
336
|
+
this._busy++;
|
|
337
|
+
try {
|
|
338
|
+
await this.progressService.withProgress(nls.localizeByDefault("Installing extension '{0}' v{1}...", this.id, this.version ?? 0), 'extensions', () =>
|
|
339
|
+
this.pluginServer.deploy(this.uri.toString(), undefined, options)
|
|
340
|
+
);
|
|
341
|
+
} finally {
|
|
342
|
+
this._busy--;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
handleContextMenu(e: React.MouseEvent<HTMLElement, MouseEvent>): void {
|
|
347
|
+
e.preventDefault();
|
|
348
|
+
this.contextMenuRenderer.render({
|
|
349
|
+
menuPath: EXTENSIONS_CONTEXT_MENU,
|
|
350
|
+
anchor: {
|
|
351
|
+
x: e.clientX,
|
|
352
|
+
y: e.clientY,
|
|
353
|
+
},
|
|
354
|
+
args: [this]
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get the registry link for the given extension.
|
|
360
|
+
* @param path the url path.
|
|
361
|
+
* @returns the registry link for the given extension at the path.
|
|
362
|
+
*/
|
|
363
|
+
async getRegistryLink(path = ''): Promise<URI> {
|
|
364
|
+
const registryUri = new URI(await this.registryUri);
|
|
365
|
+
if (this.downloadUrl) {
|
|
366
|
+
const downloadUri = new URI(this.downloadUrl);
|
|
367
|
+
if (downloadUri.authority !== registryUri.authority) {
|
|
368
|
+
throw new Error('cannot generate a valid URL');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return registryUri.resolve('extension/' + this.id.replace('.', '/')).resolve(path);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async serialize(): Promise<string> {
|
|
375
|
+
const serializedExtension: string[] = [];
|
|
376
|
+
serializedExtension.push(`Name: ${this.displayName}`);
|
|
377
|
+
serializedExtension.push(`Id: ${this.id}`);
|
|
378
|
+
serializedExtension.push(`Description: ${this.description}`);
|
|
379
|
+
serializedExtension.push(`Version: ${this.version}`);
|
|
380
|
+
serializedExtension.push(`Publisher: ${this.publisher}`);
|
|
381
|
+
if (this.downloadUrl !== undefined) {
|
|
382
|
+
const registryLink = await this.getRegistryLink();
|
|
383
|
+
serializedExtension.push(`Open VSX Link: ${registryLink.toString()}`);
|
|
384
|
+
};
|
|
385
|
+
return serializedExtension.join('\n');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async open(options: OpenerOptions = { mode: 'reveal' }): Promise<void> {
|
|
389
|
+
await this.doOpen(this.uri, options);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async doOpen(uri: URI, options?: OpenerOptions): Promise<void> {
|
|
393
|
+
await open(this.openerService, uri, options);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
render(host: TreeWidget): React.ReactNode {
|
|
397
|
+
return <VSXExtensionComponent extension={this} host={host} hoverService={this.hoverService} />;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export abstract class AbstractVSXExtensionComponent<Props extends AbstractVSXExtensionComponent.Props = AbstractVSXExtensionComponent.Props> extends React.Component<Props> {
|
|
402
|
+
|
|
403
|
+
readonly install = async (event?: React.MouseEvent) => {
|
|
404
|
+
event?.stopPropagation();
|
|
405
|
+
this.forceUpdate();
|
|
406
|
+
try {
|
|
407
|
+
const pending = this.props.extension.install();
|
|
408
|
+
this.forceUpdate();
|
|
409
|
+
await pending;
|
|
410
|
+
} finally {
|
|
411
|
+
this.forceUpdate();
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
readonly uninstall = async (event?: React.MouseEvent) => {
|
|
416
|
+
event?.stopPropagation();
|
|
417
|
+
try {
|
|
418
|
+
const pending = this.props.extension.uninstall();
|
|
419
|
+
this.forceUpdate();
|
|
420
|
+
await pending;
|
|
421
|
+
} finally {
|
|
422
|
+
this.forceUpdate();
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
readonly reloadWindow = (event?: React.MouseEvent) => {
|
|
427
|
+
event?.stopPropagation();
|
|
428
|
+
this.props.extension.reloadWindow();
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
protected readonly manage = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
|
432
|
+
e.stopPropagation();
|
|
433
|
+
this.props.extension.handleContextMenu(e);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
protected renderAction(host?: TreeWidget): React.ReactNode {
|
|
437
|
+
const { builtin, busy, plugin } = this.props.extension;
|
|
438
|
+
const isFocused = (host?.model.getFocusedNode() as TreeElementNode)?.element === this.props.extension;
|
|
439
|
+
const tabIndex = (!host || isFocused) ? 0 : undefined;
|
|
440
|
+
const installed = !!plugin;
|
|
441
|
+
const outOfSynch = plugin?.metadata.outOfSync;
|
|
442
|
+
if (builtin) {
|
|
443
|
+
return <div className="codicon codicon-settings-gear action" tabIndex={tabIndex} onClick={this.manage}></div>;
|
|
444
|
+
}
|
|
445
|
+
if (busy) {
|
|
446
|
+
if (installed) {
|
|
447
|
+
return <button className="theia-button action theia-mod-disabled">{nls.localizeByDefault('Uninstalling')}</button>;
|
|
448
|
+
}
|
|
449
|
+
return <button className="theia-button action prominent theia-mod-disabled">{nls.localizeByDefault('Installing')}</button>;
|
|
450
|
+
}
|
|
451
|
+
if (installed) {
|
|
452
|
+
return <div>
|
|
453
|
+
{
|
|
454
|
+
outOfSynch
|
|
455
|
+
? <button className="theia-button action" onClick={this.reloadWindow}>{nls.localizeByDefault('Reload Window')}</button>
|
|
456
|
+
: <button className="theia-button action" onClick={this.uninstall}>{nls.localizeByDefault('Uninstall')}</button>
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
<div className="codicon codicon-settings-gear action" onClick={this.manage}></div>
|
|
460
|
+
</div>;
|
|
461
|
+
}
|
|
462
|
+
return <button className="theia-button prominent action" onClick={this.install}>{nls.localizeByDefault('Install')}</button>;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
}
|
|
466
|
+
export namespace AbstractVSXExtensionComponent {
|
|
467
|
+
export interface Props {
|
|
468
|
+
extension: VSXExtension;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const downloadFormatter = new Intl.NumberFormat();
|
|
473
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
474
|
+
const downloadCompactFormatter = new Intl.NumberFormat('en-US', { notation: 'compact', compactDisplay: 'short' } as any);
|
|
475
|
+
const averageRatingFormatter = (averageRating: number): number => Math.round(averageRating * 2) / 2;
|
|
476
|
+
const getAverageRatingTitle = (averageRating: number): string =>
|
|
477
|
+
nls.localizeByDefault('Average rating: {0} out of 5', averageRatingFormatter(averageRating));
|
|
478
|
+
|
|
479
|
+
export namespace VSXExtensionComponent {
|
|
480
|
+
export interface Props extends AbstractVSXExtensionComponent.Props {
|
|
481
|
+
host: TreeWidget;
|
|
482
|
+
hoverService: HoverService;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export class VSXExtensionComponent<Props extends VSXExtensionComponent.Props = VSXExtensionComponent.Props> extends AbstractVSXExtensionComponent<Props> {
|
|
487
|
+
override render(): React.ReactNode {
|
|
488
|
+
const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating, tooltip, verified } = this.props.extension;
|
|
489
|
+
|
|
490
|
+
return <div
|
|
491
|
+
className='theia-vsx-extension noselect'
|
|
492
|
+
onMouseEnter={event => {
|
|
493
|
+
this.props.hoverService.requestHover({
|
|
494
|
+
content: new MarkdownStringImpl(tooltip),
|
|
495
|
+
target: event.currentTarget,
|
|
496
|
+
position: 'right'
|
|
497
|
+
});
|
|
498
|
+
}}
|
|
499
|
+
onMouseUp={event => {
|
|
500
|
+
if (event.button === 2) {
|
|
501
|
+
this.manage(event);
|
|
502
|
+
}
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
{iconUrl ?
|
|
506
|
+
<img className='theia-vsx-extension-icon' src={iconUrl} /> :
|
|
507
|
+
<div className='theia-vsx-extension-icon placeholder' />}
|
|
508
|
+
<div className='theia-vsx-extension-content'>
|
|
509
|
+
<div className='title'>
|
|
510
|
+
<div className='noWrapInfo'>
|
|
511
|
+
<span className='name'>{displayName}</span> <span className='version'>{VSXExtension.formatVersion(version)}</span>
|
|
512
|
+
</div>
|
|
513
|
+
<div className='stat'>
|
|
514
|
+
{!!downloadCount && <span className='download-count'><i className={codicon('cloud-download')} />{downloadCompactFormatter.format(downloadCount)}</span>}
|
|
515
|
+
{!!averageRating && <span className='average-rating'><i className={codicon('star-full')} />{averageRatingFormatter(averageRating)}</span>}
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
<div className='noWrapInfo theia-vsx-extension-description'>{description}</div>
|
|
519
|
+
<div className='theia-vsx-extension-action-bar'>
|
|
520
|
+
<div className='theia-vsx-extension-publisher-container'>
|
|
521
|
+
{verified === true ? (
|
|
522
|
+
<i className={codicon('verified-filled')} />
|
|
523
|
+
) : verified === false ? (
|
|
524
|
+
<i className={codicon('verified')} />
|
|
525
|
+
) : (
|
|
526
|
+
<i className={codicon('question')} />
|
|
527
|
+
)}
|
|
528
|
+
<span className='noWrapInfo theia-vsx-extension-publisher'>{publisher}</span>
|
|
529
|
+
</div>
|
|
530
|
+
{this.renderAction(this.props.host)}
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
</div >;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export class VSXExtensionEditorComponent extends AbstractVSXExtensionComponent {
|
|
538
|
+
protected header: HTMLElement | undefined;
|
|
539
|
+
protected body: HTMLElement | undefined;
|
|
540
|
+
protected _scrollContainer: HTMLElement | undefined;
|
|
541
|
+
|
|
542
|
+
get scrollContainer(): HTMLElement | undefined {
|
|
543
|
+
return this._scrollContainer;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
override render(): React.ReactNode {
|
|
547
|
+
const {
|
|
548
|
+
builtin, preview, id, iconUrl, publisher, displayName, description, version,
|
|
549
|
+
averageRating, downloadCount, repository, license, readme
|
|
550
|
+
} = this.props.extension;
|
|
551
|
+
|
|
552
|
+
const sanitizedReadme = !!readme ? DOMPurify.sanitize(readme) : undefined;
|
|
553
|
+
|
|
554
|
+
return <React.Fragment>
|
|
555
|
+
<div className='header' ref={ref => this.header = (ref || undefined)}>
|
|
556
|
+
{iconUrl ?
|
|
557
|
+
<img className='icon-container' src={iconUrl} /> :
|
|
558
|
+
<div className='icon-container placeholder' />}
|
|
559
|
+
<div className='details'>
|
|
560
|
+
<div className='title'>
|
|
561
|
+
<span title='Extension name' className='name' onClick={this.openExtension}>{displayName}</span>
|
|
562
|
+
<span title='Extension identifier' className='identifier'>{id}</span>
|
|
563
|
+
{preview && <span className='preview'>Preview</span>}
|
|
564
|
+
{builtin && <span className='builtin'>Built-in</span>}
|
|
565
|
+
</div>
|
|
566
|
+
<div className='subtitle'>
|
|
567
|
+
<span title='Publisher name' className='publisher' onClick={this.searchPublisher}>
|
|
568
|
+
{this.renderNamespaceAccess()}
|
|
569
|
+
{publisher}
|
|
570
|
+
</span>
|
|
571
|
+
{!!downloadCount && <span className='download-count' onClick={this.openExtension}>
|
|
572
|
+
<i className={codicon('cloud-download')} />{downloadFormatter.format(downloadCount)}</span>}
|
|
573
|
+
{
|
|
574
|
+
averageRating !== undefined &&
|
|
575
|
+
<span className='average-rating' title={getAverageRatingTitle(averageRating)} onClick={this.openAverageRating}>{this.renderStars()}</span>
|
|
576
|
+
}
|
|
577
|
+
{repository && <span className='repository' onClick={this.openRepository}>Repository</span>}
|
|
578
|
+
{license && <span className='license' onClick={this.openLicense}>{license}</span>}
|
|
579
|
+
{version && <span className='version'>{VSXExtension.formatVersion(version)}</span>}
|
|
580
|
+
</div>
|
|
581
|
+
<div className='description noWrapInfo'>{description}</div>
|
|
582
|
+
{this.renderAction()}
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
{
|
|
586
|
+
sanitizedReadme &&
|
|
587
|
+
<div className='scroll-container'
|
|
588
|
+
ref={ref => this._scrollContainer = (ref || undefined)}>
|
|
589
|
+
<div className='body'
|
|
590
|
+
ref={ref => this.body = (ref || undefined)}
|
|
591
|
+
onClick={this.openLink}
|
|
592
|
+
// eslint-disable-next-line react/no-danger
|
|
593
|
+
dangerouslySetInnerHTML={{ __html: sanitizedReadme }}
|
|
594
|
+
/>
|
|
595
|
+
</div>
|
|
596
|
+
}
|
|
597
|
+
</React.Fragment >;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
protected renderNamespaceAccess(): React.ReactNode {
|
|
601
|
+
const { publisher, namespaceAccess, publishedBy } = this.props.extension;
|
|
602
|
+
if (namespaceAccess === undefined) {
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
605
|
+
let tooltip = publishedBy ? ` Published by "${publishedBy.loginName}".` : '';
|
|
606
|
+
let icon;
|
|
607
|
+
if (namespaceAccess === 'public') {
|
|
608
|
+
icon = 'globe';
|
|
609
|
+
tooltip = `Everyone can publish to "${publisher}" namespace.` + tooltip;
|
|
610
|
+
} else {
|
|
611
|
+
icon = 'shield';
|
|
612
|
+
tooltip = `Only verified owners can publish to "${publisher}" namespace.` + tooltip;
|
|
613
|
+
}
|
|
614
|
+
return <i className={`${codicon(icon)} namespace-access`} title={tooltip} onClick={this.openPublishedBy} />;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
protected renderStars(): React.ReactNode {
|
|
618
|
+
const rating = this.props.extension.averageRating || 0;
|
|
619
|
+
|
|
620
|
+
const renderStarAt = (position: number) => position <= rating ?
|
|
621
|
+
<i className={codicon('star-full')} /> :
|
|
622
|
+
position > rating && position - rating < 1 ?
|
|
623
|
+
<i className={codicon('star-half')} /> :
|
|
624
|
+
<i className={codicon('star-empty')} />;
|
|
625
|
+
return <React.Fragment>
|
|
626
|
+
{renderStarAt(1)}{renderStarAt(2)}{renderStarAt(3)}{renderStarAt(4)}{renderStarAt(5)}
|
|
627
|
+
</React.Fragment>;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// TODO replace with webview
|
|
631
|
+
readonly openLink = (event: React.MouseEvent) => {
|
|
632
|
+
if (!this.body) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const target = event.nativeEvent.target;
|
|
636
|
+
if (!(target instanceof HTMLElement)) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
let node = target;
|
|
640
|
+
while (node.tagName.toLowerCase() !== 'a') {
|
|
641
|
+
if (node === this.body) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (!(node.parentElement instanceof HTMLElement)) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
node = node.parentElement;
|
|
648
|
+
}
|
|
649
|
+
const href = node.getAttribute('href');
|
|
650
|
+
if (href && !href.startsWith('#')) {
|
|
651
|
+
event.preventDefault();
|
|
652
|
+
this.props.extension.doOpen(new URI(href));
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
readonly openExtension = async (e: React.MouseEvent) => {
|
|
657
|
+
e.stopPropagation();
|
|
658
|
+
e.preventDefault();
|
|
659
|
+
|
|
660
|
+
const extension = this.props.extension;
|
|
661
|
+
const uri = await extension.getRegistryLink();
|
|
662
|
+
extension.doOpen(uri);
|
|
663
|
+
};
|
|
664
|
+
readonly searchPublisher = (e: React.MouseEvent) => {
|
|
665
|
+
e.stopPropagation();
|
|
666
|
+
e.preventDefault();
|
|
667
|
+
|
|
668
|
+
const extension = this.props.extension;
|
|
669
|
+
if (extension.publisher) {
|
|
670
|
+
extension.search.query = extension.publisher;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
readonly openPublishedBy = async (e: React.MouseEvent) => {
|
|
674
|
+
e.stopPropagation();
|
|
675
|
+
e.preventDefault();
|
|
676
|
+
|
|
677
|
+
const extension = this.props.extension;
|
|
678
|
+
const homepage = extension.publishedBy && extension.publishedBy.homepage;
|
|
679
|
+
if (homepage) {
|
|
680
|
+
extension.doOpen(new URI(homepage));
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
readonly openAverageRating = async (e: React.MouseEvent) => {
|
|
684
|
+
e.stopPropagation();
|
|
685
|
+
e.preventDefault();
|
|
686
|
+
|
|
687
|
+
const extension = this.props.extension;
|
|
688
|
+
const uri = await extension.getRegistryLink('reviews');
|
|
689
|
+
extension.doOpen(uri);
|
|
690
|
+
};
|
|
691
|
+
readonly openRepository = (e: React.MouseEvent) => {
|
|
692
|
+
e.stopPropagation();
|
|
693
|
+
e.preventDefault();
|
|
694
|
+
|
|
695
|
+
const extension = this.props.extension;
|
|
696
|
+
if (extension.repository) {
|
|
697
|
+
extension.doOpen(new URI(extension.repository));
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
readonly openLicense = (e: React.MouseEvent) => {
|
|
701
|
+
e.stopPropagation();
|
|
702
|
+
e.preventDefault();
|
|
703
|
+
|
|
704
|
+
const extension = this.props.extension;
|
|
705
|
+
const licenseUrl = extension.licenseUrl;
|
|
706
|
+
if (licenseUrl) {
|
|
707
|
+
extension.doOpen(new URI(licenseUrl));
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
}
|