diff --git a/package-lock.json b/package-lock.json index 8f2fbb68..a6a6e3b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2965,7 +2965,7 @@ "dev": true, "optional": true, "requires": { - "minimist": "0.0.8" + "minimist": "1.2.5" }, "dependencies": { "minimist": { diff --git a/package.json b/package.json index f5e7509e..f01a39db 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ ], "main": "./dist/extension", "extensionDependencies": [ - "redhat.vscode-microprofile", "redhat.java", "vscjava.vscode-java-debug", "redhat.vscode-commons" diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 66e2e274..0aa7e07f 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -1,13 +1,14 @@ import { commands, ExtensionContext, window } from "vscode"; import { VSCodeCommands } from "../definitions/constants"; import { ProjectLabelInfo } from "../definitions/ProjectLabelInfo"; +import { installMPExtForFeature, isToolsForMicroProfileInstalled, microProfileToolsStarted } from "../requirements/toolsForMicroProfile"; import { requestStandardMode } from "../utils/requestStandardMode"; import { sendCommandFailedTelemetry, sendCommandSucceededTelemetry } from "../utils/telemetryUtils"; import { WelcomeWebview } from "../webviews/WelcomeWebview"; import { addExtensionsWizard } from "../wizards/addExtensions/addExtensionsWizard"; import { startDebugging } from "../wizards/debugging/startDebugging"; -import { generateProjectWizard } from "../wizards/generateProject/generationWizard"; import { deployToOpenShift } from "../wizards/deployToOpenShift/deployToOpenShift"; +import { generateProjectWizard } from "../wizards/generateProject/generationWizard"; const NOT_A_QUARKUS_PROJECT = new Error('No Quarkus projects were detected in this folder'); const STANDARD_MODE_REQUEST_FAILED = new Error('Error occurred while requesting standard mode from the Java language server'); @@ -89,6 +90,15 @@ async function registerCommandWithTelemetry(context: ExtensionContext, commandNa */ function withStandardMode(commandAction: () => Promise, commandDescription: string): () => Promise { return async () => { + if (!isToolsForMicroProfileInstalled()) { + await installMPExtForFeature(commandDescription); + // You need to reload the window after installing Tools for MicroProfile + // before any of the features are available. + // Return early instead of attempting to run the command. + return; + } else { + await microProfileToolsStarted(); + } let isStandardMode = false; try { isStandardMode = await requestStandardMode(commandDescription); diff --git a/src/extension.ts b/src/extension.ts index 4638f9bf..22bdbf3f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,6 +25,7 @@ import quarkusProjectListener from './QuarkusProjectListener'; import { terminalCommandRunner } from './terminal/terminalCommandRunner'; import { WelcomeWebview } from './webviews/WelcomeWebview'; import { createTerminateDebugListener } from './wizards/debugging/terminateProcess'; +import { installMPExtOnStartup } from './requirements/toolsForMicroProfile'; export async function activate(context: ExtensionContext) { @@ -33,8 +34,11 @@ export async function activate(context: ExtensionContext) { QuarkusContext.setContext(context); displayWelcomePageIfNeeded(context); + commands.executeCommand('setContext', 'quarkusProjectExistsOrLightWeight', true); + installMPExtOnStartup(); + context.subscriptions.push(createTerminateDebugListener()); quarkusProjectListener.getQuarkusProjectListener().then((disposableListener: Disposable) => { context.subscriptions.push(disposableListener); diff --git a/src/requirements/toolsForMicroProfile.ts b/src/requirements/toolsForMicroProfile.ts new file mode 100644 index 00000000..36617b96 --- /dev/null +++ b/src/requirements/toolsForMicroProfile.ts @@ -0,0 +1,106 @@ +/** + * Copyright 2021 Red Hat, Inc. and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { extensions, window } from "vscode"; +import { QuarkusContext } from "../QuarkusContext"; +import { installExtension, isExtensionInstalled } from "../utils/extensionInstallationUtils"; + +const TOOLS_FOR_MICRO_PROFILE_EXT = 'redhat.vscode-microprofile'; + +const STARTUP_INSTALL_MEMO = 'mpExtInstallOnStartup.isIgnored'; + +/** + * Returns true if Tools for MicroProfile is installed, and false otherwise + * + * @returns true if Tools for MicroProfile is installed, and false otherwise + */ +export function isToolsForMicroProfileInstalled(): boolean { + return isExtensionInstalled(TOOLS_FOR_MICRO_PROFILE_EXT); +} + +/** + * Prompts the user to install Tools for MicroProfile if they don't have it installed + * + * Allows the user to silence this prompt in the future by clicking on a button. + * Warns the user that only some functionality is available if they choose not to install vscode-microprofile. + * + * @returns when the user has installed Tools for MicroProfile, + * has chosen not to install Tools for MicroProfile, + * or its detected that they've silenced this popup + */ +export async function installMPExtOnStartup(): Promise { + if (isExtensionInstalled(TOOLS_FOR_MICRO_PROFILE_EXT)) { + return; + } + const installOnStartupIsIgnored = QuarkusContext.getExtensionContext().globalState.get(STARTUP_INSTALL_MEMO, false); + if (installOnStartupIsIgnored) { + return; + } + const YES = 'Install'; + const NO = 'Don\'t install'; + const NOT_AGAIN = 'Don\'t ask me again'; + const result = await window.showWarningMessage('vscode-quarkus depends on Tools for MicroProfile for many of its features, ' + + 'but can provide some functionality without it. ' + + 'Install Tools for MicroProfile now? ' + + 'You will need to reload the window after the installation.', YES, NO, NOT_AGAIN); + if (result === YES) { + try { + await installExtension(TOOLS_FOR_MICRO_PROFILE_EXT); + } catch (e) { + window.showErrorMessage(e); + } + } else if (result === NOT_AGAIN) { + QuarkusContext.getExtensionContext().globalState.update(STARTUP_INSTALL_MEMO, true); + limitedFunctionalityWarning(); + } else { + limitedFunctionalityWarning(); + } +} + +/** + * Prompts the user to install Tools for MicroProfile in order to use a feature + * + * TODO: this workflow is a mess + * + * @param feature the feature that requires Tools for MicroProfile in order to run + * @returns when the user has attempted to install + */ +export async function installMPExtForFeature(feature: string) { + if (isExtensionInstalled(TOOLS_FOR_MICRO_PROFILE_EXT)) { + await microProfileToolsStarted(); + } + const YES = 'Install'; + const NO = `Cancel ${feature}`; + const result = await window.showWarningMessage(`${feature} requires Tools for MicroProfile. Install it now? ` + + 'You will need to reload the window after the installation.', + YES, NO); + if (result === YES) { + try { + await installExtension(TOOLS_FOR_MICRO_PROFILE_EXT); + } catch (e) { + window.showErrorMessage(e); + } + } else { + throw new Error(`${feature} requires Tools for MicroProfile, so it can't be run.`); + } +} + +export async function microProfileToolsStarted(): Promise { + await extensions.getExtension(TOOLS_FOR_MICRO_PROFILE_EXT).activate(); +} + +async function limitedFunctionalityWarning(): Promise { + await window.showInformationMessage('vscode-quarkus will run with limited functionality'); +} diff --git a/src/utils/extensionInstallationUtils.ts b/src/utils/extensionInstallationUtils.ts new file mode 100644 index 00000000..051a50ac --- /dev/null +++ b/src/utils/extensionInstallationUtils.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2021 Red Hat, Inc. and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { commands, Disposable, extensions } from "vscode"; + +export const EXT_DOWNLOAD_TIMEOUT_ERROR = new Error('Extension installation is taking a while'); + +const DOWNLOAD_TIMEOUT = 60000; + +/** + * Installs the extension with the given id + * + * @param extensionId the id (`"${publisher}.${name}"`) of the extension to check + * @returns when the extension is installed + * @throws `EXT_DOWNLOAD_TIMEOUT_ERROR` when the extension installation takes a while, + * or the extension installation fails + */ +export async function installExtension(extensionId: string): Promise { + let installListenerDisposable: Disposable; + return new Promise((resolve, reject) => { + installListenerDisposable = extensions.onDidChange(() => { + if (isExtensionInstalled(extensionId)) { + resolve(); + } + }); + commands.executeCommand("workbench.extensions.installExtension", extensionId) + .then((_unused: any) => { }, reject); + setTimeout(reject, DOWNLOAD_TIMEOUT, EXT_DOWNLOAD_TIMEOUT_ERROR); + }).finally(() => { + installListenerDisposable.dispose(); + }); +} + +/** + * Returns true if the extension is installed and false otherwise + * + * @param extensionId the id (`"${publisher}.${name}"`) of the extension to check + * @returns true if the extension is installed and false otherwise + */ +export function isExtensionInstalled(extensionId: string): boolean { + return !!extensions.getExtension(extensionId); +} diff --git a/src/utils/openShiftConnectorUtils.ts b/src/utils/openShiftConnectorUtils.ts index ee7a9eef..e49fdaf1 100644 --- a/src/utils/openShiftConnectorUtils.ts +++ b/src/utils/openShiftConnectorUtils.ts @@ -13,20 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { commands, Disposable, Extension, extensions, ProgressLocation, Uri, window } from "vscode"; +import { commands, Extension, extensions, ProgressLocation, Uri, window } from "vscode"; +import { EXT_DOWNLOAD_TIMEOUT_ERROR, installExtension, isExtensionInstalled } from "./extensionInstallationUtils"; export const OPENSHIFT_CONNECTOR_EXTENSION_ID = 'redhat.vscode-openshift-connector'; export const OPENSHIFT_CONNECTOR = 'OpenShift Connector extension'; -const DOWNLOAD_TIMEOUT = 60000; // Timeout for downloading VSCode OpenShift Connector, in milliseconds - -/** - * Returns true if the OpenShift connector extension is installed, and false otherwise - * - * @returns true if the OpenShift connector extension is installed, and false otherwise - */ -export function isOpenShiftConnectorInstalled(): boolean { - return !!extensions.getExtension(OPENSHIFT_CONNECTOR_EXTENSION_ID); -} /** * Returns the OpenShift Connector extension API @@ -35,7 +26,7 @@ export function isOpenShiftConnectorInstalled(): boolean { * @returns the OpenShift Connector extension API */ export async function getOpenShiftConnector(): Promise { - if (!isOpenShiftConnectorInstalled()) { + if (!isExtensionInstalled(OPENSHIFT_CONNECTOR_EXTENSION_ID)) { throw new Error(`${OPENSHIFT_CONNECTOR} is not installed`); } const openShiftConnector: Extension = extensions.getExtension(OPENSHIFT_CONNECTOR_EXTENSION_ID); @@ -52,19 +43,14 @@ export async function getOpenShiftConnector(): Promise { * @throws if the user refuses to install the extension, or if the extension does not get installed within a timeout period */ async function installOpenShiftConnector(): Promise { - let installListenerDisposable: Disposable; - return new Promise((resolve, reject) => { - installListenerDisposable = extensions.onDidChange(() => { - if (isOpenShiftConnectorInstalled()) { - resolve(); - } - }); - commands.executeCommand("workbench.extensions.installExtension", OPENSHIFT_CONNECTOR_EXTENSION_ID) - .then((_unused: any) => { }, reject); - setTimeout(reject, DOWNLOAD_TIMEOUT, new Error(`${OPENSHIFT_CONNECTOR} installation is taking a while. Cancelling 'Deploy to OpenShift'. Please retry after the OpenShift Connector installation has finished`)); - }).finally(() => { - installListenerDisposable.dispose(); - }); + try { + installExtension(OPENSHIFT_CONNECTOR_EXTENSION_ID); + } catch (e) { + if (e === EXT_DOWNLOAD_TIMEOUT_ERROR) { + throw new Error(`${OPENSHIFT_CONNECTOR} installation is taking a while. Cancelling 'Deploy to OpenShift'. Please retry after the OpenShift Connector installation has finished`); + } + throw e; + } } /** diff --git a/src/wizards/deployToOpenShift/deployToOpenShift.ts b/src/wizards/deployToOpenShift/deployToOpenShift.ts index ef6dadfd..7d1df110 100644 --- a/src/wizards/deployToOpenShift/deployToOpenShift.ts +++ b/src/wizards/deployToOpenShift/deployToOpenShift.ts @@ -15,7 +15,8 @@ */ import { Uri, window } from "vscode"; import { ProjectLabelInfo } from "../../definitions/ProjectLabelInfo"; -import { deployQuarkusProject, getOpenShiftConnector, installOpenShiftConnectorWithProgress, isOpenShiftConnectorInstalled, OPENSHIFT_CONNECTOR } from "../../utils/openShiftConnectorUtils"; +import { isExtensionInstalled } from "../../utils/extensionInstallationUtils"; +import { deployQuarkusProject, getOpenShiftConnector, installOpenShiftConnectorWithProgress, OPENSHIFT_CONNECTOR, OPENSHIFT_CONNECTOR_EXTENSION_ID } from "../../utils/openShiftConnectorUtils"; import { getQuarkusProject } from "../getQuarkusProject"; /** @@ -38,7 +39,7 @@ export async function deployToOpenShift(): Promise { * @returns the OpenShift Connector extension API */ async function installOpenShiftConnectorIfNeeded(): Promise { - if (isOpenShiftConnectorInstalled()) { + if (isExtensionInstalled(OPENSHIFT_CONNECTOR_EXTENSION_ID)) { return getOpenShiftConnector(); } return askToInstallOpenShiftConnector(); @@ -57,7 +58,7 @@ async function askToInstallOpenShiftConnector(): Promise { if (response === YES) { try { await installOpenShiftConnectorWithProgress(); - if (isOpenShiftConnectorInstalled()) { + if (isExtensionInstalled(OPENSHIFT_CONNECTOR_EXTENSION_ID)) { return getOpenShiftConnector(); } } catch (e) {