diff --git a/che-theia-init-sources.yml b/che-theia-init-sources.yml index 7cf74f1152..ba90365cad 100644 --- a/che-theia-init-sources.yml +++ b/che-theia-init-sources.yml @@ -32,4 +32,5 @@ sources: - plugins/ssh-plugin - plugins/telemetry-plugin - plugins/github-auth-plugin + - plugins/recommendations-plugin checkoutTo: master diff --git a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-workspace-main.ts b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-workspace-main.ts index e3aba26118..5089fa609d 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/browser/che-workspace-main.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/browser/che-workspace-main.ts @@ -8,12 +8,13 @@ * SPDX-License-Identifier: EPL-2.0 ***********************************************************************/ +import { WorkspaceService, WorkspaceSettings } from '@eclipse-che/theia-remote-api/lib/common/workspace-service'; + import { CheWorkspaceMain } from '../common/che-protocol'; import { ConfirmDialog } from '@theia/core/lib/browser'; import { MessageService } from '@theia/core'; import { RestartWorkspaceOptions } from '@eclipse-che/plugin'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; -import { WorkspaceService } from '@eclipse-che/theia-remote-api/lib/common/workspace-service'; import { che as cheApi } from '@eclipse-che/api'; import { interfaces } from 'inversify'; @@ -49,6 +50,9 @@ export class CheWorkspaceMainImpl implements CheWorkspaceMain { } ); } + async $getSettings(): Promise { + return this.workspaceService.getWorkspaceSettings(); + } async $update(workspaceId: string, workspace: cheApi.workspace.Workspace): Promise { return await this.workspaceService.updateWorkspace(workspaceId, workspace); diff --git a/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts b/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts index 8c39b6991d..285e354dae 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/common/che-protocol.ts @@ -38,7 +38,7 @@ export interface CheWorkspaceMain { // start(workspaceId: string, environmentName: string): Promise; // startTemporary(config: WorkspaceConfig): Promise; // stop(workspaceId: string): Promise; - // getSettings(): Promise; + $getSettings(): Promise; $restartWorkspace(machineToken: string, restartWorkspaceOptions?: che.RestartWorkspaceOptions): Promise; } diff --git a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-workspace.ts b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-workspace.ts index a4cb92045e..3ef3d68fa2 100644 --- a/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-workspace.ts +++ b/extensions/eclipse-che-theia-plugin-ext/src/plugin/che-workspace.ts @@ -23,7 +23,7 @@ export class CheWorkspaceImpl implements CheWorkspace { } getSettings(): Promise { - throw new Error('Method not implemented.'); + return this.workspaceMain.$getSettings(); } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/plugins/recommendations-plugin/.gitignore b/plugins/recommendations-plugin/.gitignore new file mode 100644 index 0000000000..2b8cac9847 --- /dev/null +++ b/plugins/recommendations-plugin/.gitignore @@ -0,0 +1,4 @@ +lib/ +node_modules/ +*.theia +coverage diff --git a/plugins/recommendations-plugin/README.md b/plugins/recommendations-plugin/README.md new file mode 100644 index 0000000000..1269de9a1f --- /dev/null +++ b/plugins/recommendations-plugin/README.md @@ -0,0 +1,2 @@ +# recommendations-plugin +recommendations-plugin providing recommendations for plug-ins to use. diff --git a/plugins/recommendations-plugin/__mocks__/@eclipse-che/plugin.ts b/plugins/recommendations-plugin/__mocks__/@eclipse-che/plugin.ts new file mode 100644 index 0000000000..ebe5c1fe64 --- /dev/null +++ b/plugins/recommendations-plugin/__mocks__/@eclipse-che/plugin.ts @@ -0,0 +1,20 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Mock of @theia/plugin module + * @author Florent Benoit + */ +const che: any = {}; +che.workspace = {}; +module.exports = che; diff --git a/plugins/recommendations-plugin/__mocks__/@theia/plugin.ts b/plugins/recommendations-plugin/__mocks__/@theia/plugin.ts new file mode 100644 index 0000000000..3357d2d3c2 --- /dev/null +++ b/plugins/recommendations-plugin/__mocks__/@theia/plugin.ts @@ -0,0 +1,34 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Mock of @theia/plugin module + * @author Florent Benoit + */ +const theia: any = {}; +const outputChannel = +{ + appendLine: jest.fn(), +} +theia.window = {}; +theia.window.createOutputChannel = jest.fn(); +theia.window.createOutputChannel.mockReturnValue(outputChannel); +theia.plugins = {}; +theia.plugins.all = []; +theia.window.showInformationMessage = jest.fn(); +theia.workspace = { + workspaceFolders: undefined, + onDidOpenTextDocument: jest.fn(), +}; +theia.plugins.getPlugin = jest.fn(); +module.exports = theia; diff --git a/plugins/recommendations-plugin/__mocks__/axios.ts b/plugins/recommendations-plugin/__mocks__/axios.ts new file mode 100644 index 0000000000..9a9017c416 --- /dev/null +++ b/plugins/recommendations-plugin/__mocks__/axios.ts @@ -0,0 +1,42 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +const axios: any = jest.createMockFromModule('axios'); + +// map between URL and content +const myContent: Map = new Map(); +const myErrors: Map = new Map(); + +function __setContent(url: string, content: string): void { + myContent.set(url, content); +} + +function __setError(url: string, error: any): void { + myErrors.set(url, error); +} + +function get(url: string): any { + if (myErrors.has(url)) { + throw myErrors.get(url); + } + + return Promise.resolve({ data: myContent.get(url) }); +} +function __clearMock(): void { + myContent.clear(); + myErrors.clear(); +} + +axios.get = jest.fn(get); +axios.__setContent = __setContent; +axios.__setError = __setError; +axios.__clearMock = __clearMock; +module.exports = axios; diff --git a/plugins/recommendations-plugin/__mocks__/globby.ts b/plugins/recommendations-plugin/__mocks__/globby.ts new file mode 100644 index 0000000000..60382612d6 --- /dev/null +++ b/plugins/recommendations-plugin/__mocks__/globby.ts @@ -0,0 +1,41 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +const globby: any = jest.requireActual('globby'); + +let customError: any | undefined = undefined; +let multipleEnd = false; + +function __setStreamError(error: string): void { + customError = error; +} + +function __setStreamEnd(): void { + multipleEnd = true; +} + +globby.__setStreamError = __setStreamError; +globby.__setStreamEnd = __setStreamEnd; + +const originalStream = globby.stream; +globby.stream = (pattern: any, options?: any) => { + const result = originalStream(pattern, options); + if (customError) { + result.emit('error', customError); + } + if (multipleEnd) { + result.emit('end'); + } + return result; +}; + +module.exports = globby; diff --git a/plugins/recommendations-plugin/package.json b/plugins/recommendations-plugin/package.json new file mode 100644 index 0000000000..bab3669d2f --- /dev/null +++ b/plugins/recommendations-plugin/package.json @@ -0,0 +1,86 @@ +{ + "name": "@eclipse-che/recommendations-plugin", + "publisher": "Eclipse-Che", + "keywords": [ + "theia-plugin" + ], + "version": "0.0.1", + "license": "EPL-2.0", + "files": [ + "src" + ], + "extensionDependencies": [ + "Eclipse Che.@eclipse-che/workspace-plugin" + ], + "dependencies": { + "@eclipse-che/plugin": "0.0.1", + "@theia/plugin": "next", + "axios": "^0.21.0", + "globby": "^11.0.1", + "inversify": "^5.0.1", + "kind-of": "^6.0.3", + "reflect-metadata": "^0.1.13" + }, + "devDependencies": { + "@types/jest": "^26", + "@theia/plugin-packager": "latest", + "@types/fs-extra": "^9.0.3", + "eslint-plugin-header": "^3.1.0", + "ts-jest": "26.4.3", + "jest": "^26.6.3", + "prettier": "^2.1.2", + "prettier-plugin-import-sort": "^0.0.6" + }, + "activationEvents": [ + "*" + ], + "scripts": { + "prepare": "yarn run clean && yarn run build && yarn test", + "clean": "rimraf lib", + "watch": "tsc -watch", + "format": "if-env SKIP_FORMAT=true && echo 'skip format check' || prettier --check '{src,tests}/**/*.ts' package.json", + "format:fix": "prettier --write '{src,tests}/**/*.ts' package.json", + "lint": "if-env SKIP_LINT=true && echo 'skip lint check' || eslint --cache=true --no-error-on-unmatched-pattern=true '{src,tests}/**/*.ts'", + "lint:fix": "eslint --fix --cache=true --no-error-on-unmatched-pattern=true \"{src,tests}/**/*.{ts,tsx}\"", + "compile": "tsc", + "build": "concurrently -n \"format,lint,compile\" -c \"red,green,blue\" \"yarn format\" \"yarn lint\" \"yarn compile\" && theia-plugin pack", + "test": "jest --forceExit", + "test-watch": "jest --watchAll" + }, + "engines": { + "theiaPlugin": "next" + }, + "theiaPlugin": { + "backend": "lib/plugin.js" + }, + "jest": { + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.ts" + ], + "testEnvironment": "node", + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + }, + "coverageDirectory": "./coverage", + "transform": { + "^.+\\.tsx?$": "ts-jest" + }, + "modulePathIgnorePatterns": [ + "/lib" + ], + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json" + ] + } +} diff --git a/plugins/recommendations-plugin/src/analyzer/analyzer-module.ts b/plugins/recommendations-plugin/src/analyzer/analyzer-module.ts new file mode 100644 index 0000000000..55a5541bdf --- /dev/null +++ b/plugins/recommendations-plugin/src/analyzer/analyzer-module.ts @@ -0,0 +1,18 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { ContainerModule, interfaces } from 'inversify'; + +import { VSCodeCurrentExtensions } from './vscode-current-extensions'; + +const analyzerModule = new ContainerModule((bind: interfaces.Bind) => { + bind(VSCodeCurrentExtensions).toSelf().inSingletonScope(); +}); + +export { analyzerModule }; diff --git a/plugins/recommendations-plugin/src/analyzer/language-information.ts b/plugins/recommendations-plugin/src/analyzer/language-information.ts new file mode 100644 index 0000000000..898204d519 --- /dev/null +++ b/plugins/recommendations-plugin/src/analyzer/language-information.ts @@ -0,0 +1,15 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +export interface LanguageInformation { + id: string; + fileExtensions: string[]; + extensions: string[]; + workspaceContains: string[]; +} diff --git a/plugins/recommendations-plugin/src/analyzer/vscode-current-extensions.ts b/plugins/recommendations-plugin/src/analyzer/vscode-current-extensions.ts new file mode 100644 index 0000000000..3bf8155b23 --- /dev/null +++ b/plugins/recommendations-plugin/src/analyzer/vscode-current-extensions.ts @@ -0,0 +1,68 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import * as theia from '@theia/plugin'; + +import { LanguageInformation } from './language-information'; +import { VSCodeExtensionsInstalledLanguages as VSCodeCurrentExtensionsLanguages } from './vscode-extensions-installed-languages'; +import { injectable } from 'inversify'; + +/** + * Allow to grab information on VS Code extensions that are installed. + */ +@injectable() +export class VSCodeCurrentExtensions { + async analyze(): Promise { + // Map between file extension and language Id + const languagesByFileExtensions = new Map(); + + // Map between a language Id and the extension's Ids + const vscodeExtensionByLanguageId = new Map(); + + theia.plugins.all.forEach(plugin => { + // populate map between a file extension and the language Id + const contributes = plugin.packageJSON.contributes || { languages: [] }; + const languages: LanguageInformation[] = contributes.languages || []; + languages.forEach(language => { + const languageId = language.id; + if (languageId) { + const fileExtensions = language.extensions || []; + fileExtensions.forEach(fileExtension => { + let existingLanguageIds = languagesByFileExtensions.get(fileExtension); + if (!existingLanguageIds) { + existingLanguageIds = []; + languagesByFileExtensions.set(fileExtension, existingLanguageIds); + } + if (!existingLanguageIds.includes(languageId)) { + existingLanguageIds.push(languageId); + } + }); + } + }); + + // populate map between a language Id and a plug-in's Id + const activationEvents: string[] = plugin.packageJSON.activationEvents || []; + activationEvents.forEach(activationEvent => { + if (activationEvent.startsWith('onLanguage:')) { + const languageId = activationEvent.substring('onLanguage:'.length); + let existingPlugins = vscodeExtensionByLanguageId.get(languageId); + if (!existingPlugins) { + existingPlugins = []; + vscodeExtensionByLanguageId.set(languageId, existingPlugins); + } + if (!existingPlugins.includes(plugin.id)) { + existingPlugins.push(plugin.id); + } + } + }); + }); + + return { languagesByFileExtensions, vscodeExtensionByLanguageId }; + } +} diff --git a/plugins/recommendations-plugin/src/analyzer/vscode-extensions-installed-languages.ts b/plugins/recommendations-plugin/src/analyzer/vscode-extensions-installed-languages.ts new file mode 100644 index 0000000000..ebd8c31654 --- /dev/null +++ b/plugins/recommendations-plugin/src/analyzer/vscode-extensions-installed-languages.ts @@ -0,0 +1,20 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/** + * Represent all languages used by VS Code extensions/Theia plugins installed in the current instance. + */ +export interface VSCodeExtensionsInstalledLanguages { + // Map between file extension and language Id + languagesByFileExtensions: Map; + + // Map between a language Id and the VS Code extension Ids + vscodeExtensionByLanguageId: Map; +} diff --git a/plugins/recommendations-plugin/src/devfile/devfile-handler.ts b/plugins/recommendations-plugin/src/devfile/devfile-handler.ts new file mode 100644 index 0000000000..32dfac2caf --- /dev/null +++ b/plugins/recommendations-plugin/src/devfile/devfile-handler.ts @@ -0,0 +1,119 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import * as che from '@eclipse-che/plugin'; +import * as theia from '@theia/plugin'; + +import { che as cheApi } from '@eclipse-che/api'; +import { injectable } from 'inversify'; + +/** + * Manage access to the devfile + */ +@injectable() +export class DevfileHandler { + public static readonly DISABLED_RECOMMENDATIONS_PROPERTY = 'extensions.ignoreRecommendations'; + public static readonly OPENFILES_RECOMMENDATIONS_PROPERTY = 'extensions.openFileRecommendations'; + + async isRecommendedExtensionsDisabled(): Promise { + const cheWorkspace = await this.getWorkspace(); + // always has a devfile now + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const devfile = cheWorkspace.devfile!; + const attributes = devfile.attributes || {}; + const ignoreRecommendations = attributes[DevfileHandler.DISABLED_RECOMMENDATIONS_PROPERTY] || 'false'; + return ignoreRecommendations === 'true'; + } + + async isRecommendedExtensionsOpenFileEnabled(): Promise { + const cheWorkspace = await this.getWorkspace(); + // always has a devfile now + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const devfile = cheWorkspace.devfile!; + const attributes = devfile.attributes || {}; + const openFilesRecommendations = attributes[DevfileHandler.OPENFILES_RECOMMENDATIONS_PROPERTY] || 'false'; + return openFilesRecommendations === 'true'; + } + + async disableRecommendations(): Promise { + const workspace = await this.getWorkspace(); + // always an id + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const workspaceId = workspace.id!; + // always has a devfile now + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const devfile = workspace.devfile!; + const attributes = devfile.attributes || {}; + attributes[DevfileHandler.DISABLED_RECOMMENDATIONS_PROPERTY] = 'true'; + devfile.attributes = attributes; + await che.workspace.update(workspaceId, workspace); + } + + /** + * Check if there are chePlugins in the current devfile + */ + async hasPlugins(): Promise { + const plugins = await this.getPlugins(); + return plugins.length > 0; + } + + /** + * Grab all plugins of the devfile + */ + async getPlugins(): Promise { + const cheWorkspace = await this.getWorkspace(); + const devfile = cheWorkspace.devfile; + const devfilePlugins: string[] = []; + if (devfile && devfile.components) { + devfile.components.forEach(component => { + let id = component.id; + if (id && component.type === 'chePlugin') { + if (id.endsWith('/latest')) { + id = id.substring(0, id.length - '/latest'.length); + } + devfilePlugins.push(id); + } + }); + } + return devfilePlugins; + } + + async timeout(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Add che plug-ins to the current devfile + * Can throw an error when updating the workspace + */ + async addPlugins(pluginIds: string[]): Promise { + const workspace = await this.getWorkspace(); + // always an id + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const workspaceId = workspace.id!; + // always has a devfile now + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const devfile = workspace.devfile!; + + const components: cheApi.workspace.devfile.Component[] = devfile.components || []; + pluginIds.forEach(plugin => components.push({ id: `${plugin}/latest`, type: 'chePlugin' })); + // use the new components + devfile.components = components; + + // can throw an error + await che.workspace.update(workspaceId, workspace); + // retry the update few seconds after + this.timeout(2000); + await che.workspace.update(workspaceId, workspace); + } + + protected async getWorkspace(): Promise { + return che.workspace.getCurrentWorkspace(); + } +} diff --git a/plugins/recommendations-plugin/src/devfile/devfile-module.ts b/plugins/recommendations-plugin/src/devfile/devfile-module.ts new file mode 100644 index 0000000000..b61635042c --- /dev/null +++ b/plugins/recommendations-plugin/src/devfile/devfile-module.ts @@ -0,0 +1,18 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { ContainerModule, interfaces } from 'inversify'; + +import { DevfileHandler } from './devfile-handler'; + +const devfileModule = new ContainerModule((bind: interfaces.Bind) => { + bind(DevfileHandler).toSelf().inSingletonScope(); +}); + +export { devfileModule }; diff --git a/plugins/recommendations-plugin/src/fetch/featured-contribute-language.ts b/plugins/recommendations-plugin/src/fetch/featured-contribute-language.ts new file mode 100644 index 0000000000..4df6d96f90 --- /dev/null +++ b/plugins/recommendations-plugin/src/fetch/featured-contribute-language.ts @@ -0,0 +1,22 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/** + * Contribution language subset as defined from https://code.visualstudio.com/api/references/contribution-points#language-example + */ +export interface FeaturedContributeLanguage { + id: string; + aliases: string[]; + /** + * file extensions, for example ".py" + */ + extensions: string[]; + filenames: string[]; +} diff --git a/plugins/recommendations-plugin/src/fetch/featured-contributes.ts b/plugins/recommendations-plugin/src/fetch/featured-contributes.ts new file mode 100644 index 0000000000..09e24ab97d --- /dev/null +++ b/plugins/recommendations-plugin/src/fetch/featured-contributes.ts @@ -0,0 +1,14 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { FeaturedContributeLanguage } from './featured-contribute-language'; + +export interface FeaturedContributes { + languages: FeaturedContributeLanguage[]; +} diff --git a/plugins/recommendations-plugin/src/fetch/featured-fetcher.ts b/plugins/recommendations-plugin/src/fetch/featured-fetcher.ts new file mode 100644 index 0000000000..1b4132c816 --- /dev/null +++ b/plugins/recommendations-plugin/src/fetch/featured-fetcher.ts @@ -0,0 +1,37 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import * as theia from '@theia/plugin'; + +import { inject, injectable } from 'inversify'; + +import AxiosInstance from 'axios'; +import { ChePluginRegistry } from '../registry/che-plugin-registry'; +import { FeaturedPlugin } from './featured-plugin'; + +@injectable() +export class FeaturedFetcher { + @inject(ChePluginRegistry) + private chePluginRegistry: ChePluginRegistry; + + async fetch(): Promise { + const pluginRegistryUrl = await this.chePluginRegistry.getUrl(); + + let featuredList: FeaturedPlugin[] = []; + // need to fetch + try { + const response = await AxiosInstance.get(`${pluginRegistryUrl}/che-theia/featured.json`); + featuredList = response.data.featured; + } catch (error) { + featuredList = []; + theia.window.showInformationMessage(`Error while fetching featured recommendation ${error}`); + } + return featuredList; + } +} diff --git a/plugins/recommendations-plugin/src/fetch/featured-plugin.ts b/plugins/recommendations-plugin/src/fetch/featured-plugin.ts new file mode 100644 index 0000000000..27142fd246 --- /dev/null +++ b/plugins/recommendations-plugin/src/fetch/featured-plugin.ts @@ -0,0 +1,17 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { FeaturedContributes } from './featured-contributes'; + +export interface FeaturedPlugin { + id: string; + onLanguages?: string[]; + workspaceContains: string[]; + contributes: FeaturedContributes; +} diff --git a/plugins/recommendations-plugin/src/fetch/fetch-module.ts b/plugins/recommendations-plugin/src/fetch/fetch-module.ts new file mode 100644 index 0000000000..bcc469c0f0 --- /dev/null +++ b/plugins/recommendations-plugin/src/fetch/fetch-module.ts @@ -0,0 +1,20 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { ContainerModule, interfaces } from 'inversify'; + +import { FeaturedFetcher } from './featured-fetcher'; +import { PluginsByLanguageFetcher } from './plugins-by-language-fetcher'; + +const fetchModule = new ContainerModule((bind: interfaces.Bind) => { + bind(FeaturedFetcher).toSelf().inSingletonScope(); + bind(PluginsByLanguageFetcher).toSelf().inSingletonScope(); +}); + +export { fetchModule }; diff --git a/plugins/recommendations-plugin/src/fetch/language-plugins.ts b/plugins/recommendations-plugin/src/fetch/language-plugins.ts new file mode 100644 index 0000000000..6195b9d75e --- /dev/null +++ b/plugins/recommendations-plugin/src/fetch/language-plugins.ts @@ -0,0 +1,13 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +export interface LanguagePlugins { + category: string; + ids: string[]; +} diff --git a/plugins/recommendations-plugin/src/fetch/plugins-by-language-fetcher.ts b/plugins/recommendations-plugin/src/fetch/plugins-by-language-fetcher.ts new file mode 100644 index 0000000000..785e926b6a --- /dev/null +++ b/plugins/recommendations-plugin/src/fetch/plugins-by-language-fetcher.ts @@ -0,0 +1,40 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import * as theia from '@theia/plugin'; + +import { inject, injectable } from 'inversify'; + +import AxiosInstance from 'axios'; +import { ChePluginRegistry } from '../registry/che-plugin-registry'; +import { LanguagePlugins } from './language-plugins'; + +@injectable() +export class PluginsByLanguageFetcher { + @inject(ChePluginRegistry) + private chePluginRegistry: ChePluginRegistry; + + async fetch(languageId: string): Promise { + let languagePlugins: LanguagePlugins[] = []; + + const pluginRegistryUrl = await this.chePluginRegistry.getUrl(); + // need to fetch + try { + const response = await AxiosInstance.get( + `${pluginRegistryUrl}/che-theia/recommendations/language/${languageId}.json` + ); + languagePlugins = response.data; + } catch (error) { + if (error.response.status !== 404) { + theia.window.showInformationMessage(`Error while fetching featured recommendations ${error}`); + } + } + return languagePlugins; + } +} diff --git a/plugins/recommendations-plugin/src/find/find-file-extensions.ts b/plugins/recommendations-plugin/src/find/find-file-extensions.ts new file mode 100644 index 0000000000..808b714768 --- /dev/null +++ b/plugins/recommendations-plugin/src/find/find-file-extensions.ts @@ -0,0 +1,63 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import * as globby from 'globby'; +import * as theia from '@theia/plugin'; + +import { injectable } from 'inversify'; + +@injectable() +export class FindFileExtensions { + public static readonly DEFAULT_SCAN_TIME_PER_WORKSPACE_FOLDER: number = 3000; + + async find( + workspaceFolders: theia.WorkspaceFolder[], + timeout: number = FindFileExtensions.DEFAULT_SCAN_TIME_PER_WORKSPACE_FOLDER + ): Promise { + // get extensions for each theia workspace + const extensions: string[][] = await Promise.all( + workspaceFolders.map(workspaceFolder => this.findInFolder(workspaceFolder.uri.path, timeout)) + ); + return extensions.reduce((acc, e) => acc.concat(e), []); + } + + findInFolder(workspaceFolder: string, timeout: number): Promise { + // do not let timeout send the event a new time. + let alreadyStopped = false; + setTimeout(() => { + if (!alreadyStopped) { + stream.emit('end'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (stream as any).destroy(); + } + }, timeout); + + const fileExtensions: string[] = []; + const options: globby.GlobbyOptions = { + gitignore: true, + cwd: workspaceFolder, + }; + const stream = globby.stream('**/*', options); + stream.on('data', entry => { + const fileExtension = entry.slice(((entry.lastIndexOf('.') - 1) >>> 0) + 1); + if (fileExtension.length > 0 && !fileExtensions.includes(fileExtension)) { + fileExtensions.push(fileExtension); + } + }); + stream.on('end', () => { + alreadyStopped = true; + }); + + return new Promise(resolve => { + stream.on('end', () => { + resolve(fileExtensions); + }); + }); + } +} diff --git a/plugins/recommendations-plugin/src/find/find-module.ts b/plugins/recommendations-plugin/src/find/find-module.ts new file mode 100644 index 0000000000..aad47c9896 --- /dev/null +++ b/plugins/recommendations-plugin/src/find/find-module.ts @@ -0,0 +1,18 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { ContainerModule, interfaces } from 'inversify'; + +import { FindFileExtensions } from './find-file-extensions'; + +const findModule = new ContainerModule((bind: interfaces.Bind) => { + bind(FindFileExtensions).toSelf().inSingletonScope(); +}); + +export { findModule }; diff --git a/plugins/recommendations-plugin/src/inject/inversify-bindings.ts b/plugins/recommendations-plugin/src/inject/inversify-bindings.ts new file mode 100644 index 0000000000..81b7d5b736 --- /dev/null +++ b/plugins/recommendations-plugin/src/inject/inversify-bindings.ts @@ -0,0 +1,37 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { Container } from 'inversify'; +import { analyzerModule } from '../analyzer/analyzer-module'; +import { devfileModule } from '../devfile/devfile-module'; +import { featuredModule } from '../strategy/featured-module'; +import { fetchModule } from '../fetch/fetch-module'; +import { findModule } from '../find/find-module'; +import { pluginModule } from '../plugin/plugin-module'; +import { registryModule } from '../registry/registry-module'; +import { workspaceModule } from '../workspace/workspace-module'; + +export class InversifyBinding { + private container: Container; + + public initBindings(): Container { + this.container = new Container(); + + this.container.load(analyzerModule); + this.container.load(devfileModule); + this.container.load(featuredModule); + this.container.load(fetchModule); + this.container.load(registryModule); + this.container.load(findModule); + this.container.load(pluginModule); + this.container.load(workspaceModule); + + return this.container; + } +} diff --git a/plugins/recommendations-plugin/src/plugin.ts b/plugins/recommendations-plugin/src/plugin.ts new file mode 100644 index 0000000000..df1d37189f --- /dev/null +++ b/plugins/recommendations-plugin/src/plugin.ts @@ -0,0 +1,23 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +import 'reflect-metadata'; + +import { InversifyBinding } from './inject/inversify-bindings'; +import { RecommendationsPlugin } from './plugin/recommendations-plugin'; + +let recommendationsPlugin: RecommendationsPlugin; + +export function start(): void { + const inversifyBinding = new InversifyBinding(); + const container = inversifyBinding.initBindings(); + recommendationsPlugin = container.get(RecommendationsPlugin); + recommendationsPlugin.start(); +} diff --git a/plugins/recommendations-plugin/src/plugin/plugin-module.ts b/plugins/recommendations-plugin/src/plugin/plugin-module.ts new file mode 100644 index 0000000000..926718073c --- /dev/null +++ b/plugins/recommendations-plugin/src/plugin/plugin-module.ts @@ -0,0 +1,18 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { ContainerModule, interfaces } from 'inversify'; + +import { RecommendationsPlugin } from './recommendations-plugin'; + +const pluginModule = new ContainerModule((bind: interfaces.Bind) => { + bind(RecommendationsPlugin).toSelf().inSingletonScope(); +}); + +export { pluginModule }; diff --git a/plugins/recommendations-plugin/src/plugin/recommendations-plugin-analysis.ts b/plugins/recommendations-plugin/src/plugin/recommendations-plugin-analysis.ts new file mode 100644 index 0000000000..499e71e8ce --- /dev/null +++ b/plugins/recommendations-plugin/src/plugin/recommendations-plugin-analysis.ts @@ -0,0 +1,17 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { FeaturedPlugin } from '../fetch/featured-plugin'; +import { VSCodeExtensionsInstalledLanguages } from '../analyzer/vscode-extensions-installed-languages'; + +export interface RecommendationsPluginAnalysis { + featuredList: FeaturedPlugin[]; + vsCodeExtensionsInstalledLanguages: VSCodeExtensionsInstalledLanguages; + devfileHasPlugins: boolean; +} diff --git a/plugins/recommendations-plugin/src/plugin/recommendations-plugin.ts b/plugins/recommendations-plugin/src/plugin/recommendations-plugin.ts new file mode 100644 index 0000000000..de84dcd16d --- /dev/null +++ b/plugins/recommendations-plugin/src/plugin/recommendations-plugin.ts @@ -0,0 +1,196 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import * as theia from '@theia/plugin'; + +import { inject, injectable } from 'inversify'; + +import { Deferred } from '../util/deferred'; +import { DevfileHandler } from '../devfile/devfile-handler'; +import { FeaturedFetcher } from '../fetch/featured-fetcher'; +import { FeaturedPluginStrategy } from '../strategy/featured-plugin-strategy'; +import { FindFileExtensions } from '../find/find-file-extensions'; +import { RecommendPluginOpenFileStrategy } from '../strategy/recommend-plugin-open-file-strategy'; +import { RecommendationsPluginAnalysis } from './recommendations-plugin-analysis'; +import { VSCodeCurrentExtensions } from '../analyzer/vscode-current-extensions'; +import { WorkspaceHandler } from '../workspace/workspace-handler'; + +/** + * Plug-in that is suggesting or adding by default recommendations + * usecases: + * - empty workspaces: + * - after initial clone on empty workspaces + * - set by default if no existing plug-ins + * - suggest if existing plug-ins + * - check .vscode/extensions.json file + * - when opening new files + */ +@injectable() +export class RecommendationsPlugin { + @inject(FindFileExtensions) + private findFileExtensions: FindFileExtensions; + + @inject(FeaturedFetcher) + private featuredFecher: FeaturedFetcher; + + @inject(VSCodeCurrentExtensions) + private vsCodeCurrentPlugins: VSCodeCurrentExtensions; + + @inject(DevfileHandler) + private devfileHandler: DevfileHandler; + + @inject(WorkspaceHandler) + private workspaceHandler: WorkspaceHandler; + + @inject(FeaturedPluginStrategy) + private featuredPluginStrategy: FeaturedPluginStrategy; + + @inject(RecommendPluginOpenFileStrategy) + private recommendPluginOpenFileStrategy: RecommendPluginOpenFileStrategy; + + private deferredSetupPromise: Promise; + + private outputChannel: theia.OutputChannel; + + constructor() { + this.outputChannel = theia.window.createOutputChannel('Recommendations Plug-in'); + } + + async start(): Promise { + // if recommendations are disabled, stop there + const enabled = !(await this.devfileHandler.isRecommendedExtensionsDisabled()); + if (enabled) { + this.outputChannel.appendLine('Enabling recommendations Plugin'); + return this.enableRecommendationsPlugin(); + } else { + this.outputChannel.appendLine('Recommendations Plugin is disabled'); + } + } + + async enableRecommendationsPlugin(): Promise { + // Bring featured recommendations after projects are cloned + const workspacePlugin = theia.plugins.getPlugin('Eclipse Che.@eclipse-che/workspace-plugin'); + if (workspacePlugin && workspacePlugin.exports && workspacePlugin.exports.onDidCloneSources) { + workspacePlugin.exports.onDidCloneSources(() => this.afterClone()); + } + + // Perform tasks in parallel + const deferredSetup = new Deferred(); + this.deferredSetupPromise = deferredSetup.promise; + + // enable the recommendation on file being opened if no plug-in is matching this file extension + if (this.devfileHandler.isRecommendedExtensionsOpenFileEnabled()) { + this.enableRecommendationsPluginWhenOpeningFiles(); + } + + // fetch all featured plug-ins from plug-in registry. + const featuredListPromise = this.featuredFecher.fetch(); + // grab all plug-ins and languages + const vsCodeCurrentPluginsPromise = this.vsCodeCurrentPlugins.analyze(); + // Grab plug-ins used in the devfile + const devfileHasPluginsPromise = this.devfileHandler.hasPlugins(); + + // wait that promises are resolved before resolving the defered + const [featuredList, vsCodeCurrentPluginsLanguages, devfileHasPlugins] = await Promise.all([ + featuredListPromise, + vsCodeCurrentPluginsPromise, + devfileHasPluginsPromise, + ]); + + this.outputChannel.appendLine('featuredList=' + JSON.stringify(featuredList, undefined, 2)); + this.outputChannel.appendLine( + 'vsCodeCurrentPluginsLanguages.languagesByFileExtensions=' + + JSON.stringify(Array.from(vsCodeCurrentPluginsLanguages.languagesByFileExtensions.entries())) + ); + this.outputChannel.appendLine( + 'vsCodeCurrentPluginsLanguages.vscodeExtensionByLanguageId=' + + JSON.stringify(Array.from(vsCodeCurrentPluginsLanguages.vscodeExtensionByLanguageId.entries())) + ); + this.outputChannel.appendLine(`devfileHasPlugins=${devfileHasPlugins}`); + + deferredSetup.resolve({ + featuredList, + vsCodeExtensionsInstalledLanguages: vsCodeCurrentPluginsLanguages, + devfileHasPlugins, + }); + } + + // called after projects are cloned (like the first import) + async afterClone(): Promise { + // current workspaces + const workspaceFolders = theia.workspace.workspaceFolders || []; + + // Grab file extensions used in all projects being in the workspace folder (that have been cloned) (with a timeout) + const extensionsInCheWorkspace = await this.findFileExtensions.find(workspaceFolders); + this.outputChannel.appendLine(`extensionsInCheWorkspace=${extensionsInCheWorkspace}`); + + // need to wait all required tasks done when starting the plug-in are finished + const workspaceAnalysis = await this.deferredSetupPromise; + this.outputChannel.appendLine(`workspaceAnalysis=${JSON.stringify(workspaceAnalysis, undefined, 2)}`); + + // convert found file extensions to languages that should be enabled + const featuredPluginStategyRequest = { ...workspaceAnalysis, extensionsInCheWorkspace }; + let featuredPlugins = await this.featuredPluginStrategy.getFeaturedPlugins(featuredPluginStategyRequest); + this.outputChannel.appendLine(`featuredPlugins=${JSON.stringify(featuredPlugins, undefined, 2)}`); + + // filter out from featured Plugins the plug-ins already installed in the devfile + const inDevfilePlugins = await this.devfileHandler.getPlugins(); + this.outputChannel.appendLine(`inDevfilePlugins=${inDevfilePlugins}`); + + featuredPlugins = featuredPlugins.filter(plugin => !inDevfilePlugins.includes(plugin)); + this.outputChannel.appendLine(`filteredFeaturedPlugins=${featuredPlugins}`); + + // do we have plugins in the devfile ? + if (featuredPlugins.length === 0) { + this.outputChannel.appendLine('no featured plugins. exiting'); + return; + } + + // No devfile plug-ins, we add without asking and we prompt to restart the workspace + if (!workspaceAnalysis.devfileHasPlugins) { + this.outputChannel.appendLine('no devfile plug-ins. Install plug-ins'); + await this.installPlugins(featuredPlugins); + } else { + // users have existing plug-ins meaning that they probably started with a custom devfile, need to suggest and not add + this.outputChannel.appendLine('existing plug-ins, prompt user to confirm'); + const yesValue = 'Yes'; + const yesNoItems: theia.MessageItem[] = [{ title: yesValue }, { title: 'No' }]; + const msg = `Do you want to install the recommended extensions ${featuredPlugins} for your workspace ?`; + const installOrNotExtensions = await theia.window.showInformationMessage(msg, ...yesNoItems); + // only if yes we install extensions + if (installOrNotExtensions && installOrNotExtensions.title === yesValue) { + await this.installPlugins(featuredPlugins); + } + } + } + + // install given plug-ins + async installPlugins(featuredPlugins: string[]): Promise { + const uniquePlugins = [...new Set(featuredPlugins)]; + try { + // add plug-ins + await this.devfileHandler.addPlugins(uniquePlugins); + + // restart the workspace ? + await this.workspaceHandler.restart( + `New featured plug-ins ${uniquePlugins} have been added to your workspace to improve the intellisense. Please restart the workspace to see the changes.` + ); + } catch (error) { + theia.window.showInformationMessage('Unable to add featured plugins' + error); + } + } + + // display recommendation when opening files + async enableRecommendationsPluginWhenOpeningFiles(): Promise { + const workspaceAnalysis = await this.deferredSetupPromise; + theia.workspace.onDidOpenTextDocument(document => + this.recommendPluginOpenFileStrategy.onOpenFile(document, workspaceAnalysis) + ); + } +} diff --git a/plugins/recommendations-plugin/src/registry/che-plugin-registry.ts b/plugins/recommendations-plugin/src/registry/che-plugin-registry.ts new file mode 100644 index 0000000000..4acfcfd7b3 --- /dev/null +++ b/plugins/recommendations-plugin/src/registry/che-plugin-registry.ts @@ -0,0 +1,38 @@ +/********************************************************************** + * Copyright (c) 2020 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import * as che from '@eclipse-che/plugin'; + +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { injectable } from 'inversify'; + +/** + * Grab the Che Plugin Registry URL + */ +@injectable() +export class ChePluginRegistry { + private pluginRegistryUrl: String; + + async getUrl(): Promise { + if (!this.pluginRegistryUrl) { + const settings = await che.workspace.getSettings(); + this.pluginRegistryUrl = + settings['cheWorkspacePluginRegistryInternalUrl'] || settings['cheWorkspacePluginRegistryUrl']; + } + return this.pluginRegistryUrl; + } +} diff --git a/plugins/recommendations-plugin/src/registry/registry-module.ts b/plugins/recommendations-plugin/src/registry/registry-module.ts new file mode 100644 index 0000000000..7db19ecedf --- /dev/null +++ b/plugins/recommendations-plugin/src/registry/registry-module.ts @@ -0,0 +1,18 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { ContainerModule, interfaces } from 'inversify'; + +import { ChePluginRegistry } from './che-plugin-registry'; + +const registryModule = new ContainerModule((bind: interfaces.Bind) => { + bind(ChePluginRegistry).toSelf().inSingletonScope(); +}); + +export { registryModule }; diff --git a/plugins/recommendations-plugin/src/strategy/feature-plugin-strategy-request.ts b/plugins/recommendations-plugin/src/strategy/feature-plugin-strategy-request.ts new file mode 100644 index 0000000000..e1d681eae9 --- /dev/null +++ b/plugins/recommendations-plugin/src/strategy/feature-plugin-strategy-request.ts @@ -0,0 +1,18 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { FeaturedPlugin } from '../fetch/featured-plugin'; +import { VSCodeExtensionsInstalledLanguages } from '../analyzer/vscode-extensions-installed-languages'; + +export interface FeaturedPluginStrategyRequest { + featuredList: FeaturedPlugin[]; + vsCodeExtensionsInstalledLanguages: VSCodeExtensionsInstalledLanguages; + devfileHasPlugins: boolean; + extensionsInCheWorkspace: string[]; +} diff --git a/plugins/recommendations-plugin/src/strategy/featured-module.ts b/plugins/recommendations-plugin/src/strategy/featured-module.ts new file mode 100644 index 0000000000..3119ef0970 --- /dev/null +++ b/plugins/recommendations-plugin/src/strategy/featured-module.ts @@ -0,0 +1,20 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { ContainerModule, interfaces } from 'inversify'; + +import { FeaturedPluginStrategy } from './featured-plugin-strategy'; +import { RecommendPluginOpenFileStrategy } from './recommend-plugin-open-file-strategy'; + +const featuredModule = new ContainerModule((bind: interfaces.Bind) => { + bind(FeaturedPluginStrategy).toSelf().inSingletonScope(); + bind(RecommendPluginOpenFileStrategy).toSelf().inSingletonScope(); +}); + +export { featuredModule }; diff --git a/plugins/recommendations-plugin/src/strategy/featured-plugin-strategy.ts b/plugins/recommendations-plugin/src/strategy/featured-plugin-strategy.ts new file mode 100644 index 0000000000..b371bf24d2 --- /dev/null +++ b/plugins/recommendations-plugin/src/strategy/featured-plugin-strategy.ts @@ -0,0 +1,66 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +import * as theia from '@theia/plugin'; + +import { FeaturedPlugin } from '../fetch/featured-plugin'; +import { FeaturedPluginStrategyRequest } from './feature-plugin-strategy-request'; +import { injectable } from 'inversify'; + +/** + * Strategy about infering the featured plug-ins based on what is currently available in the devfile, the plug-in registry and current plug-ins (could be built-in plug-ins, etc.) + */ +@injectable() +export class FeaturedPluginStrategy { + private outputChannel: theia.OutputChannel; + + constructor() { + this.outputChannel = theia.window.createOutputChannel('Recommendations Plug-in'); + } + + async getFeaturedPlugins(featurePluginStrategyRequest: FeaturedPluginStrategyRequest): Promise { + const foundLanguageIds = featurePluginStrategyRequest.extensionsInCheWorkspace + .map( + fileExtension => + featurePluginStrategyRequest.vsCodeExtensionsInstalledLanguages.languagesByFileExtensions.get( + fileExtension + ) || [] + ) + .reduce((acc, e) => acc.concat(e), []); + + this.outputChannel.appendLine(`getFeaturedPlugins.foundLanguageIds=${foundLanguageIds}`); + + // Now compare with what we have as plugin-registry recommendations + const value = foundLanguageIds + .map(languageId => this.matchingPlugins(languageId, featurePluginStrategyRequest.featuredList)) + .reduce((acc, e) => acc.concat(e), []); + this.outputChannel.appendLine(`getFeaturedPlugins.value=${value}`); + return value; + } + + protected matchingPlugins(languageId: string, featuredList: FeaturedPlugin[]): string[] { + const plugins: string[] = []; + featuredList.forEach(featured => { + const pluginId = featured.id; + const languages: string[] = featured.onLanguages || []; + if (languages.includes(languageId) && !plugins.includes(pluginId)) { + plugins.push(pluginId); + } + }); + this.outputChannel.appendLine( + `getFeaturedPlugins.matchingPlugins(${languageId}, ${JSON.stringify( + featuredList, + undefined, + 2 + )})=>return ${plugins}` + ); + return plugins; + } +} diff --git a/plugins/recommendations-plugin/src/strategy/recommend-plugin-open-file-strategy.ts b/plugins/recommendations-plugin/src/strategy/recommend-plugin-open-file-strategy.ts new file mode 100644 index 0000000000..d280d1e95e --- /dev/null +++ b/plugins/recommendations-plugin/src/strategy/recommend-plugin-open-file-strategy.ts @@ -0,0 +1,117 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +import * as theia from '@theia/plugin'; + +import { inject, injectable } from 'inversify'; + +import { DevfileHandler } from '../devfile/devfile-handler'; +import { PluginsByLanguageFetcher } from '../fetch/plugins-by-language-fetcher'; +import { RecommendationsPluginAnalysis } from '../plugin/recommendations-plugin-analysis'; +import { WorkspaceHandler } from '../workspace/workspace-handler'; + +/** + * Provides recommendation when a file is being opened. + */ +@injectable() +export class RecommendPluginOpenFileStrategy { + @inject(PluginsByLanguageFetcher) + private pluginsByLanguageFetcher: PluginsByLanguageFetcher; + + @inject(DevfileHandler) + private devfileHandler: DevfileHandler; + + @inject(WorkspaceHandler) + private workspaceHandler: WorkspaceHandler; + + private alreadyNotifiedLanguageIds: string[]; + + constructor() { + this.alreadyNotifiedLanguageIds = []; + } + + async onOpenFile(textDocument: theia.TextDocument, workspaceAnalysis: RecommendationsPluginAnalysis): Promise { + // language ID of the current file being opened + const languageId = textDocument.languageId; + + // already analyzed, skip + if (this.alreadyNotifiedLanguageIds.includes(languageId)) { + return; + } + + // current workspaces + const workspacePaths = (theia.workspace.workspaceFolders || []).map(workspaceFolder => workspaceFolder.uri.path); + + // propose stuff only for files inside current workspace + if (!workspacePaths.some(workspacePath => textDocument.fileName.startsWith(workspacePath))) { + return; + } + + const installedPlugins = workspaceAnalysis.vsCodeExtensionsInstalledLanguages.vscodeExtensionByLanguageId.get( + languageId + ); + + // if we don't have plug-ins installed locally for this languageId, ask remotely + if (!installedPlugins) { + const remoteAvailablePlugins = await this.pluginsByLanguageFetcher.fetch(languageId); + const recommendedPlugins: string[] = []; + remoteAvailablePlugins.map(pluginCategory => { + if (pluginCategory.category === 'Programming Languages') { + pluginCategory.ids.forEach(id => { + if (!recommendedPlugins.includes(id)) { + recommendedPlugins.push(id); + } + }); + } + }); + + // users have existing plug-ins meaning that they probably started with a custom devfile, need to suggest and not add + if (remoteAvailablePlugins.length > 0) { + const doNotShowAgainValue = "Don't Show Again Recommendations"; + const install = 'Install...'; + const promptItems: theia.MessageItem[] = [{ title: install }, { title: doNotShowAgainValue }]; + const recommendationsPromptResult = await theia.window.showInformationMessage( + `The plug-in registry has plug-ins that can help with '${languageId}' files: ${recommendedPlugins}`, + ...promptItems + ); + // install recommended plug-ins + if (recommendationsPromptResult && recommendationsPromptResult.title === doNotShowAgainValue) { + // do not show dialog again + await this.devfileHandler.disableRecommendations(); + } else if (recommendationsPromptResult && recommendationsPromptResult.title === install) { + // Issue https://github.com/eclipse-theia/theia/issues/5673 + // do not allow multi-select + // const quickPickItems: theia.QuickPickItem[] = recommendedPlugins.map(pluginId => ({ + // label: `${pluginId}`, + // description: `Install plug-in ${pluginId}`, + // picked: true, + // })); + + // const quickPickOptions : theia.QuickPickOptions = {canPickMany: true, + // placeHolder: `Select plug-ins to install`}; + // const selectedItems = await theia.window.showQuickPick(quickPickItems, quickPickOptions); + // if (!selectedItems) { + // return; + // } + + // install plug-ins + await this.devfileHandler.addPlugins(recommendedPlugins); + + // restart the workspace ? + await this.workspaceHandler.restart( + `Plug-ins ${recommendedPlugins} have been added to your workspace. Please restart the workspace to see the changes.` + ); + } + } + } + // flag it as being analyzed + this.alreadyNotifiedLanguageIds.push(languageId); + } +} diff --git a/plugins/recommendations-plugin/src/util/deferred.ts b/plugins/recommendations-plugin/src/util/deferred.ts new file mode 100644 index 0000000000..1c1a89081e --- /dev/null +++ b/plugins/recommendations-plugin/src/util/deferred.ts @@ -0,0 +1,18 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +export class Deferred { + resolve: (value?: T) => void; + reject: (err?: unknown) => void; + + promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); +} diff --git a/plugins/recommendations-plugin/src/workspace/workspace-handler.ts b/plugins/recommendations-plugin/src/workspace/workspace-handler.ts new file mode 100644 index 0000000000..970bfe4b86 --- /dev/null +++ b/plugins/recommendations-plugin/src/workspace/workspace-handler.ts @@ -0,0 +1,26 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import * as che from '@eclipse-che/plugin'; + +import { injectable } from 'inversify'; + +/** + * Allow to restart a workspace. + */ +@injectable() +export class WorkspaceHandler { + async restart(promptMessage: string): Promise { + const options: che.RestartWorkspaceOptions = { + prompt: true, + promptMessage, + }; + return che.workspace.restartWorkspace(options); + } +} diff --git a/plugins/recommendations-plugin/src/workspace/workspace-module.ts b/plugins/recommendations-plugin/src/workspace/workspace-module.ts new file mode 100644 index 0000000000..d525dcc6c1 --- /dev/null +++ b/plugins/recommendations-plugin/src/workspace/workspace-module.ts @@ -0,0 +1,19 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +import { ContainerModule, interfaces } from 'inversify'; + +import { WorkspaceHandler } from './workspace-handler'; + +const workspaceModule = new ContainerModule((bind: interfaces.Bind) => { + bind(WorkspaceHandler).toSelf().inSingletonScope(); +}); + +export { workspaceModule }; diff --git a/plugins/recommendations-plugin/tests/_data/analyzer/existing-java-language.json b/plugins/recommendations-plugin/tests/_data/analyzer/existing-java-language.json new file mode 100644 index 0000000000..f55757b669 --- /dev/null +++ b/plugins/recommendations-plugin/tests/_data/analyzer/existing-java-language.json @@ -0,0 +1,14 @@ +{ + "name": "java-contribute-bis", + "publisher": "test", + "contributes": { + "languages": [ + { + "id": "java", + "extensions": [ + ".class" + ] + } + ] + } +} diff --git a/plugins/recommendations-plugin/tests/_data/analyzer/ms-python.json b/plugins/recommendations-plugin/tests/_data/analyzer/ms-python.json new file mode 100644 index 0000000000..a481017d39 --- /dev/null +++ b/plugins/recommendations-plugin/tests/_data/analyzer/ms-python.json @@ -0,0 +1,2348 @@ +{ + "name": "python", + "displayName": "Python", + "description": "Linting, Debugging (multi-threaded, remote), Intellisense, code formatting, refactoring, unit tests, snippets, and more.", + "version": "2019.2.5558", + "languageServerVersion": "0.1.80", + "publisher": "ms-python", + "author": { + "name": "Microsoft Corporation" + }, + "license": "MIT", + "homepage": "https://github.com/Microsoft/vscode-python", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "qna": "https://stackoverflow.com/questions/tagged/visual-studio-code+python", + "badges": [ + { + "url": "https://vscode-python.visualstudio.com/VSCode-Python/_apis/build/status/VSCode-Python-Rolling-CI?branchName=master", + "href": "https://vscode-python.visualstudio.com/VSCode-Python/VSCode-Python%20Team/_build/index?context=allDefinitions&path=&definitionId=9", + "description": "Continuous integration (VSTS)" + }, + { + "url": "https://travis-ci.org/Microsoft/vscode-python.svg?branch=master", + "href": "https://travis-ci.org/Microsoft/vscode-python", + "description": "Continuous integration (Travis)" + }, + { + "url": "https://codecov.io/gh/Microsoft/vscode-python/branch/master/graph/badge.svg", + "href": "https://codecov.io/gh/Microsoft/vscode-python", + "description": "Test coverage" + } + ], + "icon": "icon.png", + "galleryBanner": { + "color": "#1e415e", + "theme": "dark" + }, + "engines": { + "vscode": "^1.31.0" + }, + "keywords": [ + "python", + "django", + "unittest", + "multi-root ready" + ], + "categories": [ + "Programming Languages", + "Debuggers", + "Linters", + "Snippets", + "Formatters", + "Other" + ], + "activationEvents": [ + "onLanguage:python", + "onLanguage:jupyter", + "onDebugResolve:python", + "onCommand:python.execInTerminal", + "onCommand:python.sortImports", + "onCommand:python.runtests", + "onCommand:python.debugtests", + "onCommand:python.setInterpreter", + "onCommand:python.setShebangInterpreter", + "onCommand:python.viewTestUI", + "onCommand:python.viewTestOutput", + "onCommand:python.viewOutput", + "onCommand:python.selectAndRunTestMethod", + "onCommand:python.selectAndDebugTestMethod", + "onCommand:python.selectAndRunTestFile", + "onCommand:python.runCurrentTestFile", + "onCommand:python.runFailedTests", + "onCommand:python.execSelectionInTerminal", + "onCommand:python.execSelectionInDjangoShell", + "onCommand:python.buildWorkspaceSymbols", + "onCommand:python.updateSparkLibrary", + "onCommand:python.startREPL", + "onCommand:python.goToPythonObject", + "onCommand:python.setLinter", + "onCommand:python.enableLinting", + "onCommand:python.createTerminal", + "onCommand:python.discoverTests", + "onCommand:python.configureTests", + "onCommand:python.datascience.showhistorypane", + "onCommand:python.datascience.importnotebook", + "onCommand:python.datascience.selectjupyteruri", + "onCommand:python.datascience.exportfileasnotebook", + "onCommand:python.datascience.exportfileandoutputasnotebook", + "onCommand:python.python.enableSourceMapSupport" + ], + "main": "./out/client/extension", + "contributes": { + "snippets": [ + { + "language": "python", + "path": "./snippets/python.json" + } + ], + "keybindings": [ + { + "command": "python.execSelectionInTerminal", + "key": "shift+enter", + "when": "editorFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !python.datascience.ownsSelection" + }, + { + "command": "python.datascience.execSelectionInteractive", + "key": "shift+enter", + "when": "editorFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && python.datascience.ownsSelection && python.datascience.featureenabled" + }, + { + "command": "python.datascience.runcurrentcelladvance", + "key": "shift+enter", + "when": "editorFocus && !editorHasSelection && python.datascience.hascodecells && python.datascience.featureenabled" + } + ], + "commands": [ + { + "command": "python.enableSourceMapSupport", + "title": "%python.command.python.enableSourceMapSupport.title%", + "category": "Python" + }, + { + "command": "python.sortImports", + "title": "%python.command.python.sortImports.title%", + "category": "Python Refactor" + }, + { + "command": "python.startREPL", + "title": "%python.command.python.startREPL.title%", + "category": "Python" + }, + { + "command": "python.createTerminal", + "title": "%python.command.python.createTerminal.title%", + "category": "Python" + }, + { + "command": "python.buildWorkspaceSymbols", + "title": "%python.command.python.buildWorkspaceSymbols.title%", + "category": "Python" + }, + { + "command": "python.openTestNodeInEditor", + "title": "Open", + "icon": { + "light": "resources/light/open-file.svg", + "dark": "resources/dark/open-file.svg" + } + }, + { + "command": "python.runTestNode", + "title": "Run", + "icon": { + "light": "resources/light/start.svg", + "dark": "resources/dark/start.svg" + } + }, + { + "command": "python.debugTestNode", + "title": "Debug", + "icon": { + "light": "resources/light/debug.svg", + "dark": "resources/dark/debug.svg" + } + }, + { + "command": "python.runtests", + "title": "%python.command.python.runtests.title%", + "category": "Python", + "icon": { + "light": "resources/light/run-tests.svg", + "dark": "resources/dark/run-tests.svg" + } + }, + { + "command": "python.debugtests", + "title": "%python.command.python.debugtests.title%", + "category": "Python", + "icon": { + "light": "resources/light/debug.svg", + "dark": "resources/dark/debug.svg" + } + }, + { + "command": "python.execInTerminal", + "title": "%python.command.python.execInTerminal.title%", + "category": "Python" + }, + { + "command": "python.setInterpreter", + "title": "%python.command.python.setInterpreter.title%", + "category": "Python" + }, + { + "command": "python.updateSparkLibrary", + "title": "%python.command.python.updateSparkLibrary.title%", + "category": "Python" + }, + { + "command": "python.refactorExtractVariable", + "title": "%python.command.python.refactorExtractVariable.title%", + "category": "Python Refactor" + }, + { + "command": "python.refactorExtractMethod", + "title": "%python.command.python.refactorExtractMethod.title%", + "category": "Python Refactor" + }, + { + "command": "python.viewTestOutput", + "title": "%python.command.python.viewTestOutput.title%", + "category": "Python", + "icon": { + "light": "resources/light/repl.svg", + "dark": "resources/dark/repl.svg" + } + }, + { + "command": "python.viewOutput", + "title": "%python.command.python.viewOutput.title%", + "category": "Python", + "icon": { + "light": "resources/light/repl.svg", + "dark": "resources/dark/repl.svg" + } + }, + { + "command": "python.selectAndRunTestMethod", + "title": "%python.command.python.selectAndRunTestMethod.title%", + "category": "Python" + }, + { + "command": "python.selectAndDebugTestMethod", + "title": "%python.command.python.selectAndDebugTestMethod.title%", + "category": "Python" + }, + { + "command": "python.selectAndRunTestFile", + "title": "%python.command.python.selectAndRunTestFile.title%", + "category": "Python" + }, + { + "command": "python.runCurrentTestFile", + "title": "%python.command.python.runCurrentTestFile.title%", + "category": "Python" + }, + { + "command": "python.runFailedTests", + "title": "%python.command.python.runFailedTests.title%", + "category": "Python", + "icon": { + "light": "resources/light/run-failed-tests.svg", + "dark": "resources/dark/run-failed-tests.svg" + } + }, + { + "command": "python.discoverTests", + "title": "%python.command.python.discoverTests.title%", + "category": "Python", + "icon": { + "light": "resources/light/refresh.svg", + "dark": "resources/dark/refresh.svg" + } + }, + { + "command": "python.discoveringTests", + "title": "%python.command.python.discoverTests.title%", + "category": "Python", + "icon": { + "light": "resources/light/discovering-tests.svg", + "dark": "resources/dark/discovering-tests.svg" + } + }, + { + "command": "python.stopUnitTests", + "title": "%python.command.python.stopUnitTests.title%", + "category": "Python", + "icon": { + "light": "resources/light/stop.svg", + "dark": "resources/dark/stop.svg" + } + }, + { + "command": "python.configureTests", + "title": "%python.command.python.configureTests.title%", + "category": "Python" + }, + { + "command": "python.execSelectionInTerminal", + "title": "%python.command.python.execSelectionInTerminal.title%", + "category": "Python" + }, + { + "command": "python.execSelectionInDjangoShell", + "title": "%python.command.python.execSelectionInDjangoShell.title%", + "category": "Python" + }, + { + "command": "python.goToPythonObject", + "title": "%python.command.python.goToPythonObject.title%", + "category": "Python" + }, + { + "command": "python.setLinter", + "title": "%python.command.python.setLinter.title%", + "category": "Python" + }, + { + "command": "python.enableLinting", + "title": "%python.command.python.enableLinting.title%", + "category": "Python" + }, + { + "command": "python.runLinting", + "title": "%python.command.python.runLinting.title%", + "category": "Python" + }, + { + "command": "python.datascience.runcurrentcell", + "title": "%python.command.python.datascience.runcurrentcell.title%", + "category": "Python" + }, + { + "command": "python.datascience.runcurrentcelladvance", + "title": "%python.command.python.datascience.runcurrentcelladvance.title%", + "category": "Python" + }, + { + "command": "python.datascience.execSelectionInteractive", + "title": "%python.command.python.datascience.execSelectionInteractive.title%", + "category": "Python" + }, + { + "command": "python.datascience.showhistorypane", + "title": "%python.command.python.datascience.showhistorypane.title%", + "category": "Python" + }, + { + "command": "python.datascience.runallcells", + "title": "%python.command.python.datascience.runallcells.command.title%", + "category": "Python" + }, + { + "command": "python.datascience.runcell", + "title": "%python.command.python.datascience.runcell.title%", + "category": "Python" + }, + { + "command": "python.datascience.selectjupyteruri", + "title": "%python.command.python.datascience.selectjupyteruri.title%", + "category": "Python", + "when": "python.datascience.featureenabled" + }, + { + "command": "python.datascience.importnotebook", + "title": "%python.command.python.datascience.importnotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.exportoutputasnotebook", + "title": "%python.command.python.datascience.exportoutputasnotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.exportfileasnotebook", + "title": "%python.command.python.datascience.exportfileasnotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.exportfileandoutputasnotebook", + "title": "%python.command.python.datascience.exportfileandoutputasnotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.undocells", + "title": "%python.command.python.datascience.undocells.title%", + "category": "Python" + }, + { + "command": "python.datascience.redocells", + "title": "%python.command.python.datascience.redocells.title%", + "category": "Python" + }, + { + "command": "python.datascience.removeallcells", + "title": "%python.command.python.datascience.removeallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.interruptkernel", + "title": "%python.command.python.datascience.interruptkernel.title%", + "category": "Python" + }, + { + "command": "python.datascience.restartkernel", + "title": "%python.command.python.datascience.restartkernel.title%", + "category": "Python" + }, + { + "command": "python.datascience.expandallcells", + "title": "%python.command.python.datascience.expandallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.collapseallcells", + "title": "%python.command.python.datascience.collapseallcells.title%", + "category": "Python" + } + ], + "menus": { + "editor/context": [ + { + "command": "python.refactorExtractVariable", + "title": "Refactor: Extract Variable", + "group": "Refactor", + "when": "editorHasSelection && editorLangId == python" + }, + { + "command": "python.refactorExtractMethod", + "title": "Refactor: Extract Method", + "group": "Refactor", + "when": "editorHasSelection && editorLangId == python" + }, + { + "command": "python.sortImports", + "title": "Refactor: Sort Imports", + "group": "Refactor", + "when": "editorLangId == python" + }, + { + "command": "python.execSelectionInTerminal", + "group": "Python", + "when": "editorFocus && editorLangId == python" + }, + { + "command": "python.execSelectionInDjangoShell", + "group": "Python", + "when": "editorHasSelection && editorLangId == python && python.isDjangoProject" + }, + { + "when": "resourceLangId == python", + "command": "python.execInTerminal", + "group": "Python" + }, + { + "when": "resourceLangId == python", + "command": "python.runCurrentTestFile", + "group": "Python" + }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", + "command": "python.datascience.runcurrentcell", + "group": "Python" + }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", + "command": "python.datascience.runcurrentcelladvance", + "group": "Python" + }, + { + "command": "python.datascience.runallcells", + "group": "Python", + "when": "editorFocus && editorLangId == python && python.datascience.featureenabled && python.datascience.ownsSelection" + }, + { + "command": "python.datascience.execSelectionInteractive", + "group": "Python", + "when": "editorFocus && editorLangId == python && python.datascience.featureenabled && python.datascience.ownsSelection" + }, + { + "when": "editorFocus && editorLangId == python && resourceLangId == jupyter && python.datascience.featureenabled", + "command": "python.datascience.importnotebook", + "group": "Python" + }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", + "command": "python.datascience.exportfileasnotebook", + "group": "Python2" + }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", + "command": "python.datascience.exportfileandoutputasnotebook", + "group": "Python2@2" + } + ], + "explorer/context": [ + { + "when": "resourceLangId == python && !busyTests", + "command": "python.runtests", + "group": "Python" + }, + { + "when": "resourceLangId == python && !busyTests", + "command": "python.debugtests", + "group": "Python" + }, + { + "when": "resourceLangId == python", + "command": "python.execInTerminal", + "group": "Python" + }, + { + "when": "resourceLangId == jupyter", + "command": "python.datascience.importnotebook", + "group": "Python" + } + ], + "commandPalette": [ + { + "command": "python.viewOutput", + "title": "%python.command.python.viewOutput.title%", + "category": "Python" + }, + { + "command": "python.runTestNode", + "title": "Run", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.discoveringTests", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.stopUnitTests", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.debugTestNode", + "title": "Debug", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.openTestNodeInEditor", + "title": "Open", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.runcurrentcell", + "title": "%python.command.python.datascience.runcurrentcell.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled" + }, + { + "command": "python.datascience.runcurrentcelladvance", + "title": "%python.command.python.datascience.runcurrentcelladvance.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled" + }, + { + "command": "python.datascience.showhistorypane", + "title": "%python.command.python.datascience.showhistorypane.title%", + "category": "Python", + "when": "python.datascience.featureenabled" + }, + { + "command": "python.datascience.runallcells", + "title": "%python.command.python.datascience.runallcells.command.title%", + "category": "Python", + "when": "python.datascience.featureenabled" + }, + { + "command": "python.datascience.runcell", + "title": "%python.command.python.datascience.runcell.title%", + "category": "Python", + "when": "python.datascience.featureenabled" + }, + { + "command": "python.datascience.importnotebook", + "title": "%python.command.python.datascience.importnotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.exportfileasnotebook", + "title": "%python.command.python.datascience.exportfileasnotebook.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled" + }, + { + "command": "python.datascience.exportfileandoutputasnotebook", + "title": "%python.command.python.datascience.exportfileandoutputasnotebook.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled" + }, + { + "command": "python.datascience.undocells", + "title": "%python.command.python.datascience.undocells.title%", + "category": "Python", + "when": "python.datascience.haveinteractivecells && python.datascience.featureenabled" + }, + { + "command": "python.datascience.redocells", + "title": "%python.command.python.datascience.redocells.title%", + "category": "Python", + "when": "python.datascience.haveredoablecells && python.datascience.featureenabled" + }, + { + "command": "python.datascience.removeallcells", + "title": "%python.command.python.datascience.removeallcells.title%", + "category": "Python", + "when": "python.datascience.haveinteractivecells && python.datascience.featureenabled" + }, + { + "command": "python.datascience.interruptkernel", + "title": "%python.command.python.datascience.interruptkernel.title%", + "category": "Python", + "when": "python.datascience.haveinteractive && python.datascience.featureenabled" + }, + { + "command": "python.datascience.restartkernel", + "title": "%python.command.python.datascience.restartkernel.title%", + "category": "Python", + "when": "python.datascience.haveinteractive && python.datascience.featureenabled" + }, + { + "command": "python.datascience.expandallcells", + "title": "%python.command.python.datascience.expandallcells.title%", + "category": "Python", + "when": "python.datascience.haveinteractive && python.datascience.featureenabled" + }, + { + "command": "python.datascience.collapseallcells", + "title": "%python.command.python.datascience.collapseallcells.title%", + "category": "Python", + "when": "python.datascience.haveinteractive && python.datascience.featureenabled" + }, + { + "command": "python.datascience.exportoutputasnotebook", + "title": "%python.command.python.datascience.exportoutputasnotebook.title%", + "category": "Python", + "when": "python.datascience.haveinteractive && python.datascience.featureenabled" + } + ], + "view/title": [ + { + "command": "python.debugtests", + "when": "view == python_tests && !busyTests", + "group": "navigation@3" + }, + { + "command": "python.runtests", + "when": "view == python_tests && !busyTests", + "group": "navigation@1" + }, + { + "command": "python.stopUnitTests", + "when": "view == python_tests && busyTests", + "group": "navigation@1" + }, + { + "command": "python.discoverTests", + "when": "view == python_tests && !busyTests", + "group": "navigation@4" + }, + { + "command": "python.discoveringTests", + "when": "view == python_tests && discoveringTests", + "group": "navigation@4" + }, + { + "command": "python.runFailedTests", + "when": "view == python_tests && hasFailedTests && !busyTests", + "group": "navigation@2" + }, + { + "command": "python.viewTestOutput", + "when": "view == python_tests", + "group": "navigation@5" + } + ], + "view/item/context": [ + { + "command": "python.openTestNodeInEditor", + "when": "view == python_tests && viewItem == testFunction", + "group": "inline@2" + }, + { + "command": "python.debugTestNode", + "when": "view == python_tests && viewItem == testFunction && !busyTests", + "group": "inline@1" + }, + { + "command": "python.runTestNode", + "when": "view == python_tests && viewItem == testFunction && !busyTests", + "group": "inline@0" + }, + { + "command": "python.openTestNodeInEditor", + "when": "view == python_tests && viewItem == testFile", + "group": "inline@2" + }, + { + "command": "python.debugTestNode", + "when": "view == python_tests && viewItem == testFile && !busyTests", + "group": "inline@1" + }, + { + "command": "python.runTestNode", + "when": "view == python_tests && viewItem == testFile && !busyTests", + "group": "inline@0" + }, + { + "command": "python.openTestNodeInEditor", + "when": "view == python_tests && viewItem == testSuite", + "group": "inline@2" + }, + { + "command": "python.debugTestNode", + "when": "view == python_tests && viewItem == testSuite && !busyTests", + "group": "inline@1" + }, + { + "command": "python.runTestNode", + "when": "view == python_tests && viewItem == testSuite && !busyTests", + "group": "inline@0" + } + ] + }, + "debuggers": [ + { + "type": "python", + "label": "Python", + "languages": [ + "python" + ], + "enableBreakpointsFor": { + "languageIds": [ + "python", + "html", + "jinja" + ] + }, + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", + "program": "./out/client/debugger/debugAdapter/main.js", + "runtime": "node", + "configurationSnippets": [ + { + "label": "Python: Terminal (integrated)", + "description": "%python.snippet.launch.terminal.description%", + "body": { + "name": "Python: Terminal (integrated)", + "type": "python", + "request": "launch", + "program": "^\"\\${file}\"", + "console": "integratedTerminal" + } + }, + { + "label": "Python: Terminal (external)", + "description": "%python.snippet.launch.externalTerminal.description%", + "body": { + "name": "Python: Terminal (external)", + "type": "python", + "request": "launch", + "program": "^\"\\${file}\"", + "console": "externalTerminal" + } + }, + { + "label": "Python: Module", + "description": "%python.snippet.launch.module.description%", + "body": { + "name": "Python: Module", + "type": "python", + "request": "launch", + "module": "enter-your-module-name-here", + "console": "integratedTerminal" + } + }, + { + "label": "Python: Django", + "description": "%python.snippet.launch.django.description%", + "body": { + "name": "Django", + "type": "python", + "request": "launch", + "program": "^\"\\${workspaceFolder}/manage.py\"", + "args": [ + "runserver", + "--noreload", + "--nothreading" + ], + "django": true + } + }, + { + "label": "Python: Flask", + "description": "%python.snippet.launch.flask.description%", + "body": { + "name": "Flask", + "type": "python", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "app.py", + "FLASK_ENV": "development", + "FLASK_DEBUG": "0" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "jinja": true + } + }, + { + "label": "Python: Gevent", + "description": "%python.snippet.launch.gevent.description%", + "body": { + "name": "Gevent", + "type": "python", + "request": "launch", + "program": "^\"\\${file}\"", + "gevent": true + } + }, + { + "label": "Python: PySpark", + "description": "%python.snippet.launch.pyspark.description%", + "body": { + "name": "PySpark", + "type": "python", + "request": "launch", + "osx": { + "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit\"" + }, + "windows": { + "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit.cmd\"" + }, + "linux": { + "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit\"" + }, + "program": "^\"\\${file}\"" + } + }, + { + "label": "Python: Watson", + "description": "%python.snippet.launch.watson.description%", + "body": { + "name": "Watson", + "type": "python", + "request": "launch", + "program": "^\"\\${workspaceFolder}/console.py\"", + "args": [ + "dev", + "runserver", + "--noreload=True" + ], + "jinja": true + } + }, + { + "label": "Python: Scrapy", + "description": "%python.snippet.launch.scrapy.description%", + "body": { + "name": "Scrapy", + "type": "python", + "request": "launch", + "module": "scrapy", + "args": [ + "crawl", + "specs", + "-o", + "bikes.json" + ] + } + }, + { + "label": "Python: Pyramid", + "description": "%python.snippet.launch.pyramid.description%", + "body": { + "name": "Pyramid", + "type": "python", + "request": "launch", + "args": [ + "^\"\\${workspaceFolder}/development.ini\"" + ], + "pyramid": true, + "jinja": true + } + }, + { + "label": "Python: Remote Attach", + "description": "%python.snippet.launch.attach.description%", + "body": { + "name": "Attach (Remote Debug)", + "type": "python", + "request": "attach", + "port": 5678, + "host": "localhost", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ] + } + }, + { + "label": "Python: Unit Tests", + "description": "%python.snippet.launch.unitTests.description%", + "body": { + "name": "Unit Tests", + "type": "python", + "request": "test" + } + } + ], + "configurationAttributes": { + "launch": { + "properties": { + "module": { + "type": "string", + "description": "Name of the module to be debugged.", + "default": "" + }, + "program": { + "type": "string", + "description": "Absolute path to the program.", + "default": "${file}" + }, + "pythonPath": { + "type": "string", + "description": "Path (fully qualified) to python executable. Defaults to the value in settings.json", + "default": "${config:python.pythonPath}" + }, + "args": { + "type": "array", + "description": "Command line arguments passed to the program", + "default": [], + "items": { + "type": "string" + } + }, + "stopOnEntry": { + "type": "boolean", + "description": "Automatically stop after launch.", + "default": false + }, + "showReturnValue": { + "type": "boolean", + "description": "Show return value of functions when stepping.", + "default": false + }, + "console": { + "enum": [ + "none", + "integratedTerminal", + "externalTerminal" + ], + "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", + "default": "integratedTerminal" + }, + "cwd": { + "type": "string", + "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", + "default": "${workspaceFolder}" + }, + "env": { + "type": "object", + "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", + "default": {} + }, + "envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions.", + "default": "${workspaceFolder}/.env" + }, + "port": { + "type": "number", + "description": "Debug port (default is 0, resulting in the use of a dynamic port).", + "default": 0 + }, + "host": { + "type": "string", + "description": "IP address of the of the local debug server (default is localhost).", + "default": "localhost" + }, + "logToFile": { + "type": "boolean", + "description": "Enable logging of debugger events to a log file.", + "default": false + }, + "redirectOutput": { + "type": "boolean", + "description": "Redirect output.", + "default": true + }, + "debugStdLib": { + "type": "boolean", + "description": "Debug standard library code.", + "default": false + }, + "gevent": { + "type": "boolean", + "description": "Enable debugging of gevent monkey-patched code.", + "default": false + }, + "django": { + "type": "boolean", + "description": "Django debugging.", + "default": false + }, + "jinja": { + "enum": [ + true, + false, + null + ], + "description": "Jinja template debugging (e.g. Flask).", + "default": null + }, + "sudo": { + "type": "boolean", + "description": "Running debug program under elevated permissions (on Unix).", + "default": false + }, + "pyramid": { + "type": "boolean", + "description": "Whether debugging Pyramid applications", + "default": false + }, + "subProcess": { + "type": "boolean", + "description": "Whether to enable Sub Process debugging", + "default": false + } + } + }, + "test": { + "properties": { + "pythonPath": { + "type": "string", + "description": "Path (fully qualified) to python executable. Defaults to the value in settings.json", + "default": "${config:python.pythonPath}" + }, + "stopOnEntry": { + "type": "boolean", + "description": "Automatically stop after launch.", + "default": false + }, + "showReturnValue": { + "type": "boolean", + "description": "Show return value of functions when stepping.", + "default": false + }, + "console": { + "enum": [ + "none", + "integratedTerminal", + "externalTerminal" + ], + "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", + "default": "none" + }, + "cwd": { + "type": "string", + "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", + "default": "${workspaceFolder}" + }, + "env": { + "type": "object", + "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", + "default": {} + }, + "envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions.", + "default": "${workspaceFolder}/.env" + }, + "redirectOutput": { + "type": "boolean", + "description": "Redirect output.", + "default": true + }, + "debugStdLib": { + "type": "boolean", + "description": "Debug standard library code.", + "default": false + } + } + }, + "attach": { + "required": [ + "port" + ], + "properties": { + "port": { + "type": "number", + "description": "Debug port to attach", + "default": 0 + }, + "host": { + "type": "string", + "description": "IP Address of the of remote server (default is localhost or use 127.0.0.1).", + "default": "localhost" + }, + "pathMappings": { + "type": "array", + "label": "Path mappings.", + "items": { + "type": "object", + "label": "Path mapping", + "required": [ + "localRoot", + "remoteRoot" + ], + "properties": { + "localRoot": { + "type": "string", + "label": "Local source root.", + "default": "${workspaceFolder}" + }, + "remoteRoot": { + "type": "string", + "label": "Remote source root.", + "default": "" + } + } + }, + "default": [] + }, + "logToFile": { + "type": "boolean", + "description": "Enable logging of debugger events to a log file.", + "default": false + }, + "redirectOutput": { + "type": "boolean", + "description": "Redirect output.", + "default": true + }, + "debugStdLib": { + "type": "boolean", + "description": "Debug standard library code.", + "default": false + }, + "django": { + "type": "boolean", + "description": "Django debugging.", + "default": false + }, + "jinja": { + "enum": [ + true, + false, + null + ], + "description": "Jinja template debugging (e.g. Flask).", + "default": null + }, + "subProcess": { + "type": "boolean", + "description": "Whether to enable Sub Process debugging", + "default": false + } + } + } + } + } + ], + "configuration": { + "type": "object", + "title": "Python", + "properties": { + "python.diagnostics.sourceMapsEnabled": { + "type": "boolean", + "default": false, + "description": "Enable source map support for meaningful strack traces in error logs.", + "scope": "application" + }, + "python.autoComplete.addBrackets": { + "type": "boolean", + "default": false, + "description": "Automatically add brackets for functions.", + "scope": "resource" + }, + "python.autoComplete.extraPaths": { + "type": "array", + "default": [], + "description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", + "scope": "resource" + }, + "python.autoComplete.showAdvancedMembers": { + "type": "boolean", + "default": true, + "description": "Controls appearance of methods with double underscores in the completion list.", + "scope": "resource" + }, + "python.autoComplete.typeshedPaths": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "Specifies paths to local typeshed repository clone(s) for the Python language server.", + "scope": "resource" + }, + "python.autoUpdateLanguageServer": { + "type": "boolean", + "default": true, + "description": "Automatically update the language server.", + "scope": "application" + }, + "python.dataScience.allowImportFromNotebook": { + "type": "boolean", + "default": true, + "description": "Allows a user to import a jupyter notebook into a python file anytime one is opened.", + "scope": "resource" + }, + "python.dataScience.enabled": { + "type": "boolean", + "default": true, + "description": "Enable the experimental data science features in the python extension.", + "scope": "resource" + }, + "python.dataScience.exportWithOutputEnabled": { + "type": "boolean", + "default": false, + "description": "Enable exporting a python file into a jupyter notebook and run all cells when doing so.", + "scope": "resource" + }, + "python.dataScience.jupyterLaunchTimeout": { + "type": "number", + "default": 60000, + "description": "Amount of time (in ms) to wait for the Jupyter Notebook server to start.", + "scope": "resource" + }, + "python.dataScience.jupyterServerURI": { + "type": "string", + "default": "local", + "description": "Select the Jupyter server URI to connect to. Select 'local' to launch a new Juypter server on the local machine.", + "scope": "resource" + }, + "python.dataScience.notebookFileRoot": { + "type": "string", + "default": "${workspaceFolder}", + "description": "Set the root directory for loading files for the Python Interactive window.", + "scope": "resource" + }, + "python.dataScience.searchForJupyter": { + "type": "boolean", + "default": true, + "description": "Search all installed Python interpreters for a Jupyter installation when starting the Python Interactive window", + "scope": "resource" + }, + "python.dataScience.changeDirOnImportExport": { + "type": "boolean", + "default": true, + "description": "When importing or exporting a Jupyter Notebook add a directory change command to allow relative path loading to work.", + "scope": "resource" + }, + "python.dataScience.useDefaultConfigForJupyter": { + "type": "boolean", + "default": true, + "description": "When running Jupyter locally, create a default empty Jupyter config for the Python Interactive window", + "scope": "resource" + }, + "python.dataScience.jupyterInterruptTimeout": { + "type": "number", + "default": 10000, + "description": "Amount of time (in ms) to wait for an interrupt before asking to restart the Jupyter kernel.", + "scope": "resource" + }, + "python.dataScience.allowInput": { + "type": "boolean", + "default": true, + "description": "Allow the inputting of python code directly into the Python Interactive window" + }, + "python.dataScience.showCellInputCode": { + "type": "boolean", + "default": true, + "description": "Show cell input code.", + "scope": "resource" + }, + "python.dataScience.collapseCellInputCodeByDefault": { + "type": "boolean", + "default": true, + "description": "Collapse cell input code by default.", + "scope": "resource" + }, + "python.dataScience.maxOutputSize": { + "type": "number", + "default": 400, + "description": "Maximum size (in pixels) of text output in the Python Interactive window before a scrollbar appears. Set to -1 for infinity.", + "scope": "resource" + }, + "python.dataScience.sendSelectionToInteractiveWindow": { + "type": "boolean", + "default": false, + "description": "Determines if selected code in a python file will go to the terminal or the Python Interactive window when hitting shift+enter", + "scope": "resource" + }, + "python.dataScience.codeRegularExpression": { + "type": "string", + "default": "^(#\\s*%%|#\\s*\\|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])", + "description": "Regular expression used to identify code cells. All code until the next match is considered part of this cell. \nDefaults to '^(#\\s*%%|#\\s*\\|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])' if left blank", + "scope": "resource" + }, + "python.dataScience.markdownRegularExpression": { + "type": "string", + "default": "^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\)", + "description": "Regular expression used to identify markdown cells. All comments after this expression are considered part of the markdown. \nDefaults to '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\)' if left blank", + "scope": "resource" + }, + "python.dataScience.allowLiveShare": { + "type": "boolean", + "default": false, + "description": "Allow the Python Interactive window to be shared during a Live Share session (experimental)", + "scope": "resource" + }, + "python.disableInstallationCheck": { + "type": "boolean", + "default": false, + "description": "Whether to check if Python is installed (also warn when using the macOS-installed Python).", + "scope": "resource" + }, + "python.envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions.", + "default": "${workspaceFolder}/.env", + "scope": "resource" + }, + "python.formatting.autopep8Args": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.formatting.autopep8Path": { + "type": "string", + "default": "autopep8", + "description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.formatting.provider": { + "type": "string", + "default": "autopep8", + "description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", + "enum": [ + "autopep8", + "black", + "yapf", + "none" + ], + "scope": "resource" + }, + "python.formatting.blackArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.formatting.blackPath": { + "type": "string", + "default": "black", + "description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.formatting.yapfArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.formatting.yapfPath": { + "type": "string", + "default": "yapf", + "description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.globalModuleInstallation": { + "type": "boolean", + "default": false, + "description": "Whether to install Python modules globally when not using an environment.", + "scope": "resource" + }, + "python.jediEnabled": { + "type": "boolean", + "default": true, + "description": "Enables Jedi as IntelliSense engine instead of Microsoft Python Analysis Engine.", + "scope": "resource" + }, + "python.jediMemoryLimit": { + "type": "number", + "default": 0, + "description": "Memory limit for the Jedi completion engine in megabytes. Zero (default) means 1024 MB. -1 means unlimited (disable memory limit check)", + "scope": "resource" + }, + "python.jediPath": { + "type": "string", + "default": "", + "description": "Path to directory containing the Jedi library (this path will contain the 'Jedi' sub directory).", + "scope": "resource" + }, + "python.analysis.openFilesOnly": { + "type": "boolean", + "default": true, + "description": "Only show errors and warnings for open files rather than for the entire workspace.", + "scope": "resource" + }, + "python.analysis.diagnosticPublishDelay": { + "type": "integer", + "default": 1000, + "description": "Delay before diagnostic messages are transferred to the problems list (in milliseconds).", + "scope": "resource" + }, + "python.analysis.typeshedPaths": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "Paths to look for typeshed modules.", + "scope": "resource" + }, + "python.analysis.errors": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of diagnostics messages to be shown as errors.", + "scope": "resource" + }, + "python.analysis.warnings": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of diagnostics messages to be shown as warnings.", + "scope": "resource" + }, + "python.analysis.information": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of diagnostics messages to be shown as information.", + "scope": "resource" + }, + "python.analysis.disabled": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of suppressed diagnostic messages.", + "scope": "resource" + }, + "python.analysis.logLevel": { + "type": "string", + "enum": [ + "Error", + "Warning", + "Information", + "Trace" + ], + "default": "Error", + "description": "Defines type of log messages language server writes into the output window.", + "scope": "resource" + }, + "python.analysis.symbolsHierarchyDepthLimit": { + "type": "integer", + "default": 10, + "description": "Limits depth of the symbol tree in the document outline.", + "scope": "resource" + }, + "python.linting.enabled": { + "type": "boolean", + "default": true, + "description": "Whether to lint Python files.", + "scope": "resource" + }, + "python.linting.flake8Args": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.flake8CategorySeverity.E": { + "type": "string", + "default": "Error", + "description": "Severity of Flake8 message type 'E'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.flake8CategorySeverity.F": { + "type": "string", + "default": "Error", + "description": "Severity of Flake8 message type 'F'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.flake8CategorySeverity.W": { + "type": "string", + "default": "Warning", + "description": "Severity of Flake8 message type 'W'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.flake8Enabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using flake8", + "scope": "resource" + }, + "python.linting.flake8Path": { + "type": "string", + "default": "flake8", + "description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.ignorePatterns": { + "type": "array", + "description": "Patterns used to exclude files or folders from being linted.", + "default": [ + ".vscode/*.py", + "**/site-packages/**/*.py" + ], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.lintOnSave": { + "type": "boolean", + "default": true, + "description": "Whether to lint Python files when saved.", + "scope": "resource" + }, + "python.linting.maxNumberOfProblems": { + "type": "number", + "default": 100, + "description": "Controls the maximum number of problems produced by the server.", + "scope": "resource" + }, + "python.linting.banditArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.banditEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using bandit.", + "scope": "resource" + }, + "python.linting.banditPath": { + "type": "string", + "default": "bandit", + "description": "Path to bandit, you can use a custom version of bandit by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.mypyArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [ + "--ignore-missing-imports", + "--follow-imports=silent", + "--show-column-numbers" + ], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.mypyCategorySeverity.error": { + "type": "string", + "default": "Error", + "description": "Severity of Mypy message type 'Error'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.mypyCategorySeverity.note": { + "type": "string", + "default": "Information", + "description": "Severity of Mypy message type 'Note'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.mypyEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using mypy.", + "scope": "resource" + }, + "python.linting.mypyPath": { + "type": "string", + "default": "mypy", + "description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pep8Args": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.pep8CategorySeverity.E": { + "type": "string", + "default": "Error", + "description": "Severity of Pep8 message type 'E'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pep8CategorySeverity.W": { + "type": "string", + "default": "Warning", + "description": "Severity of Pep8 message type 'W'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pep8Enabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using pep8", + "scope": "resource" + }, + "python.linting.pep8Path": { + "type": "string", + "default": "pep8", + "description": "Path to pep8, you can use a custom version of pep8 by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.prospectorArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.prospectorEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using prospector.", + "scope": "resource" + }, + "python.linting.prospectorPath": { + "type": "string", + "default": "prospector", + "description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pydocstyleArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.pydocstyleEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using pydocstyle", + "scope": "resource" + }, + "python.linting.pydocstylePath": { + "type": "string", + "default": "pydocstyle", + "description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pylamaArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.pylamaEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using pylama.", + "scope": "resource" + }, + "python.linting.pylamaPath": { + "type": "string", + "default": "pylama", + "description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pylintArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.pylintCategorySeverity.convention": { + "type": "string", + "default": "Information", + "description": "Severity of Pylint message type 'Convention/C'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pylintCategorySeverity.error": { + "type": "string", + "default": "Error", + "description": "Severity of Pylint message type 'Error/E'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pylintCategorySeverity.fatal": { + "type": "string", + "default": "Error", + "description": "Severity of Pylint message type 'Fatal/F'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pylintCategorySeverity.refactor": { + "type": "string", + "default": "Hint", + "description": "Severity of Pylint message type 'Refactor/R'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pylintCategorySeverity.warning": { + "type": "string", + "default": "Warning", + "description": "Severity of Pylint message type 'Warning/W'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pylintEnabled": { + "type": "boolean", + "default": true, + "description": "Whether to lint Python files using pylint.", + "scope": "resource" + }, + "python.linting.pylintPath": { + "type": "string", + "default": "pylint", + "description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pylintUseMinimalCheckers": { + "type": "boolean", + "default": true, + "description": "Whether to run Pylint with minimal set of rules.", + "scope": "resource" + }, + "python.pythonPath": { + "type": "string", + "default": "python", + "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.condaPath": { + "type": "string", + "default": "", + "description": "Path to the conda executable to use for activation (version 4.4+).", + "scope": "resource" + }, + "python.pipenvPath": { + "type": "string", + "default": "pipenv", + "description": "Path to the pipenv executable to use for activation.", + "scope": "resource" + }, + "python.sortImports.args": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.sortImports.path": { + "type": "string", + "description": "Path to isort script, default using inner version", + "default": "", + "scope": "resource" + }, + "python.terminal.activateEnvironment": { + "type": "boolean", + "default": true, + "description": "Activate Python Environment in Terminal created using the Extension.", + "scope": "resource" + }, + "python.terminal.executeInFileDir": { + "type": "boolean", + "default": false, + "description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", + "scope": "resource" + }, + "python.terminal.launchArgs": { + "type": "array", + "default": [], + "description": "Python launch arguments to use when executing a file in the terminal.", + "scope": "resource" + }, + "python.unitTest.cwd": { + "type": "string", + "default": null, + "description": "Optional working directory for unit tests.", + "scope": "resource" + }, + "python.unitTest.debugPort": { + "type": "number", + "default": 3000, + "description": "Port number used for debugging of unittests.", + "scope": "resource" + }, + "python.unitTest.nosetestArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.unitTest.nosetestsEnabled": { + "type": "boolean", + "default": false, + "description": "Enable unit testing using nosetests.", + "scope": "resource" + }, + "python.unitTest.nosetestPath": { + "type": "string", + "default": "nosetests", + "description": "Path to nosetests, you can use a custom version of nosetests by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.unitTest.promptToConfigure": { + "type": "boolean", + "default": true, + "description": "Prompt to configure a test framework if potential tests directories are discovered.", + "scope": "resource" + }, + "python.unitTest.pyTestArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.unitTest.pyTestEnabled": { + "type": "boolean", + "default": false, + "description": "Enable unit testing using pytest.", + "scope": "resource" + }, + "python.unitTest.pyTestPath": { + "type": "string", + "default": "pytest", + "description": "Path to pytest (pytest), you can use a custom version of pytest by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.unitTest.unittestArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [ + "-v", + "-s", + ".", + "-p", + "*test*.py" + ], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.unitTest.unittestEnabled": { + "type": "boolean", + "default": false, + "description": "Enable unit testing using unittest.", + "scope": "resource" + }, + "python.unitTest.autoTestDiscoverOnSaveEnabled": { + "type": "boolean", + "default": true, + "description": "Enable auto run test discovery when saving a unit test file.", + "scope": "resource" + }, + "python.venvFolders": { + "type": "array", + "default": [ + "envs", + ".pyenv", + ".direnv" + ], + "description": "Folders in your home directory to look into for virtual environments.", + "scope": "resource", + "items": { + "type": "string" + } + }, + "python.venvPath": { + "type": "string", + "default": "", + "description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", + "scope": "resource" + }, + "python.workspaceSymbols.ctagsPath": { + "type": "string", + "default": "ctags", + "description": "Fully qualified path to the ctags executable (else leave as ctags, assuming it is in current path).", + "scope": "resource" + }, + "python.workspaceSymbols.enabled": { + "type": "boolean", + "default": true, + "description": "Set to 'false' to disable Workspace Symbol provider using ctags.", + "scope": "resource" + }, + "python.workspaceSymbols.exclusionPatterns": { + "type": "array", + "default": [ + "**/site-packages/**" + ], + "items": { + "type": "string" + }, + "description": "Pattern used to exclude files and folders from ctags See http://ctags.sourceforge.net/ctags.html.", + "scope": "resource" + }, + "python.workspaceSymbols.rebuildOnFileSave": { + "type": "boolean", + "default": true, + "description": "Whether to re-build the tags file on when changes made to python files are saved.", + "scope": "resource" + }, + "python.workspaceSymbols.rebuildOnStart": { + "type": "boolean", + "default": true, + "description": "Whether to re-build the tags file on start (defaults to true).", + "scope": "resource" + }, + "python.workspaceSymbols.tagFilePath": { + "type": "string", + "default": "${workspaceFolder}/.vscode/tags", + "description": "Fully qualified path to tag file (exuberant ctag file), used to provide workspace symbols.", + "scope": "resource" + } + } + }, + "languages": [ + { + "id": "pip-requirements", + "aliases": [ + "pip requirements", + "requirements.txt" + ], + "filenames": [ + "requirements.txt", + "constraints.txt", + "requirements.in" + ], + "filenamePatterns": [ + "*-requirements.txt", + "requirements-*.txt", + "constraints-*.txt", + "*-constraints.txt", + "*-requirements.in", + "requirements-*.in" + ], + "configuration": "./languages/pip-requirements.json" + }, + { + "id": "yaml", + "filenames": [ + ".condarc" + ] + }, + { + "id": "toml", + "filenames": [ + "Pipfile" + ] + }, + { + "id": "json", + "filenames": [ + "Pipfile.lock" + ] + }, + { + "id": "jinja", + "extensions": [ + ".jinja2", + ".j2" + ], + "aliases": [ + "Jinja" + ] + }, + { + "id": "jupyter", + "extensions": [ + ".ipynb" + ] + } + ], + "grammars": [ + { + "language": "pip-requirements", + "scopeName": "source.pip-requirements", + "path": "./syntaxes/pip-requirements.tmLanguage.json" + } + ], + "jsonValidation": [ + { + "fileMatch": ".condarc", + "url": "./schemas/condarc.json" + }, + { + "fileMatch": "environment.yml", + "url": "./schemas/conda-environment.json" + }, + { + "fileMatch": "meta.yaml", + "url": "./schemas/conda-meta.json" + } + ], + "yamlValidation": [ + { + "fileMatch": ".condarc", + "url": "./schemas/condarc.json" + }, + { + "fileMatch": "environment.yml", + "url": "./schemas/conda-environment.json" + }, + { + "fileMatch": "meta.yaml", + "url": "./schemas/conda-meta.json" + } + ], + "views": { + "test": [ + { + "id": "python_tests", + "name": "PYTHON", + "when": "testsDiscovered" + } + ] + } + }, + "scripts": { + "package": "gulp clean && gulp prePublishBundle && vsce package", + "compile": "tsc -watch -p ./", + "compile-webviews-watch": "npx webpack --config webpack.datascience-ui.config.js --watch", + "dump-datascience-webpack-stats": "webpack --config webpack.datascience-ui.config.js --profile --json > tmp/ds-stats.json", + "compile-webviews": "gulp compile-webviews", + "compile-webviews-verbose": "npx webpack --config webpack.datascience-ui.config.js", + "postinstall": "node ./node_modules/vscode/bin/install && node ./build/ci/postInstall.js", + "test": "node ./out/test/standardTest.js && node ./out/test/multiRootTest.js", + "test:unittests": "mocha --require source-map-support/register --opts ./build/.mocha.unittests.opts", + "test:unittests:cover": "nyc --nycrc-path ./build/.nycrc npm run test:unittests", + "test:functional": "mocha --require source-map-support/register --opts ./build/.mocha.functional.opts", + "test:functional:cover": "nyc --nycrc-path ./build/.nycrc npm run test:functional", + "testDebugger": "node ./out/test/debuggerTest.js", + "testSingleWorkspace": "node ./out/test/standardTest.js", + "testMultiWorkspace": "node ./out/test/multiRootTest.js", + "testPerformance": "node ./out/test/performanceTest.js", + "testSmoke": "node ./out/test/smokeTest.js", + "lint-staged": "node gulpfile.js", + "lint": "tslint src/**/*.ts -t verbose", + "clean": "gulp clean", + "cover:enable": "gulp cover:enable", + "debugger-coverage": "gulp debugger-coverage", + "cover:inlinesource": "gulp inlinesource", + "updateBuildNumber": "gulp updateBuildNumber" + }, + "dependencies": { + "@jupyterlab/services": "^3.1.4", + "arch": "^2.1.0", + "azure-storage": "^2.10.1", + "diff-match-patch": "^1.0.0", + "file-matcher": "^1.3.0", + "fs-extra": "^4.0.3", + "fuzzy": "^0.1.3", + "get-port": "^3.2.0", + "glob": "^7.1.2", + "iconv-lite": "^0.4.21", + "inversify": "^4.11.1", + "line-by-line": "^0.1.6", + "lodash": "^4.17.11", + "md5": "^2.2.1", + "minimatch": "^3.0.4", + "named-js-regexp": "^1.3.3", + "node-stream-zip": "^1.6.0", + "pidusage": "^1.2.0", + "reflect-metadata": "^0.1.12", + "request": "^2.87.0", + "request-progress": "^3.0.0", + "rxjs": "^5.5.9", + "semver": "^5.5.0", + "stack-trace": "0.0.10", + "strip-json-comments": "^2.0.1", + "sudo-prompt": "^8.2.0", + "tmp": "^0.0.29", + "tree-kill": "^1.2.0", + "typescript-char": "^0.0.0", + "uint64be": "^1.0.1", + "unicode": "^10.0.0", + "untildify": "^3.0.2", + "vscode-debugadapter": "^1.28.0", + "vscode-debugprotocol": "^1.28.0", + "vscode-extension-telemetry": "^0.1.0", + "vscode-languageclient": "^4.4.0", + "vscode-languageserver": "^4.4.0", + "vscode-languageserver-protocol": "^3.10.3", + "vsls": "^0.3.967", + "winreg": "^1.2.4", + "xml2js": "^0.4.19" + }, + "devDependencies": { + "@babel/core": "^7.1.0", + "@babel/preset-env": "^7.1.0", + "@babel/preset-react": "^7.0.0", + "@nteract/transform-dataresource": "^4.3.5", + "@nteract/transform-geojson": "^3.2.3", + "@nteract/transform-model-debug": "^3.2.3", + "@nteract/transform-plotly": "^3.2.3", + "@nteract/transforms": "^4.4.4", + "@types/chai": "^4.1.2", + "@types/chai-arrays": "^1.0.2", + "@types/chai-as-promised": "^7.1.0", + "@types/copy-webpack-plugin": "^4.4.2", + "@types/del": "^3.0.0", + "@types/diff-match-patch": "^1.0.32", + "@types/download": "^6.2.2", + "@types/enzyme": "^3.1.14", + "@types/enzyme-adapter-react-16": "^1.0.3", + "@types/event-stream": "^3.3.33", + "@types/fs-extra": "^5.0.1", + "@types/get-port": "^3.2.0", + "@types/glob": "^5.0.35", + "@types/html-webpack-plugin": "^3.2.0", + "@types/iconv-lite": "^0.0.1", + "@types/istanbul": "^0.4.29", + "@types/jsdom": "^11.12.0", + "@types/loader-utils": "^1.1.3", + "@types/lodash": "^4.14.104", + "@types/md5": "^2.1.32", + "@types/mocha": "^2.2.48", + "@types/node": "9.4.7", + "@types/promisify-node": "^0.4.0", + "@types/react": "^16.4.14", + "@types/react-codemirror": "^1.0.2", + "@types/react-dom": "^16.0.8", + "@types/react-json-tree": "^0.6.8", + "@types/request": "^2.47.0", + "@types/semver": "^5.5.0", + "@types/shortid": "^0.0.29", + "@types/sinon": "^4.3.0", + "@types/stack-trace": "0.0.29", + "@types/strip-json-comments": "0.0.30", + "@types/temp": "^0.8.32", + "@types/tmp": "0.0.33", + "@types/untildify": "^3.0.0", + "@types/uuid": "^3.4.3", + "@types/webpack-bundle-analyzer": "^2.13.0", + "@types/winreg": "^1.2.30", + "@types/xml2js": "^0.4.2", + "JSONStream": "^1.3.2", + "ansi-to-html": "^0.6.7", + "awesome-typescript-loader": "^5.2.1", + "babel-loader": "^8.0.3", + "babel-plugin-inline-json-import": "^0.3.1", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-polyfill": "^6.26.0", + "chai": "^4.1.2", + "chai-arrays": "^2.0.0", + "chai-as-promised": "^7.1.1", + "codecov": "^3.0.0", + "colors": "^1.2.1", + "copy-webpack-plugin": "^4.6.0", + "cross-spawn": "^6.0.5", + "css-loader": "^1.0.1", + "decache": "^4.4.0", + "del": "^3.0.0", + "download": "^7.0.0", + "enzyme": "^3.7.0", + "enzyme-adapter-react-16": "^1.6.0", + "event-stream": "3.3.4", + "file-loader": "^2.0.0", + "flat": "^4.0.0", + "gulp": "^4.0.0", + "gulp-azure-storage": "^0.9.0", + "gulp-debounced-watch": "^1.0.4", + "gulp-filter": "^5.1.0", + "gulp-inline-source": "^3.2.0", + "gulp-json-editor": "^2.2.2", + "gulp-rename": "^1.4.0", + "gulp-sourcemaps": "^2.6.4", + "gulp-typescript": "^4.0.1", + "gulp-watch": "^5.0.0", + "html-webpack-plugin": "^3.2.0", + "husky": "^1.1.2", + "is-running": "^2.1.0", + "istanbul": "^0.4.5", + "jsdom": "^12.2.0", + "json-loader": "^0.5.7", + "loader-utils": "^1.1.0", + "mocha": "^5.0.4", + "mocha-junit-reporter": "^1.17.0", + "node-has-native-dependencies": "^1.0.2", + "nyc": "^13.1.0", + "raw-loader": "^0.5.1", + "react": "^16.5.2", + "react-codemirror": "^1.0.0", + "react-dev-utils": "^5.0.2", + "react-dom": "^16.5.2", + "react-json-tree": "^0.11.0", + "relative": "^3.0.2", + "remap-istanbul": "^0.10.1", + "retyped-diff-match-patch-tsd-ambient": "^1.0.0-0", + "rewiremock": "^3.13.0", + "shortid": "^2.2.8", + "style-loader": "^0.23.1", + "styled-jsx": "^3.1.0", + "svg-inline-loader": "^0.8.0", + "svg-inline-react": "^3.1.0", + "ts-loader": "^5.3.0", + "ts-mockito": "^2.3.1", + "tsconfig-paths-webpack-plugin": "^3.2.0", + "tslint": "^5.9.1", + "tslint-eslint-rules": "^5.1.0", + "tslint-microsoft-contrib": "^5.0.3", + "typed-react-markdown": "^0.1.0", + "typemoq": "^2.1.0", + "typescript": "^3.2.2", + "typescript-formatter": "^7.1.0", + "url-loader": "^1.1.1", + "uuid": "^3.3.2", + "vscode": "^1.1.30", + "vscode-debugadapter-testsupport": "^1.27.0", + "webpack": "^4.20.2", + "webpack-bundle-analyzer": "^3.0.3", + "webpack-cli": "^3.1.2", + "webpack-fix-default-import-plugin": "^1.0.3", + "webpack-merge": "^4.1.4", + "webpack-node-externals": "^1.7.2", + "yargs": "^12.0.2" + }, + "__metadata": { + "id": "f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5", + "publisherDisplayName": "Microsoft", + "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" + } +} diff --git a/plugins/recommendations-plugin/tests/_data/analyzer/no-contributes-languages-id.json b/plugins/recommendations-plugin/tests/_data/analyzer/no-contributes-languages-id.json new file mode 100644 index 0000000000..fda3c6db24 --- /dev/null +++ b/plugins/recommendations-plugin/tests/_data/analyzer/no-contributes-languages-id.json @@ -0,0 +1,14 @@ +{ + "name": "no-contributes-languages-id", + "publisher": "test", + "contributes": { + "languages": [ + { + "extensions": [ + ".class" + ], + "configuration": "./language-configuration.json" + } + ] + } +} diff --git a/plugins/recommendations-plugin/tests/_data/analyzer/no-contributes-languages.json b/plugins/recommendations-plugin/tests/_data/analyzer/no-contributes-languages.json new file mode 100644 index 0000000000..c0b78d4ce8 --- /dev/null +++ b/plugins/recommendations-plugin/tests/_data/analyzer/no-contributes-languages.json @@ -0,0 +1,21 @@ +{ + "name": "no-contributes-java", + "publisher": "test", + "contributes": { + "configuration": { + "type": "object", + "title": "Java", + "properties": { + "java.home": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Specifies the folder path to the JDK (8 or more recent) used to launch the Java Language Server.\nOn Windows, backslashes must be escaped, i.e.\n\"java.home\":\"C:\\\\Program Files\\\\Java\\\\jdk1.8.0_161\"", + "scope": "window" + } + } + } + } + } diff --git a/plugins/recommendations-plugin/tests/_data/analyzer/no-contributes.json b/plugins/recommendations-plugin/tests/_data/analyzer/no-contributes.json new file mode 100644 index 0000000000..832881b667 --- /dev/null +++ b/plugins/recommendations-plugin/tests/_data/analyzer/no-contributes.json @@ -0,0 +1,4 @@ +{ + "name": "no-contribute", + "publisher": "test" + } diff --git a/plugins/recommendations-plugin/tests/_data/analyzer/redhat-java.json b/plugins/recommendations-plugin/tests/_data/analyzer/redhat-java.json new file mode 100644 index 0000000000..10aef70829 --- /dev/null +++ b/plugins/recommendations-plugin/tests/_data/analyzer/redhat-java.json @@ -0,0 +1,799 @@ +{ + "name": "java", + "displayName": "Language Support for Java(TM) by Red Hat", + "description": "Java Linting, Intellisense, formatting, refactoring, Maven/Gradle support and more...", + "author": "Red Hat", + "icon": "icons/icon128.png", + "license": "EPL-2.0", + "version": "0.63.0", + "publisher": "redhat", + "bugs": "https://github.com/redhat-developer/vscode-java/issues", + "preview": true, + "enableProposedApi": false, + "engines": { + "vscode": "^1.44.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/redhat-developer/vscode-java" + }, + "categories": [ + "Programming Languages", + "Linters", + "Formatters", + "Snippets" + ], + "keywords": [ + "multi-root ready" + ], + "activationEvents": [ + "onLanguage:java", + "workspaceContains:pom.xml", + "workspaceContains:build.gradle", + "workspaceContains:.classpath", + "onCommand:java.project.import" + ], + "main": "./dist/extension", + "contributes": { + "languages": [ + { + "id": "java", + "extensions": [ + ".class" + ], + "configuration": "./language-configuration.json" + } + ], + "snippets": [ + { + "language": "java", + "path": "./snippets/java.json" + } + ], + "jsonValidation": [ + { + "fileMatch": "package.json", + "url": "./schemas/package.schema.json" + } + ], + "configuration": { + "type": "object", + "title": "Java", + "properties": { + "java.home": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Specifies the folder path to the JDK (8 or more recent) used to launch the Java Language Server.\nOn Windows, backslashes must be escaped, i.e.\n\"java.home\":\"C:\\\\Program Files\\\\Java\\\\jdk1.8.0_161\"", + "scope": "window" + }, + "java.jdt.ls.vmargs": { + "type": [ + "string", + "null" + ], + "default": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m", + "description": "Specifies extra VM arguments used to launch the Java Language Server. Eg. use `-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m ` to optimize memory usage for container environments with the parallel garbage collector", + "scope": "window" + }, + "java.errors.incompleteClasspath.severity": { + "type": [ + "string" + ], + "enum": [ + "ignore", + "info", + "warning", + "error" + ], + "default": "warning", + "description": "Specifies the severity of the message when the classpath is incomplete for a Java file", + "scope": "window" + }, + "java.configuration.checkProjectSettingsExclusions": { + "type": "boolean", + "default": true, + "description": "Checks if the extension-generated project settings files (.project, .classpath, .factorypath, .settings/) should be excluded from the file explorer.", + "scope": "window" + }, + "java.configuration.updateBuildConfiguration": { + "type": [ + "string" + ], + "enum": [ + "disabled", + "interactive", + "automatic" + ], + "default": "interactive", + "description": "Specifies how modifications on build files update the Java classpath/configuration", + "scope": "window" + }, + "java.trace.server": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "description": "Traces the communication between VS Code and the Java language server.", + "scope": "window" + }, + "java.import.maven.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable the Maven importer.", + "scope": "window" + }, + "java.import.gradle.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable the Gradle importer.", + "scope": "window" + }, + "java.import.gradle.wrapper.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable the Gradle wrapper.", + "scope": "window" + }, + "java.import.gradle.offline.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable the Gradle offline mode.", + "scope": "window" + }, + "java.import.gradle.version": { + "type": "string", + "default": null, + "description": "Gradle version, used if the gradle wrapper is missing or disabled.", + "scope": "window" + }, + "java.import.gradle.arguments": { + "type": "string", + "default": null, + "description": "Arguments to pass to Gradle.", + "scope": "window" + }, + "java.import.gradle.jvmArguments": { + "type": "string", + "default": null, + "description": "JVM arguments to pass to Gradle.", + "scope": "window" + }, + "java.import.gradle.home": { + "type": "string", + "default": null, + "description": "Setting for GRADLE_HOME.", + "scope": "window" + }, + "java.import.gradle.user.home": { + "type": "string", + "default": null, + "description": "Setting for GRADLE_USER_HOME.", + "scope": "window" + }, + "java.maven.downloadSources": { + "type": "boolean", + "default": false, + "description": "Enable/disable eager download of Maven source artifacts.", + "scope": "window" + }, + "java.maven.updateSnapshots": { + "type": "boolean", + "default": false, + "description": "Force update of Snapshots/Releases.", + "scope": "window" + }, + "java.referencesCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable the references code lens.", + "scope": "window" + }, + "java.signatureHelp.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable the signature help.", + "scope": "window" + }, + "java.implementationsCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable the implementations code lens.", + "scope": "window" + }, + "java.configuration.maven.userSettings": { + "type": "string", + "default": null, + "description": "Path to Maven's settings.xml", + "scope": "window" + }, + "java.format.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable default Java formatter", + "scope": "window" + }, + "java.saveActions.organizeImports": { + "type": "boolean", + "default": false, + "description": "Enable/disable auto organize imports on save action", + "scope": "window" + }, + "java.import.exclusions": { + "type": "array", + "description": "Configure glob patterns for excluding folders. Use `!` to negate patterns to allow subfolders imports. You have to include a parent directory. The order is important.", + "default": [ + "**/node_modules/**", + "**/.metadata/**", + "**/archetype-resources/**", + "**/META-INF/maven/**" + ], + "scope": "window" + }, + "java.project.referencedLibraries": { + "type": [ + "array", + "object" + ], + "description": "Configure glob patterns for referencing local libraries to a Java project.", + "default": [ + "lib/**/*.jar" + ], + "properties": { + "include": { + "type": "array" + }, + "exclude": { + "type": "array" + }, + "sources": { + "type": "object" + } + }, + "required": [ + "include" + ], + "additionalProperties": false, + "scope": "window" + }, + "java.contentProvider.preferred": { + "type": "string", + "description": "Preferred content provider (a 3rd party decompiler id, usually)", + "default": null, + "scope": "window" + }, + "java.autobuild.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable the 'auto build'", + "scope": "window" + }, + "java.maxConcurrentBuilds": { + "type": "integer", + "default": 1, + "description": "Max simultaneous project builds", + "scope": "window", + "minimum": 1 + }, + "java.completion.maxResults": { + "type": "integer", + "default": 50, + "description": "Maximum number of completion results (not including snippets).\nSetting 0 will disable the limit and return all results. Be aware the performance will be very negatively impacted.", + "scope": "window" + }, + "java.completion.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable code completion support", + "scope": "window" + }, + "java.completion.overwrite": { + "type": "boolean", + "default": true, + "description": "When set to true, code completion overwrites the current text. When set to false, code is simply added instead.", + "scope": "window" + }, + "java.completion.guessMethodArguments": { + "type": "boolean", + "default": false, + "description": "When set to true, method arguments are guessed when a method is selected from as list of code assist proposals.", + "scope": "window" + }, + "java.completion.favoriteStaticMembers": { + "type": "array", + "description": "Defines a list of static members or types with static members. Content assist will propose those static members even if the import is missing.", + "default": [ + "org.junit.Assert.*", + "org.junit.Assume.*", + "org.junit.jupiter.api.Assertions.*", + "org.junit.jupiter.api.Assumptions.*", + "org.junit.jupiter.api.DynamicContainer.*", + "org.junit.jupiter.api.DynamicTest.*", + "org.mockito.Mockito.*", + "org.mockito.ArgumentMatchers.*", + "org.mockito.Answers.*" + ], + "scope": "window" + }, + "java.completion.filteredTypes": { + "type": "array", + "description": "Defines the type filters. All types whose fully qualified name matches the selected filter strings will be ignored in content assist or quick fix proposals and when organizing imports. For example 'java.awt.*' will hide all types from the awt packages.", + "default": [ + "java.awt.*", + "com.sun.*" + ], + "scope": "window" + }, + "java.completion.importOrder": { + "type": "array", + "description": "Defines the sorting order of import statements. A package or type name prefix (e.g. 'org.eclipse') is a valid entry. An import is always added to the most specific group.", + "default": [ + "java", + "javax", + "com", + "org" + ], + "scope": "window" + }, + "java.foldingRange.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable smart folding range support. If disabled, it will use the default indentation-based folding range provided by VS Code.", + "scope": "window" + }, + "java.progressReports.enabled": { + "type": "boolean", + "description": "[Experimental] Enable/disable progress reports from background processes on the server.", + "default": true, + "scope": "window" + }, + "java.format.settings.url": { + "type": "string", + "description": "Specifies the url or file path to the [Eclipse formatter xml settings](https://github.com/redhat-developer/vscode-java/wiki/Formatter-settings).", + "default": null, + "scope": "window" + }, + "java.format.settings.profile": { + "type": "string", + "description": "Optional formatter profile name from the Eclipse formatter settings.", + "default": null, + "scope": "window" + }, + "java.format.comments.enabled": { + "type": "boolean", + "description": "Includes the comments during code formatting.", + "default": true, + "scope": "window" + }, + "java.format.onType.enabled": { + "type": "boolean", + "description": "Enable/disable automatic block formatting when typing `;`, `` or `}`", + "default": true, + "scope": "window" + }, + "java.codeGeneration.hashCodeEquals.useJava7Objects": { + "type": "boolean", + "description": "Use Objects.hash and Objects.equals when generating the hashCode and equals methods. This setting only applies to Java 7 and higher.", + "default": false, + "scope": "window" + }, + "java.codeGeneration.hashCodeEquals.useInstanceof": { + "type": "boolean", + "description": "Use 'instanceof' to compare types when generating the hashCode and equals methods.", + "default": false, + "scope": "window" + }, + "java.codeGeneration.useBlocks": { + "type": "boolean", + "description": "Use blocks in 'if' statements when generating the methods.", + "default": false, + "scope": "window" + }, + "java.codeGeneration.generateComments": { + "type": "boolean", + "description": "Generate method comments when generating the methods.", + "default": false, + "scope": "window" + }, + "java.codeGeneration.toString.template": { + "type": "string", + "description": "The template for generating the toString method.", + "default": "${object.className} [${member.name()}=${member.value}, ${otherMembers}]" + }, + "java.codeGeneration.toString.codeStyle": { + "type": "string", + "enum": [ + "STRING_CONCATENATION", + "STRING_BUILDER", + "STRING_BUILDER_CHAINED", + "STRING_FORMAT" + ], + "enumDescriptions": [ + "String concatenation", + "StringBuilder/StringBuffer", + "StringBuilder/StringBuffer - chained call", + "String.format/MessageFormat" + ], + "description": "The code style for generating the toString method.", + "default": "STRING_CONCATENATION" + }, + "java.codeGeneration.toString.skipNullValues": { + "type": "boolean", + "description": "Skip null values when generating the toString method.", + "default": false, + "scope": "window" + }, + "java.codeGeneration.toString.listArrayContents": { + "type": "boolean", + "description": "List contents of arrays instead of using native toString().", + "default": true, + "scope": "window" + }, + "java.codeGeneration.toString.limitElements": { + "type": "integer", + "description": "Limit number of items in arrays/collections/maps to list, if 0 then list all.", + "default": 0, + "scope": "window" + }, + "java.selectionRange.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable Smart Selection support for Java. Disabling this option will not affect the VS Code built-in word-based and bracket-based smart selection.", + "scope": "window" + }, + "java.showBuildStatusOnStart.enabled": { + "type": "boolean", + "description": "Automatically show build status on startup.", + "default": false, + "scope": "window" + }, + "java.configuration.runtimes": { + "type": "array", + "description": "Map Java Execution Environments to local JDKs.", + "items": { + "type": "object", + "default": {}, + "required": [ + "path", + "name" + ], + "properties": { + "name": { + "type": "string", + "enum": [ + "J2SE-1.5", + "JavaSE-1.6", + "JavaSE-1.7", + "JavaSE-1.8", + "JavaSE-9", + "JavaSE-10", + "JavaSE-11", + "JavaSE-12", + "JavaSE-13", + "JavaSE-14" + ], + "description": "Java Execution Environment name. Must be unique." + }, + "path": { + "type": "string", + "description": "JDK path.\nOn Windows, backslashes must be escaped, i.e.\n\"path\":\"C:\\\\Program Files\\\\Java\\\\jdk1.8.0_161\"." + }, + "sources": { + "type": "string", + "description": "JDK sources path." + }, + "javadoc": { + "type": "string", + "description": "JDK javadoc path." + }, + "default": { + "type": "boolean", + "description": "Is default runtime? Only one runtime can be default." + } + }, + "additionalProperties": false + }, + "default": [], + "scope": "machine" + }, + "java.server.launchMode": { + "type": "string", + "enum": [ + "Standard", + "LightWeight", + "Hybrid" + ], + "enumDescriptions": [ + "Provides full features such as intellisense, refactoring, building, Maven/Gradle support etc.", + "Starts a syntax server with lower start-up cost. Only provides syntax features such as outline, navigation, javadoc, syntax errors.", + "Provides full features with better responsiveness. It starts a standard language server and a secondary syntax server. The syntax server provides syntax features until the standard server is ready." + ], + "description": "The launch mode for the Java extension", + "default": "Hybrid", + "scope": "window" + }, + "java.sources.organizeImports.starThreshold": { + "type": "integer", + "description": "Specifies the number of imports added before a star-import declaration is used.", + "default": 99, + "scope": "window" + }, + "java.sources.organizeImports.staticStarThreshold": { + "type": "integer", + "description": "Specifies the number of static imports added before a star-import declaration is used.", + "default": 99 + }, + "java.semanticHighlighting.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable the semantic highlighting.", + "scope": "window" + }, + "java.requirements.JDK11Warning": { + "type": "boolean", + "description": "Enable/disable a warning about the impending requirement of Java 11.", + "default": true, + "scope": "window" + }, + "java.refactor.renameFromFileExplorer": { + "type": "string", + "enum": [ + "never", + "autoApply", + "preview", + "prompt" + ], + "enumDescriptions": [ + "Don't enable refactoring for rename operations on File Explorer.", + "Always automatically update the imports and package declarations.", + "Always preview the changes before applying.", + "Ask user to confirm whether to bypass refactor preview." + ], + "description": "Specifies whether to update imports and package declarations when renaming files from File Explorer.", + "default": "prompt", + "scope": "window" + }, + "java.imports.gradle.wrapper.checksums": { + "type": "array", + "items": { + "type": "object", + "default": {}, + "required": [ + "sha256" + ], + "properties": { + "sha256": { + "type": "string", + "label": "SHA-256 checksum." + }, + "allowed": { + "type": "boolean", + "default": true, + "label": "Is allowed?" + } + }, + "additionalProperties": false, + "uniqueItems": true + }, + "description": "Defines allowed/disallowed SHA-256 checksums of Gradle Wrappers", + "default": [], + "scope": "application" + } + } + }, + "configurationDefaults": { + "[java]": { + "editor.suggest.snippetsPreventQuickSuggestions": false + } + }, + "commands": [ + { + "command": "java.server.mode.switch", + "title": "Switch to Standard mode", + "category": "Java" + }, + { + "command": "java.projectConfiguration.update", + "title": "Update project configuration", + "category": "Java" + }, + { + "command": "java.project.import", + "title": "Import Java projects in workspace", + "category": "Java" + }, + { + "command": "java.open.serverLog", + "title": "Open Java language server log file", + "category": "Java" + }, + { + "command": "java.open.clientLog", + "title": "Open Java extension log file", + "category": "Java" + }, + { + "command": "java.open.logs", + "title": "Open all log files", + "category": "Java" + }, + { + "command": "java.workspace.compile", + "title": "Force Java compilation", + "category": "Java" + }, + { + "command": "java.open.formatter.settings", + "title": "Open Java formatter settings", + "category": "Java" + }, + { + "command": "java.clean.workspace", + "title": "Clean the Java language server workspace", + "category": "Java" + }, + { + "command": "java.project.updateSourceAttachment", + "title": "Attach Source", + "category": "Java" + }, + { + "command": "java.project.addToSourcePath", + "title": "Add Folder to Java Source Path", + "category": "Java" + }, + { + "command": "java.project.removeFromSourcePath", + "title": "Remove Folder from Java Source Path", + "category": "Java" + }, + { + "command": "java.project.listSourcePaths", + "title": "List all Java source paths", + "category": "Java" + }, + { + "command": "java.show.server.task.status", + "title": "Show Build Job Status", + "category": "Java" + } + ], + "keybindings": [ + { + "command": "java.projectConfiguration.update", + "key": "shift+alt+u", + "when": "editorFocus" + }, + { + "command": "java.workspace.compile", + "key": "shift+alt+b" + }, + { + "command": "java.action.clipboardPasteAction", + "key": "ctrl+shift+v", + "mac": "cmd+shift+v", + "when": "javaLSReady && editorLangId == java" + } + ], + "menus": { + "explorer/context": [ + { + "command": "java.projectConfiguration.update", + "when": "resourceFilename =~ /(.*\\.gradle)|(pom.xml)$/", + "group": "1_javaactions" + }, + { + "when": "explorerResourceIsFolder&&javaLSReady", + "command": "java.project.addToSourcePath", + "group": "1_javaactions@1" + }, + { + "when": "explorerResourceIsFolder&&javaLSReady", + "command": "java.project.removeFromSourcePath", + "group": "1_javaactions@2" + } + ], + "editor/context": [ + { + "command": "java.project.updateSourceAttachment", + "when": "editorReadonly && editorLangId == java", + "group": "1_javaactions" + }, + { + "command": "java.projectConfiguration.update", + "when": "resourceFilename =~ /(.*\\.gradle)|(pom.xml)$/", + "group": "1_javaactions" + } + ], + "commandPalette": [ + { + "command": "java.projectConfiguration.update", + "when": "javaLSReady" + }, + { + "command": "java.project.import", + "when": "javaLSReady" + }, + { + "command": "java.workspace.compile", + "when": "javaLSReady" + }, + { + "command": "java.project.listSourcePaths", + "when": "javaLSReady" + }, + { + "command": "java.project.updateSourceAttachment", + "when": "false" + }, + { + "command": "java.project.addToSourcePath", + "when": "false" + }, + { + "command": "java.project.removeFromSourcePath", + "when": "false" + }, + { + "command": "java.show.server.task.status", + "when": "serverMode != LightWeight" + }, + { + "command": "java.server.mode.switch", + "when": "serverMode == LightWeight" + } + ] + } + }, + "resolutions": { + "minimist": "^1.2.5" + }, + "scripts": { + "preinstall": "npx npm-force-resolutions", + "vscode:prepublish": "webpack --mode production", + "compile": "tsc -p ./&webpack --mode development", + "watch": "webpack --mode development --watch --info-verbosity verbose", + "pretest": "npm run compile", + "test": "node ./out/test/runtest.js", + "build-server": "./node_modules/.bin/gulp build_server", + "watch-server": "./node_modules/.bin/gulp watch_server", + "tslint": "tslint -p ." + }, + "devDependencies": { + "@types/fs-extra": "^8.0.0", + "@types/glob": "5.0.30", + "@types/lodash.findindex": "^4.6.6", + "@types/mocha": "^5.2.5", + "@types/node": "^8.10.51", + "@types/vscode": "^1.44.0", + "@types/winston": "^2.4.4", + "gulp": "^4.0.0", + "gulp-decompress": "2.0.1", + "gulp-download": "0.0.1", + "lodash.findindex": "^4.6.0", + "lodash.template": ">=4.5.0", + "mocha": "^5.2.0", + "ts-loader": "^5.3.1", + "tslint": "^5.11.0", + "typescript": "^3.7.3", + "typescript-tslint-plugin": "^0.3.1", + "vscode-test": "^1.4.0", + "webpack": "^4.27.1", + "webpack-cli": "^3.1.2", + "minimist": ">=1.2.5" + }, + "dependencies": { + "vscode-languageclient": "6.0.0-next.9", + "find-java-home": "1.1.0", + "expand-home-dir": "^0.0.3", + "fs-extra": "^8.1.0", + "glob": "^7.1.3", + "winston": "^3.2.1", + "winston-daily-rotate-file": "^3.10.0" + } + } diff --git a/plugins/recommendations-plugin/tests/_data/analyzer/sonarlint.json b/plugins/recommendations-plugin/tests/_data/analyzer/sonarlint.json new file mode 100644 index 0000000000..6be115656f --- /dev/null +++ b/plugins/recommendations-plugin/tests/_data/analyzer/sonarlint.json @@ -0,0 +1,483 @@ +{ + "name": "sonarlint-vscode", + "displayName": "SonarLint", + "description": "SonarLint is an IDE extension that helps you detect and fix quality issues as you write code in JavaScript, TypeScript, Python, Java, HTML and PHP.", + "version": "1.16.0", + "icon": "images/sonarlint_wave_128px.png", + "publisher": "SonarSource", + "homepage": "http://www.sonarlint.org", + "repository": { + "type": "git", + "url": "https://github.com/SonarSource/sonarlint-vscode.git" + }, + "bugs": { + "url": "https://jira.sonarsource.com/browse/SLVSCODE" + }, + "license": "SEE LICENSE IN LICENSE.txt", + "engines": { + "vscode": "^1.37.0" + }, + "categories": [ + "Linters" + ], + "keywords": [ + "code analysis", + "linters" + ], + "qna": "https://community.sonarsource.com/c/help/sl", + "activationEvents": [ + "onLanguage:java", + "onLanguage:javascript", + "onLanguage:javascriptreact", + "onLanguage:typescript", + "onLanguage:typescriptreact", + "onLanguage:python", + "onLanguage:php", + "onLanguage:vue", + "onLanguage:html", + "onLanguage:jsp", + "onLanguage:apex", + "onLanguage:plsql", + "onCommand:SonarLint.UpdateAllBindings", + "onView:SonarLint.AllRules" + ], + "extensionDependency": [ + "typescript" + ], + "contributes": { + "configuration": { + "type": "object", + "title": "SonarLint", + "properties": { + "sonarlint.output.showAnalyzerLogs": { + "type": "boolean", + "default": false, + "description": "Show analyzer's logs in the SonarLint output.", + "scope": "window" + }, + "sonarlint.output.showVerboseLogs": { + "type": "boolean", + "default": false, + "description": "Enable verbose log level (for both SonarLint and analyzer) in the SonarLint output.", + "scope": "window" + }, + "sonarlint.trace.server": { + "default": "off", + "description": "Traces the communication between VS Code and the SonarLint language server.", + "scope": "window", + "anyOf": [ + { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off" + }, + { + "type": "object", + "properties": { + "verbosity": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off" + }, + "format": { + "type": "string", + "enum": [ + "text", + "json" + ], + "default": "text" + } + }, + "additionalProperties": false + } + ] + }, + "sonarlint.testFilePattern": { + "type": "string", + "default": "", + "markdownDescription": "Files whose name match this [glob pattern](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob) are considered as test files by analyzers. Most rules are *not* evaluated on test files. Example: `{**/test/**,**/*test*,**/*Test*}`", + "scope": "resource" + }, + "sonarlint.analyzerProperties": { + "type": "object", + "patternProperties": { + "^.*$": { + "type": "string", + "markdownDescription": "One entry value" + }, + "additionalProperties": false + }, + "markdownDescription": "Extra properties that could be passed to the code analyzers. e.g. `{\"sonar.javascript.globals\": \"xxx\"}`. See [documentation](https://redirect.sonarsource.com/doc/plugin-library.html) of each analyzers.", + "scope": "resource" + }, + "sonarlint.disableTelemetry": { + "type": "boolean", + "default": false, + "markdownDescription": "Disable sending anonymous usage statistics to SonarSource. Click [here](https://github.com/SonarSource/sonarlint-vscode/blob/master/telemetry-sample.md) to see a sample of the data that are collected.", + "scope": "window" + }, + "sonarlint.rules": { + "type": "object", + "scope": "application", + "default": {}, + "markdownDescription": "Customize applied rule set. This property contains a list of rules whose activation level differ from the one provided by default. See _SonarLint Rules_ view for the full list of available rules. In connected mode, this configuration is overridden by the projects's quality profile, as configured on server side.\n\nExample:\n\n \"sonarlint.rules\": {\n \"javascript:UnusedVariable\": {\n \"level\": \"off\",\n \"javascript:S3757\": {\n \"level\": \"on\"\n }\n }\n", + "patternProperties": { + "^[^:]+:[^:]+$": { + "type": "object", + "markdownDescription": "Property names are rule keys in the form: `repo:key`", + "properties": { + "level": { + "type": "string", + "anyOf": [ + "off", + "on" + ], + "markdownDescription": "When set to `off`, disable the rule. When set to `on`, enable the rule." + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "sonarlint.ls.javaHome": { + "type": "string", + "markdownDescription": "Path to a Java Runtime Environment (8 or more recent) used to launch the SonarLint Language Server. \n* On Windows, backslashes must be escaped, e.g. `C:\\\\Program Files\\\\Java\\\\jdk1.8.0_161` \n* On macOS, this path should include the `/Contents/Home` directory, e.g `/Library/Java/JavaVirtualMachines/jdk1.8.0_161.jdk/Contents/Home`", + "scope": "window" + }, + "sonarlint.ls.vmargs": { + "type": "string", + "markdownDescription": "Extra JVM arguments used to launch the SonarLint Language Server. e.g. `-Xmx1024m`", + "scope": "window" + }, + "sonarlint.connectedMode.servers": { + "deprecationMessage": "The setting is deprecated. Use `sonarlint.connectedMode.connections.sonarqube` or `sonarlint.connectedMode.connections.sonarcloud` instead.", + "type": "array", + "scope": "application", + "default": [], + "markdownDescription": "Configure one or more connection(s) to SonarQube/SonarCloud. For security reasons, the token should not be stored in SCM with workspace settings. The `serverId` can be any identifier and will be referenced in `#sonarlint.connectedMode.project#`.\n\nExample for SonarCloud:\n\n \"sonarlint.connectedMode.servers\": [\n {\n \"serverId\": \"my_orga_in_sonarcloud.io\",\n \"serverUrl\": \"https://sonarcloud.io\",\n \"organizationKey\": \"my_organization\",\n \"token\": \"V2VkIE1...\"\n }\n ]\n\nExample for SonarQube:\n\n \"sonarlint.connectedMode.servers\": [\n {\n \"serverId\": \"my_sonarqube\",\n \"serverUrl\": \"https://sonar.mycompany.com\",\n \"token\": \"V2VkIE1...\"\n }\n ]", + "items": { + "properties": { + "serverId": { + "type": "string", + "description": "A unique identifier for this server connection. Will be referenced from `#sonarlint.connectedMode.project#`" + }, + "serverUrl": { + "type": "string", + "description": "URL of the server. Use https://sonarcloud.io for SonarCloud." + }, + "token": { + "type": "string", + "description": "Token generated from My Account>Security in SonarQube/SonarCloud" + }, + "organizationKey": { + "type": "string", + "description": "Only used for SonarCloud" + } + }, + "additionalProperties": false + } + }, + "sonarlint.connectedMode.connections.sonarqube": { + "type": "array", + "scope": "application", + "default": [], + "markdownDescription": "Configure connection(s) to [SonarQube](https://sonarqube.org). Don't forget to also configure the project binding in `#sonarlint.connectedMode.project#`.\n\nExample:\n\n \"sonarlint.connectedMode.connections.sonarqube\": [\n {\n \"serverUrl\": \"https://sonar.mycompany.com\",\n \"token\": \"V2VkIE1...\"\n }\n ]\n\nSpecify a `connectionId` if you want to define multiple connections.", + "examples": [ + { + "serverUrl": "https://", + "token": "" + }, + { + "connectionId": "", + "serverUrl": "https://", + "token": "" + } + ], + "items": { + "type": "object", + "properties": { + "connectionId": { + "type": "string", + "description": "A unique identifier for this connection to be used as a reference in `#sonarlint.connectedMode.project#`. Only needed if you plan to use multiple connections to SonarQube/SonarCloud." + }, + "serverUrl": { + "type": "string", + "description": "URL of the server." + }, + "token": { + "type": "string", + "description": "Token generated from My Account>Security in SonarQube" + } + }, + "additionalProperties": false, + "required": [ + "serverUrl", + "token" + ] + } + }, + "sonarlint.connectedMode.connections.sonarcloud": { + "type": "array", + "scope": "application", + "default": [], + "markdownDescription": "Configure connection(s) to [SonarCloud](https://sonarcloud.io). Don't forget to also configure the project binding in `#sonarlint.connectedMode.project#`.\nIf you have projects in multiple SonarCloud organizations, simply declare multiple connections.\n\nExample:\n\n \"sonarlint.connectedMode.connections.sonarcloud\": [\n {\n \"organizationKey\": \"myOrg\",\n \"token\": \"V2VkIE1...\"\n }\n ]\n\nSpecify a `connectionId` if you want to define multiple connections.", + "examples": [ + { + "organizationKey": "", + "token": "" + }, + { + "connectionId": "", + "organizationKey": "", + "token": "" + } + ], + "items": { + "type": "object", + "properties": { + "connectionId": { + "type": "string", + "description": "A unique identifier for this connection to be used as a reference in `#sonarlint.connectedMode.project#`. Only needed if you plan to use multiple connections to SonarQube/SonarCloud." + }, + "organizationKey": { + "type": "string", + "description": "A SonarCloud organization key. If you want to bind different projects that are in different organizations, simply declare multiple connections." + }, + "token": { + "type": "string", + "description": "Token generated from [My Account>Security](https://sonarcloud.io/account/security/) in SonarCloud" + } + }, + "additionalProperties": false, + "required": [ + "organizationKey", + "token" + ] + } + }, + "sonarlint.connectedMode.project": { + "markdownDescription": "Bind the current workspace folder to a [SonarQube](https://sonarqube.org) or [SonarCloud](https://sonarcloud.io) project. Requires connection details to be defined in the setting `#sonarlint.connectedMode.connections.sonarqube#` or `#sonarlint.connectedMode.connections.sonarcloud#`.\n\nBinding a workspace folder to a project allows to use the same code analyzers, rules and configuration that are defined in the server, as well as issue suppressions.\n\nExample:\n\n \"sonarlint.connectedMode.project\": {\n \"projectKey\": \"my_project\"\n }\n\nSpecify the `connectionId` if you have defined multiple connections.", + "examples": [ + { + "projectKey": "" + }, + { + "connectionId": "", + "projectKey": "" + } + ], + "default": {}, + "anyOf": [ + { + "type": "object", + "properties": { + "serverId": { + "type": "string", + "description": "Identifier of the server connection declared in `#sonarlint.connectedMode.connections.sonarqube#` or `#sonarlint.connectedMode.connections.sonarcloud#`" + }, + "projectKey": { + "type": "string", + "description": "Key of the project in SonarQube/SonarCloud" + } + }, + "additionalProperties": false, + "required": [ + "serverId", + "projectKey" + ], + "deprecationMessage": "Replace `serverId` attribute by `connectionId`." + }, + { + "type": "object", + "properties": { + "connectionId": { + "type": "string", + "description": "Identifier of the server connection declared in `#sonarlint.connectedMode.connections.sonarqube#` or `#sonarlint.connectedMode.connections.sonarcloud#`" + }, + "projectKey": { + "type": "string", + "description": "Key of the project in SonarQube/SonarCloud (can be found on project homepage)" + } + }, + "additionalProperties": false, + "required": [ + "projectKey" + ] + } + ], + "scope": "resource" + } + } + }, + "commands": [ + { + "command": "SonarLint.UpdateAllBindings", + "title": "Update all project bindings to SonarQube/SonarCloud", + "category": "SonarLint" + }, + { + "command": "SonarLint.DeactivateRule", + "title": "Deactivate", + "icon": { + "light": "images/activation/light/cross.svg", + "dark": "images/activation/dark/cross.svg" + } + }, + { + "command": "SonarLint.ActivateRule", + "title": "Activate", + "icon": { + "light": "images/activation/light/check.svg", + "dark": "images/activation/dark/check.svg" + } + }, + { + "command": "SonarLint.ResetDefaultRule", + "title": "Reset", + "enablement": "view == SonarLint.AllRules" + }, + { + "command": "SonarLint.ShowAllRules", + "title": "All", + "enablement": "view == SonarLint.AllRules" + }, + { + "command": "SonarLint.ShowActiveRules", + "title": "Active", + "enablement": "view == SonarLint.AllRules" + }, + { + "command": "SonarLint.ShowInactiveRules", + "title": "Inactive", + "enablement": "view == SonarLint.AllRules" + }, + { + "command": "SonarLint.FindRuleByKey", + "title": "Find Rule By Key", + "enablement": "view == SonarLint.AllRules" + } + ], + "views": { + "explorer": [ + { + "id": "SonarLint.AllRules", + "name": "SonarLint Rules" + } + ] + }, + "menus": { + "view/title": [ + { + "command": "SonarLint.ShowAllRules", + "when": "view == SonarLint.AllRules", + "group": "navigation" + }, + { + "command": "SonarLint.ShowActiveRules", + "when": "view == SonarLint.AllRules", + "group": "navigation" + }, + { + "command": "SonarLint.ShowInactiveRules", + "when": "view == SonarLint.AllRules", + "group": "navigation" + }, + { + "command": "SonarLint.FindRuleByKey", + "when": "view == SonarLint.AllRules" + } + ], + "view/item/context": [ + { + "command": "SonarLint.DeactivateRule", + "when": "view == SonarLint.AllRules && viewItem == rule-on", + "group": "inline" + }, + { + "command": "SonarLint.ActivateRule", + "when": "view == SonarLint.AllRules && viewItem == rule-off", + "group": "inline" + } + ] + } + }, + "main": "./dist/extension", + "files": [ + "server/sonarlint-ls.jar", + "analyzers" + ], + "scripts": { + "vscode:prepublish": "node scripts/prepare.js && webpack --mode production", + "compile": "tsc -p ./", + "webpack": "webpack --mode development", + "pretest": "webpack --mode development && tsc -p ./", + "test": "node out/test/runTest.js", + "test-cov": "node out/test/runTest.js --coverage", + "prepare": "node scripts/prepare.js" + }, + "dependencies": { + "expand-home-dir": "0.0.3", + "find-java-home": "1.1.0", + "follow-redirects": "1.10.0", + "inly": "4.0.4", + "open": "6.0.0", + "path-exists": "3.0.0", + "compare-versions": "3.6.0", + "vscode-languageclient": "5.2.1" + }, + "devDependencies": { + "@types/chai": "^4.2.10", + "@types/follow-redirects": "1.8.0", + "@types/glob": "5.0.30", + "@types/mocha": "^5.2.5", + "@types/node": "^10.17.17", + "@types/vscode": "^1.37.0", + "chai": "^4.2.0", + "crypto": "^0.0.3", + "dateformat": "^2.0.0", + "del": "^2.2.2", + "expect.js": "^0.3.1", + "glob": "^7.1.6", + "gulp": "^4.0.2", + "gulp-artifactory-upload": "^1.4.0", + "gulp-bump": "^3.1.3", + "gulp-cli": "^2.2.0", + "gulp-download": "^0.0.1", + "gulp-rename": "^1.4.0", + "gulp-util": "^3.0.8", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.7", + "mocha": "^5.2.0", + "mocha-multi-reporters": "^1.1.7", + "sonarqube-scanner": "^2.5.0", + "through2": "^2.0.5", + "ts-loader": "6.0.4", + "typescript": "^3.8.3", + "vsce": "^1.74.0", + "vscode-test": "^1.3.0", + "webpack": "^4.42.0", + "webpack-cli": "3.3.6" + }, + "prettier": { + "jsxBracketSameLine": true, + "printWidth": 120, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "avoid", + "trailingComma": "none", + "bracketSpacing": true + } +} diff --git a/plugins/recommendations-plugin/tests/_data/fetch/featured.json b/plugins/recommendations-plugin/tests/_data/fetch/featured.json new file mode 100644 index 0000000000..ee9d23fe9b --- /dev/null +++ b/plugins/recommendations-plugin/tests/_data/fetch/featured.json @@ -0,0 +1,110 @@ +{ + "version": "1.0.0", + "featured": [ + { + "id": "redhat/java", + "onLanguages": [ + "java" + ], + "workspaceContains": [ + "pom.xml", + "build.gradle", + ".classpath" + ], + "contributes": { + "languages": [ + { + "id": "java", + "extensions": [ + ".class" + ] + } + ] + } + }, + { + "id": "redhat/vscode-yaml", + "onLanguages": [ + "yaml" + ], + "contributes": { + "languages": [ + { + "id": "yaml", + "extensions": [ + ".yml", + ".eyaml", + ".eyml", + ".yaml" + ] + } + ] + } + }, + { + "id": "ms-python/python", + "onLanguages": [ + "python", + "jupyter" + ], + "contributes": { + "languages": [ + { + "id": "pip-requirements", + "aliases": [ + "pip requirements", + "requirements.txt" + ], + "filenames": [ + "requirements.txt", + "constraints.txt", + "requirements.in" + ], + "filenamePatterns": [ + "*-requirements.txt", + "requirements-*.txt", + "constraints-*.txt", + "*-constraints.txt", + "*-requirements.in", + "requirements-*.in" + ] + }, + { + "id": "yaml", + "filenames": [ + ".condarc" + ] + }, + { + "id": "toml", + "filenames": [ + "Pipfile" + ] + }, + { + "id": "json", + "filenames": [ + "Pipfile.lock" + ] + }, + { + "id": "jinja", + "extensions": [ + ".jinja2", + ".j2" + ], + "aliases": [ + "Jinja" + ] + }, + { + "id": "jupyter", + "extensions": [ + ".ipynb" + ] + } + ] + } + } + ] +} diff --git a/plugins/recommendations-plugin/tests/_data/fetch/language-go.json b/plugins/recommendations-plugin/tests/_data/fetch/language-go.json new file mode 100644 index 0000000000..7323e15276 --- /dev/null +++ b/plugins/recommendations-plugin/tests/_data/fetch/language-go.json @@ -0,0 +1,32 @@ +[ + { + "category": "Programming Languages", + "ids": [ + "golang/go/latest" + ] + }, + { + "category": "Snippets", + "ids": [ + "golang/go/latest" + ] + }, + { + "category": "Linters", + "ids": [ + "golang/go/latest" + ] + }, + { + "category": "Debuggers", + "ids": [ + "golang/go/latest" + ] + }, + { + "category": "Formatters", + "ids": [ + "golang/go/latest" + ] + } +] diff --git a/plugins/recommendations-plugin/tests/_data/find/level1/folder3/foo.json b/plugins/recommendations-plugin/tests/_data/find/level1/folder3/foo.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/recommendations-plugin/tests/_data/find/level1/foo.java b/plugins/recommendations-plugin/tests/_data/find/level1/foo.java new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/recommendations-plugin/tests/_data/find/level1/foo.php b/plugins/recommendations-plugin/tests/_data/find/level1/foo.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/recommendations-plugin/tests/_data/find/level1/level2/bar.java b/plugins/recommendations-plugin/tests/_data/find/level1/level2/bar.java new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/recommendations-plugin/tests/_data/find/level1/level2/foo.py b/plugins/recommendations-plugin/tests/_data/find/level1/level2/foo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/recommendations-plugin/tests/analyzer/vscode-current-extensions.spec.ts b/plugins/recommendations-plugin/tests/analyzer/vscode-current-extensions.spec.ts new file mode 100644 index 0000000000..c44e837615 --- /dev/null +++ b/plugins/recommendations-plugin/tests/analyzer/vscode-current-extensions.spec.ts @@ -0,0 +1,128 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata'; + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as theia from '@theia/plugin'; + +import { Container } from 'inversify'; +import { VSCodeCurrentExtensions } from '../../src/analyzer/vscode-current-extensions'; + +describe('Test VSCodeCurrentExtensions', () => { + let container: Container; + + beforeEach(() => { + container = new Container(); + jest.mock('axios'); + container.bind(VSCodeCurrentExtensions).toSelf().inSingletonScope(); + }); + + test('analyze', async () => { + const redhatJavaPackageJsonRaw = await fs.readFile( + path.join(__dirname, '..', '_data', 'analyzer', 'redhat-java.json'), + 'utf8' + ); + const msPythonPackageJsonRaw = await fs.readFile( + path.join(__dirname, '..', '_data', 'analyzer', 'ms-python.json'), + 'utf8' + ); + const sonarLintPackageJsonRaw = await fs.readFile( + path.join(__dirname, '..', '_data', 'analyzer', 'sonarlint.json'), + 'utf8' + ); + const noContributesPackageJsonRaw = await fs.readFile( + path.join(__dirname, '..', '_data', 'analyzer', 'no-contributes.json'), + 'utf8' + ); + const noContributesLanguagePackageJsonRaw = await fs.readFile( + path.join(__dirname, '..', '_data', 'analyzer', 'no-contributes-languages.json'), + 'utf8' + ); + const noContributesLanguageIdPackageJsonRaw = await fs.readFile( + path.join(__dirname, '..', '_data', 'analyzer', 'no-contributes-languages-id.json'), + 'utf8' + ); + const existingJavaPackageJsonRaw = await fs.readFile( + path.join(__dirname, '..', '_data', 'analyzer', 'existing-java-language.json'), + 'utf8' + ); + + const redhatJavaPlugin = jest.fn() as any; + redhatJavaPlugin.packageJSON = JSON.parse(redhatJavaPackageJsonRaw); + + const msPythonPlugin = jest.fn() as any; + msPythonPlugin.packageJSON = JSON.parse(msPythonPackageJsonRaw); + + const sonarLintPlugin = jest.fn() as any; + sonarLintPlugin.packageJSON = JSON.parse(sonarLintPackageJsonRaw); + + const noContributesPlugin = jest.fn() as any; + noContributesPlugin.packageJSON = JSON.parse(noContributesPackageJsonRaw); + + const noContributesLanguagesPlugin = jest.fn() as any; + noContributesLanguagesPlugin.packageJSON = JSON.parse(noContributesLanguagePackageJsonRaw); + + const noContributesLanguagesIdPlugin = jest.fn() as any; + noContributesLanguagesIdPlugin.packageJSON = JSON.parse(noContributesLanguageIdPackageJsonRaw); + + const existingJavaPlugin = jest.fn() as any; + existingJavaPlugin.packageJSON = JSON.parse(existingJavaPackageJsonRaw); + + // add twice the redhatJava plug-in + theia.plugins.all = [ + redhatJavaPlugin, + redhatJavaPlugin, + msPythonPlugin, + sonarLintPlugin, + noContributesPlugin, + noContributesLanguagesPlugin, + noContributesLanguagesIdPlugin, + existingJavaPlugin, + ]; + theia.plugins.all.forEach(plugin => { + (plugin as any).id = `${plugin.packageJSON.publisher}/${plugin.packageJSON.name}`; + }); + + const vsCodeCurrentExtensions = container.get(VSCodeCurrentExtensions); + const vsCodeCurrentExtensionsLanguages = await vsCodeCurrentExtensions.analyze(); + expect(vsCodeCurrentExtensionsLanguages).toBeDefined(); + + // test plugins by languages + expect(vsCodeCurrentExtensionsLanguages.vscodeExtensionByLanguageId).toBeDefined(); + expect(vsCodeCurrentExtensionsLanguages.vscodeExtensionByLanguageId.has('java')).toBeTruthy(); + expect(vsCodeCurrentExtensionsLanguages.vscodeExtensionByLanguageId.has('javascript')).toBeTruthy(); + expect(vsCodeCurrentExtensionsLanguages.vscodeExtensionByLanguageId.has('python')).toBeTruthy(); + expect( + vsCodeCurrentExtensionsLanguages.vscodeExtensionByLanguageId.get('java')!.includes('redhat/java') + ).toBeTruthy(); + expect( + vsCodeCurrentExtensionsLanguages.vscodeExtensionByLanguageId.get('java')!.includes('SonarSource/sonarlint-vscode') + ).toBeTruthy(); + expect( + vsCodeCurrentExtensionsLanguages.vscodeExtensionByLanguageId.get('python')!.includes('ms-python/python') + ).toBeTruthy(); + expect( + vsCodeCurrentExtensionsLanguages.vscodeExtensionByLanguageId + .get('python')! + .includes('SonarSource/sonarlint-vscode') + ).toBeTruthy(); + + // test plugins by file extensions + expect(vsCodeCurrentExtensionsLanguages.languagesByFileExtensions).toBeDefined(); + expect(vsCodeCurrentExtensionsLanguages.languagesByFileExtensions.has('.class')).toBeTruthy(); + expect(vsCodeCurrentExtensionsLanguages.languagesByFileExtensions.has('.ipynb')).toBeTruthy(); + expect(vsCodeCurrentExtensionsLanguages.languagesByFileExtensions.get('.class')!.includes('java')).toBeTruthy(); + expect(vsCodeCurrentExtensionsLanguages.languagesByFileExtensions.get('.ipynb')!.includes('jupyter')).toBeTruthy(); + }); +}); diff --git a/plugins/recommendations-plugin/tests/devfile/devfile-handler.spec.ts b/plugins/recommendations-plugin/tests/devfile/devfile-handler.spec.ts new file mode 100644 index 0000000000..a2e7f8f993 --- /dev/null +++ b/plugins/recommendations-plugin/tests/devfile/devfile-handler.spec.ts @@ -0,0 +1,210 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata'; + +import * as che from '@eclipse-che/plugin'; + +import { Container } from 'inversify'; +import { DevfileHandler } from '../../src/devfile/devfile-handler'; +import { che as cheApi } from '@eclipse-che/api'; + +describe('Test DevfileHandler', () => { + let container: Container; + + const getCurrentWorkspace = jest.fn(); + + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + che.workspace.getCurrentWorkspace = getCurrentWorkspace; + container = new Container(); + container.bind(DevfileHandler).toSelf().inSingletonScope(); + }); + + describe('hasPlugins', () => { + test('hasPlugins true', async () => { + const redhatJavaPlugin: cheApi.workspace.devfile.Component = { + id: 'redhat/java/latest', + type: 'chePlugin', + }; + const anotherJavaPlugin: cheApi.workspace.devfile.Component = { + id: 'invalid', + type: 'chePlugin', + }; + + const editor: cheApi.workspace.devfile.Component = { + id: 'my-editor', + type: 'cheEditor', + }; + + const devfile: cheApi.workspace.devfile.Devfile = { + components: [editor, anotherJavaPlugin, redhatJavaPlugin], + }; + + const workspace: cheApi.workspace.Workspace = { + devfile, + }; + getCurrentWorkspace.mockReturnValue(workspace); + + const devfileHandler = container.get(DevfileHandler); + const hasPlugins = await devfileHandler.hasPlugins(); + expect(hasPlugins).toBeTruthy(); + }); + + test('hasPlugins false', async () => { + const devfile: cheApi.workspace.devfile.Devfile = {}; + + const workspace: cheApi.workspace.Workspace = { + devfile, + }; + getCurrentWorkspace.mockReturnValue(workspace); + + const devfileHandler = container.get(DevfileHandler); + const hasPlugins = await devfileHandler.hasPlugins(); + expect(hasPlugins).toBeFalsy(); + }); + }); + + describe('addPlugins', () => { + test('able to add plug-ins', async () => { + const devfile: cheApi.workspace.devfile.Devfile = {}; + + const id = '1234'; + const workspace: cheApi.workspace.Workspace = { + id, + devfile, + }; + getCurrentWorkspace.mockReturnValue(workspace); + + const devfileHandler = container.get(DevfileHandler); + const plugins = ['redhat/java']; + // before, no components + expect(devfile.components).toBeUndefined(); + + const updateMethod = jest.fn(); + che.workspace.update = updateMethod; + + const updateMock = updateMethod.mock; + await devfileHandler.addPlugins(plugins); + + // after, some components + expect(updateMethod).toBeCalled(); + expect(updateMock.calls[0][0]).toBe(id); + const workspaceProvided = updateMock.calls[0][1]; + expect(workspaceProvided.devfile.components).toBeDefined(); + expect(workspaceProvided.devfile.components.length).toBe(1); + expect(workspaceProvided.devfile.components[0].id).toBe('redhat/java/latest'); + expect(workspaceProvided.devfile.components[0].type).toBe('chePlugin'); + }); + }); + + describe('isRecommendedExtensionsDisabled', () => { + test('devfile with empty attributes has not disabled recommendations', async () => { + const devfile = {}; + const id = '1234'; + const workspace = { + id, + devfile, + }; + getCurrentWorkspace.mockReturnValue(workspace); + + const devfileHandler = container.get(DevfileHandler); + const isDisabled = await devfileHandler.isRecommendedExtensionsDisabled(); + + // after, some components + expect(isDisabled).toBeFalsy(); + }); + + test('devfile has disabled recommendations', async () => { + const devfile = { + attributes: {} as any, + }; + devfile.attributes[DevfileHandler.DISABLED_RECOMMENDATIONS_PROPERTY] = 'true'; + + const id = '1234'; + const workspace = { + id, + devfile, + }; + getCurrentWorkspace.mockReturnValue(workspace); + + const devfileHandler = container.get(DevfileHandler); + const isDisabled = await devfileHandler.isRecommendedExtensionsDisabled(); + + // after, some components + expect(isDisabled).toBeTruthy(); + }); + }); + + describe('isRecommendedExtensionsOpenFileEnabled', () => { + test('devfile with empty attributes has not open files recommendations', async () => { + const devfile = {}; + const id = '1234'; + const workspace = { + id, + devfile, + }; + getCurrentWorkspace.mockReturnValue(workspace); + + const devfileHandler = container.get(DevfileHandler); + const isEnabled = await devfileHandler.isRecommendedExtensionsOpenFileEnabled(); + + expect(isEnabled).toBeFalsy(); + }); + + test('devfile has enabled recommendations', async () => { + const devfile = { + attributes: {} as any, + }; + devfile.attributes[DevfileHandler.OPENFILES_RECOMMENDATIONS_PROPERTY] = 'true'; + + const id = '1234'; + const workspace = { + id, + devfile, + }; + getCurrentWorkspace.mockReturnValue(workspace); + + const devfileHandler = container.get(DevfileHandler); + const isEnabled = await devfileHandler.isRecommendedExtensionsOpenFileEnabled(); + + expect(isEnabled).toBeTruthy(); + }); + }); + + describe('disableRecommendations', () => { + test('devfile with no attributes is updated', async () => { + const devfile: any = {}; + const id = '1234'; + const workspace = { + id, + devfile, + }; + getCurrentWorkspace.mockReturnValue(workspace); + + const devfileHandler = container.get(DevfileHandler); + const updateMethod = jest.fn(); + const updateMock = updateMethod.mock; + che.workspace.update = updateMethod; + await devfileHandler.disableRecommendations(); + + // after, some components + expect(updateMethod).toBeCalled(); + expect(updateMock.calls[0][0]).toBe(id); + const workspaceProvided = updateMock.calls[0][1]; + expect(workspaceProvided.devfile.attributes).toBeDefined(); + expect(workspaceProvided.devfile.attributes[DevfileHandler.DISABLED_RECOMMENDATIONS_PROPERTY]).toBeDefined(); + expect(workspaceProvided.devfile.attributes[DevfileHandler.DISABLED_RECOMMENDATIONS_PROPERTY]).toBe('true'); + }); + }); +}); diff --git a/plugins/recommendations-plugin/tests/fetch/featured-fetcher.spec.ts b/plugins/recommendations-plugin/tests/fetch/featured-fetcher.spec.ts new file mode 100644 index 0000000000..22fbc4bb55 --- /dev/null +++ b/plugins/recommendations-plugin/tests/fetch/featured-fetcher.spec.ts @@ -0,0 +1,62 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata'; + +import * as fs from 'fs-extra'; +import * as path from 'path'; + +import { ChePluginRegistry } from '../../src/registry/che-plugin-registry'; +import { Container } from 'inversify'; +import { FeaturedFetcher } from '../../src/fetch/featured-fetcher'; +import axios from 'axios'; + +// import AxiosInstance from 'axios'; +describe('Test FeaturedFetcher', () => { + let container: Container; + + const chePluginRegistryGetUrlMock = jest.fn(); + const chePluginRegistry = { + getUrl: chePluginRegistryGetUrlMock, + } as any; + const fakeUrl = 'https://my.registry/v3'; + chePluginRegistryGetUrlMock.mockResolvedValue(fakeUrl); + + beforeEach(() => { + container = new Container(); + jest.mock('axios'); + container.bind(ChePluginRegistry).toConstantValue(chePluginRegistry); + container.bind(FeaturedFetcher).toSelf().inSingletonScope(); + }); + + test('get featured', async () => { + const json = await fs.readFile(path.join(__dirname, '..', '_data', 'fetch', 'featured.json'), 'utf8'); + (axios as any).__setContent('https://my.registry/v3/che-theia/featured.json', JSON.parse(json)); + + const featuredFetcher = container.get(FeaturedFetcher); + const featuredList = await featuredFetcher.fetch(); + expect(featuredList).toBeDefined(); + expect(featuredList.length).toBe(3); + + expect((axios as any).get).toBeCalledWith(`${fakeUrl}/che-theia/featured.json`); + }); + + test('failure', async () => { + (axios as any).__setError('https://my.registry/v3/che-theia/featured.json', 'invalid json'); + + const featuredFetcher = container.get(FeaturedFetcher); + const featuredList = await featuredFetcher.fetch(); + // no content + expect(featuredList).toBeDefined(); + expect(featuredList.length).toBe(0); + expect((axios as any).get).toBeCalledWith(`${fakeUrl}/che-theia/featured.json`); + }); +}); diff --git a/plugins/recommendations-plugin/tests/fetch/plugins-by-language-fetcher.spec.ts b/plugins/recommendations-plugin/tests/fetch/plugins-by-language-fetcher.spec.ts new file mode 100644 index 0000000000..b4f10de4cc --- /dev/null +++ b/plugins/recommendations-plugin/tests/fetch/plugins-by-language-fetcher.spec.ts @@ -0,0 +1,85 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata'; + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as theia from '@theia/plugin'; + +import { ChePluginRegistry } from '../../src/registry/che-plugin-registry'; +import { Container } from 'inversify'; +import { PluginsByLanguageFetcher } from '../../src/fetch/plugins-by-language-fetcher'; +import axios from 'axios'; + +describe('Test PluginsByLanguageFetcher', () => { + let container: Container; + + const chePluginRegistryGetUrlMock = jest.fn(); + const chePluginRegistry = { + getUrl: chePluginRegistryGetUrlMock, + } as any; + const fakeUrl = 'https://my.registry/v3'; + + beforeEach(() => { + container = new Container(); + jest.mock('axios'); + container.bind(ChePluginRegistry).toConstantValue(chePluginRegistry); + (axios as any).__clearMock(); + container.bind(PluginsByLanguageFetcher).toSelf().inSingletonScope(); + chePluginRegistryGetUrlMock.mockResolvedValue(fakeUrl); + }); + + test('check with language being there', async () => { + const json = await fs.readFile(path.join(__dirname, '..', '_data', 'fetch', 'language-go.json'), 'utf8'); + (axios as any).__setContent('https://my.registry/v3/che-theia/recommendations/language/go.json', JSON.parse(json)); + + const pluginsByLanguageFetcher = container.get(PluginsByLanguageFetcher); + const languagesByPlugins = await pluginsByLanguageFetcher.fetch('go'); + expect(languagesByPlugins).toBeDefined(); + expect(languagesByPlugins.length).toBe(5); + const programmingLanguages = languagesByPlugins.filter(plugin => plugin.category === 'Programming Languages'); + expect(programmingLanguages.length).toBe(1); + expect(programmingLanguages[0].ids).toEqual(['golang/go/latest']); + }); + + test('check with language not being there', async () => { + const error = { + response: { + status: 404, + }, + }; + (axios as any).__setError('https://my.registry/v3/che-theia/recommendations/language/foo.json', error); + + const pluginsByLanguageFetcher = container.get(PluginsByLanguageFetcher); + const languagesByPlugins = await pluginsByLanguageFetcher.fetch('foo'); + expect(languagesByPlugins).toBeDefined(); + expect(languagesByPlugins.length).toBe(0); + expect(theia.window.showInformationMessage as jest.Mock).toBeCalledTimes(0); + }); + + test('unexpected error', async () => { + const error = { + response: { + status: 500, + }, + }; + (axios as any).__setError('https://my.registry/v3/che-theia/recommendations/language/java.json', error); + + const pluginsByLanguageFetcher = container.get(PluginsByLanguageFetcher); + const languageByPlugins = await pluginsByLanguageFetcher.fetch('java'); + // no content + expect(languageByPlugins).toBeDefined(); + expect(languageByPlugins.length).toBe(0); + // notify the user + expect(theia.window.showInformationMessage as jest.Mock).toBeCalled(); + }); +}); diff --git a/plugins/recommendations-plugin/tests/find/find-file-extensions.spec.ts b/plugins/recommendations-plugin/tests/find/find-file-extensions.spec.ts new file mode 100644 index 0000000000..c33b0f100f --- /dev/null +++ b/plugins/recommendations-plugin/tests/find/find-file-extensions.spec.ts @@ -0,0 +1,67 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata'; + +import * as globby from 'globby'; +import * as path from 'path'; +import * as theia from '@theia/plugin'; + +import { Container } from 'inversify'; +import { FindFileExtensions } from '../../src/find/find-file-extensions'; + +describe('Test FindFile implementation', () => { + let container: Container; + + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + container = new Container(); + container.bind(FindFileExtensions).toSelf().inSingletonScope(); + }); + + test('find', async () => { + const findFileExtensions = container.get(FindFileExtensions); + const findPath = path.join(__dirname, '..', '_data', 'find'); + const uri = { path: findPath }; + const workspaceFolder = { uri } as theia.WorkspaceFolder; + const workspaceFolders: theia.WorkspaceFolder[] = [workspaceFolder]; + const fileExtensions = await findFileExtensions.find(workspaceFolders); + expect(fileExtensions).toBeDefined(); + expect(fileExtensions.length).toBe(4); + expect(fileExtensions.includes('.php')).toBeTruthy(); + expect(fileExtensions.includes('.java')).toBeTruthy(); + expect(fileExtensions.includes('.py')).toBeTruthy(); + expect(fileExtensions.includes('.json')).toBeTruthy(); + }); + + test('stop fast', async () => { + const findFileExtensions = container.get(FindFileExtensions); + const findPath = path.join(__dirname, '..', '_data', 'find'); + const fileExtensions = await findFileExtensions.findInFolder(findPath, 0); + expect(fileExtensions).toBeDefined(); + expect(fileExtensions.length >= 0).toBeTruthy(); + }); + + function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + test('multiple end event', async () => { + (globby as any).__setStreamEnd(); + const findFileExtensions = container.get(FindFileExtensions); + const findPath = path.join(__dirname, '..', '_data', 'find'); + const fileExtensions = await findFileExtensions.findInFolder(findPath, 100); + await sleep(1000); + expect(fileExtensions).toBeDefined(); + expect(fileExtensions.length).toBe(4); + }); +}); diff --git a/plugins/recommendations-plugin/tests/inject/inversify-bindings.spec.ts b/plugins/recommendations-plugin/tests/inject/inversify-bindings.spec.ts new file mode 100644 index 0000000000..a3257ad8a6 --- /dev/null +++ b/plugins/recommendations-plugin/tests/inject/inversify-bindings.spec.ts @@ -0,0 +1,59 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +import 'reflect-metadata'; + +import { ChePluginRegistry } from '../../src/registry/che-plugin-registry'; +import { Container } from 'inversify'; +import { DevfileHandler } from '../../src/devfile/devfile-handler'; +import { FeaturedFetcher } from '../../src/fetch/featured-fetcher'; +import { FeaturedPluginStrategy } from '../../src/strategy/featured-plugin-strategy'; +import { FindFileExtensions } from '../../src/find/find-file-extensions'; +import { InversifyBinding } from '../../src/inject/inversify-bindings'; +import { PluginsByLanguageFetcher } from '../../src/fetch/plugins-by-language-fetcher'; +import { RecommendPluginOpenFileStrategy } from '../../src/strategy/recommend-plugin-open-file-strategy'; +import { RecommendationsPlugin } from '../../src/plugin/recommendations-plugin'; +import { VSCodeCurrentExtensions } from '../../src/analyzer/vscode-current-extensions'; +import { WorkspaceHandler } from '../../src/workspace/workspace-handler'; + +describe('Test InversifyBinding', () => { + test('bindings', async () => { + const inversifyBinding = new InversifyBinding(); + const container: Container = inversifyBinding.initBindings(); + + expect(inversifyBinding).toBeDefined(); + + // check analyzer + expect(container.get(VSCodeCurrentExtensions)).toBeDefined(); + + // check devfile + expect(container.get(DevfileHandler)).toBeDefined(); + + // check fetch + expect(container.get(FeaturedFetcher)).toBeDefined(); + expect(container.get(PluginsByLanguageFetcher)).toBeDefined(); + + // check find + expect(container.get(FindFileExtensions)).toBeDefined(); + + // check strategy + expect(container.get(FeaturedPluginStrategy)).toBeDefined(); + expect(container.get(RecommendPluginOpenFileStrategy)).toBeDefined(); + + // check plugin + expect(container.get(RecommendationsPlugin)).toBeDefined(); + + // check registry + expect(container.get(ChePluginRegistry)).toBeDefined(); + + // check workspace + expect(container.get(WorkspaceHandler)).toBeDefined(); + }); +}); diff --git a/plugins/recommendations-plugin/tests/plugin.spec.ts b/plugins/recommendations-plugin/tests/plugin.spec.ts new file mode 100644 index 0000000000..86678382e4 --- /dev/null +++ b/plugins/recommendations-plugin/tests/plugin.spec.ts @@ -0,0 +1,44 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +import 'reflect-metadata'; + +import * as plugin from '../src/plugin'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Container } from 'inversify'; +import { InversifyBinding } from '../src/inject/inversify-bindings'; +import { RecommendationsPlugin } from '../src/plugin/recommendations-plugin'; + +describe('Test Plugin', () => { + jest.mock('../src/inject/inversify-bindings'); + let oldBindings: any; + let initBindings: jest.Mock; + + beforeEach(() => { + oldBindings = InversifyBinding.prototype.initBindings; + initBindings = jest.fn(); + InversifyBinding.prototype.initBindings = initBindings; + }); + + afterEach(() => { + InversifyBinding.prototype.initBindings = oldBindings; + }); + + test('basics', async () => { + const container = new Container(); + const morecommendationsPluginMock = { start: jest.fn(), stop: jest.fn() }; + container.bind(RecommendationsPlugin).toConstantValue(morecommendationsPluginMock as any); + initBindings.mockReturnValue(container); + + plugin.start(); + expect(morecommendationsPluginMock.start).toBeCalled(); + }); +}); diff --git a/plugins/recommendations-plugin/tests/plugin/recommendations-plugin.spec.ts b/plugins/recommendations-plugin/tests/plugin/recommendations-plugin.spec.ts new file mode 100644 index 0000000000..dce84adbbd --- /dev/null +++ b/plugins/recommendations-plugin/tests/plugin/recommendations-plugin.spec.ts @@ -0,0 +1,284 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata'; + +import * as theia from '@theia/plugin'; + +import { Container } from 'inversify'; +import { DevfileHandler } from '../../src/devfile/devfile-handler'; +import { FeaturedFetcher } from '../../src/fetch/featured-fetcher'; +import { FeaturedPluginStrategy } from '../../src/strategy/featured-plugin-strategy'; +import { FindFileExtensions } from '../../src/find/find-file-extensions'; +import { RecommendPluginOpenFileStrategy } from '../../src/strategy/recommend-plugin-open-file-strategy'; +import { RecommendationsPlugin } from '../../src/plugin/recommendations-plugin'; +import { VSCodeCurrentExtensions } from '../../src/analyzer/vscode-current-extensions'; +import { WorkspaceHandler } from '../../src/workspace/workspace-handler'; + +describe('Test recommendation Plugin', () => { + let container: Container; + + const findFileExtensions = { + find: jest.fn(), + } as any; + + const vsCodeCurrentExtensionsContent = { + languagesByFileExtensions: new Map(), + vscodeExtensionByLanguageId: new Map(), + }; + const analyzeVsCodeCurrentExtensionsMethod = jest.fn(); + const vsCodeCurrentExtensions = { + analyze: analyzeVsCodeCurrentExtensionsMethod, + } as any; + + const featuredFetcher = { + fetch: jest.fn(), + } as any; + + const devfileHandlerGetPluginsMock = jest.fn(); + const devfileHandlerHasPluginsMock = jest.fn(); + const devfileHandlerAddPluginsMock = jest.fn(); + const devfileHandlerIsRecommendedExtensionsDisabledMock = jest.fn(); + const devfileHandlerIsRecommendedExtensionsOpenFileEnabledMock = jest.fn(); + const devfileHandler = { + addPlugins: devfileHandlerAddPluginsMock, + getPlugins: devfileHandlerGetPluginsMock, + hasPlugins: devfileHandlerHasPluginsMock, + isRecommendedExtensionsOpenFileEnabled: devfileHandlerIsRecommendedExtensionsOpenFileEnabledMock, + isRecommendedExtensionsDisabled: devfileHandlerIsRecommendedExtensionsDisabledMock, + } as any; + + const restartWorkspaceHandlerMock = jest.fn(); + const workspaceHandler = { + restart: restartWorkspaceHandlerMock, + }; + + const getFeaturedPluginsMock = jest.fn(); + const featuredPluginStrategy = { + getFeaturedPlugins: getFeaturedPluginsMock, + } as any; + + const onOpenFileRecommendPluginOpenFileStrategyStrategyMock = jest.fn(); + const recommendPluginOpenFileStrategy = { + onOpenFile: onOpenFileRecommendPluginOpenFileStrategyStrategyMock, + } as any; + + const workspacePluginMock = { + exports: { + onDidCloneSources: jest.fn(), + }, + }; + + const outputChannelMock = { + appendLine: jest.fn(), + }; + + beforeEach(() => { + container = new Container(); + jest.resetAllMocks(); + container.bind(FeaturedPluginStrategy).toConstantValue(featuredPluginStrategy); + container.bind(RecommendPluginOpenFileStrategy).toConstantValue(recommendPluginOpenFileStrategy); + container.bind(WorkspaceHandler).toConstantValue(workspaceHandler); + container.bind(DevfileHandler).toConstantValue(devfileHandler); + container.bind(VSCodeCurrentExtensions).toConstantValue(vsCodeCurrentExtensions); + container.bind(FeaturedFetcher).toConstantValue(featuredFetcher); + container.bind(FindFileExtensions).toConstantValue(findFileExtensions); + container.bind(RecommendationsPlugin).toSelf().inSingletonScope(); + getFeaturedPluginsMock.mockReturnValue([]); + devfileHandlerGetPluginsMock.mockReturnValue([]); + analyzeVsCodeCurrentExtensionsMethod.mockReturnValue(vsCodeCurrentExtensionsContent); + (theia.window.createOutputChannel as jest.Mock).mockReturnValue(outputChannelMock); + }); + + test('Check onClone callback is not called if workspacePlugin is not there', async () => { + const recommendationsPlugin = container.get(RecommendationsPlugin); + const spyAfterClone = jest.spyOn(recommendationsPlugin, 'afterClone'); + + await recommendationsPlugin.start(); + expect(workspacePluginMock.exports.onDidCloneSources).toBeCalledTimes(0); + expect(spyAfterClone).toBeCalledTimes(0); + }); + + test('Check onClone callback is registered', async () => { + (theia.plugins.getPlugin as jest.Mock).mockReturnValue(workspacePluginMock); + const recommendationsPlugin = container.get(RecommendationsPlugin); + const spyAfterClone = jest.spyOn(recommendationsPlugin, 'afterClone'); + + await recommendationsPlugin.start(); + expect(workspacePluginMock.exports.onDidCloneSources).toBeCalled(); + const onDidCloneSourceCalback = workspacePluginMock.exports.onDidCloneSources.mock.calls[0]; + + const anonymousFunctionCallback = onDidCloneSourceCalback[0]; + expect(spyAfterClone).toBeCalledTimes(0); + await anonymousFunctionCallback(); + expect(spyAfterClone).toBeCalled(); + }); + + test('Check featuredPlugins with no plugins in the devfile', async () => { + (theia.plugins.getPlugin as jest.Mock).mockReturnValue(workspacePluginMock); + + // no devfile plugins + devfileHandlerHasPluginsMock.mockReturnValue(false); + + const recommendationsPlugin = container.get(RecommendationsPlugin); + const spyInstallPlugins = jest.spyOn(recommendationsPlugin, 'installPlugins'); + expect(spyInstallPlugins).toBeCalledTimes(0); + + getFeaturedPluginsMock.mockReset(); + getFeaturedPluginsMock.mockResolvedValue(['redhat/java']); + + await recommendationsPlugin.start(); + // call the callback + await workspacePluginMock.exports.onDidCloneSources.mock.calls[0][0](); + expect(spyInstallPlugins).toBeCalled(); + expect(spyInstallPlugins.mock.calls[0][0]).toEqual(['redhat/java']); + + // check restart callback is called + expect(restartWorkspaceHandlerMock).toBeCalled(); + expect(restartWorkspaceHandlerMock.mock.calls[0][0]).toContain( + 'have been added to your workspace to improve the intellisense' + ); + }); + + test('Check featuredPlugins with no plugins in the devfile with error in install plug-ins', async () => { + (theia.plugins.getPlugin as jest.Mock).mockReturnValue(workspacePluginMock); + + // no devfile plugins + devfileHandlerHasPluginsMock.mockReturnValue(false); + + const recommendationsPlugin = container.get(RecommendationsPlugin); + getFeaturedPluginsMock.mockReset(); + getFeaturedPluginsMock.mockResolvedValue(['redhat/java']); + + devfileHandlerAddPluginsMock.mockRejectedValue('Unable to install plug-ins'); + + await recommendationsPlugin.start(); + // call the callback + await workspacePluginMock.exports.onDidCloneSources.mock.calls[0][0](); + + // restart not called due to the error + expect(restartWorkspaceHandlerMock).toBeCalledTimes(0); + const showInformationMessageMock = theia.window.showInformationMessage as jest.Mock; + expect(showInformationMessageMock.mock.calls[0][0]).toContain('Unable to add featured plugins'); + }); + + test('Check featuredPlugins with plugins (but not related to suggested) in the devfile (user click Yes on suggestion)', async () => { + (theia.plugins.getPlugin as jest.Mock).mockReturnValue(workspacePluginMock); + + // no devfile plugins + devfileHandlerHasPluginsMock.mockReturnValue(true); + + const recommendationsPlugin = container.get(RecommendationsPlugin); + const spyInstallPlugins = jest.spyOn(recommendationsPlugin, 'installPlugins'); + expect(spyInstallPlugins).toBeCalledTimes(0); + + await recommendationsPlugin.start(); + + // user click on yes, I want to install recommendations + const showInformationMessageMock = theia.window.showInformationMessage as jest.Mock; + showInformationMessageMock.mockResolvedValue({ title: 'Yes' }); + + getFeaturedPluginsMock.mockReset(); + getFeaturedPluginsMock.mockResolvedValue(['redhat/java']); + + // call the callback + await workspacePluginMock.exports.onDidCloneSources.mock.calls[0][0](); + expect(showInformationMessageMock).toBeCalled(); + expect(showInformationMessageMock.mock.calls[0][0]).toContain('Do you want to install the recommended extensions'); + + expect(spyInstallPlugins).toBeCalled(); + }); + + test('Check featuredPlugins with suggested plugins already in the devfile', async () => { + (theia.plugins.getPlugin as jest.Mock).mockReturnValue(workspacePluginMock); + + // no devfile plugins + devfileHandlerHasPluginsMock.mockReturnValue(true); + + const recommendationsPlugin = container.get(RecommendationsPlugin); + const spyInstallPlugins = jest.spyOn(recommendationsPlugin, 'installPlugins'); + expect(spyInstallPlugins).toBeCalledTimes(0); + + const suggestedAndInDevfilePlugin = 'redhat/java'; + devfileHandlerGetPluginsMock.mockReset(); + devfileHandlerGetPluginsMock.mockResolvedValue([suggestedAndInDevfilePlugin]); + await recommendationsPlugin.start(); + + // user click on yes, I want to install recommendations + const showInformationMessageMock = theia.window.showInformationMessage as jest.Mock; + showInformationMessageMock.mockResolvedValue({ title: 'Yes' }); + + getFeaturedPluginsMock.mockReset(); + getFeaturedPluginsMock.mockResolvedValue([suggestedAndInDevfilePlugin]); + + // call the clone callback + await workspacePluginMock.exports.onDidCloneSources.mock.calls[0][0](); + + // nothing should be suggested as we already have this plug-in + expect(showInformationMessageMock).toBeCalledTimes(0); + expect(spyInstallPlugins).toBeCalledTimes(0); + }); + + test('Check featuredPlugins with plugins in the devfile (user click no on suggestion)', async () => { + (theia.plugins.getPlugin as jest.Mock).mockReturnValue(workspacePluginMock); + + // no devfile plugins + devfileHandlerHasPluginsMock.mockReturnValue(true); + + const recommendationsPlugin = container.get(RecommendationsPlugin); + const spyInstallPlugins = jest.spyOn(recommendationsPlugin, 'installPlugins'); + expect(spyInstallPlugins).toBeCalledTimes(0); + + await recommendationsPlugin.start(); + + // user click on yes, I want to install recommendations + const showInformationMessageMock = theia.window.showInformationMessage as jest.Mock; + showInformationMessageMock.mockResolvedValue({ title: 'No' }); + + getFeaturedPluginsMock.mockReset(); + getFeaturedPluginsMock.mockResolvedValue(['redhat/java']); + + // call the callback + await workspacePluginMock.exports.onDidCloneSources.mock.calls[0][0](); + expect(showInformationMessageMock).toBeCalled(); + expect(showInformationMessageMock.mock.calls[0][0]).toContain('Do you want to install the recommended extensions'); + + // we never install plug-ins + expect(spyInstallPlugins).toBeCalledTimes(0); + }); + + test('Check recommendation when opening files', async () => { + // no devfile plugins + devfileHandlerHasPluginsMock.mockReturnValue(false); + devfileHandlerIsRecommendedExtensionsOpenFileEnabledMock.mockReturnValue(true); + const recommendationsPlugin = container.get(RecommendationsPlugin); + + await recommendationsPlugin.start(); + const onDidOpenTextDocumentMethodCalback = (theia.workspace.onDidOpenTextDocument as jest.Mock).mock.calls[0]; + + // call the callback + await onDidOpenTextDocumentMethodCalback[0](); + + // check onOpenFile is being called + expect(onOpenFileRecommendPluginOpenFileStrategyStrategyMock).toBeCalled(); + }); + + test('Skip recommendation if flag is in devfile', async () => { + devfileHandlerIsRecommendedExtensionsDisabledMock.mockResolvedValue(true); + + const recommendationsPlugin = container.get(RecommendationsPlugin); + const enableRecommendationsPluginMethod = jest.spyOn(recommendationsPlugin, 'enableRecommendationsPlugin'); + + await recommendationsPlugin.start(); + // never call the enable method + expect(enableRecommendationsPluginMethod).toBeCalledTimes(0); + }); +}); diff --git a/plugins/recommendations-plugin/tests/registry/che-plugin-registry.spec.ts b/plugins/recommendations-plugin/tests/registry/che-plugin-registry.spec.ts new file mode 100644 index 0000000000..fe9c6afd66 --- /dev/null +++ b/plugins/recommendations-plugin/tests/registry/che-plugin-registry.spec.ts @@ -0,0 +1,57 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata'; + +import * as che from '@eclipse-che/plugin'; + +import { ChePluginRegistry } from '../../src/registry/che-plugin-registry'; +import { Container } from 'inversify'; + +describe('Test ChePluginRegistry', () => { + let container: Container; + const getSettingsMock = jest.fn(); + + beforeEach(() => { + container = new Container(); + che.workspace.getSettings = getSettingsMock; + jest.resetAllMocks(); + container.bind(ChePluginRegistry).toSelf().inSingletonScope(); + }); + + test('check internal', async () => { + const dummyUrl = 'https://foo.registry'; + const fakeSettings = { + cheWorkspacePluginRegistryInternalUrl: dummyUrl, + }; + getSettingsMock.mockResolvedValue(fakeSettings); + const chePluginRegistry = container.get(ChePluginRegistry); + const registryUrl = await chePluginRegistry.getUrl(); + expect(registryUrl).toBe(fakeSettings.cheWorkspacePluginRegistryInternalUrl); + + const anotherCallRegistryUrl = await chePluginRegistry.getUrl(); + expect(anotherCallRegistryUrl).toEqual(registryUrl); + + // API is called only once + expect(getSettingsMock).toBeCalledTimes(1); + }); + + test('check external', async () => { + const dummyUrl = 'https://foo.registry'; + const fakeSettings = { + cheWorkspacePluginRegistryUrl: dummyUrl, + }; + getSettingsMock.mockResolvedValue(fakeSettings); + const chePluginRegistry = container.get(ChePluginRegistry); + const registryUrl = await chePluginRegistry.getUrl(); + expect(registryUrl).toBe(fakeSettings.cheWorkspacePluginRegistryUrl); + }); +}); diff --git a/plugins/recommendations-plugin/tests/strategy/featured-plugin-strategy.spec.ts b/plugins/recommendations-plugin/tests/strategy/featured-plugin-strategy.spec.ts new file mode 100644 index 0000000000..6463ac012d --- /dev/null +++ b/plugins/recommendations-plugin/tests/strategy/featured-plugin-strategy.spec.ts @@ -0,0 +1,131 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata'; + +import { Container } from 'inversify'; +import { FeaturedPlugin } from '../../src/fetch/featured-plugin'; +import { FeaturedPluginStrategy } from '../../src/strategy/featured-plugin-strategy'; +import { FeaturedPluginStrategyRequest } from '../../src/strategy/feature-plugin-strategy-request'; +import { VSCodeExtensionsInstalledLanguages } from '../../src/analyzer/vscode-extensions-installed-languages'; + +describe('Test FeaturedPluginStrategy', () => { + let container: Container; + + const languagesByFileExtensions = new Map(); + const vscodeExtensionByLanguageId = new Map(); + + const vsCodeExtensionsInstalledLanguages: VSCodeExtensionsInstalledLanguages = { + languagesByFileExtensions, + vscodeExtensionByLanguageId, + }; + + beforeEach(() => { + languagesByFileExtensions.clear(); + vscodeExtensionByLanguageId.clear(); + container = new Container(); + container.bind(FeaturedPluginStrategy).toSelf().inSingletonScope(); + }); + + test('basic java', async () => { + const featuredPluginStrategy = container.get(FeaturedPluginStrategy); + + languagesByFileExtensions.set('.java', ['java']); + vscodeExtensionByLanguageId.set('java', ['redhat/java']); + + const featured: FeaturedPlugin = { + id: 'redhat/java', + onLanguages: ['java'], + workspaceContains: [], + contributes: { + languages: [ + { + id: 'java', + aliases: [], + extensions: ['.java'], + filenames: [], + }, + ], + }, + }; + const featuredList = [featured]; + const extensionsInCheWorkspace = ['.java']; + const devfileHasPlugins = true; + + const request: FeaturedPluginStrategyRequest = { + featuredList, + vsCodeExtensionsInstalledLanguages, + devfileHasPlugins, + extensionsInCheWorkspace, + }; + + const featuredPlugins = await featuredPluginStrategy.getFeaturedPlugins(request); + expect(featuredPlugins).toBeDefined(); + expect(featuredPlugins.length).toBe(1); + expect(featuredPlugins[0]).toBe('redhat/java'); + }); + + test('basic unknown language', async () => { + const featuredPluginStrategy = container.get(FeaturedPluginStrategy); + + const featuredList: FeaturedPlugin[] = []; + const extensionsInCheWorkspace = ['.java']; + const devfileHasPlugins = true; + + const request: FeaturedPluginStrategyRequest = { + featuredList, + vsCodeExtensionsInstalledLanguages, + devfileHasPlugins, + extensionsInCheWorkspace, + }; + + const featuredPlugins = await featuredPluginStrategy.getFeaturedPlugins(request); + expect(featuredPlugins).toBeDefined(); + expect(featuredPlugins.length).toBe(0); + }); + + test('basic featured without language', async () => { + const featuredPluginStrategy = container.get(FeaturedPluginStrategy); + + languagesByFileExtensions.set('.java', ['java']); + vscodeExtensionByLanguageId.set('java', ['redhat/java']); + + const featured: FeaturedPlugin = { + id: 'redhat/java', + workspaceContains: [], + contributes: { + languages: [ + { + id: 'java', + aliases: [], + extensions: ['.java'], + filenames: [], + }, + ], + }, + }; + const featuredList = [featured]; + + const extensionsInCheWorkspace = ['.java']; + const devfileHasPlugins = true; + + const request: FeaturedPluginStrategyRequest = { + featuredList, + vsCodeExtensionsInstalledLanguages, + devfileHasPlugins, + extensionsInCheWorkspace, + }; + + const featuredPlugins = await featuredPluginStrategy.getFeaturedPlugins(request); + expect(featuredPlugins).toBeDefined(); + expect(featuredPlugins.length).toBe(0); + }); +}); diff --git a/plugins/recommendations-plugin/tests/strategy/recommend-plugin-open-file-strategy.spec.ts b/plugins/recommendations-plugin/tests/strategy/recommend-plugin-open-file-strategy.spec.ts new file mode 100644 index 0000000000..dd75e6e1f8 --- /dev/null +++ b/plugins/recommendations-plugin/tests/strategy/recommend-plugin-open-file-strategy.spec.ts @@ -0,0 +1,295 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata'; + +import * as theia from '@theia/plugin'; + +import { Container } from 'inversify'; +import { DevfileHandler } from '../../src/devfile/devfile-handler'; +import { LanguagePlugins } from '../../src/fetch/language-plugins'; +import { PluginsByLanguageFetcher } from '../../src/fetch/plugins-by-language-fetcher'; +import { RecommendPluginOpenFileStrategy } from '../../src/strategy/recommend-plugin-open-file-strategy'; +import { RecommendationsPluginAnalysis } from '../../src/plugin/recommendations-plugin-analysis'; +import { VSCodeExtensionsInstalledLanguages } from '../../src/analyzer/vscode-extensions-installed-languages'; +import { WorkspaceHandler } from '../../src/workspace/workspace-handler'; + +describe('Test RecommendPluginOpenFileStrategy', () => { + let container: Container; + + const languagesByFileExtensions = new Map(); + const vscodeExtensionByLanguageId = new Map(); + const vsCodeExtensionsInstalledLanguages: VSCodeExtensionsInstalledLanguages = { + languagesByFileExtensions, + vscodeExtensionByLanguageId, + }; + + const installPluginsMock = jest.fn(); + const fetchMethodMock = jest.fn(); + const pluginsByLanguageFetcher = { + fetch: fetchMethodMock, + } as any; + + const devfileHandlerAddPluginsMock = jest.fn(); + const devfileHandlerDisableRecommendationsMock = jest.fn(); + const devfileHandler = { + addPlugins: devfileHandlerAddPluginsMock, + disableRecommendations: devfileHandlerDisableRecommendationsMock, + } as any; + + const workspaceHandlerRestartMock = jest.fn(); + const workspaceHandler = { + restart: workspaceHandlerRestartMock, + } as any; + + beforeEach(() => { + languagesByFileExtensions.clear(); + vscodeExtensionByLanguageId.clear(); + jest.resetAllMocks(); + container = new Container(); + container.bind(PluginsByLanguageFetcher).toConstantValue(pluginsByLanguageFetcher); + container.bind(DevfileHandler).toConstantValue(devfileHandler); + container.bind(WorkspaceHandler).toConstantValue(workspaceHandler); + container.bind(RecommendPluginOpenFileStrategy).toSelf().inSingletonScope(); + }); + + test('suggest java as no plug-in yet', async () => { + const recommendPluginOpenFileStrategy = container.get(RecommendPluginOpenFileStrategy); + + const devfileHasPlugins = true; + const workspaceFolder = { uri: { path: '/projects' } } as any; + const workspaceFolders: theia.WorkspaceFolder[] = [workspaceFolder]; + theia.workspace.workspaceFolders = workspaceFolders; + const document = { + fileName: '/projects/helloworld.java', + languageId: 'java', + } as any; + + const remoteAvailablePlugins: LanguagePlugins[] = [ + { + category: 'Programming Languages', + ids: ['plugin/java/latest'], + }, + { + category: 'Other', + ids: ['plugin/java2/latest'], + }, + { + category: 'Programming Languages', + ids: ['plugin/java/latest'], + }, + ]; + fetchMethodMock.mockResolvedValue(remoteAvailablePlugins); + + const recommendationsPluginAnalysis: RecommendationsPluginAnalysis = { + vsCodeExtensionsInstalledLanguages, + devfileHasPlugins, + featuredList: [], + }; + + await recommendPluginOpenFileStrategy.onOpenFile(document, recommendationsPluginAnalysis); + const showInformationMessageMock = theia.window.showInformationMessage as jest.Mock; + expect(showInformationMessageMock).toBeCalled(); + expect(showInformationMessageMock.mock.calls[0][0]).toContain( + "The plug-in registry has plug-ins that can help with 'java' files: plugin/java/latest" + ); + + // now retry to open the same file, we should not get recommendation as it has been already suggested + showInformationMessageMock.mockReset(); + await recommendPluginOpenFileStrategy.onOpenFile(document, recommendationsPluginAnalysis); + expect(showInformationMessageMock).toBeCalledTimes(0); + }); + + test('suggest go but disable any recommandations after that', async () => { + const recommendPluginOpenFileStrategy = container.get(RecommendPluginOpenFileStrategy); + + const devfileHasPlugins = true; + const workspaceFolder = { uri: { path: '/projects' } } as any; + const workspaceFolders: theia.WorkspaceFolder[] = [workspaceFolder]; + theia.workspace.workspaceFolders = workspaceFolders; + const document = { + fileName: '/projects/helloworld.go', + languageId: 'go', + } as any; + + const remoteAvailablePlugins: LanguagePlugins[] = [ + { + category: 'Programming Languages', + ids: ['plugin/go/latest'], + }, + ]; + fetchMethodMock.mockResolvedValue(remoteAvailablePlugins); + + const recommendationsPluginAnalysis: RecommendationsPluginAnalysis = { + vsCodeExtensionsInstalledLanguages, + devfileHasPlugins, + featuredList: [], + }; + + // click on don't show again + const showInformationMessageMock = theia.window.showInformationMessage as jest.Mock; + showInformationMessageMock.mockReset(); + showInformationMessageMock.mockResolvedValue({ title: "Don't Show Again Recommendations" }); + + await recommendPluginOpenFileStrategy.onOpenFile(document, recommendationsPluginAnalysis); + expect(showInformationMessageMock).toBeCalled(); + expect(showInformationMessageMock.mock.calls[0][0]).toContain( + "The plug-in registry has plug-ins that can help with 'go' files: plugin/go/latest" + ); + + // check that we've called the disable + expect(devfileHandlerDisableRecommendationsMock).toBeCalled(); + }); + + test('suggest go and validate install', async () => { + const recommendPluginOpenFileStrategy = container.get(RecommendPluginOpenFileStrategy); + + const devfileHasPlugins = true; + const workspaceFolder = { uri: { path: '/projects' } } as any; + const workspaceFolders: theia.WorkspaceFolder[] = [workspaceFolder]; + theia.workspace.workspaceFolders = workspaceFolders; + const document = { + fileName: '/projects/helloworld.go', + languageId: 'go', + } as any; + + const remoteAvailablePlugins: LanguagePlugins[] = [ + { + category: 'Programming Languages', + ids: ['plugin/go/latest'], + }, + ]; + fetchMethodMock.mockResolvedValue(remoteAvailablePlugins); + + const recommendationsPluginAnalysis: RecommendationsPluginAnalysis = { + vsCodeExtensionsInstalledLanguages, + devfileHasPlugins, + featuredList: [], + }; + + // click on install + const showInformationMessageMock = theia.window.showInformationMessage as jest.Mock; + showInformationMessageMock.mockReset(); + showInformationMessageMock.mockResolvedValue({ title: 'Install...' }); + + await recommendPluginOpenFileStrategy.onOpenFile(document, recommendationsPluginAnalysis); + expect(showInformationMessageMock).toBeCalled(); + expect(showInformationMessageMock.mock.calls[0][0]).toContain( + "The plug-in registry has plug-ins that can help with 'go' files: plugin/go/latest" + ); + + // check that we've called the install + expect(devfileHandlerAddPluginsMock).toHaveBeenCalledWith(['plugin/go/latest']); + + // check that we've called the restart + expect(workspaceHandlerRestartMock).toHaveBeenCalledWith( + 'Plug-ins plugin/go/latest have been added to your workspace. Please restart the workspace to see the changes.' + ); + + // but not the disable recommendations + expect(devfileHandlerDisableRecommendationsMock).toBeCalledTimes(0); + }); + + test('do not suggest when there are already plugins for the current language ID', async () => { + const recommendPluginOpenFileStrategy = container.get(RecommendPluginOpenFileStrategy); + languagesByFileExtensions.set('.java', ['java']); + vscodeExtensionByLanguageId.set('java', ['redhat/java']); + + const devfileHasPlugins = true; + const workspaceFolder = { uri: { path: '/projects' } } as any; + const workspaceFolders: theia.WorkspaceFolder[] = [workspaceFolder]; + theia.workspace.workspaceFolders = workspaceFolders; + const document = { + fileName: '/projects/helloworld.java', + languageId: 'java', + } as any; + + const remoteAvailablePlugins: LanguagePlugins[] = []; + fetchMethodMock.mockResolvedValue(remoteAvailablePlugins); + + const recommendationsPluginAnalysis: RecommendationsPluginAnalysis = { + vsCodeExtensionsInstalledLanguages, + devfileHasPlugins, + featuredList: [], + }; + + await recommendPluginOpenFileStrategy.onOpenFile(document, recommendationsPluginAnalysis); + const showInformationMessageMock = theia.window.showInformationMessage as jest.Mock; + expect(showInformationMessageMock).toBeCalledTimes(0); + }); + + test('do not suggest when there is no remote plug-ins for the current language ID', async () => { + const recommendPluginOpenFileStrategy = container.get(RecommendPluginOpenFileStrategy); + languagesByFileExtensions.set('.java', ['java']); + + const devfileHasPlugins = true; + const workspaceFolder = { uri: { path: '/projects' } } as any; + const workspaceFolders: theia.WorkspaceFolder[] = [workspaceFolder]; + theia.workspace.workspaceFolders = workspaceFolders; + const document = { + fileName: '/projects/helloworld.java', + languageId: 'java', + } as any; + + const remoteAvailablePlugins: LanguagePlugins[] = []; + fetchMethodMock.mockResolvedValue(remoteAvailablePlugins); + + const recommendationsPluginAnalysis: RecommendationsPluginAnalysis = { + vsCodeExtensionsInstalledLanguages, + devfileHasPlugins, + featuredList: [], + }; + + await recommendPluginOpenFileStrategy.onOpenFile(document, recommendationsPluginAnalysis); + const showInformationMessageMock = theia.window.showInformationMessage as jest.Mock; + expect(showInformationMessageMock).toBeCalledTimes(0); + }); + + test('No suggestion when document is not part of workspace', async () => { + const recommendPluginOpenFileStrategy = container.get(RecommendPluginOpenFileStrategy); + const workspaceFolder = { uri: { path: '/projects' } } as any; + const workspaceFolders: theia.WorkspaceFolder[] = [workspaceFolder]; + theia.workspace.workspaceFolders = workspaceFolders; + const document = { + fileName: '/external/external.java', + languageId: 'java', + } as any; + + const recommendationsPluginAnalysis: RecommendationsPluginAnalysis = { + vsCodeExtensionsInstalledLanguages, + devfileHasPlugins: false, + featuredList: [], + }; + + await recommendPluginOpenFileStrategy.onOpenFile(document, recommendationsPluginAnalysis); + // we do not call fetch + expect(fetchMethodMock).toBeCalledTimes(0); + }); + + test('No suggestion when no workspace folders', async () => { + const recommendPluginOpenFileStrategy = container.get(RecommendPluginOpenFileStrategy); + theia.workspace.workspaceFolders = undefined; + const document = { + fileName: '/external/external.java', + languageId: 'java', + } as any; + + const recommendationsPluginAnalysis: RecommendationsPluginAnalysis = { + vsCodeExtensionsInstalledLanguages, + devfileHasPlugins: false, + featuredList: [], + }; + + await recommendPluginOpenFileStrategy.onOpenFile(document, recommendationsPluginAnalysis); + // we do not call fetch + expect(fetchMethodMock).toBeCalledTimes(0); + }); +}); diff --git a/plugins/recommendations-plugin/tests/util/util.spec.ts b/plugins/recommendations-plugin/tests/util/util.spec.ts new file mode 100644 index 0000000000..aae0d15b22 --- /dev/null +++ b/plugins/recommendations-plugin/tests/util/util.spec.ts @@ -0,0 +1,21 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +import { Deferred } from '../../src/util/deferred'; + +describe('Test Deferred', () => { + test('deferred', async () => { + const deferredBoolean = new Deferred(); + setTimeout(() => deferredBoolean.resolve(true), 500); + const promise = deferredBoolean.promise; + const result = await promise; + expect(result).toBeTruthy(); + }); +}); diff --git a/plugins/recommendations-plugin/tests/workspace/workspace-handler.spec.ts b/plugins/recommendations-plugin/tests/workspace/workspace-handler.spec.ts new file mode 100644 index 0000000000..a4d4e84013 --- /dev/null +++ b/plugins/recommendations-plugin/tests/workspace/workspace-handler.spec.ts @@ -0,0 +1,36 @@ +/********************************************************************** + * Copyright (c) 2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import 'reflect-metadata'; + +import * as che from '@eclipse-che/plugin'; + +import { Container } from 'inversify'; +import { WorkspaceHandler } from '../../src/workspace/workspace-handler'; + +describe('Test WorkspaceHandler', () => { + let container: Container; + + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + container = new Container(); + container.bind(WorkspaceHandler).toSelf().inSingletonScope(); + }); + + test('basics', async () => { + const workspaceHandler = container.get(WorkspaceHandler); + const restartMethod = jest.fn(); + che.workspace.restartWorkspace = restartMethod; + await workspaceHandler.restart('Hello this is my Message'); + expect(restartMethod).toBeCalled(); + const registerCallbackCall = restartMethod.mock.calls[0]; + expect(registerCallbackCall[0]).toEqual({ prompt: true, promptMessage: 'Hello this is my Message' }); + }); +}); diff --git a/plugins/recommendations-plugin/tsconfig.json b/plugins/recommendations-plugin/tsconfig.json new file mode 100644 index 0000000000..fb22f97b0a --- /dev/null +++ b/plugins/recommendations-plugin/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "outDir": "./lib", + "sourceMap": true, + "noEmitOnError": true, + "noImplicitReturns" : true, + "noImplicitThis": true, + "noUnusedLocals": false, + "removeComments": true, + "noFallthroughCasesInSwitch": true, + "strict": true, + "strictPropertyInitialization": false, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "types": ["reflect-metadata", "jest"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "lib": [ + "es6", + ], + } + , + "include": [ + "src/**/*" + ] + , + "exclude": [ + "node_modules", + ] + } diff --git a/tsconfig.json b/tsconfig.json index f627fbccb9..2ec6ae9d6e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "generator/tests", "plugins/*/src", "plugins/*/tests", + "plugins/*/__mocks__", "extensions/*/src", "extensions/*/tests", "tools/*/src"