mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 05:19:39 +00:00
Refactor extensionTipsService for better readability
This commit is contained in:
parent
21405d5eb7
commit
24b0f75b1e
|
@ -426,7 +426,6 @@ export interface IExtensionTipsService {
|
|||
getOtherRecommendations(): Promise<IExtensionRecommendation[]>;
|
||||
getWorkspaceRecommendations(): Promise<IExtensionRecommendation[]>;
|
||||
getKeymapRecommendations(): IExtensionRecommendation[];
|
||||
getKeywordsForExtension(extension: string): string[];
|
||||
toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void;
|
||||
getAllIgnoredRecommendations(): { global: string[], workspace: string[] };
|
||||
onRecommendationChange: Event<RecommendationChangeNotification>;
|
||||
|
|
|
@ -43,6 +43,7 @@ import { URI } from 'vs/base/common/uri';
|
|||
import { areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/workbench/parts/experiments/node/experimentService';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { getKeywordsForExtension } from 'vs/workbench/parts/extensions/electron-browser/extensionsUtils';
|
||||
|
||||
const milliSecondsInADay = 1000 * 60 * 60 * 24;
|
||||
const choiceNever = localize('neverShowAgain', "Don't Show Again");
|
||||
|
@ -160,24 +161,6 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
}));
|
||||
}
|
||||
|
||||
private fetchProactiveRecommendations(calledDuringStartup?: boolean): Promise<void> {
|
||||
let fetchPromise = Promise.resolve(null);
|
||||
if (!this.proactiveRecommendationsFetched) {
|
||||
this.proactiveRecommendationsFetched = true;
|
||||
|
||||
// Executable based recommendations carry out a lot of file stats, so run them after 10 secs
|
||||
// So that the startup is not affected
|
||||
|
||||
fetchPromise = new Promise((c, e) => {
|
||||
setTimeout(() => {
|
||||
Promise.all([this.fetchExecutableRecommendations(), this.fetchDynamicWorkspaceRecommendations()]).then(() => c(null));
|
||||
}, calledDuringStartup ? 10000 : 0);
|
||||
});
|
||||
|
||||
}
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
private isEnabled(): boolean {
|
||||
return this._galleryService.isEnabled() && !this.environmentService.extensionDevelopmentLocationURI;
|
||||
}
|
||||
|
@ -232,12 +215,48 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
};
|
||||
}
|
||||
|
||||
toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean) {
|
||||
const lowerId = extensionId.toLowerCase();
|
||||
if (shouldIgnore) {
|
||||
const reason = this.getAllRecommendationsWithReason()[lowerId];
|
||||
if (reason && reason.reasonId) {
|
||||
/* __GDPR__
|
||||
"extensionsRecommendations:ignoreRecommendation" : {
|
||||
"recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
|
||||
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('extensionsRecommendations:ignoreRecommendation', { id: extensionId, recommendationReason: reason.reasonId });
|
||||
}
|
||||
}
|
||||
|
||||
this._globallyIgnoredRecommendations = shouldIgnore ?
|
||||
distinct([...this._globallyIgnoredRecommendations, lowerId].map(id => id.toLowerCase())) :
|
||||
this._globallyIgnoredRecommendations.filter(id => id !== lowerId);
|
||||
|
||||
this.storageService.store('extensionsAssistant/ignored_recommendations', JSON.stringify(this._globallyIgnoredRecommendations), StorageScope.GLOBAL);
|
||||
this._allIgnoredRecommendations = distinct([...this._globallyIgnoredRecommendations, ...this._workspaceIgnoredRecommendations]);
|
||||
|
||||
this._onRecommendationChange.fire({ extensionId: extensionId, isRecommended: !shouldIgnore });
|
||||
}
|
||||
|
||||
getKeymapRecommendations(): IExtensionRecommendation[] {
|
||||
return (product.keymapExtensionTips || [])
|
||||
.filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId))
|
||||
.map(extensionId => (<IExtensionRecommendation>{ extensionId, sources: ['application'] }));
|
||||
}
|
||||
|
||||
//#region workspaceRecommendations
|
||||
|
||||
getWorkspaceRecommendations(): Promise<IExtensionRecommendation[]> {
|
||||
if (!this.isEnabled()) { return Promise.resolve([]); }
|
||||
return this.fetchWorkspaceRecommendations()
|
||||
.then(() => this._allWorkspaceRecommendedExtensions.filter(rec => this.isExtensionAllowedToBeRecommended(rec.extensionId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all extensions.json files, fetch workspace recommendations, filter out invalid and unwanted ones
|
||||
*/
|
||||
private fetchWorkspaceRecommendations(): Promise<void> {
|
||||
|
||||
if (!this.isEnabled) { return Promise.resolve(null); }
|
||||
|
@ -286,6 +305,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all extensions.json files, fetch workspace recommendations
|
||||
*/
|
||||
private fetchExtensionRecommendationContents(): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource }[]> {
|
||||
const workspace = this.contextService.getWorkspace();
|
||||
return Promise.all<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource }>([
|
||||
|
@ -294,6 +316,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
]).then(contents => coalesce(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the extensions.json file for given workspace and return the recommendations
|
||||
* @param workspace
|
||||
*/
|
||||
private resolveWorkspaceExtensionConfig(workspace: IWorkspace): Promise<IExtensionsConfigContent | null> {
|
||||
if (!workspace.configuration) {
|
||||
return Promise.resolve(null);
|
||||
|
@ -303,6 +329,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
.then(content => <IExtensionsConfigContent>(json.parse(content.value)['extensions']), err => null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the extensions.json files for given workspace folder and return the recommendations
|
||||
* @param workspaceFolder
|
||||
*/
|
||||
private resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise<IExtensionsConfigContent | null> {
|
||||
const extensionsJsonUri = workspaceFolder.toResource(paths.join('.vscode', 'extensions.json'));
|
||||
|
||||
|
@ -311,6 +341,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
.then(content => <IExtensionsConfigContent>json.parse(content.value), err => null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the extensions.json file contents using regex and querying the gallery
|
||||
* @param contents
|
||||
*/
|
||||
private async validateExtensions(contents: IExtensionsConfigContent[]): Promise<{ invalidExtensions: string[], message: string }> {
|
||||
const extensionsContent: IExtensionsConfigContent = {
|
||||
recommendations: distinct(flatten(contents.map(content => content.recommendations || []))),
|
||||
|
@ -359,10 +393,6 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
return { invalidExtensions, message };
|
||||
}
|
||||
|
||||
private isExtensionAllowedToBeRecommended(id: string): boolean {
|
||||
return this._allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1;
|
||||
}
|
||||
|
||||
private onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): void {
|
||||
if (event.added.length) {
|
||||
const oldWorkspaceRecommended = this._allWorkspaceRecommendedExtensions;
|
||||
|
@ -377,325 +407,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
this._dynamicWorkspaceRecommendations = [];
|
||||
}
|
||||
|
||||
getFileBasedRecommendations(): IExtensionRecommendation[] {
|
||||
return Object.keys(this._fileBasedRecommendations)
|
||||
.sort((a, b) => {
|
||||
if (this._fileBasedRecommendations[a].recommendedTime === this._fileBasedRecommendations[b].recommendedTime) {
|
||||
if (!product.extensionImportantTips || caseInsensitiveGet(product.extensionImportantTips, a)) {
|
||||
return -1;
|
||||
}
|
||||
if (caseInsensitiveGet(product.extensionImportantTips, b)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return this._fileBasedRecommendations[a].recommendedTime > this._fileBasedRecommendations[b].recommendedTime ? -1 : 1;
|
||||
})
|
||||
.filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId))
|
||||
.map(extensionId => (<IExtensionRecommendation>{ extensionId, sources: this._fileBasedRecommendations[extensionId].sources }));
|
||||
}
|
||||
|
||||
getOtherRecommendations(): Promise<IExtensionRecommendation[]> {
|
||||
return this.fetchProactiveRecommendations().then(() => {
|
||||
const others = distinct([
|
||||
...Object.keys(this._exeBasedRecommendations),
|
||||
...this._dynamicWorkspaceRecommendations,
|
||||
...Object.keys(this._experimentalRecommendations),
|
||||
]).filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId));
|
||||
shuffle(others, this.sessionSeed);
|
||||
return others.map(extensionId => {
|
||||
const sources: ExtensionRecommendationSource[] = [];
|
||||
if (this._exeBasedRecommendations[extensionId]) {
|
||||
sources.push('executable');
|
||||
}
|
||||
if (this._dynamicWorkspaceRecommendations.indexOf(extensionId) !== -1) {
|
||||
sources.push('dynamic');
|
||||
}
|
||||
return (<IExtensionRecommendation>{ extensionId, sources });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getKeymapRecommendations(): IExtensionRecommendation[] {
|
||||
return (product.keymapExtensionTips || [])
|
||||
.filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId))
|
||||
.map(extensionId => (<IExtensionRecommendation>{ extensionId, sources: ['application'] }));
|
||||
}
|
||||
|
||||
private fetchFileBasedRecommendations() {
|
||||
const extensionTips = product.extensionTips;
|
||||
if (!extensionTips) {
|
||||
return;
|
||||
}
|
||||
|
||||
// group ids by pattern, like {**/*.md} -> [ext.foo1, ext.bar2]
|
||||
this._availableRecommendations = Object.create(null);
|
||||
forEach(extensionTips, entry => {
|
||||
let { key: id, value: pattern } = entry;
|
||||
let ids = this._availableRecommendations[pattern];
|
||||
if (!ids) {
|
||||
this._availableRecommendations[pattern] = [id.toLowerCase()];
|
||||
} else {
|
||||
ids.push(id.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
forEach(product.extensionImportantTips, entry => {
|
||||
let { key: id, value } = entry;
|
||||
const { pattern } = value;
|
||||
let ids = this._availableRecommendations[pattern];
|
||||
if (!ids) {
|
||||
this._availableRecommendations[pattern] = [id.toLowerCase()];
|
||||
} else {
|
||||
ids.push(id.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
const allRecommendations: string[] = flatten((Object.keys(this._availableRecommendations).map(key => this._availableRecommendations[key])));
|
||||
|
||||
// retrieve ids of previous recommendations
|
||||
const storedRecommendationsJson = JSON.parse(this.storageService.get('extensionsAssistant/recommendations', StorageScope.GLOBAL, '[]'));
|
||||
|
||||
if (Array.isArray<string>(storedRecommendationsJson)) {
|
||||
for (let id of <string[]>storedRecommendationsJson) {
|
||||
if (allRecommendations.indexOf(id) > -1) {
|
||||
this._fileBasedRecommendations[id.toLowerCase()] = { recommendedTime: Date.now(), sources: ['cached'] };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const now = Date.now();
|
||||
forEach(storedRecommendationsJson, entry => {
|
||||
if (typeof entry.value === 'number') {
|
||||
const diff = (now - entry.value) / milliSecondsInADay;
|
||||
if (diff <= 7 && allRecommendations.indexOf(entry.key) > -1) {
|
||||
this._fileBasedRecommendations[entry.key.toLowerCase()] = { recommendedTime: entry.value, sources: ['cached'] };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getMimeTypes(path: string): Promise<string[]> {
|
||||
return Promise.resolve(this.extensionService.whenInstalledExtensionsRegistered().then(() => {
|
||||
return guessMimeTypes(path);
|
||||
}));
|
||||
}
|
||||
|
||||
private promptFiletypeBasedRecommendations(model: ITextModel): void {
|
||||
let hasSuggestion = false;
|
||||
|
||||
const uri = model.uri;
|
||||
if (!uri || !this.fileService.canHandleResource(uri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fileExtension = paths.extname(uri.path);
|
||||
if (fileExtension) {
|
||||
if (processedFileExtensions.indexOf(fileExtension) > -1) {
|
||||
return;
|
||||
}
|
||||
processedFileExtensions.push(fileExtension);
|
||||
}
|
||||
|
||||
// re-schedule this bit of the operation to be off
|
||||
// the critical path - in case glob-match is slow
|
||||
setImmediate(() => {
|
||||
|
||||
let recommendationsToSuggest: string[] = [];
|
||||
const now = Date.now();
|
||||
forEach(this._availableRecommendations, entry => {
|
||||
let { key: pattern, value: ids } = entry;
|
||||
if (match(pattern, uri.path)) {
|
||||
for (let id of ids) {
|
||||
if (caseInsensitiveGet(product.extensionImportantTips, id)) {
|
||||
recommendationsToSuggest.push(id);
|
||||
}
|
||||
const filedBasedRecommendation = this._fileBasedRecommendations[id.toLowerCase()] || { recommendedTime: now, sources: [] };
|
||||
filedBasedRecommendation.recommendedTime = now;
|
||||
if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) {
|
||||
filedBasedRecommendation.sources.push(uri);
|
||||
}
|
||||
this._fileBasedRecommendations[id.toLowerCase()] = filedBasedRecommendation;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.storageService.store(
|
||||
'extensionsAssistant/recommendations',
|
||||
JSON.stringify(Object.keys(this._fileBasedRecommendations).reduce((result, key) => { result[key] = this._fileBasedRecommendations[key].recommendedTime; return result; }, {})),
|
||||
StorageScope.GLOBAL
|
||||
);
|
||||
|
||||
const config = this.configurationService.getValue<IExtensionsConfiguration>(ConfigurationKey);
|
||||
if (config.ignoreRecommendations || config.showRecommendationsOnlyOnDemand) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importantRecommendationsIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/importantRecommendationsIgnore', StorageScope.GLOBAL, '[]'));
|
||||
recommendationsToSuggest = recommendationsToSuggest.filter(id => importantRecommendationsIgnoreList.indexOf(id) === -1 && this.isExtensionAllowedToBeRecommended(id));
|
||||
|
||||
const importantTipsPromise = recommendationsToSuggest.length === 0 ? Promise.resolve(null) : this.extensionWorkbenchService.queryLocal().then(local => {
|
||||
const localExtensions = local.map(e => e.id);
|
||||
recommendationsToSuggest = recommendationsToSuggest.filter(id => localExtensions.every(local => local !== id.toLowerCase()));
|
||||
if (!recommendationsToSuggest.length) {
|
||||
return;
|
||||
}
|
||||
const id = recommendationsToSuggest[0];
|
||||
const name = caseInsensitiveGet(product.extensionImportantTips, id)['name'];
|
||||
|
||||
// Indicates we have a suggested extension via the whitelist
|
||||
hasSuggestion = true;
|
||||
|
||||
let message = localize('reallyRecommended2', "The '{0}' extension is recommended for this file type.", name);
|
||||
// Temporary fix for the only extension pack we recommend. See https://github.com/Microsoft/vscode/issues/35364
|
||||
if (id === 'vscjava.vscode-java-pack') {
|
||||
message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", name);
|
||||
}
|
||||
|
||||
this.notificationService.prompt(Severity.Info, message,
|
||||
[{
|
||||
label: localize('install', 'Install'),
|
||||
run: () => {
|
||||
/* __GDPR__
|
||||
"extensionRecommendations:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'install', extensionId: name });
|
||||
this.instantiationService.createInstance(InstallRecommendedExtensionAction, id).run();
|
||||
}
|
||||
}, {
|
||||
label: localize('showRecommendations', "Show Recommendations"),
|
||||
run: () => {
|
||||
/* __GDPR__
|
||||
"extensionRecommendations:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'show', extensionId: name });
|
||||
|
||||
const recommendationsAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations"));
|
||||
recommendationsAction.run();
|
||||
recommendationsAction.dispose();
|
||||
}
|
||||
}, {
|
||||
label: choiceNever,
|
||||
isSecondary: true,
|
||||
run: () => {
|
||||
importantRecommendationsIgnoreList.push(id);
|
||||
this.storageService.store(
|
||||
'extensionsAssistant/importantRecommendationsIgnore',
|
||||
JSON.stringify(importantRecommendationsIgnoreList),
|
||||
StorageScope.GLOBAL
|
||||
);
|
||||
/* __GDPR__
|
||||
"extensionRecommendations:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: name });
|
||||
this.promptIgnoreExtensionRecommendations();
|
||||
}
|
||||
}],
|
||||
{
|
||||
sticky: true,
|
||||
onCancel: () => {
|
||||
/* __GDPR__
|
||||
"extensionRecommendations:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: name });
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const mimeTypesPromise = this.getMimeTypes(uri.fsPath);
|
||||
Promise.all([importantTipsPromise, mimeTypesPromise]).then(result => {
|
||||
|
||||
const fileExtensionSuggestionIgnoreList = <string[]>JSON.parse(this.storageService.get
|
||||
('extensionsAssistant/fileExtensionsSuggestionIgnore', StorageScope.GLOBAL, '[]'));
|
||||
const mimeTypes = result[1];
|
||||
|
||||
if (fileExtension) {
|
||||
fileExtension = fileExtension.substr(1); // Strip the dot
|
||||
}
|
||||
|
||||
if (hasSuggestion ||
|
||||
!fileExtension ||
|
||||
mimeTypes.length !== 1 ||
|
||||
mimeTypes[0] !== MIME_UNKNOWN ||
|
||||
fileExtensionSuggestionIgnoreList.indexOf(fileExtension) > -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keywords = this.getKeywordsForExtension(fileExtension);
|
||||
this._galleryService.query({ text: `tag:"__ext_${fileExtension}" ${keywords.map(tag => `tag:"${tag}"`)}` }).then(pager => {
|
||||
if (!pager || !pager.firstPage || !pager.firstPage.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notificationService.prompt(
|
||||
Severity.Info,
|
||||
localize('showLanguageExtensions', "The Marketplace has extensions that can help with '.{0}' files", fileExtension),
|
||||
[{
|
||||
label: searchMarketplace,
|
||||
run: () => {
|
||||
/* __GDPR__
|
||||
"fileExtensionSuggestion:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"fileExtension": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('fileExtensionSuggestion:popup', { userReaction: 'ok', fileExtension: fileExtension });
|
||||
this.viewletService.openViewlet('workbench.view.extensions', true)
|
||||
.then(viewlet => viewlet as IExtensionsViewlet)
|
||||
.then(viewlet => {
|
||||
viewlet.search(`ext:${fileExtension}`);
|
||||
viewlet.focus();
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: localize('dontShowAgainExtension', "Don't Show Again for '.{0}' files", fileExtension),
|
||||
run: () => {
|
||||
fileExtensionSuggestionIgnoreList.push(fileExtension);
|
||||
this.storageService.store(
|
||||
'extensionsAssistant/fileExtensionsSuggestionIgnore',
|
||||
JSON.stringify(fileExtensionSuggestionIgnoreList),
|
||||
StorageScope.GLOBAL
|
||||
);
|
||||
/* __GDPR__
|
||||
"fileExtensionSuggestion:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"fileExtension": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('fileExtensionSuggestion:popup', { userReaction: 'neverShowAgain', fileExtension: fileExtension });
|
||||
}
|
||||
}],
|
||||
{
|
||||
sticky: true,
|
||||
onCancel: () => {
|
||||
/* __GDPR__
|
||||
"fileExtensionSuggestion:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"fileExtension": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('fileExtensionSuggestion:popup', { userReaction: 'cancelled', fileExtension: fileExtension });
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to install workspace recommendations if there are any not already installed
|
||||
*/
|
||||
private promptWorkspaceRecommendations(): void {
|
||||
const storageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
|
||||
const config = this.configurationService.getValue<IExtensionsConfiguration>(ConfigurationKey);
|
||||
|
@ -784,20 +498,373 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
});
|
||||
}
|
||||
|
||||
private promptIgnoreExtensionRecommendations() {
|
||||
this.notificationService.prompt(
|
||||
Severity.Info,
|
||||
localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"),
|
||||
[{
|
||||
label: localize('ignoreAll', "Yes, Ignore All"),
|
||||
run: () => this.setIgnoreRecommendationsConfig(true)
|
||||
}, {
|
||||
label: localize('no', "No"),
|
||||
run: () => this.setIgnoreRecommendationsConfig(false)
|
||||
}]
|
||||
);
|
||||
//#endregion
|
||||
|
||||
//#region fileBasedRecommendations
|
||||
|
||||
getFileBasedRecommendations(): IExtensionRecommendation[] {
|
||||
return Object.keys(this._fileBasedRecommendations)
|
||||
.sort((a, b) => {
|
||||
if (this._fileBasedRecommendations[a].recommendedTime === this._fileBasedRecommendations[b].recommendedTime) {
|
||||
if (!product.extensionImportantTips || caseInsensitiveGet(product.extensionImportantTips, a)) {
|
||||
return -1;
|
||||
}
|
||||
if (caseInsensitiveGet(product.extensionImportantTips, b)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return this._fileBasedRecommendations[a].recommendedTime > this._fileBasedRecommendations[b].recommendedTime ? -1 : 1;
|
||||
})
|
||||
.filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId))
|
||||
.map(extensionId => (<IExtensionRecommendation>{ extensionId, sources: this._fileBasedRecommendations[extensionId].sources }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all file based recommendations from product.extensionTips
|
||||
* Retire existing recommendations if they are older than a week or are not part of product.extensionTips anymore
|
||||
*/
|
||||
private fetchFileBasedRecommendations() {
|
||||
const extensionTips = product.extensionTips;
|
||||
if (!extensionTips) {
|
||||
return;
|
||||
}
|
||||
|
||||
// group ids by pattern, like {**/*.md} -> [ext.foo1, ext.bar2]
|
||||
this._availableRecommendations = Object.create(null);
|
||||
forEach(extensionTips, entry => {
|
||||
let { key: id, value: pattern } = entry;
|
||||
let ids = this._availableRecommendations[pattern];
|
||||
if (!ids) {
|
||||
this._availableRecommendations[pattern] = [id.toLowerCase()];
|
||||
} else {
|
||||
ids.push(id.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
forEach(product.extensionImportantTips, entry => {
|
||||
let { key: id, value } = entry;
|
||||
const { pattern } = value;
|
||||
let ids = this._availableRecommendations[pattern];
|
||||
if (!ids) {
|
||||
this._availableRecommendations[pattern] = [id.toLowerCase()];
|
||||
} else {
|
||||
ids.push(id.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
const allRecommendations: string[] = flatten((Object.keys(this._availableRecommendations).map(key => this._availableRecommendations[key])));
|
||||
|
||||
// retrieve ids of previous recommendations
|
||||
const storedRecommendationsJson = JSON.parse(this.storageService.get('extensionsAssistant/recommendations', StorageScope.GLOBAL, '[]'));
|
||||
|
||||
if (Array.isArray<string>(storedRecommendationsJson)) {
|
||||
for (let id of <string[]>storedRecommendationsJson) {
|
||||
if (allRecommendations.indexOf(id) > -1) {
|
||||
this._fileBasedRecommendations[id.toLowerCase()] = { recommendedTime: Date.now(), sources: ['cached'] };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const now = Date.now();
|
||||
forEach(storedRecommendationsJson, entry => {
|
||||
if (typeof entry.value === 'number') {
|
||||
const diff = (now - entry.value) / milliSecondsInADay;
|
||||
if (diff <= 7 && allRecommendations.indexOf(entry.key) > -1) {
|
||||
this._fileBasedRecommendations[entry.key.toLowerCase()] = { recommendedTime: entry.value, sources: ['cached'] };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to either install the recommended extension for the file type in the current editor model
|
||||
* or prompt to search the marketplace if it has extensions that can support the file type
|
||||
* @param model
|
||||
*/
|
||||
private promptFiletypeBasedRecommendations(model: ITextModel): void {
|
||||
let hasSuggestion = false;
|
||||
|
||||
const uri = model.uri;
|
||||
if (!uri || !this.fileService.canHandleResource(uri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fileExtension = paths.extname(uri.path);
|
||||
if (fileExtension) {
|
||||
if (processedFileExtensions.indexOf(fileExtension) > -1) {
|
||||
return;
|
||||
}
|
||||
processedFileExtensions.push(fileExtension);
|
||||
}
|
||||
|
||||
// re-schedule this bit of the operation to be off
|
||||
// the critical path - in case glob-match is slow
|
||||
setImmediate(() => {
|
||||
|
||||
let recommendationsToSuggest: string[] = [];
|
||||
const now = Date.now();
|
||||
forEach(this._availableRecommendations, entry => {
|
||||
let { key: pattern, value: ids } = entry;
|
||||
if (match(pattern, uri.path)) {
|
||||
for (let id of ids) {
|
||||
if (caseInsensitiveGet(product.extensionImportantTips, id)) {
|
||||
recommendationsToSuggest.push(id);
|
||||
}
|
||||
const filedBasedRecommendation = this._fileBasedRecommendations[id.toLowerCase()] || { recommendedTime: now, sources: [] };
|
||||
filedBasedRecommendation.recommendedTime = now;
|
||||
if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) {
|
||||
filedBasedRecommendation.sources.push(uri);
|
||||
}
|
||||
this._fileBasedRecommendations[id.toLowerCase()] = filedBasedRecommendation;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.storageService.store(
|
||||
'extensionsAssistant/recommendations',
|
||||
JSON.stringify(Object.keys(this._fileBasedRecommendations).reduce((result, key) => { result[key] = this._fileBasedRecommendations[key].recommendedTime; return result; }, {})),
|
||||
StorageScope.GLOBAL
|
||||
);
|
||||
|
||||
const config = this.configurationService.getValue<IExtensionsConfiguration>(ConfigurationKey);
|
||||
if (config.ignoreRecommendations || config.showRecommendationsOnlyOnDemand) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importantRecommendationsIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/importantRecommendationsIgnore', StorageScope.GLOBAL, '[]'));
|
||||
recommendationsToSuggest = recommendationsToSuggest.filter(id => importantRecommendationsIgnoreList.indexOf(id) === -1 && this.isExtensionAllowedToBeRecommended(id));
|
||||
|
||||
const importantTipsPromise = recommendationsToSuggest.length === 0 ? Promise.resolve(null) : this.extensionWorkbenchService.queryLocal().then(local => {
|
||||
const localExtensions = local.map(e => e.id);
|
||||
recommendationsToSuggest = recommendationsToSuggest.filter(id => localExtensions.every(local => local !== id.toLowerCase()));
|
||||
if (!recommendationsToSuggest.length) {
|
||||
return;
|
||||
}
|
||||
const id = recommendationsToSuggest[0];
|
||||
const name = caseInsensitiveGet(product.extensionImportantTips, id)['name'];
|
||||
|
||||
// Indicates we have a suggested extension via the whitelist
|
||||
hasSuggestion = true;
|
||||
|
||||
let message = localize('reallyRecommended2', "The '{0}' extension is recommended for this file type.", name);
|
||||
// Temporary fix for the only extension pack we recommend. See https://github.com/Microsoft/vscode/issues/35364
|
||||
if (id === 'vscjava.vscode-java-pack') {
|
||||
message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", name);
|
||||
}
|
||||
|
||||
const setIgnoreRecommendationsConfig = (configVal: boolean) => {
|
||||
this.configurationService.updateValue('extensions.ignoreRecommendations', configVal, ConfigurationTarget.USER);
|
||||
if (configVal) {
|
||||
const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
|
||||
this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE);
|
||||
}
|
||||
};
|
||||
|
||||
this.notificationService.prompt(Severity.Info, message,
|
||||
[{
|
||||
label: localize('install', 'Install'),
|
||||
run: () => {
|
||||
/* __GDPR__
|
||||
"extensionRecommendations:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'install', extensionId: name });
|
||||
this.instantiationService.createInstance(InstallRecommendedExtensionAction, id).run();
|
||||
}
|
||||
}, {
|
||||
label: localize('showRecommendations', "Show Recommendations"),
|
||||
run: () => {
|
||||
/* __GDPR__
|
||||
"extensionRecommendations:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'show', extensionId: name });
|
||||
|
||||
const recommendationsAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations"));
|
||||
recommendationsAction.run();
|
||||
recommendationsAction.dispose();
|
||||
}
|
||||
}, {
|
||||
label: choiceNever,
|
||||
isSecondary: true,
|
||||
run: () => {
|
||||
importantRecommendationsIgnoreList.push(id);
|
||||
this.storageService.store(
|
||||
'extensionsAssistant/importantRecommendationsIgnore',
|
||||
JSON.stringify(importantRecommendationsIgnoreList),
|
||||
StorageScope.GLOBAL
|
||||
);
|
||||
/* __GDPR__
|
||||
"extensionRecommendations:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: name });
|
||||
this.notificationService.prompt(
|
||||
Severity.Info,
|
||||
localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"),
|
||||
[{
|
||||
label: localize('ignoreAll', "Yes, Ignore All"),
|
||||
run: () => setIgnoreRecommendationsConfig(true)
|
||||
}, {
|
||||
label: localize('no', "No"),
|
||||
run: () => setIgnoreRecommendationsConfig(false)
|
||||
}]
|
||||
);
|
||||
}
|
||||
}],
|
||||
{
|
||||
sticky: true,
|
||||
onCancel: () => {
|
||||
/* __GDPR__
|
||||
"extensionRecommendations:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: name });
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const mimeTypesPromise = this.extensionService.whenInstalledExtensionsRegistered()
|
||||
.then(() => {
|
||||
return guessMimeTypes(uri.fsPath);
|
||||
});
|
||||
|
||||
Promise.all([importantTipsPromise, mimeTypesPromise]).then(result => {
|
||||
|
||||
const fileExtensionSuggestionIgnoreList = <string[]>JSON.parse(this.storageService.get
|
||||
('extensionsAssistant/fileExtensionsSuggestionIgnore', StorageScope.GLOBAL, '[]'));
|
||||
const mimeTypes = result[1];
|
||||
|
||||
if (fileExtension) {
|
||||
fileExtension = fileExtension.substr(1); // Strip the dot
|
||||
}
|
||||
|
||||
if (hasSuggestion ||
|
||||
!fileExtension ||
|
||||
mimeTypes.length !== 1 ||
|
||||
mimeTypes[0] !== MIME_UNKNOWN ||
|
||||
fileExtensionSuggestionIgnoreList.indexOf(fileExtension) > -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keywords = getKeywordsForExtension(fileExtension);
|
||||
this._galleryService.query({ text: `tag:"__ext_${fileExtension}" ${keywords.map(tag => `tag:"${tag}"`)}` }).then(pager => {
|
||||
if (!pager || !pager.firstPage || !pager.firstPage.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notificationService.prompt(
|
||||
Severity.Info,
|
||||
localize('showLanguageExtensions', "The Marketplace has extensions that can help with '.{0}' files", fileExtension),
|
||||
[{
|
||||
label: searchMarketplace,
|
||||
run: () => {
|
||||
/* __GDPR__
|
||||
"fileExtensionSuggestion:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"fileExtension": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('fileExtensionSuggestion:popup', { userReaction: 'ok', fileExtension: fileExtension });
|
||||
this.viewletService.openViewlet('workbench.view.extensions', true)
|
||||
.then(viewlet => viewlet as IExtensionsViewlet)
|
||||
.then(viewlet => {
|
||||
viewlet.search(`ext:${fileExtension}`);
|
||||
viewlet.focus();
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: localize('dontShowAgainExtension', "Don't Show Again for '.{0}' files", fileExtension),
|
||||
run: () => {
|
||||
fileExtensionSuggestionIgnoreList.push(fileExtension);
|
||||
this.storageService.store(
|
||||
'extensionsAssistant/fileExtensionsSuggestionIgnore',
|
||||
JSON.stringify(fileExtensionSuggestionIgnoreList),
|
||||
StorageScope.GLOBAL
|
||||
);
|
||||
/* __GDPR__
|
||||
"fileExtensionSuggestion:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"fileExtension": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('fileExtensionSuggestion:popup', { userReaction: 'neverShowAgain', fileExtension: fileExtension });
|
||||
}
|
||||
}],
|
||||
{
|
||||
sticky: true,
|
||||
onCancel: () => {
|
||||
/* __GDPR__
|
||||
"fileExtensionSuggestion:popup" : {
|
||||
"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"fileExtension": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('fileExtensionSuggestion:popup', { userReaction: 'cancelled', fileExtension: fileExtension });
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region otherRecommendations
|
||||
|
||||
getOtherRecommendations(): Promise<IExtensionRecommendation[]> {
|
||||
return this.fetchProactiveRecommendations().then(() => {
|
||||
const others = distinct([
|
||||
...Object.keys(this._exeBasedRecommendations),
|
||||
...this._dynamicWorkspaceRecommendations,
|
||||
...Object.keys(this._experimentalRecommendations),
|
||||
]).filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId));
|
||||
shuffle(others, this.sessionSeed);
|
||||
return others.map(extensionId => {
|
||||
const sources: ExtensionRecommendationSource[] = [];
|
||||
if (this._exeBasedRecommendations[extensionId]) {
|
||||
sources.push('executable');
|
||||
}
|
||||
if (this._dynamicWorkspaceRecommendations.indexOf(extensionId) !== -1) {
|
||||
sources.push('dynamic');
|
||||
}
|
||||
return (<IExtensionRecommendation>{ extensionId, sources });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private fetchProactiveRecommendations(calledDuringStartup?: boolean): Promise<void> {
|
||||
let fetchPromise = Promise.resolve(null);
|
||||
if (!this.proactiveRecommendationsFetched) {
|
||||
this.proactiveRecommendationsFetched = true;
|
||||
|
||||
// Executable based recommendations carry out a lot of file stats, so run them after 10 secs
|
||||
// So that the startup is not affected
|
||||
|
||||
fetchPromise = new Promise((c, e) => {
|
||||
setTimeout(() => {
|
||||
Promise.all([this.fetchExecutableRecommendations(), this.fetchDynamicWorkspaceRecommendations()]).then(() => c(null));
|
||||
}, calledDuringStartup ? 10000 : 0);
|
||||
});
|
||||
|
||||
}
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* If user has any of the tools listed in product.exeBasedExtensionTips, fetch corresponding recommendations
|
||||
*/
|
||||
private fetchExecutableRecommendations(): Promise<any> {
|
||||
const homeDir = os.homedir();
|
||||
let foundExecutables: Set<string> = new Set<string>();
|
||||
|
@ -843,14 +910,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
private setIgnoreRecommendationsConfig(configVal: boolean) {
|
||||
this.configurationService.updateValue('extensions.ignoreRecommendations', configVal, ConfigurationTarget.USER);
|
||||
if (configVal) {
|
||||
const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
|
||||
this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch extensions used by others on the same workspace as recommendations from cache
|
||||
*/
|
||||
private fetchCachedDynamicWorkspaceRecommendations() {
|
||||
if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER) {
|
||||
return;
|
||||
|
@ -879,6 +941,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch extensions used by others on the same workspace as recommendations from recommendation service
|
||||
*/
|
||||
private fetchDynamicWorkspaceRecommendations(): Promise<void> {
|
||||
if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER
|
||||
|| !this.fileService.canHandleResource(this.contextService.getWorkspace().folders[0].uri)
|
||||
|
@ -930,6 +995,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch extension recommendations from currently running experiments
|
||||
*/
|
||||
private fetchExperimentalRecommendations() {
|
||||
this.experimentService.getExperimentsByType(ExperimentActionType.AddToRecommendations).then(experiments => {
|
||||
(experiments || []).forEach(experiment => {
|
||||
|
@ -942,34 +1010,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
|
|||
});
|
||||
}
|
||||
|
||||
getKeywordsForExtension(extension: string): string[] {
|
||||
const keywords = product.extensionKeywords || {};
|
||||
return keywords[extension] || [];
|
||||
}
|
||||
//#endregion
|
||||
|
||||
toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean) {
|
||||
const lowerId = extensionId.toLowerCase();
|
||||
if (shouldIgnore) {
|
||||
const reason = this.getAllRecommendationsWithReason()[lowerId];
|
||||
if (reason && reason.reasonId) {
|
||||
/* __GDPR__
|
||||
"extensionsRecommendations:ignoreRecommendation" : {
|
||||
"recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
|
||||
"extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this.telemetryService.publicLog('extensionsRecommendations:ignoreRecommendation', { id: extensionId, recommendationReason: reason.reasonId });
|
||||
}
|
||||
}
|
||||
|
||||
this._globallyIgnoredRecommendations = shouldIgnore ?
|
||||
distinct([...this._globallyIgnoredRecommendations, lowerId].map(id => id.toLowerCase())) :
|
||||
this._globallyIgnoredRecommendations.filter(id => id !== lowerId);
|
||||
|
||||
this.storageService.store('extensionsAssistant/ignored_recommendations', JSON.stringify(this._globallyIgnoredRecommendations), StorageScope.GLOBAL);
|
||||
this._allIgnoredRecommendations = distinct([...this._globallyIgnoredRecommendations, ...this._workspaceIgnoredRecommendations]);
|
||||
|
||||
this._onRecommendationChange.fire({ extensionId: extensionId, isRecommended: !shouldIgnore });
|
||||
private isExtensionAllowedToBeRecommended(id: string): boolean {
|
||||
return this._allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati
|
|||
import { areSameExtensions, adoptToGalleryExtensionId, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
|
||||
import { Severity, INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import product from 'vs/platform/node/product';
|
||||
|
||||
export interface IExtensionStatus {
|
||||
identifier: IExtensionIdentifier;
|
||||
|
@ -142,3 +143,8 @@ export function isKeymapExtension(tipsService: IExtensionTipsService, extension:
|
|||
function stripVersion(id: string): string {
|
||||
return getIdAndVersionFromLocalExtensionId(id).id;
|
||||
}
|
||||
|
||||
export function getKeywordsForExtension(extension: string): string[] {
|
||||
const keywords = product.extensionKeywords || {};
|
||||
return keywords[extension] || [];
|
||||
}
|
|
@ -40,6 +40,7 @@ import { alert } from 'vs/base/browser/ui/aria/aria';
|
|||
import { IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list';
|
||||
import { createErrorWithActions } from 'vs/base/common/errorsWithActions';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { getKeywordsForExtension } from 'vs/workbench/parts/extensions/electron-browser/extensionsUtils';
|
||||
|
||||
export class ExtensionsListView extends ViewletPanel {
|
||||
|
||||
|
@ -338,7 +339,7 @@ export class ExtensionsListView extends ViewletPanel {
|
|||
text = query.value.replace(extensionRegex, (m, ext) => {
|
||||
|
||||
// Get curated keywords
|
||||
const keywords = this.tipsService.getKeywordsForExtension(ext);
|
||||
const keywords = getKeywordsForExtension(ext);
|
||||
|
||||
// Get mode name
|
||||
const modeId = this.modeService.getModeIdByFilepathOrFirstLine(`.${ext}`);
|
||||
|
|
Loading…
Reference in a new issue