提交 5b33ba2a 编写于 作者: S Sandeep Somavarapu

Support workspace config based recommendations

上级 fa4c3ae9
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { endsWith } from 'vs/base/common/strings';
const SshProtocolMatcher = /^([^@:]+@)?([^:]+):/;
const SshUrlMatcher = /^([^@:]+@)?([^:]+):(.+)$/;
const AuthorityMatcher = /^([^@]+@)?([^:]+)(:\d+)?$/;
const SecondLevelDomainMatcher = /([^@:.]+\.[^@:.]+)(:\d+)?$/;
const RemoteMatcher = /^\s*url\s*=\s*(.+\S)\s*$/mg;
const AnyButDot = /[^.]/g;
export const SecondLevelDomainWhitelist = [
'github.com',
'bitbucket.org',
'visualstudio.com',
'gitlab.com',
'heroku.com',
'azurewebsites.net',
'ibm.com',
'amazon.com',
'amazonaws.com',
'cloudapp.net',
'rhcloud.com',
'google.com',
'azure.com'
];
function stripLowLevelDomains(domain: string): string | null {
const match = domain.match(SecondLevelDomainMatcher);
return match ? match[1] : null;
}
function extractDomain(url: string): string | null {
if (url.indexOf('://') === -1) {
const match = url.match(SshProtocolMatcher);
if (match) {
return stripLowLevelDomains(match[2]);
} else {
return null;
}
}
try {
const uri = URI.parse(url);
if (uri.authority) {
return stripLowLevelDomains(uri.authority);
}
} catch (e) {
// ignore invalid URIs
}
return null;
}
export function getDomainsOfRemotes(text: string, whitelist: string[]): string[] {
const domains = new Set<string>();
let match: RegExpExecArray | null;
while (match = RemoteMatcher.exec(text)) {
const domain = extractDomain(match[1]);
if (domain) {
domains.add(domain);
}
}
const whitemap = whitelist.reduce((map, key) => {
map[key] = true;
return map;
}, Object.create(null));
const elements: string[] = [];
domains.forEach(e => elements.push(e));
return elements
.map(key => whitemap[key] ? key : key.replace(AnyButDot, 'a'));
}
function stripPort(authority: string): string | null {
const match = authority.match(AuthorityMatcher);
return match ? match[2] : null;
}
function normalizeRemote(host: string | null, path: string, stripEndingDotGit: boolean): string | null {
if (host && path) {
if (stripEndingDotGit && endsWith(path, '.git')) {
path = path.substr(0, path.length - 4);
}
return (path.indexOf('/') === 0) ? `${host}${path}` : `${host}/${path}`;
}
return null;
}
function extractRemote(url: string, stripEndingDotGit: boolean): string | null {
if (url.indexOf('://') === -1) {
const match = url.match(SshUrlMatcher);
if (match) {
return normalizeRemote(match[2], match[3], stripEndingDotGit);
}
}
try {
const uri = URI.parse(url);
if (uri.authority) {
return normalizeRemote(stripPort(uri.authority), uri.path, stripEndingDotGit);
}
} catch (e) {
// ignore invalid URIs
}
return null;
}
export function getRemotes(text: string, stripEndingDotGit: boolean = false): string[] {
const remotes: string[] = [];
let match: RegExpExecArray | null;
while (match = RemoteMatcher.exec(text)) {
const remote = extractRemote(match[1], stripEndingDotGit);
if (remote) {
remotes.push(remote);
}
}
return remotes;
}
......@@ -222,6 +222,13 @@ export interface IGlobalExtensionEnablementService {
}
export type IConfigBasedExtensionTip = {
readonly extensionId: string,
readonly extensionName: string,
readonly isExtensionPack: boolean,
readonly configName: string,
readonly important: boolean,
};
export type IExecutableBasedExtensionTip = { extensionId: string } & Omit<Omit<IExeBasedExtensionTip, 'recommendations'>, 'important'>;
export type IWorkspaceTips = { readonly remoteSet: string[]; readonly recommendations: string[]; };
......@@ -229,6 +236,7 @@ export const IExtensionTipsService = createDecorator<IExtensionTipsService>('IEx
export interface IExtensionTipsService {
_serviceBrand: undefined;
getConfigBasedTips(folder: URI): Promise<IConfigBasedExtensionTip[]>;
getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]>;
getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]>;
getAllWorkspacesTips(): Promise<IWorkspaceTips[]>;
......
......@@ -142,6 +142,7 @@ export class ExtensionTipsChannel implements IServerChannel {
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'getConfigBasedTips': return this.service.getConfigBasedTips(URI.revive(args[0]));
case 'getImportantExecutableBasedTips': return this.service.getImportantExecutableBasedTips();
case 'getOtherExecutableBasedTips': return this.service.getOtherExecutableBasedTips();
case 'getAllWorkspacesTips': return this.service.getAllWorkspacesTips();
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IProductService, IConfigBasedExtensionTip as IRawConfigBasedExtensionTip } from 'vs/platform/product/common/productService';
import { IFileService } from 'vs/platform/files/common/files';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IExtensionTipsService, IExecutableBasedExtensionTip, IWorkspaceTips, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { forEach } from 'vs/base/common/collections';
import { IRequestService, asJson } from 'vs/platform/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ILogService } from 'vs/platform/log/common/log';
import { joinPath } from 'vs/base/common/resources';
import { getDomainsOfRemotes } from 'vs/platform/extensionManagement/common/configRemotes';
import { keys } from 'vs/base/common/map';
export class ExtensionTipsService implements IExtensionTipsService {
_serviceBrand: any;
private readonly allConfigBasedTips: Map<string, IRawConfigBasedExtensionTip> = new Map<string, IRawConfigBasedExtensionTip>();
constructor(
@IFileService protected readonly fileService: IFileService,
@IProductService private readonly productService: IProductService,
@IRequestService private readonly requestService: IRequestService,
@ILogService private readonly logService: ILogService,
) {
if (this.productService.configBasedExtensionTips) {
forEach(this.productService.configBasedExtensionTips, ({ value }) => this.allConfigBasedTips.set(value.configPath, value));
}
}
getConfigBasedTips(folder: URI): Promise<IConfigBasedExtensionTip[]> {
return this.getValidConfigBasedTips(folder);
}
getAllWorkspacesTips(): Promise<IWorkspaceTips[]> {
return this.fetchWorkspacesTips();
}
async getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return [];
}
async getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return [];
}
private async getValidConfigBasedTips(folder: URI): Promise<IConfigBasedExtensionTip[]> {
const result: IConfigBasedExtensionTip[] = [];
for (const [configPath, tip] of this.allConfigBasedTips) {
try {
const content = await this.fileService.readFile(joinPath(folder, configPath));
const recommendationByRemote: Map<string, IConfigBasedExtensionTip> = new Map<string, IConfigBasedExtensionTip>();
forEach(tip.recommendations, ({ key, value }) => {
if (isNonEmptyArray(value.remotes)) {
for (const remote of value.remotes) {
recommendationByRemote.set(remote, {
extensionId: key,
extensionName: value.name,
configName: tip.configName,
important: !!value.important,
isExtensionPack: !!value.isExtensionPack
});
}
} else {
result.push({
extensionId: key,
extensionName: value.name,
configName: tip.configName,
important: !!value.important,
isExtensionPack: !!value.isExtensionPack
});
}
});
const remotes = getDomainsOfRemotes(content.value.toString(), keys(recommendationByRemote));
remotes.forEach(remote => result.push(recommendationByRemote.get(remote)!));
} catch (error) { /* Ignore */ }
}
return result;
}
private async fetchWorkspacesTips(): Promise<IWorkspaceTips[]> {
if (!this.productService.extensionsGallery?.recommendationsUrl) {
return [];
}
try {
const context = await this.requestService.request({ type: 'GET', url: this.productService.extensionsGallery?.recommendationsUrl }, CancellationToken.None);
if (context.res.statusCode !== 200) {
return [];
}
const result = await asJson<{ workspaceRecommendations?: IWorkspaceTips[] }>(context);
if (!result) {
return [];
}
return result.workspaceRecommendations || [];
} catch (error) {
this.logService.error(error);
return [];
}
}
}
......@@ -12,13 +12,13 @@ import { INativeEnvironmentService } from 'vs/platform/environment/node/environm
import { IFileService } from 'vs/platform/files/common/files';
import { isWindows } from 'vs/base/common/platform';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IExtensionTipsService, IExecutableBasedExtensionTip, IWorkspaceTips } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExecutableBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IStringDictionary, forEach } from 'vs/base/common/collections';
import { IRequestService, asJson } from 'vs/platform/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IRequestService } from 'vs/platform/request/common/request';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtensionTipsService as BaseExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionTipsService';
export class ExtensionTipsService implements IExtensionTipsService {
export class ExtensionTipsService extends BaseExtensionTipsService {
_serviceBrand: any;
......@@ -26,14 +26,15 @@ export class ExtensionTipsService implements IExtensionTipsService {
private readonly allOtherExecutableTips: IStringDictionary<IExeBasedExtensionTip> = {};
constructor(
@IFileService private readonly fileService: IFileService,
@IEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IProductService private readonly productService: IProductService,
@IRequestService private readonly requestService: IRequestService,
@ILogService private readonly logService: ILogService,
@IFileService fileService: IFileService,
@IProductService productService: IProductService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService,
) {
if (this.productService.exeBasedExtensionTips) {
forEach(this.productService.exeBasedExtensionTips, ({ key, value }) => {
super(fileService, productService, requestService, logService);
if (productService.exeBasedExtensionTips) {
forEach(productService.exeBasedExtensionTips, ({ key, value }) => {
if (value.important) {
this.allImportantExecutableTips[key] = value;
} else {
......@@ -43,10 +44,6 @@ export class ExtensionTipsService implements IExtensionTipsService {
}
}
getAllWorkspacesTips(): Promise<IWorkspaceTips[]> {
return this.fetchWorkspacesTips();
}
getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return this.getValidExecutableBasedExtensionTips(this.allImportantExecutableTips);
}
......@@ -99,24 +96,4 @@ export class ExtensionTipsService implements IExtensionTipsService {
return result;
}
private async fetchWorkspacesTips(): Promise<IWorkspaceTips[]> {
if (!this.productService.extensionsGallery?.recommendationsUrl) {
return [];
}
try {
const context = await this.requestService.request({ type: 'GET', url: this.productService.extensionsGallery?.recommendationsUrl }, CancellationToken.None);
if (context.res.statusCode !== 200) {
return [];
}
const result = await asJson<{ workspaceRecommendations?: IWorkspaceTips[] }>(context);
if (!result) {
return [];
}
return result.workspaceRecommendations || [];
} catch (error) {
this.logService.error(error);
return [];
}
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { getDomainsOfRemotes, getRemotes } from 'vs/platform/extensionManagement/common/configRemotes';
suite('Config Remotes', () => {
const whitelist = [
'github.com',
'github2.com',
'github3.com',
'example.com',
'example2.com',
'example3.com',
'server.org',
'server2.org',
];
test('HTTPS remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://github.com/Microsoft/vscode.git'), whitelist), ['github.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://git.example.com/gitproject.git'), whitelist), ['example.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://username@github2.com/username/repository.git'), whitelist), ['github2.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://username:password@github3.com/username/repository.git'), whitelist), ['github3.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://username:password@example2.com:1234/username/repository.git'), whitelist), ['example2.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://example3.com:1234/username/repository.git'), whitelist), ['example3.com']);
});
test('SSH remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('ssh://user@git.server.org/project.git'), whitelist), ['server.org']);
});
test('SCP-like remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('git@github.com:Microsoft/vscode.git'), whitelist), ['github.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('user@git.server.org:project.git'), whitelist), ['server.org']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('git.server2.org:project.git'), whitelist), ['server2.org']);
});
test('Local remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('/opt/git/project.git'), whitelist), []);
assert.deepStrictEqual(getDomainsOfRemotes(remote('file:///opt/git/project.git'), whitelist), []);
});
test('Multiple remotes', function () {
const config = ['https://github.com/Microsoft/vscode.git', 'https://git.example.com/gitproject.git'].map(remote).join('');
assert.deepStrictEqual(getDomainsOfRemotes(config, whitelist).sort(), ['example.com', 'github.com']);
});
test('Whitelisting', () => {
const config = ['https://github.com/Microsoft/vscode.git', 'https://git.foobar.com/gitproject.git'].map(remote).join('');
assert.deepStrictEqual(getDomainsOfRemotes(config, whitelist).sort(), ['aaaaaa.aaa', 'github.com']);
});
test('HTTPS remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('https://github.com/Microsoft/vscode.git')), ['github.com/Microsoft/vscode.git']);
assert.deepStrictEqual(getRemotes(remote('https://git.example.com/gitproject.git')), ['git.example.com/gitproject.git']);
assert.deepStrictEqual(getRemotes(remote('https://username@github2.com/username/repository.git')), ['github2.com/username/repository.git']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@github3.com/username/repository.git')), ['github3.com/username/repository.git']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@example2.com:1234/username/repository.git')), ['example2.com/username/repository.git']);
assert.deepStrictEqual(getRemotes(remote('https://example3.com:1234/username/repository.git')), ['example3.com/username/repository.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(remote('https://github.com/Microsoft/vscode.git'), true), ['github.com/Microsoft/vscode']);
assert.deepStrictEqual(getRemotes(remote('https://git.example.com/gitproject.git'), true), ['git.example.com/gitproject']);
assert.deepStrictEqual(getRemotes(remote('https://username@github2.com/username/repository.git'), true), ['github2.com/username/repository']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@github3.com/username/repository.git'), true), ['github3.com/username/repository']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@example2.com:1234/username/repository.git'), true), ['example2.com/username/repository']);
assert.deepStrictEqual(getRemotes(remote('https://example3.com:1234/username/repository.git'), true), ['example3.com/username/repository']);
// Compare Striped .git with no .git
assert.deepStrictEqual(getRemotes(remote('https://github.com/Microsoft/vscode.git'), true), getRemotes(remote('https://github.com/Microsoft/vscode')));
assert.deepStrictEqual(getRemotes(remote('https://git.example.com/gitproject.git'), true), getRemotes(remote('https://git.example.com/gitproject')));
assert.deepStrictEqual(getRemotes(remote('https://username@github2.com/username/repository.git'), true), getRemotes(remote('https://username@github2.com/username/repository')));
assert.deepStrictEqual(getRemotes(remote('https://username:password@github3.com/username/repository.git'), true), getRemotes(remote('https://username:password@github3.com/username/repository')));
assert.deepStrictEqual(getRemotes(remote('https://username:password@example2.com:1234/username/repository.git'), true), getRemotes(remote('https://username:password@example2.com:1234/username/repository')));
assert.deepStrictEqual(getRemotes(remote('https://example3.com:1234/username/repository.git'), true), getRemotes(remote('https://example3.com:1234/username/repository')));
});
test('SSH remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('ssh://user@git.server.org/project.git')), ['git.server.org/project.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(remote('ssh://user@git.server.org/project.git'), true), ['git.server.org/project']);
// Compare Striped .git with no .git
assert.deepStrictEqual(getRemotes(remote('ssh://user@git.server.org/project.git'), true), getRemotes(remote('ssh://user@git.server.org/project')));
});
test('SCP-like remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('git@github.com:Microsoft/vscode.git')), ['github.com/Microsoft/vscode.git']);
assert.deepStrictEqual(getRemotes(remote('user@git.server.org:project.git')), ['git.server.org/project.git']);
assert.deepStrictEqual(getRemotes(remote('git.server2.org:project.git')), ['git.server2.org/project.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(remote('git@github.com:Microsoft/vscode.git'), true), ['github.com/Microsoft/vscode']);
assert.deepStrictEqual(getRemotes(remote('user@git.server.org:project.git'), true), ['git.server.org/project']);
assert.deepStrictEqual(getRemotes(remote('git.server2.org:project.git'), true), ['git.server2.org/project']);
// Compare Striped .git with no .git
assert.deepStrictEqual(getRemotes(remote('git@github.com:Microsoft/vscode.git'), true), getRemotes(remote('git@github.com:Microsoft/vscode')));
assert.deepStrictEqual(getRemotes(remote('user@git.server.org:project.git'), true), getRemotes(remote('user@git.server.org:project')));
assert.deepStrictEqual(getRemotes(remote('git.server2.org:project.git'), true), getRemotes(remote('git.server2.org:project')));
});
test('Local remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('/opt/git/project.git')), []);
assert.deepStrictEqual(getRemotes(remote('file:///opt/git/project.git')), []);
});
test('Multiple remotes to be hashed', function () {
const config = ['https://github.com/Microsoft/vscode.git', 'https://git.example.com/gitproject.git'].map(remote).join(' ');
assert.deepStrictEqual(getRemotes(config), ['github.com/Microsoft/vscode.git', 'git.example.com/gitproject.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(config, true), ['github.com/Microsoft/vscode', 'git.example.com/gitproject']);
// Compare Striped .git with no .git
const noDotGitConfig = ['https://github.com/Microsoft/vscode', 'https://git.example.com/gitproject'].map(remote).join(' ');
assert.deepStrictEqual(getRemotes(config, true), getRemotes(noDotGitConfig));
});
function remote(url: string): string {
return `[remote "origin"]
url = ${url}
fetch = +refs/heads/*:refs/remotes/origin/*
`;
}
});
......@@ -60,6 +60,7 @@ export interface IProductConfiguration {
readonly extensionTips?: { [id: string]: string; };
readonly extensionImportantTips?: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; };
readonly configBasedExtensionTips?: { [id: string]: IConfigBasedExtensionTip; };
readonly exeBasedExtensionTips?: { [id: string]: IExeBasedExtensionTip; };
readonly remoteExtensionTips?: { [remoteName: string]: IRemoteExtensionTip; };
readonly extensionKeywords?: { [extension: string]: readonly string[]; };
......@@ -119,6 +120,12 @@ export interface IProductConfiguration {
readonly 'configurationSync.store'?: ConfigurationSyncStore;
}
export interface IConfigBasedExtensionTip {
configPath: string;
configName: string;
recommendations: IStringDictionary<{ name: string, remotes?: string[], important?: boolean, isExtensionPack?: boolean }>;
}
export interface IExeBasedExtensionTip {
friendlyName: string;
windowsPath?: string;
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionTipsService, IExtensionManagementService, ILocalExtension, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { localize } from 'vs/nls';
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { distinct } from 'vs/base/common/arrays';
import { values } from 'vs/base/common/map';
export class ConfigBasedRecommendations extends ExtensionRecommendations {
private importantTips: IConfigBasedExtensionTip[] = [];
private otherTips: IConfigBasedExtensionTip[] = [];
private _recommendations: ExtensionRecommendation[] = [];
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
}
protected async doActivate(): Promise<void> {
await this.fetch();
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e)));
this.promptWorkspaceRecommendations();
}
private async fetch(): Promise<void> {
const workspace = this.workspaceContextService.getWorkspace();
const importantTips: Map<string, IConfigBasedExtensionTip> = new Map<string, IConfigBasedExtensionTip>();
const otherTips: Map<string, IConfigBasedExtensionTip> = new Map<string, IConfigBasedExtensionTip>();
for (const folder of workspace.folders) {
const configBasedTips = await this.extensionTipsService.getConfigBasedTips(folder.uri);
for (const tip of configBasedTips) {
if (tip.important) {
importantTips.set(tip.extensionId, tip);
} else {
otherTips.set(tip.extensionId, tip);
}
}
}
this.importantTips = values(importantTips);
this.otherTips = values(otherTips).filter(tip => !importantTips.has(tip.extensionId));
this._recommendations = [...this.importantTips, ...this.otherTips].map(tip => this.toExtensionRecommendation(tip));
}
private async promptWorkspaceRecommendations(): Promise<void> {
if (this.hasToIgnoreRecommendationNotifications()) {
return;
}
if (this.importantTips.length === 0) {
return;
}
const local = await this.extensionManagementService.getInstalled(ExtensionType.User);
const { uninstalled } = this.groupByInstalled(distinct(this.importantTips.map(({ extensionId }) => extensionId)), local);
if (uninstalled.length === 0) {
return;
}
const importantExtensions = this.filterIgnoredOrNotAllowed(uninstalled);
if (importantExtensions.length === 0) {
return;
}
for (const extension of importantExtensions) {
const tip = this.importantTips.filter(tip => tip.extensionId === extension)[0];
const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended for this workspace.", tip.extensionName)
: localize('extensionRecommended', "The '{0}' extension is recommended for this workspace.", tip.extensionName);
this.promptImportantExtensionInstallNotification(extension, message);
}
}
private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } {
const installed: string[] = [], uninstalled: string[] = [];
const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
recommendationsToSuggest.forEach(id => {
if (installedExtensionsIds.has(id.toLowerCase())) {
installed.push(id);
} else {
uninstalled.push(id);
}
});
return { installed, uninstalled };
}
private async onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): Promise<void> {
if (event.added.length) {
const oldImportantRecommended = this.importantTips;
await this.fetch();
// Suggest only if at least one of the newly added recommendations was not suggested before
if (this.importantTips.some(current => oldImportantRecommended.every(old => current.extensionId !== old.extensionId))) {
return this.promptWorkspaceRecommendations();
}
}
}
private toExtensionRecommendation(tip: IConfigBasedExtensionTip): ExtensionRecommendation {
return {
extensionId: tip.extensionId,
source: 'config',
reason: {
reasonId: ExtensionRecommendationReason.WorkspaceConfig,
reasonText: localize('exeBasedRecommendation', "This extension is recommended because the workspace is configured with.", tip.configName)
}
};
}
}
......@@ -15,8 +15,6 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { basename } from 'vs/base/common/path';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ShowRecommendationsOnlyOnDemandKey } from 'vs/workbench/contrib/extensions/common/extensions';
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
......@@ -34,7 +32,6 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@ILifecycleService lifecycleService: ILifecycleService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
......@@ -49,10 +46,6 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
Also fetch important exe based recommendations for reporting telemetry
*/
timeout(3000).then(() => this.fetchAndPromptImportantExeBasedRecommendations());
if (!this.configurationService.getValue<boolean>(ShowRecommendationsOnlyOnDemandKey)) {
lifecycleService.when(LifecyclePhase.Eventually).then(() => this.activate());
}
}
protected async doActivate(): Promise<void> {
......
......@@ -24,6 +24,7 @@ import { FileBasedRecommendations } from 'vs/workbench/contrib/extensions/browse
import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/keymapRecommendations';
import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations';
type IgnoreRecommendationClassification = {
recommendationReason: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
......@@ -40,6 +41,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
private readonly fileBasedRecommendations: FileBasedRecommendations;
private readonly workspaceRecommendations: WorkspaceRecommendations;
private readonly experimentalRecommendations: ExperimentalRecommendations;
private readonly configBasedRecommendations: ConfigBasedRecommendations;
private readonly exeBasedRecommendations: ExeBasedRecommendations;
private readonly dynamicWorkspaceRecommendations: DynamicWorkspaceRecommendations;
private readonly keymapRecommendations: KeymapRecommendations;
......@@ -72,6 +74,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations, isExtensionAllowedToBeRecommended);
this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations, isExtensionAllowedToBeRecommended);
this.experimentalRecommendations = instantiationService.createInstance(ExperimentalRecommendations, isExtensionAllowedToBeRecommended);
this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations, isExtensionAllowedToBeRecommended);
this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations, isExtensionAllowedToBeRecommended);
this.dynamicWorkspaceRecommendations = instantiationService.createInstance(DynamicWorkspaceRecommendations, isExtensionAllowedToBeRecommended);
this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations, isExtensionAllowedToBeRecommended);
......@@ -102,7 +105,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
}
private async activateProactiveRecommendations(): Promise<void> {
await Promise.all([this.dynamicWorkspaceRecommendations.activate(), this.exeBasedRecommendations.activate()]);
await Promise.all([this.dynamicWorkspaceRecommendations.activate(), this.exeBasedRecommendations.activate(), this.configBasedRecommendations.activate()]);
}
getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason, reasonText: string }; } {
......@@ -113,6 +116,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
const allRecommendations = [
...this.dynamicWorkspaceRecommendations.recommendations,
...this.configBasedRecommendations.recommendations,
...this.exeBasedRecommendations.recommendations,
...this.experimentalRecommendations.recommendations,
...this.fileBasedRecommendations.recommendations,
......@@ -129,10 +133,16 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
return output;
}
async getConfigBasedRecommendations(): Promise<IExtensionRecommendation[]> {
await this.configBasedRecommendations.activate();
return this.toExtensionRecommendations(this.configBasedRecommendations.recommendations);
}
async getOtherRecommendations(): Promise<IExtensionRecommendation[]> {
await this.activateProactiveRecommendations();
const recommendations = [
...this.configBasedRecommendations.recommendations,
...this.exeBasedRecommendations.recommendations,
...this.dynamicWorkspaceRecommendations.recommendations,
...this.experimentalRecommendations.recommendations
......
......@@ -553,12 +553,13 @@ export class ExtensionsListView extends ViewPane {
.then(result => result.filter(e => e.type === ExtensionType.User))
.then(local => {
const fileBasedRecommendations = this.tipsService.getFileBasedRecommendations();
const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations();
const othersPromise = this.tipsService.getOtherRecommendations();
const workspacePromise = this.tipsService.getWorkspaceRecommendations();
return Promise.all([othersPromise, workspacePromise])
.then(([others, workspaceRecommendations]) => {
const names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, others, workspaceRecommendations);
return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise])
.then(([others, workspaceRecommendations, configBasedRecommendations]) => {
const names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, configBasedRecommendations, others, workspaceRecommendations);
const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason();
/* __GDPR__
"extensionAllRecommendations:open" : {
......@@ -608,15 +609,17 @@ export class ExtensionsListView extends ViewPane {
.then(result => result.filter(e => e.type === ExtensionType.User))
.then(local => {
let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations();
const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations();
const othersPromise = this.tipsService.getOtherRecommendations();
const workspacePromise = this.tipsService.getWorkspaceRecommendations();
return Promise.all([othersPromise, workspacePromise])
.then(([others, workspaceRecommendations]) => {
return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise])
.then(([others, workspaceRecommendations, configBasedRecommendations]) => {
configBasedRecommendations = configBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId));
fileBasedRecommendations = fileBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId));
others = others.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId));
const names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, others, []);
const names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, configBasedRecommendations, others, []);
const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason();
/* __GDPR__
......@@ -649,28 +652,36 @@ export class ExtensionsListView extends ViewPane {
}
// Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions
private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, fileBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workspaceRecommendations: IExtensionRecommendation[]): string[] {
const totalCount = 8;
private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, fileBasedRecommendations: IExtensionRecommendation[], configBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workspaceRecommendations: IExtensionRecommendation[]): string[] {
const totalCount = 10;
workspaceRecommendations = workspaceRecommendations
.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
configBasedRecommendations = configBasedRecommendations
.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
fileBasedRecommendations = fileBasedRecommendations.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
otherRecommendations = otherRecommendations.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& fileBasedRecommendations.every(fileBasedRecommendation => fileBasedRecommendation.extensionId !== recommendation.extensionId)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
const otherCount = Math.min(2, otherRecommendations.length);
const fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workspaceRecommendations.length - otherCount);
const recommendations = workspaceRecommendations;
const fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workspaceRecommendations.length - configBasedRecommendations.length - otherCount);
const recommendations = [...workspaceRecommendations, ...configBasedRecommendations];
recommendations.push(...fileBasedRecommendations.splice(0, fileBasedCount));
recommendations.push(...otherRecommendations.splice(0, otherCount));
......
......@@ -233,7 +233,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
private async onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): Promise<void> {
if (event.added.length) {
const oldWorkspaceRecommended = this._recommendations;
await this.activate();
await this.fetch();
// Suggest only if at least one of the newly added recommendations was not suggested before
if (this._recommendations.some(current => oldWorkspaceRecommended.every(old => current.extensionId !== old.extensionId))) {
this.promptWorkspaceRecommendations();
......
......@@ -67,6 +67,7 @@ suite('ExtensionsListView Tests', () => {
const workspaceRecommendationA = aGalleryExtension('workspace-recommendation-A');
const workspaceRecommendationB = aGalleryExtension('workspace-recommendation-B');
const configBasedRecommendationA = aGalleryExtension('configbased-recommendation-A');
const fileBasedRecommendationA = aGalleryExtension('filebased-recommendation-A');
const fileBasedRecommendationB = aGalleryExtension('filebased-recommendation-B');
const otherRecommendationA = aGalleryExtension('other-recommendation-A');
......@@ -114,12 +115,18 @@ suite('ExtensionsListView Tests', () => {
reasons[fileBasedRecommendationA.identifier.id] = { reasonId: ExtensionRecommendationReason.File };
reasons[fileBasedRecommendationB.identifier.id] = { reasonId: ExtensionRecommendationReason.File };
reasons[otherRecommendationA.identifier.id] = { reasonId: ExtensionRecommendationReason.Executable };
reasons[configBasedRecommendationA.identifier.id] = { reasonId: ExtensionRecommendationReason.WorkspaceConfig };
instantiationService.stub(IExtensionRecommendationsService, <Partial<IExtensionRecommendationsService>>{
getWorkspaceRecommendations() {
return Promise.resolve([
{ extensionId: workspaceRecommendationA.identifier.id },
{ extensionId: workspaceRecommendationB.identifier.id }]);
},
getConfigBasedRecommendations() {
return Promise.resolve([
{ extensionId: configBasedRecommendationA.identifier.id }
]);
},
getFileBasedRecommendations() {
return [
{ extensionId: fileBasedRecommendationA.identifier.id },
......@@ -341,6 +348,7 @@ suite('ExtensionsListView Tests', () => {
test('Test @recommended query', () => {
const allRecommendedExtensions = [
configBasedRecommendationA,
fileBasedRecommendationA,
fileBasedRecommendationB,
otherRecommendationA
......@@ -365,6 +373,7 @@ suite('ExtensionsListView Tests', () => {
const allRecommendedExtensions = [
workspaceRecommendationA,
workspaceRecommendationB,
configBasedRecommendationA,
fileBasedRecommendationA,
fileBasedRecommendationB,
otherRecommendationA
......
......@@ -10,127 +10,13 @@ import { IFileService, IFileStat } from 'vs/platform/files/common/files';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { endsWith } from 'vs/base/common/strings';
import { ITextFileService, } from 'vs/workbench/services/textfile/common/textfiles';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { IWorkspaceTagsService, Tags } from 'vs/workbench/contrib/tags/common/workspaceTags';
import { IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics';
import { IRequestService } from 'vs/platform/request/common/request';
import { isWindows } from 'vs/base/common/platform';
const SshProtocolMatcher = /^([^@:]+@)?([^:]+):/;
const SshUrlMatcher = /^([^@:]+@)?([^:]+):(.+)$/;
const AuthorityMatcher = /^([^@]+@)?([^:]+)(:\d+)?$/;
const SecondLevelDomainMatcher = /([^@:.]+\.[^@:.]+)(:\d+)?$/;
const RemoteMatcher = /^\s*url\s*=\s*(.+\S)\s*$/mg;
const AnyButDot = /[^.]/g;
const SecondLevelDomainWhitelist = [
'github.com',
'bitbucket.org',
'visualstudio.com',
'gitlab.com',
'heroku.com',
'azurewebsites.net',
'ibm.com',
'amazon.com',
'amazonaws.com',
'cloudapp.net',
'rhcloud.com',
'google.com',
'azure.com'
];
function stripLowLevelDomains(domain: string): string | null {
const match = domain.match(SecondLevelDomainMatcher);
return match ? match[1] : null;
}
function extractDomain(url: string): string | null {
if (url.indexOf('://') === -1) {
const match = url.match(SshProtocolMatcher);
if (match) {
return stripLowLevelDomains(match[2]);
} else {
return null;
}
}
try {
const uri = URI.parse(url);
if (uri.authority) {
return stripLowLevelDomains(uri.authority);
}
} catch (e) {
// ignore invalid URIs
}
return null;
}
export function getDomainsOfRemotes(text: string, whitelist: string[]): string[] {
const domains = new Set<string>();
let match: RegExpExecArray | null;
while (match = RemoteMatcher.exec(text)) {
const domain = extractDomain(match[1]);
if (domain) {
domains.add(domain);
}
}
const whitemap = whitelist.reduce((map, key) => {
map[key] = true;
return map;
}, Object.create(null));
const elements: string[] = [];
domains.forEach(e => elements.push(e));
return elements
.map(key => whitemap[key] ? key : key.replace(AnyButDot, 'a'));
}
function stripPort(authority: string): string | null {
const match = authority.match(AuthorityMatcher);
return match ? match[2] : null;
}
function normalizeRemote(host: string | null, path: string, stripEndingDotGit: boolean): string | null {
if (host && path) {
if (stripEndingDotGit && endsWith(path, '.git')) {
path = path.substr(0, path.length - 4);
}
return (path.indexOf('/') === 0) ? `${host}${path}` : `${host}/${path}`;
}
return null;
}
function extractRemote(url: string, stripEndingDotGit: boolean): string | null {
if (url.indexOf('://') === -1) {
const match = url.match(SshUrlMatcher);
if (match) {
return normalizeRemote(match[2], match[3], stripEndingDotGit);
}
}
try {
const uri = URI.parse(url);
if (uri.authority) {
return normalizeRemote(stripPort(uri.authority), uri.path, stripEndingDotGit);
}
} catch (e) {
// ignore invalid URIs
}
return null;
}
export function getRemotes(text: string, stripEndingDotGit: boolean = false): string[] {
const remotes: string[] = [];
let match: RegExpExecArray | null;
while (match = RemoteMatcher.exec(text)) {
const remote = extractRemote(match[1], stripEndingDotGit);
if (remote) {
remotes.push(remote);
}
}
return remotes;
}
import { getRemotes, SecondLevelDomainWhitelist, getDomainsOfRemotes } from 'vs/platform/extensionManagement/common/configRemotes';
export function getHashedRemotesFromConfig(text: string, stripEndingDotGit: boolean = false): string[] {
return getRemotes(text, stripEndingDotGit).map(r => {
......
......@@ -5,7 +5,7 @@
import * as assert from 'assert';
import * as crypto from 'crypto';
import { getDomainsOfRemotes, getRemotes, getHashedRemotesFromConfig } from 'vs/workbench/contrib/tags/electron-browser/workspaceTags';
import { getHashedRemotesFromConfig } from 'vs/workbench/contrib/tags/electron-browser/workspaceTags';
function hash(value: string): string {
return crypto.createHash('sha1').update(value.toString()).digest('hex');
......@@ -13,119 +13,6 @@ function hash(value: string): string {
suite('Telemetry - WorkspaceTags', () => {
const whitelist = [
'github.com',
'github2.com',
'github3.com',
'example.com',
'example2.com',
'example3.com',
'server.org',
'server2.org',
];
test('HTTPS remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://github.com/Microsoft/vscode.git'), whitelist), ['github.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://git.example.com/gitproject.git'), whitelist), ['example.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://username@github2.com/username/repository.git'), whitelist), ['github2.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://username:password@github3.com/username/repository.git'), whitelist), ['github3.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://username:password@example2.com:1234/username/repository.git'), whitelist), ['example2.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('https://example3.com:1234/username/repository.git'), whitelist), ['example3.com']);
});
test('SSH remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('ssh://user@git.server.org/project.git'), whitelist), ['server.org']);
});
test('SCP-like remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('git@github.com:Microsoft/vscode.git'), whitelist), ['github.com']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('user@git.server.org:project.git'), whitelist), ['server.org']);
assert.deepStrictEqual(getDomainsOfRemotes(remote('git.server2.org:project.git'), whitelist), ['server2.org']);
});
test('Local remotes', function () {
assert.deepStrictEqual(getDomainsOfRemotes(remote('/opt/git/project.git'), whitelist), []);
assert.deepStrictEqual(getDomainsOfRemotes(remote('file:///opt/git/project.git'), whitelist), []);
});
test('Multiple remotes', function () {
const config = ['https://github.com/Microsoft/vscode.git', 'https://git.example.com/gitproject.git'].map(remote).join('');
assert.deepStrictEqual(getDomainsOfRemotes(config, whitelist).sort(), ['example.com', 'github.com']);
});
test('Whitelisting', () => {
const config = ['https://github.com/Microsoft/vscode.git', 'https://git.foobar.com/gitproject.git'].map(remote).join('');
assert.deepStrictEqual(getDomainsOfRemotes(config, whitelist).sort(), ['aaaaaa.aaa', 'github.com']);
});
test('HTTPS remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('https://github.com/Microsoft/vscode.git')), ['github.com/Microsoft/vscode.git']);
assert.deepStrictEqual(getRemotes(remote('https://git.example.com/gitproject.git')), ['git.example.com/gitproject.git']);
assert.deepStrictEqual(getRemotes(remote('https://username@github2.com/username/repository.git')), ['github2.com/username/repository.git']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@github3.com/username/repository.git')), ['github3.com/username/repository.git']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@example2.com:1234/username/repository.git')), ['example2.com/username/repository.git']);
assert.deepStrictEqual(getRemotes(remote('https://example3.com:1234/username/repository.git')), ['example3.com/username/repository.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(remote('https://github.com/Microsoft/vscode.git'), true), ['github.com/Microsoft/vscode']);
assert.deepStrictEqual(getRemotes(remote('https://git.example.com/gitproject.git'), true), ['git.example.com/gitproject']);
assert.deepStrictEqual(getRemotes(remote('https://username@github2.com/username/repository.git'), true), ['github2.com/username/repository']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@github3.com/username/repository.git'), true), ['github3.com/username/repository']);
assert.deepStrictEqual(getRemotes(remote('https://username:password@example2.com:1234/username/repository.git'), true), ['example2.com/username/repository']);
assert.deepStrictEqual(getRemotes(remote('https://example3.com:1234/username/repository.git'), true), ['example3.com/username/repository']);
// Compare Striped .git with no .git
assert.deepStrictEqual(getRemotes(remote('https://github.com/Microsoft/vscode.git'), true), getRemotes(remote('https://github.com/Microsoft/vscode')));
assert.deepStrictEqual(getRemotes(remote('https://git.example.com/gitproject.git'), true), getRemotes(remote('https://git.example.com/gitproject')));
assert.deepStrictEqual(getRemotes(remote('https://username@github2.com/username/repository.git'), true), getRemotes(remote('https://username@github2.com/username/repository')));
assert.deepStrictEqual(getRemotes(remote('https://username:password@github3.com/username/repository.git'), true), getRemotes(remote('https://username:password@github3.com/username/repository')));
assert.deepStrictEqual(getRemotes(remote('https://username:password@example2.com:1234/username/repository.git'), true), getRemotes(remote('https://username:password@example2.com:1234/username/repository')));
assert.deepStrictEqual(getRemotes(remote('https://example3.com:1234/username/repository.git'), true), getRemotes(remote('https://example3.com:1234/username/repository')));
});
test('SSH remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('ssh://user@git.server.org/project.git')), ['git.server.org/project.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(remote('ssh://user@git.server.org/project.git'), true), ['git.server.org/project']);
// Compare Striped .git with no .git
assert.deepStrictEqual(getRemotes(remote('ssh://user@git.server.org/project.git'), true), getRemotes(remote('ssh://user@git.server.org/project')));
});
test('SCP-like remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('git@github.com:Microsoft/vscode.git')), ['github.com/Microsoft/vscode.git']);
assert.deepStrictEqual(getRemotes(remote('user@git.server.org:project.git')), ['git.server.org/project.git']);
assert.deepStrictEqual(getRemotes(remote('git.server2.org:project.git')), ['git.server2.org/project.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(remote('git@github.com:Microsoft/vscode.git'), true), ['github.com/Microsoft/vscode']);
assert.deepStrictEqual(getRemotes(remote('user@git.server.org:project.git'), true), ['git.server.org/project']);
assert.deepStrictEqual(getRemotes(remote('git.server2.org:project.git'), true), ['git.server2.org/project']);
// Compare Striped .git with no .git
assert.deepStrictEqual(getRemotes(remote('git@github.com:Microsoft/vscode.git'), true), getRemotes(remote('git@github.com:Microsoft/vscode')));
assert.deepStrictEqual(getRemotes(remote('user@git.server.org:project.git'), true), getRemotes(remote('user@git.server.org:project')));
assert.deepStrictEqual(getRemotes(remote('git.server2.org:project.git'), true), getRemotes(remote('git.server2.org:project')));
});
test('Local remotes to be hashed', function () {
assert.deepStrictEqual(getRemotes(remote('/opt/git/project.git')), []);
assert.deepStrictEqual(getRemotes(remote('file:///opt/git/project.git')), []);
});
test('Multiple remotes to be hashed', function () {
const config = ['https://github.com/Microsoft/vscode.git', 'https://git.example.com/gitproject.git'].map(remote).join(' ');
assert.deepStrictEqual(getRemotes(config), ['github.com/Microsoft/vscode.git', 'git.example.com/gitproject.git']);
// Strip .git
assert.deepStrictEqual(getRemotes(config, true), ['github.com/Microsoft/vscode', 'git.example.com/gitproject']);
// Compare Striped .git with no .git
const noDotGitConfig = ['https://github.com/Microsoft/vscode', 'https://git.example.com/gitproject'].map(remote).join(' ');
assert.deepStrictEqual(getRemotes(config, true), getRemotes(noDotGitConfig));
});
test('Single remote hashed', function () {
assert.deepStrictEqual(getHashedRemotesFromConfig(remote('https://username:password@github3.com/username/repository.git')), [hash('github3.com/username/repository.git')]);
assert.deepStrictEqual(getHashedRemotesFromConfig(remote('ssh://user@git.server.org/project.git')), [hash('git.server.org/project.git')]);
......
......@@ -85,11 +85,12 @@ export type RecommendationChangeNotification = {
};
export type DynamicRecommendation = 'dynamic';
export type ConfigRecommendation = 'config';
export type ExecutableRecommendation = 'executable';
export type CachedRecommendation = 'cached';
export type ApplicationRecommendation = 'application';
export type ExperimentalRecommendation = 'experimental';
export type ExtensionRecommendationSource = IWorkspace | IWorkspaceFolder | URI | DynamicRecommendation | ExecutableRecommendation | CachedRecommendation | ApplicationRecommendation | ExperimentalRecommendation;
export type ExtensionRecommendationSource = IWorkspace | IWorkspaceFolder | URI | DynamicRecommendation | ExecutableRecommendation | CachedRecommendation | ApplicationRecommendation | ExperimentalRecommendation | ConfigRecommendation;
export interface IExtensionRecommendation {
extensionId: string;
......@@ -100,6 +101,7 @@ export const enum ExtensionRecommendationReason {
Workspace,
File,
Executable,
WorkspaceConfig,
DynamicWorkspace,
Experimental,
Application,
......@@ -117,6 +119,7 @@ export interface IExtensionRecommendationsService {
getAllRecommendationsWithReason(): IStringDictionary<IExtensionRecommendationReson>;
getFileBasedRecommendations(): IExtensionRecommendation[];
getConfigBasedRecommendations(): Promise<IExtensionRecommendation[]>;
getOtherRecommendations(): Promise<IExtensionRecommendation[]>;
getWorkspaceRecommendations(): Promise<IExtensionRecommendation[]>;
getKeymapRecommendations(): IExtensionRecommendation[];
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IExtensionTipsService, IExecutableBasedExtensionTip, IWorkspaceTips } from 'vs/platform/extensionManagement/common/extensionManagement';
class WebExtensionTipsService implements IExtensionTipsService {
_serviceBrand: any;
constructor() { }
async getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return [];
}
async getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return [];
}
async getAllWorkspacesTips(): Promise<IWorkspaceTips[]> {
return [];
}
}
registerSingleton(IExtensionTipsService, WebExtensionTipsService);
......@@ -6,7 +6,8 @@
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IExtensionTipsService, IExecutableBasedExtensionTip, IWorkspaceTips } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionTipsService, IExecutableBasedExtensionTip, IWorkspaceTips, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { URI } from 'vs/base/common/uri';
class NativeExtensionTipsService implements IExtensionTipsService {
......@@ -20,6 +21,10 @@ class NativeExtensionTipsService implements IExtensionTipsService {
this.channel = sharedProcessService.getChannel('extensionTipsService');
}
getConfigBasedTips(folder: URI): Promise<IConfigBasedExtensionTip[]> {
return this.channel.call<IConfigBasedExtensionTip[]>('getConfigBasedTips', [folder]);
}
getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return this.channel.call<IExecutableBasedExtensionTip[]>('getImportantExecutableBasedTips');
}
......
......@@ -34,7 +34,6 @@ import 'vs/workbench/services/textfile/browser/browserTextFileService';
import 'vs/workbench/services/keybinding/browser/keymapService';
import 'vs/workbench/services/extensions/browser/extensionService';
import 'vs/workbench/services/extensionManagement/common/extensionManagementServerService';
import 'vs/workbench/services/extensionManagement/common/extensionTipsService';
import 'vs/workbench/services/telemetry/browser/telemetryService';
import 'vs/workbench/services/configurationResolver/browser/configurationResolverService';
import 'vs/workbench/services/credentials/browser/credentialsService';
......@@ -58,7 +57,8 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { ContextMenuService } from 'vs/platform/contextview/browser/contextMenuService';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionTipsService';
import { ExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { TunnelService } from 'vs/workbench/services/remote/common/tunnelService';
......@@ -92,6 +92,7 @@ registerSingleton(IAuthenticationTokenService, AuthenticationTokenService);
registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService);
registerSingleton(IUserDataSyncService, UserDataSyncService);
registerSingleton(ITitleService, TitlebarPart);
registerSingleton(IExtensionTipsService, ExtensionTipsService);
//#endregion
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册