Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/extension-editing/src/extensionLinter.ts
3291 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as path from 'path';
7
import * as fs from 'fs';
8
import { URL } from 'url';
9
10
import { parseTree, findNodeAtLocation, Node as JsonNode, getNodeValue } from 'jsonc-parser';
11
import * as MarkdownItType from 'markdown-it';
12
13
import { commands, languages, workspace, Disposable, TextDocument, Uri, Diagnostic, Range, DiagnosticSeverity, Position, env, l10n } from 'vscode';
14
import { INormalizedVersion, normalizeVersion, parseVersion } from './extensionEngineValidation';
15
import { JsonStringScanner } from './jsonReconstruct';
16
import { implicitActivationEvent, redundantImplicitActivationEvent } from './constants';
17
18
const product = JSON.parse(fs.readFileSync(path.join(env.appRoot, 'product.json'), { encoding: 'utf-8' }));
19
const allowedBadgeProviders: string[] = (product.extensionAllowedBadgeProviders || []).map((s: string) => s.toLowerCase());
20
const allowedBadgeProvidersRegex: RegExp[] = (product.extensionAllowedBadgeProvidersRegex || []).map((r: string) => new RegExp(r));
21
const extensionEnabledApiProposals: Record<string, string[]> = product.extensionEnabledApiProposals ?? {};
22
const reservedImplicitActivationEventPrefixes = ['onNotebookSerializer:'];
23
const redundantImplicitActivationEventPrefixes = ['onLanguage:', 'onView:', 'onAuthenticationRequest:', 'onCommand:', 'onCustomEditor:', 'onTerminalProfile:', 'onRenderer:', 'onTerminalQuickFixRequest:', 'onWalkthrough:'];
24
25
function isTrustedSVGSource(uri: Uri): boolean {
26
return allowedBadgeProviders.includes(uri.authority.toLowerCase()) || allowedBadgeProvidersRegex.some(r => r.test(uri.toString()));
27
}
28
29
const httpsRequired = l10n.t("Images must use the HTTPS protocol.");
30
const svgsNotValid = l10n.t("SVGs are not a valid image source.");
31
const embeddedSvgsNotValid = l10n.t("Embedded SVGs are not a valid image source.");
32
const dataUrlsNotValid = l10n.t("Data URLs are not a valid image source.");
33
const relativeUrlRequiresHttpsRepository = l10n.t("Relative image URLs require a repository with HTTPS protocol to be specified in the package.json.");
34
const relativeBadgeUrlRequiresHttpsRepository = l10n.t("Relative badge URLs require a repository with HTTPS protocol to be specified in this package.json.");
35
const apiProposalNotListed = l10n.t("This proposal cannot be used because for this extension the product defines a fixed set of API proposals. You can test your extension but before publishing you MUST reach out to the VS Code team.");
36
const bumpEngineForImplicitActivationEvents = l10n.t("This activation event can be removed for extensions targeting engine version ^1.75 as VS Code will generate these automatically from your package.json contribution declarations.");
37
const starActivation = l10n.t("Using '*' activation is usually a bad idea as it impacts performance.");
38
const parsingErrorHeader = l10n.t("Error parsing the when-clause:");
39
40
enum Context {
41
ICON,
42
BADGE,
43
MARKDOWN
44
}
45
46
interface TokenAndPosition {
47
token: MarkdownItType.Token;
48
begin: number;
49
end: number;
50
}
51
52
interface PackageJsonInfo {
53
isExtension: boolean;
54
hasHttpsRepository: boolean;
55
repository: Uri;
56
implicitActivationEvents: Set<string> | undefined;
57
engineVersion: INormalizedVersion | null;
58
}
59
60
export class ExtensionLinter {
61
62
private diagnosticsCollection = languages.createDiagnosticCollection('extension-editing');
63
private fileWatcher = workspace.createFileSystemWatcher('**/package.json');
64
private disposables: Disposable[] = [this.diagnosticsCollection, this.fileWatcher];
65
66
private folderToPackageJsonInfo: Record<string, PackageJsonInfo> = {};
67
private packageJsonQ = new Set<TextDocument>();
68
private readmeQ = new Set<TextDocument>();
69
private timer: NodeJS.Timeout | undefined;
70
private markdownIt: MarkdownItType.MarkdownIt | undefined;
71
private parse5: typeof import('parse5') | undefined;
72
73
constructor() {
74
this.disposables.push(
75
workspace.onDidOpenTextDocument(document => this.queue(document)),
76
workspace.onDidChangeTextDocument(event => this.queue(event.document)),
77
workspace.onDidCloseTextDocument(document => this.clear(document)),
78
this.fileWatcher.onDidChange(uri => this.packageJsonChanged(this.getUriFolder(uri))),
79
this.fileWatcher.onDidCreate(uri => this.packageJsonChanged(this.getUriFolder(uri))),
80
this.fileWatcher.onDidDelete(uri => this.packageJsonChanged(this.getUriFolder(uri))),
81
);
82
workspace.textDocuments.forEach(document => this.queue(document));
83
}
84
85
private queue(document: TextDocument) {
86
const p = document.uri.path;
87
if (document.languageId === 'json' && p.endsWith('/package.json')) {
88
this.packageJsonQ.add(document);
89
this.startTimer();
90
}
91
this.queueReadme(document);
92
}
93
94
private queueReadme(document: TextDocument) {
95
const p = document.uri.path;
96
if (document.languageId === 'markdown' && (p.toLowerCase().endsWith('/readme.md') || p.toLowerCase().endsWith('/changelog.md'))) {
97
this.readmeQ.add(document);
98
this.startTimer();
99
}
100
}
101
102
private startTimer() {
103
if (this.timer) {
104
clearTimeout(this.timer);
105
}
106
this.timer = setTimeout(() => {
107
this.lint()
108
.catch(console.error);
109
}, 300);
110
}
111
112
private async lint() {
113
await Promise.all([
114
this.lintPackageJson(),
115
this.lintReadme()
116
]);
117
}
118
119
private async lintPackageJson() {
120
for (const document of Array.from(this.packageJsonQ)) {
121
this.packageJsonQ.delete(document);
122
if (document.isClosed) {
123
continue;
124
}
125
126
const diagnostics: Diagnostic[] = [];
127
128
const tree = parseTree(document.getText());
129
const info = this.readPackageJsonInfo(this.getUriFolder(document.uri), tree);
130
if (tree && info.isExtension) {
131
132
const icon = findNodeAtLocation(tree, ['icon']);
133
if (icon && icon.type === 'string') {
134
this.addDiagnostics(diagnostics, document, icon.offset + 1, icon.offset + icon.length - 1, icon.value, Context.ICON, info);
135
}
136
137
const badges = findNodeAtLocation(tree, ['badges']);
138
if (badges && badges.type === 'array' && badges.children) {
139
badges.children.map(child => findNodeAtLocation(child, ['url']))
140
.filter(url => url && url.type === 'string')
141
.map(url => this.addDiagnostics(diagnostics, document, url!.offset + 1, url!.offset + url!.length - 1, url!.value, Context.BADGE, info));
142
}
143
144
const publisher = findNodeAtLocation(tree, ['publisher']);
145
const name = findNodeAtLocation(tree, ['name']);
146
const enabledApiProposals = findNodeAtLocation(tree, ['enabledApiProposals']);
147
if (publisher?.type === 'string' && name?.type === 'string' && enabledApiProposals?.type === 'array') {
148
const extensionId = `${getNodeValue(publisher)}.${getNodeValue(name)}`;
149
const effectiveProposalNames = extensionEnabledApiProposals[extensionId];
150
if (Array.isArray(effectiveProposalNames) && enabledApiProposals.children) {
151
for (const child of enabledApiProposals.children) {
152
const proposalName = child.type === 'string' ? getNodeValue(child) : undefined;
153
if (typeof proposalName === 'string' && !effectiveProposalNames.includes(proposalName.split('@')[0])) {
154
const start = document.positionAt(child.offset);
155
const end = document.positionAt(child.offset + child.length);
156
diagnostics.push(new Diagnostic(new Range(start, end), apiProposalNotListed, DiagnosticSeverity.Error));
157
}
158
}
159
}
160
}
161
const activationEventsNode = findNodeAtLocation(tree, ['activationEvents']);
162
if (activationEventsNode?.type === 'array' && activationEventsNode.children) {
163
for (const activationEventNode of activationEventsNode.children) {
164
const activationEvent = getNodeValue(activationEventNode);
165
const isImplicitActivationSupported = info.engineVersion && info.engineVersion?.majorBase >= 1 && info.engineVersion?.minorBase >= 75;
166
// Redundant Implicit Activation
167
if (info.implicitActivationEvents?.has(activationEvent) && redundantImplicitActivationEventPrefixes.some((prefix) => activationEvent.startsWith(prefix))) {
168
const start = document.positionAt(activationEventNode.offset);
169
const end = document.positionAt(activationEventNode.offset + activationEventNode.length);
170
const message = isImplicitActivationSupported ? redundantImplicitActivationEvent : bumpEngineForImplicitActivationEvents;
171
diagnostics.push(new Diagnostic(new Range(start, end), message, isImplicitActivationSupported ? DiagnosticSeverity.Warning : DiagnosticSeverity.Information));
172
}
173
174
// Reserved Implicit Activation
175
for (const implicitActivationEventPrefix of reservedImplicitActivationEventPrefixes) {
176
if (isImplicitActivationSupported && activationEvent.startsWith(implicitActivationEventPrefix)) {
177
const start = document.positionAt(activationEventNode.offset);
178
const end = document.positionAt(activationEventNode.offset + activationEventNode.length);
179
diagnostics.push(new Diagnostic(new Range(start, end), implicitActivationEvent, DiagnosticSeverity.Error));
180
}
181
}
182
183
// Star activation
184
if (activationEvent === '*') {
185
const start = document.positionAt(activationEventNode.offset);
186
const end = document.positionAt(activationEventNode.offset + activationEventNode.length);
187
const diagnostic = new Diagnostic(new Range(start, end), starActivation, DiagnosticSeverity.Information);
188
diagnostic.code = {
189
value: 'star-activation',
190
target: Uri.parse('https://code.visualstudio.com/api/references/activation-events#Start-up'),
191
};
192
diagnostics.push(diagnostic);
193
}
194
}
195
}
196
197
const whenClauseLinting = await this.lintWhenClauses(findNodeAtLocation(tree, ['contributes']), document);
198
diagnostics.push(...whenClauseLinting);
199
}
200
this.diagnosticsCollection.set(document.uri, diagnostics);
201
}
202
}
203
204
/** lints `when` and `enablement` clauses */
205
private async lintWhenClauses(contributesNode: JsonNode | undefined, document: TextDocument): Promise<Diagnostic[]> {
206
if (!contributesNode) {
207
return [];
208
}
209
210
const whenClauses: JsonNode[] = [];
211
212
function findWhens(node: JsonNode | undefined, clauseName: string) {
213
if (node) {
214
switch (node.type) {
215
case 'property':
216
if (node.children && node.children.length === 2) {
217
const key = node.children[0];
218
const value = node.children[1];
219
switch (value.type) {
220
case 'string':
221
if (key.value === clauseName && typeof value.value === 'string' /* careful: `.value` MUST be a string 1) because a when/enablement clause is string; so also, type cast to string below is safe */) {
222
whenClauses.push(value);
223
}
224
case 'object':
225
case 'array':
226
findWhens(value, clauseName);
227
}
228
}
229
break;
230
case 'object':
231
case 'array':
232
if (node.children) {
233
node.children.forEach(n => findWhens(n, clauseName));
234
}
235
}
236
}
237
}
238
239
[
240
findNodeAtLocation(contributesNode, ['menus']),
241
findNodeAtLocation(contributesNode, ['views']),
242
findNodeAtLocation(contributesNode, ['viewsWelcome']),
243
findNodeAtLocation(contributesNode, ['keybindings']),
244
].forEach(n => findWhens(n, 'when'));
245
246
findWhens(findNodeAtLocation(contributesNode, ['commands']), 'enablement');
247
248
const parseResults = await commands.executeCommand<{ errorMessage: string; offset: number; length: number }[][]>('_validateWhenClauses', whenClauses.map(w => w.value as string /* we make sure to capture only if `w.value` is string above */));
249
250
const diagnostics: Diagnostic[] = [];
251
for (let i = 0; i < parseResults.length; ++i) {
252
const whenClauseJSONNode = whenClauses[i];
253
254
const jsonStringScanner = new JsonStringScanner(document.getText(), whenClauseJSONNode.offset + 1);
255
256
for (const error of parseResults[i]) {
257
const realOffset = jsonStringScanner.getOffsetInEncoded(error.offset);
258
const realOffsetEnd = jsonStringScanner.getOffsetInEncoded(error.offset + error.length);
259
const start = document.positionAt(realOffset /* +1 to account for the quote (I think) */);
260
const end = document.positionAt(realOffsetEnd);
261
const errMsg = `${parsingErrorHeader}\n\n${error.errorMessage}`;
262
const diagnostic = new Diagnostic(new Range(start, end), errMsg, DiagnosticSeverity.Error);
263
diagnostic.code = {
264
value: 'See docs',
265
target: Uri.parse('https://code.visualstudio.com/api/references/when-clause-contexts'),
266
};
267
diagnostics.push(diagnostic);
268
}
269
}
270
return diagnostics;
271
}
272
273
private async lintReadme() {
274
for (const document of this.readmeQ) {
275
this.readmeQ.delete(document);
276
if (document.isClosed) {
277
continue;
278
}
279
280
const folder = this.getUriFolder(document.uri);
281
let info = this.folderToPackageJsonInfo[folder.toString()];
282
if (!info) {
283
const tree = await this.loadPackageJson(folder);
284
info = this.readPackageJsonInfo(folder, tree);
285
}
286
if (!info.isExtension) {
287
this.diagnosticsCollection.set(document.uri, []);
288
return;
289
}
290
291
const text = document.getText();
292
if (!this.markdownIt) {
293
this.markdownIt = new ((await import('markdown-it')).default);
294
}
295
const tokens = this.markdownIt.parse(text, {});
296
const tokensAndPositions: TokenAndPosition[] = (function toTokensAndPositions(this: ExtensionLinter, tokens: MarkdownItType.Token[], begin = 0, end = text.length): TokenAndPosition[] {
297
const tokensAndPositions = tokens.map<TokenAndPosition>(token => {
298
if (token.map) {
299
const tokenBegin = document.offsetAt(new Position(token.map[0], 0));
300
const tokenEnd = begin = document.offsetAt(new Position(token.map[1], 0));
301
return {
302
token,
303
begin: tokenBegin,
304
end: tokenEnd
305
};
306
}
307
const image = token.type === 'image' && this.locateToken(text, begin, end, token, token.attrGet('src'));
308
const other = image || this.locateToken(text, begin, end, token, token.content);
309
return other || {
310
token,
311
begin,
312
end: begin
313
};
314
});
315
return tokensAndPositions.concat(
316
...tokensAndPositions.filter(tnp => tnp.token.children && tnp.token.children.length)
317
.map(tnp => toTokensAndPositions.call(this, tnp.token.children, tnp.begin, tnp.end))
318
);
319
}).call(this, tokens);
320
321
const diagnostics: Diagnostic[] = [];
322
323
tokensAndPositions.filter(tnp => tnp.token.type === 'image' && tnp.token.attrGet('src'))
324
.map(inp => {
325
const src = inp.token.attrGet('src')!;
326
const begin = text.indexOf(src, inp.begin);
327
if (begin !== -1 && begin < inp.end) {
328
this.addDiagnostics(diagnostics, document, begin, begin + src.length, src, Context.MARKDOWN, info);
329
} else {
330
const content = inp.token.content;
331
const begin = text.indexOf(content, inp.begin);
332
if (begin !== -1 && begin < inp.end) {
333
this.addDiagnostics(diagnostics, document, begin, begin + content.length, src, Context.MARKDOWN, info);
334
}
335
}
336
});
337
338
let svgStart: Diagnostic;
339
for (const tnp of tokensAndPositions) {
340
if (tnp.token.type === 'text' && tnp.token.content) {
341
if (!this.parse5) {
342
this.parse5 = await import('parse5');
343
}
344
const parser = new this.parse5.SAXParser({ locationInfo: true });
345
parser.on('startTag', (name, attrs, _selfClosing, location) => {
346
if (name === 'img') {
347
const src = attrs.find(a => a.name === 'src');
348
if (src && src.value && location) {
349
const begin = text.indexOf(src.value, tnp.begin + location.startOffset);
350
if (begin !== -1 && begin < tnp.end) {
351
this.addDiagnostics(diagnostics, document, begin, begin + src.value.length, src.value, Context.MARKDOWN, info);
352
}
353
}
354
} else if (name === 'svg' && location) {
355
const begin = tnp.begin + location.startOffset;
356
const end = tnp.begin + location.endOffset;
357
const range = new Range(document.positionAt(begin), document.positionAt(end));
358
svgStart = new Diagnostic(range, embeddedSvgsNotValid, DiagnosticSeverity.Warning);
359
diagnostics.push(svgStart);
360
}
361
});
362
parser.on('endTag', (name, location) => {
363
if (name === 'svg' && svgStart && location) {
364
const end = tnp.begin + location.endOffset;
365
svgStart.range = new Range(svgStart.range.start, document.positionAt(end));
366
}
367
});
368
parser.write(tnp.token.content);
369
parser.end();
370
}
371
}
372
373
this.diagnosticsCollection.set(document.uri, diagnostics);
374
}
375
}
376
377
private locateToken(text: string, begin: number, end: number, token: MarkdownItType.Token, content: string | null) {
378
if (content) {
379
const tokenBegin = text.indexOf(content, begin);
380
if (tokenBegin !== -1) {
381
const tokenEnd = tokenBegin + content.length;
382
if (tokenEnd <= end) {
383
begin = tokenEnd;
384
return {
385
token,
386
begin: tokenBegin,
387
end: tokenEnd
388
};
389
}
390
}
391
}
392
return undefined;
393
}
394
395
private readPackageJsonInfo(folder: Uri, tree: JsonNode | undefined) {
396
const engine = tree && findNodeAtLocation(tree, ['engines', 'vscode']);
397
const parsedEngineVersion = engine?.type === 'string' ? normalizeVersion(parseVersion(engine.value)) : null;
398
const repo = tree && findNodeAtLocation(tree, ['repository', 'url']);
399
const uri = repo && parseUri(repo.value);
400
const activationEvents = tree && parseImplicitActivationEvents(tree);
401
402
const info: PackageJsonInfo = {
403
isExtension: !!(engine && engine.type === 'string'),
404
hasHttpsRepository: !!(repo && repo.type === 'string' && repo.value && uri && uri.scheme.toLowerCase() === 'https'),
405
repository: uri!,
406
implicitActivationEvents: activationEvents,
407
engineVersion: parsedEngineVersion
408
};
409
const str = folder.toString();
410
const oldInfo = this.folderToPackageJsonInfo[str];
411
if (oldInfo && (oldInfo.isExtension !== info.isExtension || oldInfo.hasHttpsRepository !== info.hasHttpsRepository)) {
412
this.packageJsonChanged(folder); // clears this.folderToPackageJsonInfo[str]
413
}
414
this.folderToPackageJsonInfo[str] = info;
415
return info;
416
}
417
418
private async loadPackageJson(folder: Uri) {
419
if (folder.scheme === 'git') { // #36236
420
return undefined;
421
}
422
const file = folder.with({ path: path.posix.join(folder.path, 'package.json') });
423
try {
424
const fileContents = await workspace.fs.readFile(file); // #174888
425
return parseTree(Buffer.from(fileContents).toString('utf-8'));
426
} catch (err) {
427
return undefined;
428
}
429
}
430
431
private packageJsonChanged(folder: Uri) {
432
delete this.folderToPackageJsonInfo[folder.toString()];
433
const str = folder.toString().toLowerCase();
434
workspace.textDocuments.filter(document => this.getUriFolder(document.uri).toString().toLowerCase() === str)
435
.forEach(document => this.queueReadme(document));
436
}
437
438
private getUriFolder(uri: Uri) {
439
return uri.with({ path: path.posix.dirname(uri.path) });
440
}
441
442
private addDiagnostics(diagnostics: Diagnostic[], document: TextDocument, begin: number, end: number, src: string, context: Context, info: PackageJsonInfo) {
443
const hasScheme = /^\w[\w\d+.-]*:/.test(src);
444
const uri = parseUri(src, info.repository ? info.repository.toString() : document.uri.toString());
445
if (!uri) {
446
return;
447
}
448
const scheme = uri.scheme.toLowerCase();
449
450
if (hasScheme && scheme !== 'https' && scheme !== 'data') {
451
const range = new Range(document.positionAt(begin), document.positionAt(end));
452
diagnostics.push(new Diagnostic(range, httpsRequired, DiagnosticSeverity.Warning));
453
}
454
455
if (hasScheme && scheme === 'data') {
456
const range = new Range(document.positionAt(begin), document.positionAt(end));
457
diagnostics.push(new Diagnostic(range, dataUrlsNotValid, DiagnosticSeverity.Warning));
458
}
459
460
if (!hasScheme && !info.hasHttpsRepository && context !== Context.ICON) {
461
const range = new Range(document.positionAt(begin), document.positionAt(end));
462
const message = (() => {
463
switch (context) {
464
case Context.BADGE: return relativeBadgeUrlRequiresHttpsRepository;
465
default: return relativeUrlRequiresHttpsRepository;
466
}
467
})();
468
diagnostics.push(new Diagnostic(range, message, DiagnosticSeverity.Warning));
469
}
470
471
if (uri.path.toLowerCase().endsWith('.svg') && !isTrustedSVGSource(uri)) {
472
const range = new Range(document.positionAt(begin), document.positionAt(end));
473
diagnostics.push(new Diagnostic(range, svgsNotValid, DiagnosticSeverity.Warning));
474
}
475
}
476
477
private clear(document: TextDocument) {
478
this.diagnosticsCollection.delete(document.uri);
479
this.packageJsonQ.delete(document);
480
}
481
482
public dispose() {
483
this.disposables.forEach(d => d.dispose());
484
this.disposables = [];
485
}
486
}
487
488
function parseUri(src: string, base?: string, retry: boolean = true): Uri | null {
489
try {
490
const url = new URL(src, base);
491
return Uri.parse(url.toString());
492
} catch (err) {
493
if (retry) {
494
return parseUri(encodeURI(src), base, false);
495
} else {
496
return null;
497
}
498
}
499
}
500
501
function parseImplicitActivationEvents(tree: JsonNode): Set<string> {
502
const activationEvents = new Set<string>();
503
504
// commands
505
const commands = findNodeAtLocation(tree, ['contributes', 'commands']);
506
commands?.children?.forEach(child => {
507
const command = findNodeAtLocation(child, ['command']);
508
if (command && command.type === 'string') {
509
activationEvents.add(`onCommand:${command.value}`);
510
}
511
});
512
513
// authenticationProviders
514
const authenticationProviders = findNodeAtLocation(tree, ['contributes', 'authentication']);
515
authenticationProviders?.children?.forEach(child => {
516
const id = findNodeAtLocation(child, ['id']);
517
if (id && id.type === 'string') {
518
activationEvents.add(`onAuthenticationRequest:${id.value}`);
519
}
520
});
521
522
// languages
523
const languageContributions = findNodeAtLocation(tree, ['contributes', 'languages']);
524
languageContributions?.children?.forEach(child => {
525
const id = findNodeAtLocation(child, ['id']);
526
const configuration = findNodeAtLocation(child, ['configuration']);
527
if (id && id.type === 'string' && configuration && configuration.type === 'string') {
528
activationEvents.add(`onLanguage:${id.value}`);
529
}
530
});
531
532
// customEditors
533
const customEditors = findNodeAtLocation(tree, ['contributes', 'customEditors']);
534
customEditors?.children?.forEach(child => {
535
const viewType = findNodeAtLocation(child, ['viewType']);
536
if (viewType && viewType.type === 'string') {
537
activationEvents.add(`onCustomEditor:${viewType.value}`);
538
}
539
});
540
541
// views
542
const viewContributions = findNodeAtLocation(tree, ['contributes', 'views']);
543
viewContributions?.children?.forEach(viewContribution => {
544
const views = viewContribution.children?.find((node) => node.type === 'array');
545
views?.children?.forEach(view => {
546
const id = findNodeAtLocation(view, ['id']);
547
if (id && id.type === 'string') {
548
activationEvents.add(`onView:${id.value}`);
549
}
550
});
551
});
552
553
// walkthroughs
554
const walkthroughs = findNodeAtLocation(tree, ['contributes', 'walkthroughs']);
555
walkthroughs?.children?.forEach(child => {
556
const id = findNodeAtLocation(child, ['id']);
557
if (id && id.type === 'string') {
558
activationEvents.add(`onWalkthrough:${id.value}`);
559
}
560
});
561
562
// notebookRenderers
563
const notebookRenderers = findNodeAtLocation(tree, ['contributes', 'notebookRenderer']);
564
notebookRenderers?.children?.forEach(child => {
565
const id = findNodeAtLocation(child, ['id']);
566
if (id && id.type === 'string') {
567
activationEvents.add(`onRenderer:${id.value}`);
568
}
569
});
570
571
// terminalProfiles
572
const terminalProfiles = findNodeAtLocation(tree, ['contributes', 'terminal', 'profiles']);
573
terminalProfiles?.children?.forEach(child => {
574
const id = findNodeAtLocation(child, ['id']);
575
if (id && id.type === 'string') {
576
activationEvents.add(`onTerminalProfile:${id.value}`);
577
}
578
});
579
580
// terminalQuickFixes
581
const terminalQuickFixes = findNodeAtLocation(tree, ['contributes', 'terminal', 'quickFixes']);
582
terminalQuickFixes?.children?.forEach(child => {
583
const id = findNodeAtLocation(child, ['id']);
584
if (id && id.type === 'string') {
585
activationEvents.add(`onTerminalQuickFixRequest:${id.value}`);
586
}
587
});
588
589
// tasks
590
const tasks = findNodeAtLocation(tree, ['contributes', 'taskDefinitions']);
591
tasks?.children?.forEach(child => {
592
const id = findNodeAtLocation(child, ['type']);
593
if (id && id.type === 'string') {
594
activationEvents.add(`onTaskType:${id.value}`);
595
}
596
});
597
598
return activationEvents;
599
}
600
601