@theia/vsx-registry 1.34.2 → 1.34.3
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/LICENSE +641 -641
- package/README.md +36 -36
- package/lib/browser/recommended-extensions/preference-provider-overrides.d.ts +17 -17
- package/lib/browser/recommended-extensions/preference-provider-overrides.js +95 -95
- package/lib/browser/recommended-extensions/recommended-extensions-json-schema.d.ts +14 -14
- package/lib/browser/recommended-extensions/recommended-extensions-json-schema.js +94 -94
- package/lib/browser/recommended-extensions/recommended-extensions-preference-contribution.d.ts +11 -11
- package/lib/browser/recommended-extensions/recommended-extensions-preference-contribution.js +59 -59
- package/lib/browser/vsx-extension-commands.d.ts +13 -13
- package/lib/browser/vsx-extension-commands.js +63 -63
- package/lib/browser/vsx-extension-editor-manager.d.ts +9 -9
- package/lib/browser/vsx-extension-editor-manager.js +49 -49
- package/lib/browser/vsx-extension-editor.d.ts +21 -21
- package/lib/browser/vsx-extension-editor.js +109 -109
- package/lib/browser/vsx-extension.d.ts +141 -141
- package/lib/browser/vsx-extension.js +581 -581
- package/lib/browser/vsx-extensions-contribution.d.ts +54 -54
- package/lib/browser/vsx-extensions-contribution.js +328 -328
- package/lib/browser/vsx-extensions-model.d.ts +67 -67
- package/lib/browser/vsx-extensions-model.js +368 -368
- package/lib/browser/vsx-extensions-search-bar.d.ts +14 -14
- package/lib/browser/vsx-extensions-search-bar.js +75 -75
- package/lib/browser/vsx-extensions-search-model.d.ts +21 -21
- package/lib/browser/vsx-extensions-search-model.js +70 -70
- package/lib/browser/vsx-extensions-source.d.ts +19 -19
- package/lib/browser/vsx-extensions-source.js +102 -102
- package/lib/browser/vsx-extensions-view-container.d.ts +49 -49
- package/lib/browser/vsx-extensions-view-container.js +179 -179
- package/lib/browser/vsx-extensions-widget.d.ts +29 -29
- package/lib/browser/vsx-extensions-widget.js +143 -143
- package/lib/browser/vsx-language-quick-pick-service.d.ts +13 -13
- package/lib/browser/vsx-language-quick-pick-service.js +103 -103
- package/lib/browser/vsx-registry-frontend-module.d.ts +4 -4
- package/lib/browser/vsx-registry-frontend-module.js +101 -101
- package/lib/common/ovsx-client-provider.d.ts +6 -6
- package/lib/common/ovsx-client-provider.js +28 -28
- package/lib/common/vsx-environment.d.ts +7 -7
- package/lib/common/vsx-environment.js +20 -20
- package/lib/common/vsx-extension-uri.d.ts +3 -3
- package/lib/common/vsx-extension-uri.js +20 -20
- package/lib/node/vsx-environment-impl.d.ts +10 -10
- package/lib/node/vsx-environment-impl.js +53 -53
- package/lib/node/vsx-extension-resolver.d.ts +17 -17
- package/lib/node/vsx-extension-resolver.js +137 -137
- package/lib/node/vsx-registry-backend-module.d.ts +3 -3
- package/lib/node/vsx-registry-backend-module.js +35 -35
- package/lib/package.spec.js +25 -25
- package/package.json +10 -10
- 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 +354 -354
- package/src/browser/vsx-extension-commands.ts +63 -63
- 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 +662 -662
- package/src/browser/vsx-extensions-contribution.ts +315 -315
- package/src/browser/vsx-extensions-model.ts +366 -366
- package/src/browser/vsx-extensions-search-bar.tsx +69 -69
- 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 +138 -138
- package/src/browser/vsx-language-quick-pick-service.ts +97 -97
- package/src/browser/vsx-registry-frontend-module.ts +113 -113
- package/src/common/ovsx-client-provider.ts +30 -30
- package/src/common/vsx-environment.ts +24 -24
- package/src/common/vsx-extension-uri.ts +20 -20
- package/src/node/vsx-environment-impl.ts +41 -41
- package/src/node/vsx-extension-resolver.ts +111 -111
- package/src/node/vsx-registry-backend-module.ts +37 -37
- package/src/package.spec.ts +29 -29
|
@@ -1,366 +1,366 @@
|
|
|
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 WITH Classpath-exception-2.0
|
|
15
|
-
// *****************************************************************************
|
|
16
|
-
|
|
17
|
-
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
18
|
-
import debounce from 'p-debounce';
|
|
19
|
-
import * as markdownit from '@theia/core/shared/markdown-it';
|
|
20
|
-
import * as DOMPurify from '@theia/core/shared/dompurify';
|
|
21
|
-
import { Emitter } from '@theia/core/lib/common/event';
|
|
22
|
-
import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
|
23
|
-
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
|
24
|
-
import { VSXExtension, VSXExtensionFactory } from './vsx-extension';
|
|
25
|
-
import { ProgressService } from '@theia/core/lib/common/progress-service';
|
|
26
|
-
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
|
|
27
|
-
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
28
|
-
import { PreferenceInspectionScope, PreferenceService } from '@theia/core/lib/browser';
|
|
29
|
-
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
30
|
-
import { RecommendedExtensions } from './recommended-extensions/recommended-extensions-preference-contribution';
|
|
31
|
-
import URI from '@theia/core/lib/common/uri';
|
|
32
|
-
import { VSXResponseError, VSXSearchParam } from '@theia/ovsx-client/lib/ovsx-types';
|
|
33
|
-
import { OVSXClientProvider } from '../common/ovsx-client-provider';
|
|
34
|
-
|
|
35
|
-
@injectable()
|
|
36
|
-
export class VSXExtensionsModel {
|
|
37
|
-
|
|
38
|
-
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
39
|
-
readonly onDidChange = this.onDidChangeEmitter.event;
|
|
40
|
-
|
|
41
|
-
@inject(OVSXClientProvider)
|
|
42
|
-
protected clientProvider: OVSXClientProvider;
|
|
43
|
-
|
|
44
|
-
@inject(HostedPluginSupport)
|
|
45
|
-
protected readonly pluginSupport: HostedPluginSupport;
|
|
46
|
-
|
|
47
|
-
@inject(VSXExtensionFactory)
|
|
48
|
-
protected readonly extensionFactory: VSXExtensionFactory;
|
|
49
|
-
|
|
50
|
-
@inject(ProgressService)
|
|
51
|
-
protected readonly progressService: ProgressService;
|
|
52
|
-
|
|
53
|
-
@inject(PreferenceService)
|
|
54
|
-
protected readonly preferences: PreferenceService;
|
|
55
|
-
|
|
56
|
-
@inject(WorkspaceService)
|
|
57
|
-
protected readonly workspaceService: WorkspaceService;
|
|
58
|
-
|
|
59
|
-
@inject(VSXExtensionsSearchModel)
|
|
60
|
-
readonly search: VSXExtensionsSearchModel;
|
|
61
|
-
|
|
62
|
-
protected readonly initialized = new Deferred<void>();
|
|
63
|
-
|
|
64
|
-
@postConstruct()
|
|
65
|
-
protected async init(): Promise<void> {
|
|
66
|
-
await Promise.all([
|
|
67
|
-
this.initInstalled(),
|
|
68
|
-
this.initSearchResult(),
|
|
69
|
-
this.initRecommended(),
|
|
70
|
-
]);
|
|
71
|
-
this.initialized.resolve();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
protected async initInstalled(): Promise<void> {
|
|
75
|
-
await this.pluginSupport.willStart;
|
|
76
|
-
this.pluginSupport.onDidChangePlugins(() => this.updateInstalled());
|
|
77
|
-
try {
|
|
78
|
-
await this.updateInstalled();
|
|
79
|
-
} catch (e) {
|
|
80
|
-
console.error(e);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
protected async initSearchResult(): Promise<void> {
|
|
85
|
-
this.search.onDidChangeQuery(() => this.updateSearchResult());
|
|
86
|
-
try {
|
|
87
|
-
await this.updateSearchResult();
|
|
88
|
-
} catch (e) {
|
|
89
|
-
console.error(e);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
protected async initRecommended(): Promise<void> {
|
|
94
|
-
this.preferences.onPreferenceChanged(change => {
|
|
95
|
-
if (change.preferenceName === 'extensions') {
|
|
96
|
-
this.updateRecommended();
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
await this.preferences.ready;
|
|
100
|
-
try {
|
|
101
|
-
await this.updateRecommended();
|
|
102
|
-
} catch (e) {
|
|
103
|
-
console.error(e);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* single source of all extensions
|
|
109
|
-
*/
|
|
110
|
-
protected readonly extensions = new Map<string, VSXExtension>();
|
|
111
|
-
|
|
112
|
-
protected _installed = new Set<string>();
|
|
113
|
-
get installed(): IterableIterator<string> {
|
|
114
|
-
return this._installed.values();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
isInstalled(id: string): boolean {
|
|
118
|
-
return this._installed.has(id);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
protected _searchError?: string;
|
|
122
|
-
get searchError(): string | undefined {
|
|
123
|
-
return this._searchError;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
protected _searchResult = new Set<string>();
|
|
127
|
-
get searchResult(): IterableIterator<string> {
|
|
128
|
-
return this._searchResult.values();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
protected _recommended = new Set<string>();
|
|
132
|
-
get recommended(): IterableIterator<string> {
|
|
133
|
-
return this._recommended.values();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
getExtension(id: string): VSXExtension | undefined {
|
|
137
|
-
return this.extensions.get(id);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
protected setExtension(id: string): VSXExtension {
|
|
141
|
-
let extension = this.extensions.get(id);
|
|
142
|
-
if (!extension) {
|
|
143
|
-
extension = this.extensionFactory({ id });
|
|
144
|
-
this.extensions.set(id, extension);
|
|
145
|
-
}
|
|
146
|
-
return extension;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
protected doChange<T>(task: () => Promise<T>): Promise<T>;
|
|
150
|
-
protected doChange<T>(task: () => Promise<T>, token: CancellationToken): Promise<T | undefined>;
|
|
151
|
-
protected doChange<T>(task: () => Promise<T>, token: CancellationToken = CancellationToken.None): Promise<T | undefined> {
|
|
152
|
-
return this.progressService.withProgress('', 'extensions', async () => {
|
|
153
|
-
if (token && token.isCancellationRequested) {
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
|
-
const result = await task();
|
|
157
|
-
if (token && token.isCancellationRequested) {
|
|
158
|
-
return undefined;
|
|
159
|
-
}
|
|
160
|
-
this.onDidChangeEmitter.fire(undefined);
|
|
161
|
-
return result;
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
protected searchCancellationTokenSource = new CancellationTokenSource();
|
|
166
|
-
protected updateSearchResult = debounce(() => {
|
|
167
|
-
this.searchCancellationTokenSource.cancel();
|
|
168
|
-
this.searchCancellationTokenSource = new CancellationTokenSource();
|
|
169
|
-
const query = this.search.query;
|
|
170
|
-
return this.doUpdateSearchResult({ query, includeAllVersions: true }, this.searchCancellationTokenSource.token);
|
|
171
|
-
}, 500);
|
|
172
|
-
protected doUpdateSearchResult(param: VSXSearchParam, token: CancellationToken): Promise<void> {
|
|
173
|
-
return this.doChange(async () => {
|
|
174
|
-
const searchResult = new Set<string>();
|
|
175
|
-
if (!param.query) {
|
|
176
|
-
this._searchResult = searchResult;
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const client = await this.clientProvider();
|
|
180
|
-
const result = await client.search(param);
|
|
181
|
-
this._searchError = result.error;
|
|
182
|
-
if (token.isCancellationRequested) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
for (const data of result.extensions) {
|
|
186
|
-
const id = data.namespace.toLowerCase() + '.' + data.name.toLowerCase();
|
|
187
|
-
const extension = client.getLatestCompatibleVersion(data);
|
|
188
|
-
if (!extension) {
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
this.setExtension(id).update(Object.assign(data, {
|
|
192
|
-
publisher: data.namespace,
|
|
193
|
-
downloadUrl: data.files.download,
|
|
194
|
-
iconUrl: data.files.icon,
|
|
195
|
-
readmeUrl: data.files.readme,
|
|
196
|
-
licenseUrl: data.files.license,
|
|
197
|
-
version: extension.version
|
|
198
|
-
}));
|
|
199
|
-
searchResult.add(id);
|
|
200
|
-
}
|
|
201
|
-
this._searchResult = searchResult;
|
|
202
|
-
}, token);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
protected async updateInstalled(): Promise<void> {
|
|
206
|
-
const prevInstalled = this._installed;
|
|
207
|
-
return this.doChange(async () => {
|
|
208
|
-
const plugins = this.pluginSupport.plugins;
|
|
209
|
-
const currInstalled = new Set<string>();
|
|
210
|
-
const refreshing = [];
|
|
211
|
-
for (const plugin of plugins) {
|
|
212
|
-
if (plugin.model.engine.type === 'vscode') {
|
|
213
|
-
const version = plugin.model.version;
|
|
214
|
-
const id = plugin.model.id;
|
|
215
|
-
this._installed.delete(id);
|
|
216
|
-
const extension = this.setExtension(id);
|
|
217
|
-
currInstalled.add(extension.id);
|
|
218
|
-
refreshing.push(this.refresh(id, version));
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
for (const id of this._installed) {
|
|
222
|
-
const extension = this.getExtension(id);
|
|
223
|
-
if (!extension) { continue; }
|
|
224
|
-
refreshing.push(this.refresh(id, extension.version));
|
|
225
|
-
}
|
|
226
|
-
const installed = new Set([...prevInstalled, ...currInstalled]);
|
|
227
|
-
const installedSorted = Array.from(installed).sort((a, b) => this.compareExtensions(a, b));
|
|
228
|
-
this._installed = new Set(installedSorted.values());
|
|
229
|
-
await Promise.all(refreshing);
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
protected updateRecommended(): Promise<Array<VSXExtension | undefined>> {
|
|
234
|
-
return this.doChange<Array<VSXExtension | undefined>>(async () => {
|
|
235
|
-
const allRecommendations = new Set<string>();
|
|
236
|
-
const allUnwantedRecommendations = new Set<string>();
|
|
237
|
-
|
|
238
|
-
const updateRecommendationsForScope = (scope: PreferenceInspectionScope, root?: URI) => {
|
|
239
|
-
const { recommendations, unwantedRecommendations } = this.getRecommendationsForScope(scope, root);
|
|
240
|
-
recommendations.forEach(recommendation => allRecommendations.add(recommendation));
|
|
241
|
-
unwantedRecommendations.forEach(unwantedRecommendation => allUnwantedRecommendations.add(unwantedRecommendation));
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
updateRecommendationsForScope('defaultValue'); // In case there are application-default recommendations.
|
|
245
|
-
const roots = await this.workspaceService.roots;
|
|
246
|
-
for (const root of roots) {
|
|
247
|
-
updateRecommendationsForScope('workspaceFolderValue', root.resource);
|
|
248
|
-
}
|
|
249
|
-
if (this.workspaceService.saved) {
|
|
250
|
-
updateRecommendationsForScope('workspaceValue');
|
|
251
|
-
}
|
|
252
|
-
const recommendedSorted = new Set(Array.from(allRecommendations).sort((a, b) => this.compareExtensions(a, b)));
|
|
253
|
-
allUnwantedRecommendations.forEach(unwantedRecommendation => recommendedSorted.delete(unwantedRecommendation));
|
|
254
|
-
this._recommended = recommendedSorted;
|
|
255
|
-
return Promise.all(Array.from(recommendedSorted, plugin => this.refresh(plugin)));
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
protected getRecommendationsForScope(scope: PreferenceInspectionScope, root?: URI): Required<RecommendedExtensions> {
|
|
260
|
-
const configuredValue = this.preferences.inspect<Required<RecommendedExtensions>>('extensions', root?.toString())?.[scope];
|
|
261
|
-
return {
|
|
262
|
-
recommendations: configuredValue?.recommendations ?? [],
|
|
263
|
-
unwantedRecommendations: configuredValue?.unwantedRecommendations ?? [],
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
resolve(id: string): Promise<VSXExtension> {
|
|
268
|
-
return this.doChange(async () => {
|
|
269
|
-
await this.initialized.promise;
|
|
270
|
-
const extension = await this.refresh(id);
|
|
271
|
-
if (!extension) {
|
|
272
|
-
throw new Error(`Failed to resolve ${id} extension.`);
|
|
273
|
-
}
|
|
274
|
-
if (extension.readmeUrl) {
|
|
275
|
-
try {
|
|
276
|
-
const client = await this.clientProvider();
|
|
277
|
-
const rawReadme = await client.fetchText(extension.readmeUrl);
|
|
278
|
-
const readme = this.compileReadme(rawReadme);
|
|
279
|
-
extension.update({ readme });
|
|
280
|
-
} catch (e) {
|
|
281
|
-
if (!VSXResponseError.is(e) || e.statusCode !== 404) {
|
|
282
|
-
console.error(`[${id}]: failed to compile readme, reason:`, e);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return extension;
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
protected compileReadme(readmeMarkdown: string): string {
|
|
291
|
-
const readmeHtml = markdownit({ html: true }).render(readmeMarkdown);
|
|
292
|
-
return DOMPurify.sanitize(readmeHtml);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
protected async refresh(id: string, version?: string): Promise<VSXExtension | undefined> {
|
|
296
|
-
try {
|
|
297
|
-
let extension = this.getExtension(id);
|
|
298
|
-
if (!this.shouldRefresh(extension)) {
|
|
299
|
-
return extension;
|
|
300
|
-
}
|
|
301
|
-
const client = await this.clientProvider();
|
|
302
|
-
const data = version !== undefined
|
|
303
|
-
? await client.getExtension(id, { extensionVersion: version, includeAllVersions: true })
|
|
304
|
-
: await client.getLatestCompatibleExtensionVersion(id);
|
|
305
|
-
if (!data) {
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if (data.error) {
|
|
309
|
-
return this.onDidFailRefresh(id, data.error);
|
|
310
|
-
}
|
|
311
|
-
extension = this.setExtension(id);
|
|
312
|
-
extension.update(Object.assign(data, {
|
|
313
|
-
publisher: data.namespace,
|
|
314
|
-
downloadUrl: data.files.download,
|
|
315
|
-
iconUrl: data.files.icon,
|
|
316
|
-
readmeUrl: data.files.readme,
|
|
317
|
-
licenseUrl: data.files.license,
|
|
318
|
-
version: data.version
|
|
319
|
-
}));
|
|
320
|
-
return extension;
|
|
321
|
-
} catch (e) {
|
|
322
|
-
return this.onDidFailRefresh(id, e);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Determines if the given extension should be refreshed.
|
|
328
|
-
* @param extension the extension to refresh.
|
|
329
|
-
*/
|
|
330
|
-
protected shouldRefresh(extension?: VSXExtension): boolean {
|
|
331
|
-
if (extension === undefined) {
|
|
332
|
-
return true;
|
|
333
|
-
}
|
|
334
|
-
return !extension.builtin;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
protected onDidFailRefresh(id: string, error: unknown): VSXExtension | undefined {
|
|
338
|
-
const cached = this.getExtension(id);
|
|
339
|
-
if (cached && cached.installed) {
|
|
340
|
-
return cached;
|
|
341
|
-
}
|
|
342
|
-
console.error(`[${id}]: failed to refresh, reason:`, error);
|
|
343
|
-
return undefined;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Compare two extensions based on their display name, and publisher if applicable.
|
|
348
|
-
* @param a the first extension id for comparison.
|
|
349
|
-
* @param b the second extension id for comparison.
|
|
350
|
-
*/
|
|
351
|
-
protected compareExtensions(a: string, b: string): number {
|
|
352
|
-
const extensionA = this.getExtension(a);
|
|
353
|
-
const extensionB = this.getExtension(b);
|
|
354
|
-
if (!extensionA || !extensionB) {
|
|
355
|
-
return 0;
|
|
356
|
-
}
|
|
357
|
-
if (extensionA.displayName && extensionB.displayName) {
|
|
358
|
-
return extensionA.displayName.localeCompare(extensionB.displayName);
|
|
359
|
-
}
|
|
360
|
-
if (extensionA.publisher && extensionB.publisher) {
|
|
361
|
-
return extensionA.publisher.localeCompare(extensionB.publisher);
|
|
362
|
-
}
|
|
363
|
-
return 0;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
}
|
|
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 WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
18
|
+
import debounce from 'p-debounce';
|
|
19
|
+
import * as markdownit from '@theia/core/shared/markdown-it';
|
|
20
|
+
import * as DOMPurify from '@theia/core/shared/dompurify';
|
|
21
|
+
import { Emitter } from '@theia/core/lib/common/event';
|
|
22
|
+
import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
|
23
|
+
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
|
24
|
+
import { VSXExtension, VSXExtensionFactory } from './vsx-extension';
|
|
25
|
+
import { ProgressService } from '@theia/core/lib/common/progress-service';
|
|
26
|
+
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
|
|
27
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
28
|
+
import { PreferenceInspectionScope, PreferenceService } from '@theia/core/lib/browser';
|
|
29
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
30
|
+
import { RecommendedExtensions } from './recommended-extensions/recommended-extensions-preference-contribution';
|
|
31
|
+
import URI from '@theia/core/lib/common/uri';
|
|
32
|
+
import { VSXResponseError, VSXSearchParam } from '@theia/ovsx-client/lib/ovsx-types';
|
|
33
|
+
import { OVSXClientProvider } from '../common/ovsx-client-provider';
|
|
34
|
+
|
|
35
|
+
@injectable()
|
|
36
|
+
export class VSXExtensionsModel {
|
|
37
|
+
|
|
38
|
+
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
39
|
+
readonly onDidChange = this.onDidChangeEmitter.event;
|
|
40
|
+
|
|
41
|
+
@inject(OVSXClientProvider)
|
|
42
|
+
protected clientProvider: OVSXClientProvider;
|
|
43
|
+
|
|
44
|
+
@inject(HostedPluginSupport)
|
|
45
|
+
protected readonly pluginSupport: HostedPluginSupport;
|
|
46
|
+
|
|
47
|
+
@inject(VSXExtensionFactory)
|
|
48
|
+
protected readonly extensionFactory: VSXExtensionFactory;
|
|
49
|
+
|
|
50
|
+
@inject(ProgressService)
|
|
51
|
+
protected readonly progressService: ProgressService;
|
|
52
|
+
|
|
53
|
+
@inject(PreferenceService)
|
|
54
|
+
protected readonly preferences: PreferenceService;
|
|
55
|
+
|
|
56
|
+
@inject(WorkspaceService)
|
|
57
|
+
protected readonly workspaceService: WorkspaceService;
|
|
58
|
+
|
|
59
|
+
@inject(VSXExtensionsSearchModel)
|
|
60
|
+
readonly search: VSXExtensionsSearchModel;
|
|
61
|
+
|
|
62
|
+
protected readonly initialized = new Deferred<void>();
|
|
63
|
+
|
|
64
|
+
@postConstruct()
|
|
65
|
+
protected async init(): Promise<void> {
|
|
66
|
+
await Promise.all([
|
|
67
|
+
this.initInstalled(),
|
|
68
|
+
this.initSearchResult(),
|
|
69
|
+
this.initRecommended(),
|
|
70
|
+
]);
|
|
71
|
+
this.initialized.resolve();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected async initInstalled(): Promise<void> {
|
|
75
|
+
await this.pluginSupport.willStart;
|
|
76
|
+
this.pluginSupport.onDidChangePlugins(() => this.updateInstalled());
|
|
77
|
+
try {
|
|
78
|
+
await this.updateInstalled();
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error(e);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
protected async initSearchResult(): Promise<void> {
|
|
85
|
+
this.search.onDidChangeQuery(() => this.updateSearchResult());
|
|
86
|
+
try {
|
|
87
|
+
await this.updateSearchResult();
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.error(e);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected async initRecommended(): Promise<void> {
|
|
94
|
+
this.preferences.onPreferenceChanged(change => {
|
|
95
|
+
if (change.preferenceName === 'extensions') {
|
|
96
|
+
this.updateRecommended();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
await this.preferences.ready;
|
|
100
|
+
try {
|
|
101
|
+
await this.updateRecommended();
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error(e);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* single source of all extensions
|
|
109
|
+
*/
|
|
110
|
+
protected readonly extensions = new Map<string, VSXExtension>();
|
|
111
|
+
|
|
112
|
+
protected _installed = new Set<string>();
|
|
113
|
+
get installed(): IterableIterator<string> {
|
|
114
|
+
return this._installed.values();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
isInstalled(id: string): boolean {
|
|
118
|
+
return this._installed.has(id);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected _searchError?: string;
|
|
122
|
+
get searchError(): string | undefined {
|
|
123
|
+
return this._searchError;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
protected _searchResult = new Set<string>();
|
|
127
|
+
get searchResult(): IterableIterator<string> {
|
|
128
|
+
return this._searchResult.values();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
protected _recommended = new Set<string>();
|
|
132
|
+
get recommended(): IterableIterator<string> {
|
|
133
|
+
return this._recommended.values();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getExtension(id: string): VSXExtension | undefined {
|
|
137
|
+
return this.extensions.get(id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
protected setExtension(id: string): VSXExtension {
|
|
141
|
+
let extension = this.extensions.get(id);
|
|
142
|
+
if (!extension) {
|
|
143
|
+
extension = this.extensionFactory({ id });
|
|
144
|
+
this.extensions.set(id, extension);
|
|
145
|
+
}
|
|
146
|
+
return extension;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected doChange<T>(task: () => Promise<T>): Promise<T>;
|
|
150
|
+
protected doChange<T>(task: () => Promise<T>, token: CancellationToken): Promise<T | undefined>;
|
|
151
|
+
protected doChange<T>(task: () => Promise<T>, token: CancellationToken = CancellationToken.None): Promise<T | undefined> {
|
|
152
|
+
return this.progressService.withProgress('', 'extensions', async () => {
|
|
153
|
+
if (token && token.isCancellationRequested) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const result = await task();
|
|
157
|
+
if (token && token.isCancellationRequested) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
this.onDidChangeEmitter.fire(undefined);
|
|
161
|
+
return result;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected searchCancellationTokenSource = new CancellationTokenSource();
|
|
166
|
+
protected updateSearchResult = debounce(() => {
|
|
167
|
+
this.searchCancellationTokenSource.cancel();
|
|
168
|
+
this.searchCancellationTokenSource = new CancellationTokenSource();
|
|
169
|
+
const query = this.search.query;
|
|
170
|
+
return this.doUpdateSearchResult({ query, includeAllVersions: true }, this.searchCancellationTokenSource.token);
|
|
171
|
+
}, 500);
|
|
172
|
+
protected doUpdateSearchResult(param: VSXSearchParam, token: CancellationToken): Promise<void> {
|
|
173
|
+
return this.doChange(async () => {
|
|
174
|
+
const searchResult = new Set<string>();
|
|
175
|
+
if (!param.query) {
|
|
176
|
+
this._searchResult = searchResult;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const client = await this.clientProvider();
|
|
180
|
+
const result = await client.search(param);
|
|
181
|
+
this._searchError = result.error;
|
|
182
|
+
if (token.isCancellationRequested) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
for (const data of result.extensions) {
|
|
186
|
+
const id = data.namespace.toLowerCase() + '.' + data.name.toLowerCase();
|
|
187
|
+
const extension = client.getLatestCompatibleVersion(data);
|
|
188
|
+
if (!extension) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
this.setExtension(id).update(Object.assign(data, {
|
|
192
|
+
publisher: data.namespace,
|
|
193
|
+
downloadUrl: data.files.download,
|
|
194
|
+
iconUrl: data.files.icon,
|
|
195
|
+
readmeUrl: data.files.readme,
|
|
196
|
+
licenseUrl: data.files.license,
|
|
197
|
+
version: extension.version
|
|
198
|
+
}));
|
|
199
|
+
searchResult.add(id);
|
|
200
|
+
}
|
|
201
|
+
this._searchResult = searchResult;
|
|
202
|
+
}, token);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
protected async updateInstalled(): Promise<void> {
|
|
206
|
+
const prevInstalled = this._installed;
|
|
207
|
+
return this.doChange(async () => {
|
|
208
|
+
const plugins = this.pluginSupport.plugins;
|
|
209
|
+
const currInstalled = new Set<string>();
|
|
210
|
+
const refreshing = [];
|
|
211
|
+
for (const plugin of plugins) {
|
|
212
|
+
if (plugin.model.engine.type === 'vscode') {
|
|
213
|
+
const version = plugin.model.version;
|
|
214
|
+
const id = plugin.model.id;
|
|
215
|
+
this._installed.delete(id);
|
|
216
|
+
const extension = this.setExtension(id);
|
|
217
|
+
currInstalled.add(extension.id);
|
|
218
|
+
refreshing.push(this.refresh(id, version));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const id of this._installed) {
|
|
222
|
+
const extension = this.getExtension(id);
|
|
223
|
+
if (!extension) { continue; }
|
|
224
|
+
refreshing.push(this.refresh(id, extension.version));
|
|
225
|
+
}
|
|
226
|
+
const installed = new Set([...prevInstalled, ...currInstalled]);
|
|
227
|
+
const installedSorted = Array.from(installed).sort((a, b) => this.compareExtensions(a, b));
|
|
228
|
+
this._installed = new Set(installedSorted.values());
|
|
229
|
+
await Promise.all(refreshing);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
protected updateRecommended(): Promise<Array<VSXExtension | undefined>> {
|
|
234
|
+
return this.doChange<Array<VSXExtension | undefined>>(async () => {
|
|
235
|
+
const allRecommendations = new Set<string>();
|
|
236
|
+
const allUnwantedRecommendations = new Set<string>();
|
|
237
|
+
|
|
238
|
+
const updateRecommendationsForScope = (scope: PreferenceInspectionScope, root?: URI) => {
|
|
239
|
+
const { recommendations, unwantedRecommendations } = this.getRecommendationsForScope(scope, root);
|
|
240
|
+
recommendations.forEach(recommendation => allRecommendations.add(recommendation));
|
|
241
|
+
unwantedRecommendations.forEach(unwantedRecommendation => allUnwantedRecommendations.add(unwantedRecommendation));
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
updateRecommendationsForScope('defaultValue'); // In case there are application-default recommendations.
|
|
245
|
+
const roots = await this.workspaceService.roots;
|
|
246
|
+
for (const root of roots) {
|
|
247
|
+
updateRecommendationsForScope('workspaceFolderValue', root.resource);
|
|
248
|
+
}
|
|
249
|
+
if (this.workspaceService.saved) {
|
|
250
|
+
updateRecommendationsForScope('workspaceValue');
|
|
251
|
+
}
|
|
252
|
+
const recommendedSorted = new Set(Array.from(allRecommendations).sort((a, b) => this.compareExtensions(a, b)));
|
|
253
|
+
allUnwantedRecommendations.forEach(unwantedRecommendation => recommendedSorted.delete(unwantedRecommendation));
|
|
254
|
+
this._recommended = recommendedSorted;
|
|
255
|
+
return Promise.all(Array.from(recommendedSorted, plugin => this.refresh(plugin)));
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
protected getRecommendationsForScope(scope: PreferenceInspectionScope, root?: URI): Required<RecommendedExtensions> {
|
|
260
|
+
const configuredValue = this.preferences.inspect<Required<RecommendedExtensions>>('extensions', root?.toString())?.[scope];
|
|
261
|
+
return {
|
|
262
|
+
recommendations: configuredValue?.recommendations ?? [],
|
|
263
|
+
unwantedRecommendations: configuredValue?.unwantedRecommendations ?? [],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
resolve(id: string): Promise<VSXExtension> {
|
|
268
|
+
return this.doChange(async () => {
|
|
269
|
+
await this.initialized.promise;
|
|
270
|
+
const extension = await this.refresh(id);
|
|
271
|
+
if (!extension) {
|
|
272
|
+
throw new Error(`Failed to resolve ${id} extension.`);
|
|
273
|
+
}
|
|
274
|
+
if (extension.readmeUrl) {
|
|
275
|
+
try {
|
|
276
|
+
const client = await this.clientProvider();
|
|
277
|
+
const rawReadme = await client.fetchText(extension.readmeUrl);
|
|
278
|
+
const readme = this.compileReadme(rawReadme);
|
|
279
|
+
extension.update({ readme });
|
|
280
|
+
} catch (e) {
|
|
281
|
+
if (!VSXResponseError.is(e) || e.statusCode !== 404) {
|
|
282
|
+
console.error(`[${id}]: failed to compile readme, reason:`, e);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return extension;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
protected compileReadme(readmeMarkdown: string): string {
|
|
291
|
+
const readmeHtml = markdownit({ html: true }).render(readmeMarkdown);
|
|
292
|
+
return DOMPurify.sanitize(readmeHtml);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
protected async refresh(id: string, version?: string): Promise<VSXExtension | undefined> {
|
|
296
|
+
try {
|
|
297
|
+
let extension = this.getExtension(id);
|
|
298
|
+
if (!this.shouldRefresh(extension)) {
|
|
299
|
+
return extension;
|
|
300
|
+
}
|
|
301
|
+
const client = await this.clientProvider();
|
|
302
|
+
const data = version !== undefined
|
|
303
|
+
? await client.getExtension(id, { extensionVersion: version, includeAllVersions: true })
|
|
304
|
+
: await client.getLatestCompatibleExtensionVersion(id);
|
|
305
|
+
if (!data) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (data.error) {
|
|
309
|
+
return this.onDidFailRefresh(id, data.error);
|
|
310
|
+
}
|
|
311
|
+
extension = this.setExtension(id);
|
|
312
|
+
extension.update(Object.assign(data, {
|
|
313
|
+
publisher: data.namespace,
|
|
314
|
+
downloadUrl: data.files.download,
|
|
315
|
+
iconUrl: data.files.icon,
|
|
316
|
+
readmeUrl: data.files.readme,
|
|
317
|
+
licenseUrl: data.files.license,
|
|
318
|
+
version: data.version
|
|
319
|
+
}));
|
|
320
|
+
return extension;
|
|
321
|
+
} catch (e) {
|
|
322
|
+
return this.onDidFailRefresh(id, e);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Determines if the given extension should be refreshed.
|
|
328
|
+
* @param extension the extension to refresh.
|
|
329
|
+
*/
|
|
330
|
+
protected shouldRefresh(extension?: VSXExtension): boolean {
|
|
331
|
+
if (extension === undefined) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
return !extension.builtin;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
protected onDidFailRefresh(id: string, error: unknown): VSXExtension | undefined {
|
|
338
|
+
const cached = this.getExtension(id);
|
|
339
|
+
if (cached && cached.installed) {
|
|
340
|
+
return cached;
|
|
341
|
+
}
|
|
342
|
+
console.error(`[${id}]: failed to refresh, reason:`, error);
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Compare two extensions based on their display name, and publisher if applicable.
|
|
348
|
+
* @param a the first extension id for comparison.
|
|
349
|
+
* @param b the second extension id for comparison.
|
|
350
|
+
*/
|
|
351
|
+
protected compareExtensions(a: string, b: string): number {
|
|
352
|
+
const extensionA = this.getExtension(a);
|
|
353
|
+
const extensionB = this.getExtension(b);
|
|
354
|
+
if (!extensionA || !extensionB) {
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
if (extensionA.displayName && extensionB.displayName) {
|
|
358
|
+
return extensionA.displayName.localeCompare(extensionB.displayName);
|
|
359
|
+
}
|
|
360
|
+
if (extensionA.publisher && extensionB.publisher) {
|
|
361
|
+
return extensionA.publisher.localeCompare(extensionB.publisher);
|
|
362
|
+
}
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
}
|